最小生成樹(Minimum Cost Spanning Tree)
首先,最小生成樹是一副連通加權無向圖中一棵權值最小的生成樹。
主要可以使用Prim和Kruskal算法實現,對於稀疏圖來說,用Kruskal寫最小生成樹效率更好,加上並查集,可對其進行優化。
Kruskal算法(並查集實現)
在使用Kruskal實現最小生成樹之前,先來看下並查集需要注意的兩點:
1. 針對樹可能會退化為鏈表的解決方案是,每次合並樹時,總是將矮的樹掛到高的樹下,這種方式稱為按秩合並。
2. 為了得到的樹將更加扁平,加速以后直接或者間接引用節點的速度,Find時改變每一個節點的引用到根節點,這叫路徑壓縮。
並查集的初始化:
並查集的路徑壓縮:
並查集的按秩合並
Kruskal算法的步驟包括:
1. 對所有權值進行從小到大排序(這里對邊排序時還需要記錄邊的索引,這樣以邊的權值排完序后只改變了權值的索引位置)
2. 然后每次選取最小的權值,如果和已有點集構成環則跳過,否則加到該點集中。最終有所有的點集構成的樹就是最佳的。
代碼實現如下所示:

1 #include <iostream> 2 #include <vector> 3 #include <algorithm> 4 using namespace std; 5 6 //並查集實現最小生成樹 7 vector<int> u, v, weights, w_r, father; 8 int mycmp(int i, int j) 9 { 10 return weights[i] < weights[j]; 11 } 12 int find(int x) 13 { 14 return father[x] == x ? x : father[x] = find(father[x]); 15 } 16 void kruskal_test() 17 { 18 int n; 19 cin >> n; 20 vector<vector<int> > A(n, vector<int>(n)); 21 for(int i = 0; i < n; ++i) { 22 for (int j = 0; j < n; ++j) { 23 cin >> A[i][j]; 24 } 25 } 26 27 int edges = 0; 28 // 共計n*(n - 1)/2條邊 29 for (int i = 0; i < n - 1; ++i) { 30 for (int j = i + 1; j < n; ++j) { 31 u.push_back(i); 32 v.push_back(j); 33 weights.push_back(A[i][j]); 34 w_r.push_back(edges++); 35 } 36 } 37 for (int i = 0; i < n; ++i) { 38 father.push_back(i); // 記錄n個節點的根節點,初始化為各自本身 39 } 40 41 sort(w_r.begin(), w_r.end(), mycmp); //以weight的大小來對索引值進行排序 42 43 int min_tree = 0, cnt = 0; 44 for (int i = 0; i < edges; ++i) { 45 int e = w_r[i]; //e代表排序后的權值的索引 46 int x = find(u[e]), y = find(v[e]); 47 //x不等於y表示u[e]和v[e]兩個節點沒有公共根節點,可以合並 48 if (x != y) { 49 min_tree += weights[e]; 50 father[x] = y; 51 ++cnt; 52 } 53 } 54 if (cnt < n - 1) min_tree = 0; 55 cout << min_tree << endl; 56 } 57 58 int main(void) 59 { 60 61 kruskal_test(); 62 63 return 0; 64 }
這里只用到了路徑壓縮,在合並的時候直接將后一個節點的根節點指到前一個節點的。
Prim算法(使用visited數組實現)
Prim算法求最小生成樹的時候和邊數無關,和頂點樹有關,所以適合求解稠密網的最小生成樹。
Prim算法的步驟包括:
1. 將一個圖分為兩部分,一部分歸為點集U,一部分歸為點集V,U的初始集合為{V1},V的初始集合為{ALL-V1}。
2. 針對U開始找U中各節點的所有關聯的邊的權值最小的那個,然后將關聯的節點Vi加入到U中,並且從V中刪除(注意不能形成環)。
3. 遞歸執行步驟2,直到V中的集合為空。
4. U中所有節點構成的樹就是最小生成樹。
代碼實現如下所示:

1 #include <iostream> 2 #include <vector> 3 using namespace std; 4 5 //Prim算法實現 6 void prim_test() 7 { 8 int n; 9 cin >> n; 10 vector<vector<int> > A(n, vector<int>(n)); 11 for(int i = 0; i < n ; ++i) { 12 for(int j = 0; j < n; ++j) { 13 cin >> A[i][j]; 14 } 15 } 16 17 int pos, minimum; 18 int min_tree = 0; 19 //lowcost數組記錄每2個點間最小權值,visited數組標記某點是否已訪問 20 vector<int> visited, lowcost; 21 for (int i = 0; i < n; ++i) { 22 visited.push_back(0); //初始化為0,表示都沒加入 23 } 24 visited[0] = 1; //最小生成樹從第一個頂點開始 25 for (int i = 0; i < n; ++i) { 26 lowcost.push_back(A[0][i]); //權值初始化為0 27 } 28 29 for (int i = 0; i < n; ++i) { //枚舉n個頂點 30 minimum = max_int; 31 for (int j = 0; j < n; ++j) { //找到最小權邊對應頂點 32 if(!visited[j] && minimum > lowcost[j]) { 33 minimum = lowcost[j]; 34 pos = j; 35 } 36 } 37 if (minimum == max_int) //如果min = max_int表示已經不再有點可以加入最小生成樹中 38 break; 39 min_tree += minimum; 40 visited[pos] = 1; //加入最小生成樹中 41 for (int j = 0; j < n; ++j) { 42 if(!visited[j] && lowcost[j] > A[pos][j]) lowcost[j] = A[pos][j]; //更新可更新邊的權值 43 } 44 } 45 46 cout << min_tree << endl; 47 } 48 49 int main(void) 50 { 51 prim_test(); 52 53 return 0; 54 }
注意:Prim算法實質就是每在最小生成樹集合中加入一個點就需要把這個點與集合外的點比較,不斷的尋找兩個集合之間最小的邊。
Kruskal VS Prim
方法上:Kruskal在所有邊中不斷尋找最小的邊,Prim在U和V兩個集合之間尋找權值最小的連接,共同點是構造過程都不能形成環。
時間上:Prim適合稠密圖,復雜度為O(n * n),因此通常使用鄰接矩陣儲存,復雜度為O(e * loge),而Kruskal多用鄰接表,稠密圖 Prim > Kruskal,稀疏圖 Kruskal > Prim。
空間上: Prim適合點少邊多,Kruskal適合邊多點少。