并查集的用途
主要用于不相交集合的合并和查询。这种数据结构主要用来进行表示集合信息,用以实现确定集合中含有哪些元素,判断两个元素是否在同一个集合中,求集合中元素的数量。
- 求连通分量的个数
并查集的特点
- 树的特点(双亲表示法):对于一个集合来说,一个集合可以被构建成一棵树,只是这棵和常规数据结构中的树不同的是:每一棵树的结点没有左、右孩子,而是只有一个父结点,即每一个结点(除了根节点)都会保存其父亲结点的信息,而该树的根节点被认为是集合的首领(该首领方便进行集合的合并操作即树的合并)。
- 合并:集合的合并即树的合并,就是将是一棵树的根节点进行变成另一棵树的子结点,即一棵树变成另一树的子树。
在树的合并中还要注意优化问题,否则很容易造成一棵树最终会退化成一棵链表,从而增加查找的时间。优化的方式主要就是路径压缩,从而降低树高。
并查集的构建:
一般是使用树这个数据结构进行构建并查集。实际的实现方式就是使用一维数组,如果树结点只有整型数字的话,那么就是使用一维的整型数组来构建并查集合。
采用的数据结构
采用一维数组。int Tree[N]: 用 Tree[i]来表示结点 i 的双亲结点,若 Tree[i]为-1 则表示该结点不存在双亲 结点,即结点 i 为其所在树的根结点。
涉及到的函数
-
int findRoot(int x) :查找结点x所在树的根节点, 主要有两种实现。递归显示和非递归实现
递归实现:int findRoot(int x) { if (Tree[x] == -1) return x; //若当前结点为根结点则返回该结点号 else return findRoot(Tree[x]); //否则递归查找其双亲结点的根结点 }
非递归实现:
int findRoot(int x) { int res; while (Tree[x] != -1) x = Tree[x]; //若当前结点为非根结点则一直查找其双亲结点 res = x; //返回根结点编号 return res; }
带有路径压缩的实现:
递归://递归 int findRoot(int x) { if (Tree[x] == -1) return x; else { int tmp = findRoot(Tree[x]); Tree[x] = tmp; //将当前结点的双亲结点设置为查找返回的根结点编号 return tmp; } } // 非递归 int findRoot(int x) { int res; int tmp = x; while (Tree[x] != -1) { x = Tree[x]; } res = x; //找到根节点之后,进行路径压缩,即将原始的x所经过的路径上的所有结点的父节点设置为跟几点res while (Tree[x] != -1) { int t = Tree[x]; Tree[x] = res; x = t; } return res; }
并查集的例题
求连通分量的个数
某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直 接连通的城镇。省政府“畅通工程”的目标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问最少还需要建设多少条道路?
输入:
测试输入包含若干测试用例。每个测试用例的第 1 行给出两个正整数,分别是城镇数目 N ( < 1000 )和道路数目 M;随后的 M 行对应 M 条道路,每行给出一对正整数,分别是该条道路直接连通的两个城镇的编号。为简单起见,城镇从1 到 N 编号。当 N 为 0 时,输入结束,该用例不被处理。
输出:
对每个测试用例,在 1 行里输出最少还需要建设的道路数目。
分析: 本题就是求解连通分量的个数,最初每一个结点都是一个单独的连通分量,然后对输入的每一条边的两个顶点进行和入到同一个连通分量中,即两个顶点各自寻找自己的根节点然后进行合并,最终遍历整个Tree数组,通过查看哪些节点的父节点为-1来确定其为根节点,根节点的个数也就是连通分量的个数。
#include <stdio.h>
using namespace std;
#define N 1000
int Tree[N];
int findRoot(int x) { //查找某个结点所在树的根结点
if (Tree[x] == -1) return x;
else {
int tmp = findRoot(Tree[x]);
Tree[x] = tmp;
return tmp;
}
}
int main () {
int n , m;//n表示结点的总数,m表示的是边的个数
while (scanf ("%d",&n) != EOF && n != 0) {
scanf ("%d",&m);
for (int i = 1;i <= n;i ++) Tree[i] = -1;
while(m -- != 0) { //读入边的信息
int a , b;
scanf ("%d%d",&a,&b);
a = findRoot(a);
b = findRoot(b); //查找边的两个顶点所在集合信息
if (a != b) Tree[a] = b; //若两个顶点不在同一个集合则合并这两个集合
}
int ans = 0;
for (int i = 1;i <= n;i ++) {
if (Tree[i] == -1) ans ++; //统计所有结点中根结点的个数
}
printf("%d\n",ans - 1);
}
return 0;
}
注意:在这里需要非常注意的是,在合并的时候一定要判断两个结点是否在同一个结合中如果两个结点不在同一个集合中,再进行合并,即:
if (a != b) Tree[a] = b;
如果不加判断的话,容易造成根节点的Tree数组值为其本身而不是-1,这样在以存储了-1为根节点的构造中就会出错。