圖論(九)最小生成樹-Kruskal算法

前面說過Kruskal是從最短邊着手構建最小生成樹的。其基本過程是:先對圖中的所有邊按照權重值從小到大進行排序,然後着手選取邊構建最小生成樹。如果直接從小到大按順序選取,有可能形成了環,所以對環的處理就成了核心問題。


我們還是以前面的鄉鎮假設光纖網絡爲例:

Kruskal算法工作步驟如下:

(1) 邊進行排序。

Begin

End

Weight

e

i

7

c

h

8

a

b

10

a

f

11

b

g

12

b

h

12

d

i

16

f

g

17

b

d

18

g

i

19

d

e

20

d

h

21

c

d

22

d

g

24


(2) 權重依次從小到大加入最小生成樹。

 

我們依次將邊e-i(7),c-h(8),a-b(10),a-f(11),b-g(12),b-h(12),d-i(16)加入最小生成樹中(圖中標綠色的邊),到目前爲止,沒有任何問題。


(3) 檢查新加入的邊是否構成了環。

下面,按照順序需要將邊f-g(17)加入最小生成樹。

 

很不幸,這時a-b-g-f-a構成了環。於是f-g(17)被排除,不能被加入最小生成樹。


下面按順序加入的邊是b-c(18),同樣b-c-g-b也構成了環,所以邊b-c(18)也被排除。如此依次類推,最後得到的最小生成樹是:

 

好了,算法介紹完了。就這麼簡單

 

當然,這裏有個核心問題還沒有清楚的描述,即怎樣判斷構成了環?


判斷原理也很簡單:最小生成樹加入一條邊從而構成環,那麼這條兩個頂點除了這條邊外,必然有另一條路徑,即兩個頂點在加入這個邊之前,就是連通的或者在一個連通子圖上。


因此,在G=(V,E),我們令最小生成樹初始狀態爲只有n個頂點,0邊的非連通圖T=(V,{})圖中每個頂點都是一個連通子圖n個連通子圖。依次E按順序從小到大選擇邊,該邊的兩個頂點落在T不同的連通子圖上,則將次變加入到T,否則捨去此邊,依次類推,直到T所有頂點都在同一個連通子圖上爲止


光說不練假把式。上代碼:

# -*- coding: utf-8 -*-

構建圖G,使用了鄰接矩陣表示法,9999代表非直連
=[[0,10,9999,9999,9999,11,9999,9999,9999],  # a的鄰居表
     [10,0,18,9999,9999,9999,12,12,9999],  # b的鄰居表
     [9999,18,0,22,9999,9999,9999,8,9999],  # c的鄰居表
     [9999,9999,22,0,20,9999,24,21,16],  # d的鄰居表
     [9999,9999,9999,20,0,26,9999,9999,7],  # e的鄰居表
     [11,9999,9999,9999,26,0,17,9999,9999],  # f的鄰居表
     [9999,12,9999,24,9999,17,0,9999,19],  # g的鄰居表
     [9999,12,8,21,9999,9999,9999,0,9999],  # h的鄰居表
     [9999,9999,9999,16,7,9999,19,9999,0]]  # i的鄰居表

構建頂點序號符號對應集
v_dict ={}
v_strdict ={}
v_str ='abcdefghi'
for iin range(len(G)):
    v_dict[i]v_str[i]
for i, valuein enumerate(v_str):
    v_strdict[value]i

構建邊集
=[(v_dict[u], v_dict[v], G[u][v]) for uin range(len(G))for vin range(len(G[u]))if 0G[u][v]9999and uv]

對邊集進行排序
=sorted(E,key=lambdax:x[2])

構建最小生成樹
=set()

構建頂點連通子圖
=[0]len(G)

判斷是否屬於同一連通子圖
def find_connect(V,v):
    whileV[v]0:
        vV[v]
    returnv

循環邊
for ein E:
    #邊的兩個頂點是否已連通
    v1, v2find_connect(V, v_strdict[e[0]]), find_connect(V, v_strdict[e[1]])
    ifv1 !=v2:
        V[v1]v2
        T.add(e) #將邊加入最小生成樹

輸出最小生成樹
print(T)
輸出代價
print(sum([t[2]for tin T]))

 

輸出結果:

{('e', 'i', 7), ('g', 'i', 19), ('c', 'h', 8), ('a', 'f', 11), ('b', 'g', 12), ('d', 'i', 16), ('a', 'b', 10), ('b', 'h', 12)}

95

算法的主要執行過程在最後的for循環處find_connect函數與頂點的個數V決定,時間複雜度爲O(logV)而外面是邊E循環,因此Kruskal算法的時間複雜度爲O(E logV)