一、算法介紹
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 }