一、算法介紹
Kruskal算法是一種用來查找最小生成樹的算法,由Joseph Kruskal在1956年發表。用來解決同樣問題的還有Prim算法和Boruvka算法等。三種算法都是貪心算法的應用。和Boruvka算法不同的地方是,Kruskal 算法在圖中存在相同權值的邊時也有效。最小生成樹是一副連通加權無向圖中一棵權值最小的生成樹(minimum spanning tree,簡稱MST)。生成樹的權重是賦予生成樹的每條邊的權重之和。最小生成樹具有 (V – 1) 個邊,其中 V 是給定圖中的頂點數。關於最小生成樹,它可以應用在網絡設計、NP難題之類的問題,還可以用於聚類分析,還可以間接應用於其他問題。
二、Kruskal算法查找MST的步驟
-
按權重的順序方式來對所有邊進行排序。
-
選擇權重最小的邊。檢查它是否與形成的生成樹形成一個循環。如果未形成循環,則包括該邊。否則,將其丟棄。
-
重復步驟2,直到生成樹中有(V-1)個邊。
這個算法是貪婪算法。“貪婪的選擇”是選擇迄今為止不會造成MST成環的最小的權重邊。下面來一個例子來理解:
該圖包含9個頂點(V)和14個邊(E)。因此,形成的最小生成樹將具有(9 – 1)= 8 個邊。
步驟1:每條邊按順序來排序
1 /** 2 * 排序后: 3 * 權重-src-dest 4 * 1 6 7 5 * 2 2 8 6 * 2 5 6 7 * 4 0 1 8 * 4 2 5 9 * 6 6 8 10 * 7 2 3 11 * 7 7 8 12 * 8 0 7 13 * 8 1 2 14 * 9 3 4 15 * 10 4 5 16 * 11 1 7 17 * 14 3 5 18 */
步驟2+步驟3::利用按權重排好序的邊數組,每次選取最小邊,並檢測是否成環。MST不能有環,所以這里涉及一個並查集的概念,並查集是對這個 Kruskal 算法進行優化的。
1)數組中一個接一個地選取所有邊。選取邊6-7:不形成循環,將其包括在內。
2)選取邊2-8:不形成循環,將其包括在內。
3)選取邊5-6:不形成循環,將其包括在內。
4)選取邊0-1:不形成循環,將其包括在內。
5)選取邊2-5:不形成循環,將其包括在內。
6)選取邊6-8:由於包括該邊會導致成環,因此將其丟棄。
7)選取邊2-3:不形成循環,將其包括在內。
8)選取邊7-8:由於包括該邊會導致循環,因此請將其丟棄。
9)選取邊0-7:不形成循環,將其包括在內。
10)選取邊1-2:由於包括該邊會導致循環,因此請將其丟棄。
11)選取邊3-4:不形成循環,將其包括在內。
由於包含的邊數等於(V – 1),因此算法結束。
三、算法代碼
並查集:
在計算機科學中,並查集是一種樹型的數據結構,用於處理一些不交集(Disjoint Sets)的合並及查詢問題。有一個聯合-查找算法(union-find algorithm)定義了兩個用於此數據結構的操作:
-
Find
:確定元素屬於哪一個子集。它可以被用來確定兩個元素是否屬於同一子集。 -
Union
:將兩個子集合並成同一個集合。
並查集樹是一種將每一個集合以樹表示的數據結構,其中每一個節點保存着到它的父節點的引用。
在並查集樹中,每個集合的代表即是集合的根節點。“查找”根據其父節點的引用向根行進直到到底樹根。“聯合”將兩棵樹合並到一起,這通過將一棵樹的根連接到另一棵樹的根。實現這樣操作的一種方法是:
查找元素 i 的集合,根據其父節點的引用向根行進直到到底樹根:
1 private int find(Subset[] subsets, int i) { 2 if (subsets[i].parent != i) 3 subsets[i].parent = find(subsets, subsets[i].parent); // 路徑壓縮,找到最久遠的祖先時“順便”把它的子孫直接連接到它上面 4 return subsets[i].parent; 5 }
將兩組不相交集合 x 和 y 進行並集,找到其中一個子集最父親的父親(也就是最久遠的祖先),將另外一個子集的最久遠的祖先的父親指向它:
1 public void union(Subset[] subsets, int x, int y) { 2 int xroot = find(subsets, x); 3 int yroot = find(subsets, y); 4 5 /* 在高秩樹的根下附加秩低樹(按秩划分合並) */ 6 if (subsets[xroot].rank < subsets[yroot].rank) { 7 subsets[xroot].parent = yroot; 8 } else if (subsets[xroot].rank > subsets[yroot].rank){ 9 subsets[yroot].parent = xroot; 10 } else { // 當兩棵秩同為r的樹聯合(作並集)時,它們的秩r+1 11 subsets[yroot].parent = xroot; 12 subsets[xroot].rank++; 13 } 14 }
同時使用路徑壓縮、按秩(rank)合並優化的程序每個操作的平均時間僅為 O(α (n)),其中α (n) 是 n=f(x)=A(x, x) 的反函數,A 是急速增加的阿克曼函數。因為 α(n) 是其反函數,故 α (n) 在 n 十分巨大時還是小於 5。因此,平均運行時間是一個極小的常數。實際上,這是漸近最優算法。
Kruskal算法
使用算法的思想來構造MST。
1 /** 2 * 使用Kruskal算法構造MST 3 */ 4 public void kruskalMST() { 5 Edge[] result = new Edge[V]; // 將存儲生成的MST 6 int e = 0; // 用於result[]的索引變量 7 int i = 0; // 用於排序的邊緣索引變量 8 for (i = 0; i < V; ++i) { 9 result[i] = new Edge(); 10 } 11 12 /* 步驟一:對點到點的邊的權重進行排序 */ 13 Arrays.sort(edges); 14 15 /* 創建V個子集*/ 16 Subset[] subsets = new Subset[V]; 17 for (i = 0; i < V; i++) { 18 subsets[i] = new Subset(); 19 } 20 21 /* 使用單個元素創建V子集 */ 22 for (int v = 0; v < V; v++) { 23 subsets[v].parent = v; 24 subsets[v].rank = 0; // 單元素的樹的秩定義為0 25 } 26 27 /* 用於挑選下一個邊的索引 */ 28 i = 0; 29 30 while (e < V-1) { 31 /* 步驟2:選取最小的邊緣, 並增加下一次迭代的索引 */ 32 Edge next_edge = edges[i++]; 33 34 int x = find(subsets, next_edge.src); 35 int y = find(subsets, next_edge.dest); 36 37 /* 如果包括此邊不引起mst成環(樹本無環),則將其包括在結果中並為下一個邊增加結果索引存下一條邊 */ 38 /* 這里判斷兩個元素是否屬於一個子集 */ 39 if (x != y) { 40 result[e++] = next_edge; 41 union(subsets, x, y); 42 } 43 /* 否則丟棄next_edge */ 44 } 45 46 /* 打印result[]的內容以顯示里面所構造的MST */ 47 System.out.println("Following are the edges in the constructed MST"); 48 for (i = 0; i < e; ++i) { 49 System.out.println(result[i].src + " -- " + result[i].dest + " == " + result[i].weight); 50 } 51 }
平均時間復雜度為O (|E|·log |V|),其中 E 和 V 分別是圖的邊集和點集。
本文源代碼:

1 package algorithm.mst; 2 3 import java.util.Arrays; 4 5 public class KruskalAlgorithm { 6 /* 頂點數和邊數 */ 7 private int V, E; 8 /* 所有邊的集合 */ 9 private Edge[] edges; 10 11 /** 12 * 創建一個V個頂點和E條邊的圖 13 * 14 * @param v 15 * @param e 16 */ 17 public KruskalAlgorithm(int v, int e) { 18 V = v; 19 E = e; 20 edges = new Edge[E]; 21 for (int i = 0; i < e; i++) { 22 edges[i] = new Edge(); 23 } 24 } 25 26 /** 27 * 查找元素i的集合(路徑壓縮) 28 * 根據其父節點的引用向根行進直到到底樹根 29 * 30 * @param subsets 31 * @param i 32 * @return 33 */ 34 private int find(Subset[] subsets, int i) { 35 if (subsets[i].parent != i) 36 subsets[i].parent = find(subsets, subsets[i].parent); // 路徑壓縮,找到最久遠的祖先時“順便”把它的子孫直接連接到它上面 37 return subsets[i].parent; 38 } 39 40 /** 41 * 將兩組不相交集合x和y進行並集(按秩合並) 42 * 這個方法找到其中一個子集最父親的父親(也就是最久遠的祖先),將另外一個子集的最久遠的祖先的父親指向它。 43 * <p> 44 * 並查集樹的最基礎的表示方法,這個方法不會比鏈表法好, 45 * 這是因為創建的樹可能會嚴重不平衡。 46 * 所以采用“按秩合並”來優化。 47 * </p> 48 * <p> 49 * 即總是將更小的樹連接至更大的樹上。因為影響運行時間的是樹的深度, 50 * 更小的樹添加到更深的樹的根上將不會增加秩除非它們的秩相同。 51 * 在這個算法中,術語“秩”替代了“深度”,因為同時應用了路徑壓縮時秩將不會與高度相同。 52 * </p> 53 * 54 * @param subsets 55 * @param x 56 * @param y 57 */ 58 public void union(Subset[] subsets, int x, int y) { 59 int xroot = find(subsets, x); 60 int yroot = find(subsets, y); 61 62 /* 在高秩樹的根下附加秩低樹(按秩划分合並) */ 63 if (subsets[xroot].rank < subsets[yroot].rank) { 64 subsets[xroot].parent = yroot; 65 } else if (subsets[xroot].rank > subsets[yroot].rank){ 66 subsets[yroot].parent = xroot; 67 } else { // 當兩棵秩同為r的樹聯合(作並集)時,它們的秩r+1 68 subsets[yroot].parent = xroot; 69 subsets[xroot].rank++; 70 } 71 } 72 73 /** 74 * 使用Kruskal算法構造MST 75 */ 76 public void kruskalMST() { 77 Edge[] result = new Edge[V]; // 將存儲生成的MST 78 int e = 0; // 用於result[]的索引變量 79 int i = 0; // 用於排序的邊緣索引變量 80 for (i = 0; i < V; ++i) { 81 result[i] = new Edge(); 82 } 83 84 /* 步驟一:對點到點的邊的權重進行排序 */ 85 Arrays.sort(edges); 86 87 /* 創建V個子集*/ 88 Subset[] subsets = new Subset[V]; 89 for (i = 0; i < V; i++) { 90 subsets[i] = new Subset(); 91 } 92 93 /* 使用單個元素創建V子集 */ 94 for (int v = 0; v < V; v++) { 95 subsets[v].parent = v; 96 subsets[v].rank = 0; // 單元素的樹的秩定義為0 97 } 98 99 /* 用於挑選下一個邊的索引 */ 100 i = 0; 101 102 while (e < V-1) { 103 /* 步驟2:選取最小的邊緣, 並增加下一次迭代的索引 */ 104 Edge next_edge = edges[i++]; 105 106 int x = find(subsets, next_edge.src); 107 int y = find(subsets, next_edge.dest); 108 109 /* 如果包括此邊不引起mst成環(樹本無環),則將其包括在結果中並為下一個邊增加結果索引存下一條邊 */ 110 /* 這里判斷兩個元素是否屬於一個子集 */ 111 if (x != y) { 112 result[e++] = next_edge; 113 union(subsets, x, y); 114 } 115 /* 否則丟棄next_edge */ 116 } 117 118 /* 打印result[]的內容以顯示里面所構造的MST */ 119 System.out.println("Following are the edges in the constructed MST"); 120 for (i = 0; i < e; ++i) { 121 System.out.println(result[i].src + " -- " + result[i].dest + " == " + result[i].weight); 122 } 123 } 124 125 public static void main(String[] args) { 126 /** 127 * 排序后: 128 * 權重-src-dest 129 * 1 6 7 130 * 2 2 8 131 * 2 5 6 132 * 4 0 1 133 * 4 2 5 134 * 6 6 8 135 * 7 2 3 136 * 7 7 8 137 * 8 0 7 138 * 8 1 2 139 * 9 3 4 140 * 10 4 5 141 * 11 1 7 142 * 14 3 5 143 */ 144 int V = 9; 145 int E = 14; 146 KruskalAlgorithm graph = new KruskalAlgorithm(V, E); 147 148 /* 另一個用例的圖: 149 1 --- 2 --- 3 150 / | | \ | \ 151 0 | 8 \ | 4 152 \ | / | \ | / 153 7 --- 6 --- 5 154 */ 155 156 // 添加邊 0-1 157 graph.edges[0].src = 0; 158 graph.edges[0].dest = 1; 159 graph.edges[0].weight = 4; 160 161 // 添加邊 0-7 162 graph.edges[1].src = 0; 163 graph.edges[1].dest = 7; 164 graph.edges[1].weight = 8; 165 166 // 添加邊 1-2 167 graph.edges[2].src = 1; 168 graph.edges[2].dest = 2; 169 graph.edges[2].weight = 8; 170 171 // 添加邊 1-7 172 graph.edges[3].src = 1; 173 graph.edges[3].dest = 7; 174 graph.edges[3].weight = 11; 175 176 // 添加邊 2-3 177 graph.edges[4].src = 2; 178 graph.edges[4].dest = 3; 179 graph.edges[4].weight = 7; 180 181 // 添加邊 2-5 182 graph.edges[5].src = 2; 183 graph.edges[5].dest = 5; 184 graph.edges[5].weight = 4; 185 186 // 添加邊 2-8 187 graph.edges[6].src = 2; 188 graph.edges[6].dest = 8; 189 graph.edges[6].weight = 2; 190 191 // 添加邊 3-4 192 graph.edges[7].src = 3; 193 graph.edges[7].dest = 4; 194 graph.edges[7].weight = 9; 195 196 // 添加邊 3-5 197 graph.edges[8].src = 3; 198 graph.edges[8].dest = 5; 199 graph.edges[8].weight = 14; 200 201 // 添加邊 4-5 202 graph.edges[9].src = 4; 203 graph.edges[9].dest = 5; 204 graph.edges[9].weight = 10; 205 206 // 添加邊 5-6 207 graph.edges[10].src = 5; 208 graph.edges[10].dest = 6; 209 graph.edges[10].weight = 2; 210 211 // 添加邊 6-7 212 graph.edges[11].src = 6; 213 graph.edges[11].dest = 7; 214 graph.edges[11].weight = 1; 215 216 // 添加邊 6-8 217 graph.edges[12].src = 6; 218 graph.edges[12].dest = 8; 219 graph.edges[12].weight = 6; 220 221 // 添加邊 7-8 222 graph.edges[13].src = 7; 223 graph.edges[13].dest = 8; 224 graph.edges[13].weight = 7; 225 226 graph.kruskalMST(); 227 228 /* 用例通過算法得出的MST如下: 229 1 2 -- 3 230 / | \ \ 231 0 8 \ 4 232 \ \ 233 7 -- 6 -- 5 234 */ 235 } 236 237 /** 238 * 每條邊的信息,實現了{@link Comparable}接口, 239 * 可以使用{@link Arrays}的方法隨其邊的權重的集合進行自然排序。 240 */ 241 class Edge implements Comparable<Edge> { 242 /* 這條邊的兩個頂點和它的權重 */ 243 private int src, dest, weight; 244 245 @Override 246 public int compareTo(Edge o) { 247 return this.weight - o.weight; 248 } 249 } 250 251 /** 252 * 聯合查找子集的類 253 */ 254 class Subset { 255 /* 其祖先和秩 */ 256 private int parent, rank; 257 } 258 }