最小生成樹:Kruskal演算法和Prim演算法

taixian發表於2024-03-25

首先區別一下圖跟樹:

樹不會包含環,圖可以包含環。
圖的生成樹其實就是在圖中找一棵包含圖中的所有節點的樹。專業點說,生成樹是含有圖中所有頂點的無環連通子圖。

最小生成樹就是再所有可能的生成樹中,權重和最小的那棵生成樹就叫最小生成樹(注意:最小生成樹有n-1條邊)。

Kruskal演算法和Prim演算法都是用於解決圖的最小生成樹(Minimum Spanning Tree,簡稱MST)問題的經典演算法。在這個問題中,我們需要找到一個圖中的最小生成樹,即包含所有節點但總權重最小的子圖(B站有up主做了很易懂的動畫:【最小生成樹(Kruskal(克魯斯卡爾)和Prim(普里姆))演算法動畫演示】https://www.bilibili.com/video/BV1Eb41177d1?vd_source=dd65e5938f5f6dab5dc478dad590c5f9)。

Kruskal演算法:

1. 概述:
Kruskal演算法是一種貪婪演算法,它透過不斷選擇邊來構建最小生成樹。它的基本思想是從小到大選擇圖中的邊,並且保證所選邊不構成環。

2. 演算法步驟:

  • 初始化: 將所有的邊按照權重從小到大進行排序。
  • 迴圈選擇邊: 從最小的邊開始,依次考慮每一條邊。
    • 如果當前邊不會形成環(即加入該邊後不會導致圖中出現環),則選擇該邊加入最小生成樹。
    • 如果形成環,則捨棄該邊。
  • 重複步驟直至生成樹包含了所有的節點。
    圖片來自b站up主@WAY_zhong

3. 實現要點:

  • 使用並查集來判斷是否形成環。

Prim演算法:

1. 概述:
Prim演算法也是一種貪婪演算法,它從一個節點開始逐步構建最小生成樹。與Kruskal演算法不同,Prim演算法是基於節點而不是邊的選擇來構建最小生成樹。

2. 演算法步驟:

  • 選擇初始節點: 從圖中任意一個節點開始。
  • 逐步擴充套件生成樹:
    • 在當前的最小生成樹集合和其餘節點中選擇一個最小權重的邊,將其加入最小生成樹。
    • 將新加入的節點加入到最小生成樹的集合中。
  • 重複步驟直至生成樹包含了所有的節點。

圖片來自b站up主@WAY_zhong

3. 實現要點:

  • 使用優先佇列來選擇最小權重的邊。
  • 用一個集合來記錄已經加入最小生成樹的節點,以避免重複選擇。

例題引入

leetcode 第1584題 https://leetcode.cn/problems/min-cost-to-connect-all-points/description/
題目:給你一個points 陣列,表示 2D 平面上的一些點,其中 points[i] = [xi, yi] 。

連線點 [xi, yi] 和點 [xj, yj] 的費用為它們之間的 曼哈頓距離 :|xi - xj| + |yi - yj| ,其中 |val| 表示 val 的絕對值。

請你返回將所有點連線的最小總費用。只有任意兩點之間 有且僅有 一條簡單路徑時,才認為所有點都已連線。
示例 1:

輸入:points = [[0,0],[2,2],[3,10],[5,2],[7,0]]
輸出:20
解釋:
我們可以按照上圖所示連線所有點得到最小總費用,總費用為 20 。
注意到任意兩個點之間只有唯一一條路徑互相到達

Kruskal演算法

思路:

  • 首先,遍歷點集points,構建邊,並將該邊在points中的下標i,j和曼哈頓距離d存入arr
  • 由於Kruskal特點是先新增費用小的邊,所以將arr按照曼哈頓距離從小到大排序
  • 利用並查集構建最小生成樹。如果當前邊的兩個點沒有不在最小生成樹中,則將該邊新增到最小生成樹中,更新邊數和費用,edge+=1,cost+=d,當邊數edge==n-1時,說明構建完最小生成樹。
點選檢視程式碼
class Solution:
    def minCostConnectPoints(self, points: List[List[int]]) -> int:
        res = [] #儲存每條邊在points中的兩個下標和曼哈頓距離
        n = len(points)
        # 計算任意兩點之間的曼哈頓距離,並將邊新增到邊列表中
        for i in range(n):
            x1, y1 = points[i]
            for j in range(i+1,n):
                x2,y2 = points[j]
                res.append([i,j,abs(x2-x1)+abs(y2-y1)]) #i,j表示邊的起點和終點
        #對邊排序
        res.sort(key=lambda x:x[2])
        #初始化並查集
        parent = list(range(n))#列表儲存了每個節點的父節點資訊。
        #在最初的時候,每個節點的父節點都是自身,即parent[i] = i,表示每個節點自成一個集合。
        def find(x):
            if x != parent[x]:
                parent[x] = find(parent[x])
            return parent[x]
        #構建最小生成樹
        edge = 0 #初始化邊數為0
        cost = 0
        for i ,j ,d in res:
            a,b = find(i),find(j) 
            if a!= b: #每次考慮一條邊,檢查該邊的兩個端點是否已經在同一個集合中(即是否構成環)
                parent[b] = a
                edge += 1
                cost += d
            if edge == n-1: #結束條件(最小生成樹邊數為n-1)
                break
        return cost

Prim演算法

思路:
初始化:

  • d列表用於表示各個頂點與加入最小生成樹的頂點之間的最小距離,初始值設為無窮大。
  • vis列表表示是否已經加入到了最小生成樹中,初始值設為False。

選擇初始點:

  • 選取第一個點作為起始點,將其距離設為0,表示到自身的距離為0。

逐步擴充套件生成樹:

  • 外層迴圈迭代每個點
  • 內層迴圈在未加入最小生成樹的點中找到距離最小的點。
  • 將找到的點加入最小生成樹,並更新距離列表。
  • 計算當前輪的最小距離,並加入到答案中。

返回結果:

  • 返回連線所有點的最小總費用ans。
點選檢視程式碼
        n = len(points)
        d = [float("inf")]*n # 表示各個頂點與加入最小生成樹的頂點之間的最小距離
        vis = [False]*n # 表示是否已經加入到了最小生成樹裡面
        d[0]= 0 # 選擇第一個點作為起始點,將其距離設為0,表示到自身的距離為0
        cost = 0 # 最小生成樹的總費用
        # 遍歷每個頂點,將其加入到最小生成樹中
        for _ in range(n): #也可替換成while迴圈
            # 尋找當前輪的最小d
            m = float("inf")
            node = -1  # 用於記錄當前輪中距離最小的點
            
            for i in  range(n): 
            # 如果頂點i未加入到最小生成樹,並且與已加入點之間的距離小於當前最小距離,則更新最小距離
                if not vis[i] and d[i] < m:
                    node = i
                    m = d[i]
            vis[node] = True # 將該點標記為已加入最小生成樹
            cost += m # 更新最小生成樹的總費用

            # 更新與加入的頂點相連線的頂點的d
            for i in range(n):
                 # 如果頂點i未加入到最小生成樹,則更新其與加入頂點的距離
                if not vis[i]:
                    # 計算頂點i與加入頂點之間的曼哈頓距離,並更新距離列表d[i]
                    d[i] = min(d[i],abs(points[i][0]-points[node][0])+abs(points[i][1]-points[node][1]))
        return cost     

總結比較:

  • Kruskal演算法:基於邊的選擇,適用於稀疏圖,實現相對簡單,使用並查集來檢測環。
  • Prim演算法:基於節點的選擇,適用於稠密圖,實現相對簡單,使用優先佇列來選擇最小權重的邊。

在選擇演算法時,可以根據具體的圖的特點來決定使用哪種演算法。如果圖比較稀疏,即邊數相對節點數較少,則Kruskal演算法可能更有效率;而如果圖比較稠密,即邊數接近節點數的平方,則Prim演算法可能更合適。

相關文章