之前對最小生成樹Prim算法進行了一定的總結,並給出了代碼實現,詳見:http://www.cnblogs.com/dzkang2011/p/prim_1.html
一、介紹
由於忙於各類事務,在算法方面的學習有所停滯,現在將求最小生成樹的另外一種算法補上,也就是Kruskal算法。有關最小生成樹算法正確性的證明見上面鏈接。
Kruskal算法是一種實現起來較為簡單,比較容易理解的算法,在圖較為稀疏時能夠獲得很好的性能,但當圖較為稠密時(|E|>>|V|)性能便不如Prim算法。後面我們會根據實際代碼對Kruskal算法的時間復雜度進行分析。
Krusksl算法采用並查集(算法導論上的表示為不相交集合)實現。初始時,我們的所有頂點都構成單獨的一棵樹,也就是說由|V|棵樹組成的森林。算法首先要做的是,將所有的邊,按權值大小進行非降序排列。然後,按照權值從小到大來選擇邊,若該邊的兩個端點不在一棵樹中,則將他們合並,並將該邊置於最小生成樹中。一直訪問完所有的邊為止。可以看出Kruskal算法屬於一種貪心算法,而且能保證找到全局最優解。算法理解相應簡單,我們不給出相應的證明,有興趣的可以去鑽研一下算法導論。
二、偽代碼
算法的偽代碼如下,其中A保存最小生成樹,MAKE-SET(v)表示構造一棵只有頂點v的樹,FIND-SET(u)表示找u所在樹的根節點,Union(u, v)表示合並u和v的所在樹:
MST-KRUSKAL(G, W)
A←∅
for each vertex v∈V[G]
do MAKE-SET(v)
sort the eages of E into nondecreasing order by weight w
for each eage(u, v)∈E, teken in nondecreasing order by weight
do if(FIND-SET(u) ≠ FIND-SET(v))
then A←A∪{ (u, v) }
Union(u, v)
return A
三、實現
下面給出C++的實現,之後再來分析時間復雜度:
1 #include <iostream>
2 #include <cstdio>
3 #include <algorithm>
4 #include <cstring>
5 using namespace std;
6
7 #define maxn 110 //最多點個數
8 int n, m; //點個數,邊數
9 int parent[maxn]; //父親節點,當值為-1時表示根節點
10 int ans; //存放最小生成樹權值
11 struct eage //邊的結構體,u、v為兩端點,w為邊權值
12 {
13 int u, v, w;
14 }EG[5010];
15
16 bool cmp(eage a, eage b) //排序調用
17 {
18 return a.w < b.w;
19 }
20
21 int Find(int x) //尋找根節點,判斷是否在同一棵樹中的依據
22 {
23 if(parent[x] == -1) return x;
24 return Find(parent[x]);
25 }
26
27 void Kruskal() //Kruskal算法,parent能夠還原一棵生成樹,或者森林
28 {
29 memset(parent, -1, sizeof(parent));
30 sort(EG+1, EG+m+1, cmp); //按權值將邊從小到大排序
31 ans = 0;
32 for(int i = 1; i <= m; i++) //按權值從小到大選擇邊
33 {
34 int t1 = Find(EG[i].u), t2 = Find(EG[i].v);
35 if(t1 != t2) //若不在同一棵樹種則選擇該邊,合並兩棵樹
36 {
37 ans += EG[i].w;
38 parent[t1] = t2;
39 }
40 }
41 }
42 int main()
43 {
44 while(~scanf("%d%d", &n,&m))
45 {
46 for(int i = 1; i <= m; i++)
47 scanf("%d%d%d", &EG[i].u, &EG[i].v, &EG[i].w);
48 Kruskal();
49 printf("%d\n", ans);
50 }
51 return 0;
52 }
四、時間復雜度
分析以上代碼中Kruskal算法的時間復雜度,n用V表示,m用E表示:
29行:將V個點父節點都置為-1,時間為O(V)
30行:stl中的sort函數,頭文件為#include <algorithm>,時間復雜度為O(E log E)
32-40行:由於FIND-SET是樹搜索操作,平均時間復雜度為O(lg V),因為這幾行時間復雜度平均為O(E log V)
綜上:總復雜度表示為:O(V) + O(E log E) + O(E log V);
當圖為稠密圖時,時間復雜度可表示為 O(E log E);
一般圖基本為稀疏圖|E| < |V|^2,時間復雜度可表示為 O(E log V)。
因而,從時間復雜度來看,當圖為稀疏圖時,Kruskal算法性能和Prim相當,當為稠密圖時,Prim性能更好。
五、小技巧
注意每合並兩棵樹,樹的棵樹就減少1,當根節點的個數只有一個即只有一棵樹時,說明生成了最小生成樹,此時程序便可終止,沒有必要去查看後面權值更大的邊,因而判斷根節點的數目,在圖較為稠密時,能夠提高一定的性能,代碼修改如下,圖為完全圖:
1 #include <iostream>
2 #include <cstdio>
3 #include <algorithm>
4 #include <cstring>
5 using namespace std;
6
7 #define maxn 110 //最多點個數
8 int n, m; //點個數,邊數
9 int parent[maxn]; //父親節點,當值為-1時表示根節點
10 int ans; //存放最小生成樹權值
11 struct eage //邊的結構體,u、v為兩端點,w為邊權值
12 {
13 int u, v, w;
14 }EG[5010];
15
16 bool cmp(eage a, eage b) //排序調用
17 {
18 return a.w < b.w;
19 }
20
21 int Find(int x) //尋找根節點,判斷是否在同一棵樹中的依據
22 {
23 if(parent[x] == -1) return x;
24 return Find(parent[x]);
25 }
26
27 void Kruskal() //Kruskal算法,parent能夠還原一棵生成樹,或者森林
28 {
29 memset(parent, -1, sizeof(parent));
30 int cnt = n; //初始時根節點數目為n個
31 sort(EG+1, EG+m+1, cmp); //按權值將邊從小到大排序
32 ans = 0;
33 for(int i = 1; i <= m; i++) //按權值從小到大選擇邊
34 {
35 if(cnt == 1) break; //當根節點只有1個時,跳出循環
36 int t1 = Find(EG[i].u), t2 = Find(EG[i].v);
37 if(t1 != t2) //若不在同一棵樹種則選擇該邊,
38 {
39 ans += EG[i].w;
40 parent[t1] = t2;
41 cnt--; //每次合並,減少一個根節點
42 }
43 }
44 }
45 int main()
46 {
47 while(scanf("%d", &n), n)
48 {
49 m = n*(n-1)/2; //完全圖
50 for(int i = 1; i <= m; i++)
51 scanf("%d%d%d", &EG[i].u, &EG[i].v, &EG[i].w);
52 Kruskal();
53 printf("%d\n", ans);
54 }
55 return 0;
56 }