一,介紹
本文介紹使用Kruskal算法求解無向圖的最小生成樹。Kruskal是一個貪心算法,並且使用了並查集這種數據結構。
關於並查集的介紹,參考:數據結構--並查集的原理及實現
二,構造一個無向圖
圖,肯定有頂點和邊。由於求解最小生成樹,故邊還需要有權值。此外,對於每一條邊,需要找到與它相關聯的兩個頂點,因為在將這條邊加入到最小生成樹時需要判斷這兩個頂點是否已經連通了。頂點類定義如下:
1 private class Vertex { 2 private String vertexLabel; 3 private List<Edge> adjEdges;// 鄰接表 4 5 public Vertex(String vertexLabel) { 6 this.vertexLabel = vertexLabel; 7 adjEdges = new LinkedList<Edge>(); 8 } 9 }
表明,圖是采用鄰接表的形式存儲的。
邊類的定義如下:
1 private class Edge implements Comparable<Edge> { 2 private Vertex startVertex; 3 private Vertex endVertex; 4 private int weight;// 邊的權值 5 6 public Edge(Vertex start, Vertex end, int weight) { 7 this.startVertex = start; 8 this.endVertex = end; 9 this.weight = weight; 10 } 11 12 @Override 13 public int compareTo(Edge e) { 14 15 if (weight > e.weight) 16 return 1; 17 else if (weight < e.weight) 18 return -1; 19 else 20 return 0; 21 } 22 }
邊實現了Comparable接口。因為,Kruskal算法使用優先級隊列來存儲邊,邊根據權值來進行比較。
假設圖存儲在一個文件中,每一行包含如下的信息:LinkID,SourceID,DestinationID,Cost(邊的編號,起始頂點的標識,終點的標識,邊上的權值)
文件格式如下:
無向圖如下:
private Map<String, Vertex> nonDirectedGraph;
另外,用一個Map來存儲圖的頂點,圖采用鄰接表形式表示。Map的Key為頂點的標識,Value為頂點類。
三,求解最小生成樹的Kruskal算法分析
Kruskal算法是一個貪心算法,它與Dijkstra算法非常的相似。Kruskal算法貪心的地方在於:它總是選取圖中當前權值最小的邊的加入到樹中(該邊加入到樹中之后不能出現環)。因此,這里就有個問題,如何選取當前權值最小的邊?
Kruskal算法用到了並查集。因為,算法初始時將圖中的各個頂點視為獨立的,不連通的,隨着一步步將當前權值最小的邊加入,就將各個頂點連接起來了(使用並查集的Union操作實現連接)
關於選取最小權值的邊,最常用的就是使用優先級隊列了,而優先級隊列則可以使用二叉堆來實現。關於二叉堆參考:數據結構--堆的實現(下)
算法的總體步驟:
①構造一個無向圖啊。求解該圖的最小生成樹。----需要測試代碼是否正確,得有一個實際的圖。
②根據無向圖中的頂點來 初始化 並查集----初始化過程和 並查集的應用之求解無向圖中的連接分量個數 里面講到的圖的初始化過程一樣。
③創建一個優先級隊列來存儲圖中的邊,這樣每次選取邊時,直接出隊列,這比查找圖中所有的邊然后選擇最小權值的邊效率要高一點啊。
④判斷這條邊關聯的兩個頂點是否已經連通,如果已經連通了,再將該邊加入到生成樹中會導致環。
⑤直到所有的頂點都已經並入到生成樹時,算法結束。
看完這個過程,感覺這個算法和並查集的應用之求解無向圖中的連接分量個數--求解連通分量的算法沒啥大區別。
只不過Kruskal算法額外多用了一個優先級隊列而已。
四,代碼實現
Kruskal算法用到並查集,那肯定需要實現並查集的基本操作,關於並查集,參考:數據結構--並查集的原理及實現
關於並查集基本操作的實現與並查集的應用之求解無向圖中的連接分量個數 基本一樣。
關於存儲並查集的一維數組的說明如下:
使用一個一維數組來存儲並查集,這里一維數組的下標表示圖的頂點標識,數組元素s[i]有兩種表示含義:當數組元素大於0時,表示的是 頂點 i 的父結點位置 ;當數組元素s[i]小於0時,表示的是 頂點 i 為根的子樹的高度(秩!)。從而將數組的下標與圖的頂點一 一 對應起來。
下面重點來看下最小生成樹算法的實現:
1 /** 2 * 3 * @param graph 4 * 求解graph的一棵最小生成樹 5 * @return 組成最小生成樹上的所有的邊 6 */ 7 public List<Edge> kruskal(Map<String, Vertex> graph) { 8 List<Edge> miniEdges = new ArrayList<Edge>(graph.size() - 1); 9 10 while (miniEdges.size() != graph.size() - 1) { 11 Edge e = pq.remove(); 12 int start = Integer.valueOf(e.startVertex.vertexLabel); 13 int end = Integer.valueOf(e.endVertex.vertexLabel); 14 if (find(start) != find(end)) { 15 union(start, end); 16 miniEdges.add(e); 17 } 18 } 19 return miniEdges; 20 }
第8行構造一個ArrayList存儲最小生成樹中的邊。其中,最小生成樹中的邊的數目為頂點的數目減1。graph.size()返回頂點的個數。
在第10行的while循環中構建最小生成樹,第12行和第13行獲得頂點的標識。頂點的標識與數組下標對應,比如 頂點4 ’存儲‘ 在 s數組中的下標4中。
第14行判斷兩個頂點是否已經連通,若不連通,則將這條邊加入到最小生成樹中。初始時,所有的頂點都在各自的子樹中,互不連通(見make_set方法)
關於算法效率的一點分析:
上面的方法中,不斷地從優先級隊列中彈出權值最小的邊,在大部分的情況下,是不需要將優先級隊列中的所有的邊都彈出完的。
但是,存在這樣一種情況:圖中權值最大的那條邊是唯一到某個頂點的路徑,則需要把優先級隊列中的所有的邊都彈出后,才能構造一棵最小生成樹。
就拿上面的那個圖來說:頂點4到 頂點6 這條邊的權值為6,是所有邊的最大的,而且到達頂點4只能經過 權值為6 的這條邊。而我們所有的邊都存儲在優先級隊列中,故權值為6這條邊一定是最后才彈出的,在權值為6的這條邊出隊列時,圖中所有的邊都已經出隊列了(優先級隊列嘛,權值越小,越先出隊列)。
假設把頂點4到 頂點6 這條邊的權值 6改成 1,在 頂點2 到 頂點5的權值為5的邊、頂點3到 頂點6 權值為4 的邊 出隊列之前,最小生成樹就已經構造好了。
因為,優先級隊列會優先選擇頂點3到頂點2的這條邊,以及 頂點5 到 頂點6的邊。
五,整個完整代碼如下:
1 import java.util.ArrayList; 2 import java.util.LinkedHashMap; 3 import java.util.LinkedList; 4 import java.util.List; 5 import java.util.Map; 6 import java.util.PriorityQueue; 7 8 import c9.topo.FileUtil; 9 10 public class MinSpanningTree { 11 private class Vertex { 12 private String vertexLabel; 13 private List<Edge> adjEdges;// 鄰接表 14 15 public Vertex(String vertexLabel) { 16 this.vertexLabel = vertexLabel; 17 adjEdges = new LinkedList<Edge>(); 18 } 19 } 20 21 private class Edge implements Comparable<Edge> { 22 private Vertex startVertex; 23 private Vertex endVertex; 24 private int weight;// 邊的權值 25 26 public Edge(Vertex start, Vertex end, int weight) { 27 this.startVertex = start; 28 this.endVertex = end; 29 this.weight = weight; 30 } 31 32 @Override 33 public int compareTo(Edge e) { 34 35 if (weight > e.weight) 36 return 1; 37 else if (weight < e.weight) 38 return -1; 39 else 40 return 0; 41 } 42 } 43 44 private Map<String, Vertex> nonDirectedGraph; 45 PriorityQueue<Edge> pq = new PriorityQueue<MinSpanningTree.Edge>();// 優先級隊列存儲邊 46 47 private void buildGraph(String graphContent) { 48 String[] lines = graphContent.split("\n"); 49 50 String startNodeLabel, endNodeLabel; 51 Vertex startNode, endNode; 52 for (int i = 0; i < lines.length; i++) { 53 String[] nodesInfo = lines[i].split(","); 54 startNodeLabel = nodesInfo[1]; 55 endNodeLabel = nodesInfo[2]; 56 57 endNode = nonDirectedGraph.get(endNodeLabel); 58 if (endNode == null) { 59 endNode = new Vertex(endNodeLabel); 60 nonDirectedGraph.put(endNodeLabel, endNode); 61 } 62 63 startNode = nonDirectedGraph.get(startNodeLabel); 64 if (startNode == null) { 65 startNode = new Vertex(startNodeLabel); 66 nonDirectedGraph.put(startNodeLabel, startNode); 67 } 68 Edge e = new Edge(startNode, endNode, Integer.valueOf(nodesInfo[3])); 69 // 對於無向圖而言,起點和終點都要添加邊 70 endNode.adjEdges.add(e); 71 startNode.adjEdges.add(e); 72 73 pq.add(e);// 將邊加入到優先級隊列中 74 } 75 } 76 77 private int[] s;// 存儲並查集的一維數組 78 79 public MinSpanningTree(String graphContent) { 80 nonDirectedGraph = new LinkedHashMap<String, MinSpanningTree.Vertex>(); 81 buildGraph(graphContent); 82 83 make_set(nonDirectedGraph);// 初始化並查集 84 } 85 86 private void make_set(Map<String, Vertex> graph) { 87 int size = graph.size(); 88 s = new int[size]; 89 for (Vertex v : graph.values()) { 90 s[Integer.valueOf(v.vertexLabel)] = -1;// 頂點的標識是從0開始連續的數字 91 } 92 } 93 94 private int find(int root) { 95 if (s[root] < 0) 96 return root; 97 else 98 return s[root] = find(s[root]); 99 } 100 101 private void union(int root1, int root2) { 102 if (find(root1) == find(root2)) 103 return; 104 // union中的參數是合並任意兩個頂點,但是對於並查集,合並的對象是該頂點所在集合的代表頂點(根頂點) 105 root1 = find(root1);// 查找頂點root1所在的子樹的根 106 root2 = find(root2);// 查找頂點root2所在的子樹的根 107 108 if (s[root2] < s[root1])// root2 is deeper 109 s[root1] = root2; 110 else { 111 if (s[root1] == s[root2])// 一樣高 112 s[root1]--;// 合並得到的新的子樹高度增1 (以root1作為新的子樹的根) 113 s[root2] = root1;// root1 is deeper 114 } 115 } 116 117 /** 118 * 119 * @param graph 120 * 求解graph的一棵最小生成樹 121 * @return 組成最小生成樹上的所有的邊 122 */ 123 public List<Edge> kruskal(Map<String, Vertex> graph) { 124 List<Edge> miniEdges = new ArrayList<Edge>(graph.size() - 1); 125 126 while (miniEdges.size() != graph.size() - 1) { 127 Edge e = pq.remove(); 128 //生成並查集操作的對應的標點位置 129 int start = Integer.valueOf(e.startVertex.vertexLabel); 130 int end = Integer.valueOf(e.endVertex.vertexLabel); 131 if (find(start) != find(end)) { 132 union(start, end); 133 miniEdges.add(e); 134 } 135 } 136 return miniEdges; 137 } 138 139 // for test purpose 140 public static void main(String[] args) { 141 String graphFilePath; 142 if (args.length == 0) 143 graphFilePath = "F:\\graph.txt"; 144 else 145 graphFilePath = args[0]; 146 147 String graphContent = FileUtil.read(graphFilePath, null);// 從文件中讀取圖的數據 148 MinSpanningTree mst = new MinSpanningTree(graphContent); 149 150 // 獲得圖中組成最小生成樹的所有邊 151 List<Edge> edges = mst.kruskal(mst.nonDirectedGraph); 152 for (Edge edge : edges) { 153 System.out.print(edge.startVertex.vertexLabel + "-->" 154 + edge.endVertex.vertexLabel); 155 System.out.println(" weight: " + edge.weight); 156 } 157 } 158 }
FileUtil類可參考中的完整代碼實現。
六,實驗結果
求得的最小生成樹的信息如下:
0-->3 表示頂點0到頂點3的邊,邊的權值為1
5-->6 表示頂點5到頂點6的邊,邊的權值為1
.......