并查集与贪心算法的应用之求解无向图的最小生成树
一,介紹
本文介紹使用Kruskal算法求解無向圖的最小生成樹。Kruskal是一個(gè)貪心算法,并且使用了并查集這種數(shù)據(jù)結(jié)構(gòu)。
關(guān)于并查集的介紹,參考:數(shù)據(jù)結(jié)構(gòu)--并查集的原理及實(shí)現(xiàn)
?
二,構(gòu)造一個(gè)無向圖
圖,肯定有頂點(diǎn)和邊。由于求解最小生成樹,故邊還需要有權(quán)值。此外,對(duì)于每一條邊,需要找到與它相關(guān)聯(lián)的兩個(gè)頂點(diǎn),因?yàn)樵趯⑦@條邊加入到最小生成樹時(shí)需要判斷這兩個(gè)頂點(diǎn)是否已經(jīng)連通了。頂點(diǎn)類定義如下:
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 }表明,圖是采用鄰接表的形式存儲(chǔ)的。
邊類的定義如下:
1 private class Edge implements Comparable<Edge> { 2 private Vertex startVertex; 3 private Vertex endVertex; 4 private int weight;// 邊的權(quán)值 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 }邊實(shí)現(xiàn)了Comparable接口。因?yàn)?#xff0c;Kruskal算法使用優(yōu)先級(jí)隊(duì)列來存儲(chǔ)邊,邊根據(jù)權(quán)值來進(jìn)行比較。
假設(shè)圖存儲(chǔ)在一個(gè)文件中,每一行包含如下的信息:LinkID,SourceID,DestinationID,Cost(邊的編號(hào),起始頂點(diǎn)的標(biāo)識(shí),終點(diǎn)的標(biāo)識(shí),邊上的權(quán)值)
?文件格式如下:
無向圖如下:
?
private Map<String, Vertex> nonDirectedGraph;另外,用一個(gè)Map來存儲(chǔ)圖的頂點(diǎn),圖采用鄰接表形式表示。Map的Key為頂點(diǎn)的標(biāo)識(shí),Value為頂點(diǎn)類。
?
三,求解最小生成樹的Kruskal算法分析
Kruskal算法是一個(gè)貪心算法,它與Dijkstra算法非常的相似。Kruskal算法貪心的地方在于:它總是選取圖中當(dāng)前權(quán)值最小的邊的加入到樹中(該邊加入到樹中之后不能出現(xiàn)環(huán))。因此,這里就有個(gè)問題,如何選取當(dāng)前權(quán)值最小的邊?
Kruskal算法用到了并查集。因?yàn)?#xff0c;算法初始時(shí)將圖中的各個(gè)頂點(diǎn)視為獨(dú)立的,不連通的,隨著一步步將當(dāng)前權(quán)值最小的邊加入,就將各個(gè)頂點(diǎn)連接起來了(使用并查集的Union操作實(shí)現(xiàn)連接)
關(guān)于選取最小權(quán)值的邊,最常用的就是使用優(yōu)先級(jí)隊(duì)列了,而優(yōu)先級(jí)隊(duì)列則可以使用二叉堆來實(shí)現(xiàn)。關(guān)于二叉堆參考:數(shù)據(jù)結(jié)構(gòu)--堆的實(shí)現(xiàn)(下)
算法的總體步驟:
①構(gòu)造一個(gè)無向圖啊。求解該圖的最小生成樹。----需要測(cè)試代碼是否正確,得有一個(gè)實(shí)際的圖。
②根據(jù)無向圖中的頂點(diǎn)來 初始化 并查集----初始化過程和?并查集的應(yīng)用之求解無向圖中的連接分量個(gè)數(shù)?里面講到的圖的初始化過程一樣。
③創(chuàng)建一個(gè)優(yōu)先級(jí)隊(duì)列來存儲(chǔ)圖中的邊,這樣每次選取邊時(shí),直接出隊(duì)列,這比查找圖中所有的邊然后選擇最小權(quán)值的邊效率要高一點(diǎn)啊。
④判斷這條邊關(guān)聯(lián)的兩個(gè)頂點(diǎn)是否已經(jīng)連通,如果已經(jīng)連通了,再將該邊加入到生成樹中會(huì)導(dǎo)致環(huán)。
⑤直到所有的頂點(diǎn)都已經(jīng)并入到生成樹時(shí),算法結(jié)束。
看完這個(gè)過程,感覺這個(gè)算法和并查集的應(yīng)用之求解無向圖中的連接分量個(gè)數(shù)--求解連通分量的算法沒啥大區(qū)別。
只不過Kruskal算法額外多用了一個(gè)優(yōu)先級(jí)隊(duì)列而已。
?
四,代碼實(shí)現(xiàn)
?Kruskal算法用到并查集,那肯定需要實(shí)現(xiàn)并查集的基本操作,關(guān)于并查集,參考:數(shù)據(jù)結(jié)構(gòu)--并查集的原理及實(shí)現(xiàn)
關(guān)于并查集基本操作的實(shí)現(xiàn)與并查集的應(yīng)用之求解無向圖中的連接分量個(gè)數(shù)?基本一樣。
關(guān)于存儲(chǔ)并查集的一維數(shù)組的說明如下:
使用一個(gè)一維數(shù)組來存儲(chǔ)并查集,這里一維數(shù)組的下標(biāo)表示圖的頂點(diǎn)標(biāo)識(shí),數(shù)組元素s[i]有兩種表示含義:當(dāng)數(shù)組元素大于0時(shí),表示的是 頂點(diǎn) i 的父結(jié)點(diǎn)位置 ;當(dāng)數(shù)組元素s[i]小于0時(shí),表示的是 頂點(diǎn) i 為根的子樹的高度(秩!)。從而將數(shù)組的下標(biāo)與圖的頂點(diǎn)一 一 對(duì)應(yīng)起來。
下面重點(diǎn)來看下最小生成樹算法的實(shí)現(xiàn):
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行構(gòu)造一個(gè)ArrayList存儲(chǔ)最小生成樹中的邊。其中,最小生成樹中的邊的數(shù)目為頂點(diǎn)的數(shù)目減1。graph.size()返回頂點(diǎn)的個(gè)數(shù)。
在第10行的while循環(huán)中構(gòu)建最小生成樹,第12行和第13行獲得頂點(diǎn)的標(biāo)識(shí)。頂點(diǎn)的標(biāo)識(shí)與數(shù)組下標(biāo)對(duì)應(yīng),比如 頂點(diǎn)4 ’存儲(chǔ)‘ 在 s數(shù)組中的下標(biāo)4中。
第14行判斷兩個(gè)頂點(diǎn)是否已經(jīng)連通,若不連通,則將這條邊加入到最小生成樹中。初始時(shí),所有的頂點(diǎn)都在各自的子樹中,互不連通(見make_set方法)
關(guān)于算法效率的一點(diǎn)分析:
上面的方法中,不斷地從優(yōu)先級(jí)隊(duì)列中彈出權(quán)值最小的邊,在大部分的情況下,是不需要將優(yōu)先級(jí)隊(duì)列中的所有的邊都彈出完的。
但是,存在這樣一種情況:圖中權(quán)值最大的那條邊是唯一到某個(gè)頂點(diǎn)的路徑,則需要把優(yōu)先級(jí)隊(duì)列中的所有的邊都彈出后,才能構(gòu)造一棵最小生成樹。
就拿上面的那個(gè)圖來說:頂點(diǎn)4到 頂點(diǎn)6 這條邊的權(quán)值為6,是所有邊的最大的,而且到達(dá)頂點(diǎn)4只能經(jīng)過 權(quán)值為6 的這條邊。而我們所有的邊都存儲(chǔ)在優(yōu)先級(jí)隊(duì)列中,故權(quán)值為6這條邊一定是最后才彈出的,在權(quán)值為6的這條邊出隊(duì)列時(shí),圖中所有的邊都已經(jīng)出隊(duì)列了(優(yōu)先級(jí)隊(duì)列嘛,權(quán)值越小,越先出隊(duì)列)。
假設(shè)把頂點(diǎn)4到 頂點(diǎn)6 這條邊的權(quán)值 6改成 1,在 頂點(diǎn)2 到 頂點(diǎn)5的權(quán)值為5的邊、頂點(diǎn)3到 頂點(diǎn)6 權(quán)值為4 的邊 出隊(duì)列之前,最小生成樹就已經(jīng)構(gòu)造好了。
因?yàn)?#xff0c;優(yōu)先級(jí)隊(duì)列會(huì)優(yōu)先選擇頂點(diǎn)3到頂點(diǎn)2的這條邊,以及 頂點(diǎn)5 到 頂點(diǎn)6的邊。
?
五,整個(gè)完整代碼如下:
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;// 邊的權(quán)值 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>();// 優(yōu)先級(jí)隊(duì)列存儲(chǔ)邊 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 // 對(duì)于無向圖而言,起點(diǎn)和終點(diǎn)都要添加邊 70 endNode.adjEdges.add(e); 71 startNode.adjEdges.add(e); 72 73 pq.add(e);// 將邊加入到優(yōu)先級(jí)隊(duì)列中 74 } 75 } 76 77 private int[] s;// 存儲(chǔ)并查集的一維數(shù)組 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;// 頂點(diǎn)的標(biāo)識(shí)是從0開始連續(xù)的數(shù)字 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中的參數(shù)是合并任意兩個(gè)頂點(diǎn),但是對(duì)于并查集,合并的對(duì)象是該頂點(diǎn)所在集合的代表頂點(diǎn)(根頂點(diǎn)) 105 root1 = find(root1);// 查找頂點(diǎn)root1所在的子樹的根 106 root2 = find(root2);// 查找頂點(diǎn)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 //生成并查集操作的對(duì)應(yīng)的標(biāo)點(diǎn)位置 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);// 從文件中讀取圖的數(shù)據(jù) 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類可參考中的完整代碼實(shí)現(xiàn)。
?
六,實(shí)驗(yàn)結(jié)果
求得的最小生成樹的信息如下:
0-->3 表示頂點(diǎn)0到頂點(diǎn)3的邊,邊的權(quán)值為1
5-->6 表示頂點(diǎn)5到頂點(diǎn)6的邊,邊的權(quán)值為1
.......
分類:?數(shù)據(jù)結(jié)構(gòu) 本文轉(zhuǎn)自hapjin博客園博客,原文鏈接:http://www.cnblogs.com/hapjin/p/5490248.html,如需轉(zhuǎn)載請(qǐng)自行聯(lián)系原作者總結(jié)
以上是生活随笔為你收集整理的并查集与贪心算法的应用之求解无向图的最小生成树的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安装路由器的详细步骤 安装路由器的详细步
- 下一篇: java集合基础复习