重拾算法(5)——最小生成樹的兩種算法及其對比測試
什么是最小生成樹
求解最小生成樹(Minimum Cost Spanning Tree,以下簡寫做MST)是圖相關的算法中常見的一個,用於解決類似如下的問題:
假設要在N個城市之間建立通信聯絡網,那么連通N個城市只需N-1條線路。這時自然會考慮這樣一個問題:如何在最節省經費的前提下建立這個通信網。
在任意兩個城市間都可以設置一條線路,相應地都要付出一定的經濟代價。N個城市之間最多可能設置N(N-1)/2條線路,那么如何在這些線路中選擇N-1條,以使總的耗費最少呢?
可以用連通圖來表示N個城市,頂點表示城市,通信線路就是帶有權值的邊。一般而言,一個連通圖有多個生成樹,這些生成樹的總的權值不同,其中會有1或多個的總權值最小,這1或多個生成樹就是我們要求解的MST。(只需求出其中一個即可)
本篇介紹兩種求解MST的算法:Prim和Kruskal,然后測試之。
Prim算法
現在有一個連通圖G,要求MST,我們把G中的頂點划分到U、V兩個集合里,假設我們已經知道了MST在U范圍內的連接方式,(為什么做這種假設?只可意會不可言傳)尚未獲知MST在V范圍內的連接方式,也不知道MST在U、V之間的連接方式。
現在,我們有辦法求得U、V之間的連接方式了:從U、V中分別任取一個頂點(記作u、v),我們知道這有多種取法。U中的頂點有其MST-U,V也有MST-V,而邊u-v將MST-U和MST-V的各個頂點都聯系起來了,所以MST-U、u-v、MST-V三者就構成了一個生成樹ST,而當邊u-v是所有取法中最小的那個時,ST自然就是MST了。
整理一下,就是Prim算法的思路:初始時隨便取一個頂點vertex作為U,其它頂點作為V。這時,只含有1個頂點的U,其MST-U就是vertex本身,這是已知的。(已經符合假設了)而V范圍內的連接方式尚屬未知。根據剛剛的推理,我們已經可以求得連接MST-U和MST-V的那條邊u-v,順着u-v,就可以把V中的頂點v拉到U里邊。這樣重復地拉取頂點,直到V中沒有頂點時,U就覆蓋了整個G,我們就求得了整個G的MST!
1 public partial class AdjacencyListGraph<TVertex, TEdge> 2 { 3 AdjacencyListGraph<TVertex, TEdge> Prim() 4 { 5 if (this.Vertexes.Count == 0) { return null; } 6 7 var result = new AdjacencyListGraph<TVertex, TEdge>(); 8 var firstVertex = this.Vertexes[0].ShallowCopy();// get a copy of this.Vertex[0] without its edges' information. 9 result.Vertexes.Add(firstVertex);// initialize U collection. 10 var VCollection = new List<AdjacencyListVertex<TVertex, TEdge>>(from item in this.Vertexes where item != this.Vertexes[0] select item);// initialize V collection. 11 var this2ResultVertex = new Dictionary<AdjacencyListVertex<TVertex, TEdge>, AdjacencyListVertex<TVertex, TEdge>>(); 12 var result2ThisVertex = new Dictionary<AdjacencyListVertex<TVertex, TEdge>, AdjacencyListVertex<TVertex, TEdge>>(); 13 this2ResultVertex.Add(this.Vertexes[0], firstVertex); 14 result2ThisVertex.Add(firstVertex, this.Vertexes[0]); 15 while (VCollection.Count > 0) 16 { 17 AdjacencyListVertex<TVertex, TEdge> u = null; 18 AdjacencyListEdge<TVertex, TEdge> e = null; 19 AdjacencyListVertex<TVertex, TEdge> v = null; 20 foreach (var item in result.Vertexes) 21 { 22 var tmpU = result2ThisVertex[item];// get back to 'this' vertex of U 23 foreach (var tmpE in tmpU.Edges) 24 { 25 var tmpV = tmpE.GetTheOtherVertex(tmpU); 26 if (tmpV != null) 27 { 28 if (VCollection.Contains(tmpV))// if (tmpV is in V collection) 29 { 30 if ((e == null) || (tmpE.Weight < e.Weight)) 31 { 32 u = tmpU; e = tmpE; v = tmpV; 33 } 34 } 35 } 36 else 37 { 38 throw new Exception("Graph has invalid edge!"); 39 } 40 } 41 } 42 43 if (e != null) 44 { 45 var nextVertex = v.ShallowCopy(); 46 result2ThisVertex.Add(nextVertex, v); 47 this2ResultVertex.Add(v, nextVertex); 48 var vertex = this2ResultVertex[u]; 49 var edge = new AdjacencyListEdge<TVertex, TEdge>(vertex, nextVertex, e.Weight); 50 vertex.Edges.Add(edge); nextVertex.Edges.Add(edge); 51 result.Vertexes.Add(nextVertex);// add new vertex to U collection. 52 VCollection.Remove(v);// remove v from V collection. 53 } 54 else// the graph is not connected! 55 { 56 result = null; 57 break; 58 } 59 } 60 61 return result; 62 } 63 } 64 public partial class AdjacencyListEdge<TVertex, TEdge> 65 { 66 public AdjacencyListVertex<TVertex, TEdge> Vertex1 { get;set; } 67 public AdjacencyListVertex<TVertex, TEdge> Vertex2 { get;set; } 68 public virtual int Weight { get;set; } 69 70 public AdjacencyListVertex<TVertex, TEdge> GetTheOtherVertex(AdjacencyListVertex<TVertex, TEdge> vertex) 71 { 72 if (vertex == null) { return null; } 73 74 if (this.Vertex1 == vertex) { return this.Vertex2; } 75 else if (this.Vertex2 == vertex) { return this.Vertex1; } 76 else 77 { throw new ArgumentException("edge is not banded to vertex!"); } 78 } 79 }
Kruskal算法
關於Kruskal(以及任何算法),網上給的反證法、數學歸納法都不適合學習和鍛煉算法能力,那些都是在感性認知和靈感走通了迷途之后才適合進行的形而上的東西。
還是按這樣的思路來想吧:假設我們已經得到了有N個頂點的圖G1的MST-G1,其中G1至少有1個頂點。(又是這種神奇的假設)現在給G1增加一個新頂點v,並用若干條邊將v和G1中的某些頂點連接起來。這樣我們得到了一個新圖G2。與v相連的邊中,我們取權值最小的那個(記作v-w),那么MST-G1和v-w加起來就是G2的一個生成樹ST。
如果v-w不取最小的那個,那么得到的ST的權值就會增大。如果ST中的MST-G1部分有所改變,那么ST的權值也會增大(或不變)。也就是說,無論我們如何修改ST中的邊,ST的權值都不會減小(只會增大或保持不變)。所以ST其實就是G2的最小生成樹MST-G2。
由於對G1的約束條件只有"至少有1個頂點"這么一丟丟的限制,那么G2其實可以是任何頂點數V>1的圖。
也就是說對於任何頂點數V>1的圖G,都可以使用如下方法得到其MST:
-
設置數組M,用於保存構成MST的邊。
-
在所有未加入M的邊中,選取最小的一條邊E,如果E滿足"E與所有已加入M的邊都不構成回路",就把E加入M。
-
重復執行上一步,直到標記了V-1條邊為止。
注:對於頂點數V=1的圖,上述方法也適用。
上述三步就是Kruskal算法。它利用了上文的推理:如果得到了N個頂點的圖的MST,就能夠得到N+1個頂點的圖的MST。這就意味着,我們可以從只有1個頂點的圖開始,得到任意有限個頂點的圖的MST。它的做法是"每次都盡可能取權值小的邊"。這是算法理論中的貪心思想。
注:為了方便進行"選取最小的一條邊"這個重復性操作,首先需要對邊進行升序排序。
1 public partial class AdjacencyListGraph<TVertex, TEdge> 2 { 3 AdjacencyListGraph<TVertex, TEdge> Kruskal() 4 { 5 if (this.Vertexes.Count == 0) { return null; } 6 7 var edgeSorter = new EdgeSorter<TVertex, TEdge>(); 8 var report = this.Traverse(edgeSorter, GraphTraverseOrder.DepthFirst, true);// sort edges. 9 if (report.ConnectedComponents.Count > 1) { return null; } 10 11 var count = this.Vertexes.Count - 1; 12 var selectedEdges = new List<AdjacencyListEdge<TVertex, TEdge>>(); 13 var components = new List<List<AdjacencyListVertex<TVertex, TEdge>>>(); 14 foreach (var edge in edgeSorter.sortedEdges)// get selected edges for MST. 15 { 16 if (selectedEdges.Count >= count) { break; } 17 TryAddEdge(edge, selectedEdges, components); 18 } 19 20 var result = GetMST(selectedEdges); 21 return result; 22 } 23 24 AdjacencyListGraph<TVertex, TEdge> GetMST(List<AdjacencyListEdge<TVertex, TEdge>> selectedEdges) 25 { 26 var result = new AdjacencyListGraph<TVertex, TEdge>(); 27 var this2ResultDict = new Dictionary<AdjacencyListVertex<TVertex, TEdge>, AdjacencyListVertex<TVertex, TEdge>>(); 28 foreach (var vertex in this.Vertexes)// get vertexes for MST. 29 { 30 var newVertex = vertex.ShallowCopy(); 31 this2ResultDict.Add(vertex, newVertex); 32 result.Vertexes.Add(newVertex); 33 } 34 35 foreach (var selected in selectedEdges)// get edges for MST. 36 { 37 var v1 = this2ResultDict[selected.Vertex1]; 38 var v2 = this2ResultDict[selected.Vertex2]; 39 var newEdge = new AdjacencyListEdge<TVertex, TEdge>(v1, v2, selected.Weight); 40 v1.Edges.Add(newEdge); 41 v2.Edges.Add(newEdge); 42 } 43 return result; 44 } 45 46 bool TryAddEdge(AdjacencyListEdge<TVertex, TEdge> edge, List<AdjacencyListEdge<TVertex, TEdge>> selectedEdges, List<List<AdjacencyListVertex<TVertex, TEdge>>> components) 47 { 48 List<AdjacencyListVertex<TVertex, TEdge>> component1 = null; 49 List<AdjacencyListVertex<TVertex, TEdge>> component2 = null; 50 foreach (var component in components) 51 { 52 foreach (var v in component) 53 { 54 if (v == edge.Vertex1) 55 { component1 = component; } 56 if (v == edge.Vertex2) 57 { component2 = component; } 58 } 59 } 60 if (component1 != null) 61 { 62 if (component2 != null) 63 { 64 if (component1 == component2) 65 { return false; } 66 else 67 { 68 component1.AddRange(component2); 69 components.Remove(component2); 70 selectedEdges.Add(edge); 71 return true; 72 } 73 } 74 else 75 { 76 component1.Add(edge.Vertex2); 77 selectedEdges.Add(edge); 78 return true; 79 } 80 } 81 else 82 { 83 if (component2 != null) 84 { 85 component2.Add(edge.Vertex1); 86 selectedEdges.Add(edge); 87 return true; 88 } 89 else 90 { 91 var component = new List<AdjacencyListVertex<TVertex, TEdge>>(); 92 component.Add(edge.Vertex1); 93 component.Add(edge.Vertex2); 94 components.Add(component); 95 selectedEdges.Add(edge); 96 return true; 97 } 98 } 99 } 100 }
對邊排序我使用了折半插入排序。
1 public class EdgeSorter<TVertex, TEdge> : GraphNodeWorker<TVertex, TEdge> 2 { 3 public IList<AdjacencyListEdge<TVertex, TEdge>> sortedEdges = new List<AdjacencyListEdge<TVertex, TEdge>>(); 4 int count = 0; 5 public override void DoActionOnNode(AdjacencyListVertex<TVertex, TEdge> vertex) 6 { 7 if (vertex == null) { return; } 8 foreach (var edge in vertex.Edges) 9 { 10 if (!sortedEdges.Contains(edge)) 11 { 12 InsertElement(edge); 13 } 14 } 15 } 16 // optimized verstion of InsertElement() method. 17 void InsertElement(AdjacencyListEdge<TVertex, TEdge> element) 18 { 19 var min = 0; 20 var max = count - 1; 21 while (min <= max) 22 { 23 var middle = (min + max) / 2; 24 if (element.Weight - this.sortedEdges[middle].Weight < 0) 25 { max = middle - 1; } 26 else 27 { min = middle + 1; } 28 } 29 30 this.sortedEdges.Insert(min, element); 31 count++; 32 } 33 34 /* this is the original version which is not optimized. 35 void InsertElement(AdjacencyListEdge<TVertex, TEdge> element) 36 { 37 if (count == 0) { this.sortedEdges.Add(element); return; } 38 var min = 0; 39 var max = count - 1; 40 var middle = 0; 41 var compare = 0; 42 while (min <= max) 43 { 44 middle = (min + max) / 2; 45 compare = element.Weight - sortedEdges[middle].Weight; 46 if (compare < 0) 47 { max = middle - 1; } 48 else 49 { min = middle + 1; } 50 } 51 if (compare < 0) 52 { 53 this.sortedEdges.Insert(middle, element);//(min) equals (middle) equals (max + 1) 54 } 55 else 56 { 57 this.sortedEdges.Insert(middle + 1, element);//(min) equals (middle + 1) equals (max + 1) 58 } 59 count++; 60 } 61 */ 62 }
對比測試
實現了Prim和Kruskal算法后,就該測試一下他們的正確性了。思考了2天后,我認為如下方案既簡單可行又有足夠的說服力:仍舊用上一篇的方法自動生成頂點數為1、2、3、4、5、6的所有圖的形態,並對其邊隨機賦予權值;然后對得到的每一個圖G,分別用Prim和Kruskal算法計算MST;然后對比兩種算法得到的MST的總權值是否相同;如果不同,就說明至少有一個算法的代碼有問題;如果對所有情形的圖的計算,其Prim和Kruskal得到的MST權值都分別相同,那就很強地說明代碼沒問題。由於各個邊的權值是隨機給的,可以通過多次重復測試的方式,進一步加強說明代碼的正確性。
這種逼近式的對比測試,其實現很容易且說服力很強;同時,真正的完全測試我實在精力不足,這決定了我采用如上所述的測試策略。
測試思路已經給出了,代碼也簡單地很,下面只給出一個測試用例作為示例。

這個測試用例說明了如下內容:
這是第1099個測試用例。
這個圖只有1個連通分量(即這個圖是一個連通圖)
這個圖有5個頂點(其ID分別為0、1、2、3、4)
這個圖的各個邊用折線表示如上圖所示(右側的數值為該邊的權值)
排序后的邊情況如下(格式為"頂點ID-權值-頂點ID")
MST總權值為8。
(由於Prim和Kruskal計算結果相同,沒有其他信息)
最后一行統計了出錯的測試用例數量:0。
需要本文源代碼的話麻煩點個贊並留下你的Email~
