查找最小生成樹:克魯斯克爾算法(Kruskal)算法

一、算法介紹

  Kruskal算法是一種用來查找最小生成樹的算法,由Joseph Kruskal在1956年發表。用來解決同樣問題的還有Prim算法Boruvka算法等。三種算法都是貪心算法的應用。和Boruvka算法不同的地方是,Kruskal 算法在圖中存在相同權值的邊時也有效。最小生成樹是一副連通加權無向圖中一棵權值最小生成樹(minimum spanning tree,簡稱MST生成樹的權重是賦予生成樹的每條邊的權重之和。最小生成樹具有 (V – 1) 個邊,其中 V 是給定圖中的頂點數。關於最小生成樹,它可以應用在網絡設計、NP難題之類的問題,還可以用於聚類分析,還可以間接應用於其他問題。

二、Kruskal算法查找MST的步驟

  1.  按權重的順序方式來對所有邊進行排序

  2.  選擇權重最小的邊。檢查它是否與形成的生成樹形成一個循環。如果未形成循環,則包括該邊。否則,將其丟棄

  3.  重複步驟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 }
View Code
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章