一,介紹
本文使用數據結構:並查集 來實現 求解無向圖的連通分量個數。
無向圖的連通分量就是:無向圖的一個極大連通子圖,在極大連通子圖中任意兩個頂點之間一定存在一條路徑。對於連通的無向圖而言,只有一個連通分量。
二,構造一個簡單的無向圖
這里僅演示求解無向圖的連通分量,因此需要先構造一個無向圖。圖由頂點和邊組成,並采用圖的鄰接表形式存儲。頂點類和邊類的定義如下:
1 private class Vertex{ 2 private Integer vertexLabel; 3 private List<Edge> adjEdges;//鄰接表 4 public Vertex(Integer vertexLabel) { 5 this.vertexLabel = vertexLabel; 6 adjEdges = new LinkedList<ConnectedComponents.Edge>(); 7 } 8 } 9 10 private class Edge{ 11 private Vertex endVertex; 12 public Edge(Vertex v) { 13 this.endVertex = v; 14 } 15 }
然后,再使用一個Map來存儲圖中的頂點。Map的Key為頂點的標識,Value為Vertex頂點對象。關於如何定義一個圖,可參考。
private Map<String, Vertex> nondirectedGraph;
三,求解無向圖的連通分量的思路
首先需要一個一維數組來存儲並查集。這里一維數組的下標表示圖的頂點標識,數組元素s[i]有兩種表示含義:當數組元素大於0時,表示的是 頂點 i 的父結點位置 ;當數組元素s[i]小於0時,表示的是 頂點 i 為根的子樹的高度(秩!)。從而將數組的下標與圖的頂點一 一 對應起來。
private int[] s; private int tree_numbers;//並查集中子樹的棵數
求解連通的分量的總體思路如下:
①構造一個圖。或者說得先有一個圖
②根據圖中的每一個頂點,初始化並查集。也就是對於每個頂點,構造一棵只有一個頂點的子樹(並查集的子樹)。
③對於圖中的每一條邊,這條邊一定關聯了兩個頂點,檢查這兩個頂點是否在同一個子集合中,如果不在,則執行union操作將這兩個頂點合並到同一個集合中
④當遍歷完所有的邊之后,並查集中子樹的個數即為連通分量的個數
偽代碼如下:
CONNECTED-COMPONENTS(G) for each vertex v belongs to V(G) do MAKE-SET(v) for each edge(u,v) belongs to E(G) do if FIND(u) != FIND(v) then UNION(u,v)
四,代碼分析及實現
關於並查集的理解參考這篇文章:數據結構--並查集的原理及實現
為方便起見,這里假設頂點的標識從0開始的字符串類型的連續的數字,如 0,1,2,3,4.......
make_set方法初始化並查集
1 private void make_set(Map<String, Vertex> graph){ 2 int size = graph.size();//頂點的個數 3 s = new int[size]; 4 for(Vertex v : graph.values()){ 5 s[Integer.valueOf(v.vertexLabel)] = -1;//頂點的標識是從0開始連續的數字 6 } 7 8 tree_numbers = size;//初始時,一共有 |V| 個子樹 9 }
s數組是並查集的存儲結構,第2行獲取圖中頂點的個數,構造並查集數組。其中,數組的下標表示圖中頂點的標識,數組元素s[i]有兩種表示含義:當數組元素大於0時,表示的是 頂點 i 的父結點位置 ;當數組元素s[i]小於0時,表示的是 頂點 i 為根的子樹的高度(秩!)
由於約定了頂點的標識為0,1,2,3.....故第5行根據圖中每個頂點來構造一棵單節點的樹。
假設無向圖如下:頂點的標識為 0,1,2,3,4,5,6
構造初始並查集后,s數組內容如下:
union操作
1 private void union(int root1, int root2) { 2 if (find(root1) == find(root2)) 3 return; 4 //union中的參數是合並任意兩個頂點,但是對於並查集,合並的對象是該頂點所在集合的代表頂點(根頂點) 5 root1 = find(root1);//查找頂點root1所在的子樹的根 6 root2 = find(root2);//查找頂點root2所在的子樹的根 7 8 if (s[root2] < s[root1])// root2 is deeper 9 s[root1] = root2; 10 else { 11 if (s[root1] == s[root2])// 一樣高 12 s[root1]--;// 合並得到的新的子樹高度增1 (以root1作為新的子樹的根) 13 s[root2] = root1;// root1 is deeper 14 } 15 tree_numbers--;// 合並后,子樹的數目減少1 16 }
注意第5、6行。union操作的對象應該是子樹的樹根。因為,union時使用了按秩求並,使用的是子樹的根結點的秩。
否則的話,程序將會有bug---求出錯誤的連通分量個數。
求解連通分量的代碼如下:
1 public int connectedComponents(Map<String, Vertex> graph) { 2 for (Vertex v : graph.values()) { 3 int startLabel = Integer.valueOf(v.vertexLabel); 4 5 List<Edge> edgeList = v.adjEdges; 6 for (Edge e : edgeList) { 7 Vertex end = e.endVertex;// 獲得該邊的終點 8 int endLabel = Integer.valueOf(end.vertexLabel); 9 10 if (find(startLabel) != find(endLabel)) 11 union(startLabel, endLabel);//這兩個頂點不在同一個子樹中,需要union 12 } 13 } 14 return tree_numbers; 15 }
第5行,遍歷圖中的每一條邊。第10行,對該邊關聯的兩個頂點進行判斷:這兩個頂點是否已經連通了(在同一棵子樹中了)
求解連通分量時,對圖中的每個頂點和每條邊都進行了一次遍歷,故算法的時間復雜度為O(V+E)
五,整個完整代碼
1 import java.util.LinkedHashMap; 2 import java.util.LinkedList; 3 import java.util.List; 4 import java.util.Map; 5 6 import c9.topo.FileUtil; 7 8 public class ConnectedComponents { 9 private class Vertex { 10 private String vertexLabel; 11 private List<Edge> adjEdges;// 鄰接表 12 13 public Vertex(String vertexLabel) { 14 this.vertexLabel = vertexLabel; 15 adjEdges = new LinkedList<ConnectedComponents.Edge>(); 16 } 17 } 18 19 private class Edge { 20 private Vertex endVertex; 21 22 public Edge(Vertex v) { 23 this.endVertex = v; 24 } 25 } 26 27 private Map<String, Vertex> nonDirectedGraph; 28 29 public ConnectedComponents(String graphContent) { 30 nonDirectedGraph = new LinkedHashMap<String, ConnectedComponents.Vertex>(); 31 32 buildGraph(graphContent); 33 34 make_set(nonDirectedGraph);// 初始化並查集 35 } 36 37 private void buildGraph(String graphContent){ 38 String[] lines = graphContent.split("\n"); 39 40 String startNodeLabel, endNodeLabel; 41 Vertex startNode, endNode; 42 for(int i = 0; i < lines.length; i++){ 43 if(lines[i].length()==1)//某行只有一個頂點 44 { 45 startNodeLabel = lines[i]; 46 nonDirectedGraph.put(startNodeLabel, new Vertex(startNodeLabel)); 47 continue; 48 } 49 String[] nodesInfo = lines[i].split(","); 50 startNodeLabel = nodesInfo[0]; 51 endNodeLabel = nodesInfo[1]; 52 53 endNode = nonDirectedGraph.get(endNodeLabel); 54 if(endNode == null){ 55 endNode = new Vertex(endNodeLabel); 56 nonDirectedGraph.put(endNodeLabel, endNode); 57 } 58 59 startNode = nonDirectedGraph.get(startNodeLabel); 60 if(startNode == null){ 61 startNode = new Vertex(startNodeLabel); 62 nonDirectedGraph.put(startNodeLabel, startNode); 63 } 64 Edge e = new Edge(endNode); 65 //對於無向圖而言,起點和終點都要添加邊 66 endNode.adjEdges.add(e); 67 startNode.adjEdges.add(e); 68 } 69 } 70 71 private int[] s; 72 private int tree_numbers; 73 74 private void make_set(Map<String, Vertex> graph) { 75 int size = graph.size(); 76 s = new int[size]; 77 for (Vertex v : graph.values()) { 78 s[Integer.valueOf(v.vertexLabel)] = -1;// 頂點的標識是從0開始連續的數字 79 } 80 81 tree_numbers = size;// 初始時,一共有 |V| 個子樹 82 } 83 84 private void union(int root1, int root2) { 85 if (find(root1) == find(root2)) 86 return; 87 //union中的參數是合並任意兩個頂點,但是對於並查集,合並的對象是該頂點所在集合的代表頂點(根頂點) 88 root1 = find(root1);//查找頂點root1所在的子樹的根 89 root2 = find(root2);//查找頂點root2所在的子樹的根 90 91 if (s[root2] < s[root1])// root2 is deeper 92 s[root1] = root2; 93 else { 94 if (s[root1] == s[root2])// 一樣高 95 s[root1]--;// 合並得到的新的子樹高度增1 (以root1作為新的子樹的根) 96 s[root2] = root1;// root1 is deeper 97 } 98 tree_numbers--;// 合並后,子樹的數目減少1 99 } 100 101 private int find(int root) { 102 if (s[root] < 0) 103 return root; 104 else 105 return s[root] = find(s[root]); 106 } 107 108 public int connectedComponents(Map<String, Vertex> graph) { 109 for (Vertex v : graph.values()) { 110 int startLabel = Integer.valueOf(v.vertexLabel); 111 112 List<Edge> edgeList = v.adjEdges; 113 for (Edge e : edgeList) { 114 Vertex end = e.endVertex;// 獲得該邊的終點 115 int endLabel = Integer.valueOf(end.vertexLabel); 116 117 if (find(startLabel) != find(endLabel)) 118 union(startLabel, endLabel); 119 } 120 } 121 return tree_numbers; 122 } 123 124 // for test purposes 125 public static void main(String[] args) { 126 String graphFilePath; 127 if (args.length == 0) 128 graphFilePath = "F:\\graph.txt"; 129 else 130 graphFilePath = args[0]; 131 132 String graphContent = FileUtil.read(graphFilePath, null);// 從文件中讀取圖的數據 133 ConnectedComponents cc = new ConnectedComponents(graphContent); 134 int count = cc.connectedComponents(cc.nonDirectedGraph); 135 System.out.println("連通分量個數:" + count); 136 } 137 }
FileUtil類可參考中的完整代碼實現。
六,測試
文件格式如下:
第一列表示起始頂點的標識,第二列表示終點的標識。若沒有邊,則一行中只有一個頂點。
生成的圖如下:
整個程序運行完成后,並查集數組內容如下:
結果如下: