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

一,介绍

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

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

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

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

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

六,测试

文件格式如下:

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

生成的图如下:

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

结果如下:

时间: 2024-07-29 05:35:56

并查集的应用之求解无向图中的连接分量个数的相关文章

csu 1601: War(并查集求每次去掉一条边后连通分量的个数)

1601: War Time Limit: 1 Sec  Memory Limit: 128 MB Submit: 85  Solved: 25 [Submit][Status][Web Board] Description AME decided to destroy CH's country. In CH' country, There are N villages, which are numbered from 1 to N. We say two village A and B are

More is better(MST)(求无向图中最大集合元素个数)

More is better Time Limit:1000MS     Memory Limit:102400KB     64bit IO Format:%I64d & %I64u Submit Status Practice HDU 1856 Description Mr Wang wants some boys to help him with a project. Because the project is rather complex, the more boys come, th

浅谈并查集 By cellur925【内含题目食物链、银河英雄传说等】

什么是并查集? 合并!查询!集合! 专业点说? 动态维护若干不重叠的和,支持合并查询的数据结构!(lyd老师说的) 数据结构特点:代表元.即为每个集合选择一个固定的元素,作为整个集合的代表,利用树形结构存储,每个节点都是一个元素,树根是集合的代表元素.(还是lyd老师说的) 两大基本操作 一.合并(merge()) 即把两个集合合并到一个的操作.通俗的说,即令其中一个树根为另一个树根的子节点. void merge(int x,int y) { fa[getf(x)]=getf(y); } 二.

hdu1878 欧拉回路(并查集+无向图欧拉回路)

Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 20997    Accepted Submission(s): 8152 Problem Description 欧拉回路是指不令笔离开纸面,可画过图中每条边仅一次,且可以回到起点的一条回路.现给定一个图,问是否存在欧拉回路? Input 测试输入包含若干测试用例.每个测试用例的第1行给出

数据结构--并查集的原理及实现

一,并查集的介绍 并查集(Union/Find)从名字可以看出,主要涉及两种基本操作:合并和查找.这说明,初始时并查集中的元素是不相交的,经过一系列的基本操作(Union),最终合并成一个大的集合. 而在某次合并之后,有一种合理的需求:某两个元素是否已经处在同一个集合中了?因此就需要Find操作. 并查集是一种 不相交集合 的数据结构,设有一个动态集合S={s1,s2,s3,.....sn},每个集合通过一个代表来标识,该代表中集合中的某个元素. 比如,若某个元素 x 是否在集合 s1 中(Fi

hdu 5458 Stability(树链剖分+并查集)

Stability Time Limit: 3000/2000 MS (Java/Others)    Memory Limit: 65535/102400 K (Java/Others)Total Submission(s): 1347    Accepted Submission(s): 319 Problem Description Given an undirected connected graph G with n nodes and m edges, with possibly r

POJ3694:Network(并查集+缩点+lca)

Network Time Limit: 5000MS   Memory Limit: 65536K Total Submissions: 13172   Accepted: 4774 题目链接:http://poj.org/problem?id=3694 Description: A network administrator manages a large network. The network consists of N computers and M links between pair

FZU 2112 并查集、欧拉通路

原题:http://acm.fzu.edu.cn/problem.php?pid=2112 首先是,票上没有提到的点是不需要去的. 然后我们先考虑这个图有几个联通分量,我们可以用一个并查集来维护,假设有n个联通分量,我们就需要n-1条边把他们连起来. 最后对于每个联通分量来说,我们要使它能一次走完,就是要求他是否满足欧拉通路,也就是这个联通分量中至多有2个度为奇数的点,每多出2个度为奇数的点,就多需要一条边(因为单个连通分量的所有点的度数之和为偶数,所以不可能存在奇数个奇数度数的点). 1 #i

并查集间单个节点的转移(UVa 11987 Almost Union-Find)

从来没有这么艰难地完成一道算法题过!经过8次失败之后总算提交成功了!所以决定写一篇博文,对并查集的相关内容做一些总结. 普通并查集的操作无非是两种,find_set(x)即找到节点x所在的集合的代表节点,或者是union_set(x,y),即将x和y所在的两个集合合并起来.如下图所示,有左右两个并集 通常,我们会选用并查集中父节点为自己的元素作为这个并查集的代表,例如图中的节点a和节点e.那么,我们如何通过集合中的一个节点找到该节点所在集合的代表节点呢?其实很简单,例如上图中的d节点,它首先通过