並查集的應用之求解無向圖中的連接分量個數


一,介紹

本文使用數據結構:並查集 來實現 求解無向圖的連通分量個數。

無向圖的連通分量就是:無向圖的一個極大連通子圖,在極大連通子圖中任意兩個頂點之間一定存在一條路徑。對於連通的無向圖而言,只有一個連通分量。

 

二,構造一個簡單的無向圖

這里僅演示求解無向圖的連通分量,因此需要先構造一個無向圖。圖由頂點和邊組成,並采用圖的鄰接表形式存儲。頂點類和邊類的定義如下:

 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類可參考中的完整代碼實現。

 

六,測試

文件格式如下:

第一列表示起始頂點的標識,第二列表示終點的標識。若沒有邊,則一行中只有一個頂點。

生成的圖如下:

整個程序運行完成后,並查集數組內容如下:

 

結果如下:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM