• 并查集的应用之求解无向图中的连接分量个数


    一,介绍

    本文使用数据结构:并查集 来实现 求解无向图的连通分量个数。

    无向图的连通分量就是:无向图的一个极大连通子图,在极大连通子图中任意两个顶点之间一定存在一条路径。对于连通的无向图而言,只有一个连通分量。

    二,构造一个简单的无向图

    这里仅演示求解无向图的连通分量,因此需要先构造一个无向图。图由顶点和边组成,并采用图的邻接表形式存储。顶点类和边类的定义如下:

     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("
    ");
     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类可参考中的完整代码实现。

    六,测试

    文件格式如下:

    第一列表示起始顶点的标识,第二列表示终点的标识。若没有边,则一行中只有一个顶点。

    生成的图如下:

    整个程序运行完成后,并查集数组内容如下:

    结果如下:

  • 相关阅读:
    通配符
    Hibernate入门简介
    Java的参数传递是值传递?
    Java线程详解
    java基础总结
    谈谈对Spring IOC的理解
    Oracle SQL语句之常见优化方法总结--不定更新
    JVM 工作原理和流程
    Java中的String为什么是不可变的? -- String源码分析
    Spring AOP 简介
  • 原文地址:https://www.cnblogs.com/hapjin/p/5486564.html
Copyright © 2020-2023  润新知