CHAPTER_9 提高篇(3)——数据结构(2)
9.6.1并查集的定义
并查集是一种维护集合的数据结构。并查集支持下面两个操作:
(1)合并:合并两个集合。
(2)查找:判断两个元素是否在一个集合。
那么并查集是用什么实现的呢?其实就是用一个数组:
int father[N];
其中father[i]表示元素i的父亲节点,而父亲节点本身也是这个集合内的元素。另外对于father[i]==i,则说明元素i是这个集合的根节点,但对同一个集合来说只存在一个根节点,并将它视作所属集合的标识。
如上图这个并查集例子,father[1]=1说明元素1是根节点。
9.6.2并查集的操作
总体来说,并查集的使用需要先初始化father数组,然后再更具需要进行查找或合并操作。
初始化
一开始,所有元素都是独立的一个集合,因此需要令所有father[i]等于i:
for(int i=1;i<=N;i++) {
father[i]=i;
}
查找
同一个集合只能有一个根节点,因此查找操作就是对给定的节点寻找其根节点的过程:
int findFather(int x) {
while(x!=father[x]) { //如果不是根节点,则继续循环
x=father[x]; //获得父亲节点
}
}
合并
合并是指把两个集合合并成一个集合,题目中一般给出两个元素,要求把这两个元素所在的集合合并。具体实现上一般是先判断两个元素是否属于同一集合,只有当两个元素属于不同集合时才合并,而合并的过程一般是把其中一个集合的根节点的父亲指向另一个集合的根节点。
于是思路就比较清晰了,分为以下两步:
(1)对于给定的两个元素a、b,判断它们是否属于同一集合。可以调用上面的查找函数,对两个 元素a、b分别查找根节点,然后再判断其根节点是否相同。
(2)合并两个集合:在(1)中已经获得了两个元素的根节点faA与faB,因此只需要把其中一个的父 亲节点指向另一个节点。
void Union(int a,int b) {
int faA=findFather(a);
int faB=findFather(b);
if(faA!=faB) {
father[faA]=faB;
}
}
9.6.3路径压缩
上面讲解的并查集查找函数是没有经过优化的,在极端情况下效率较低。现在考虑一种情况,即题目给出的元素数量非常多并且形成一条链,那么这个查找函数的效率就会非常低。这样下来,查找根节点花费的时间太大,这显然无法接受。
那么应该如何去优化查询操作呢?
由于findFather函数的目的就是查找根节点,例如下面这个例子:
father[1]=1;
father[2]=1;
father[3]=2;
father[4]=3;
因此,如果只是为了查找根节点,那么完全可以想办法把操作等价地变成:
father[1]=1;
father[2]=1;
father[3]=1;
father[4]=1;
对应的图形变化过程如下:
这样相当于把当前查询节点的路径上的所有节点的父亲都指向根节点,查找的失手就不需要一直回溯去找父亲了,复杂度降为O(1)。
这种转换的过程可以概括为如下两个步骤:
(1)按原先的写法获得x的根节点r。
(2)重新从x开始走一遍寻找根节点的过程,把路径上经过的所有结点的父亲全部改为根节点r。
代码如下:
int findFather(int x) {
int a=x; //x后续会做修改,用a保存x
while(x!=father[x]) {
x=father[x]; //找到x的根节点
}
while(a!=father[a]) {
int z=a; //下一步a要变成fathera,故用z保存a
a=father[a]; //回溯父亲节点
if(x!=father[z]) {
father[z]=x; //将原先节点父亲改为根节点
}
}
return x;
}
下面通过一道例题,来练习并查集的相关操作。
题目:
有一片空间生活着许许多多的数码宝贝,这片空间有两条不成文的规定:
第一,数码宝贝A与B是好朋友等价于数码宝贝B与A是好朋友。
第二,如果数码宝贝A与C是好朋友,而数码宝贝B与A是好朋友,那么A与B也是好朋友。
现在给出这些数码宝贝中所有好朋友的信息,问:可以把这些数码宝贝分成多少组,满足每组中的任意两只数码宝贝都是好朋友,且任意两组间的数码宝贝都不是好朋友。
输入格式:
输入的第一行有两个正整数n(n<=100)和m(m<=100),分别表示数码宝贝的个数和好朋友的组数,其中数码宝贝编号为1-n。
接下来有m行,每行两个正整数a和b,表示a和b是好朋友。
输出格式:
输出一个整数,表示这些数码宝贝可以分成的组数。
样例输入1:
4 2
1 4
2 3
样例输出1:
2
样例输入2:
7 5
1 2
2 3
3 1
1 4
5 6
样例输出2:
3
思路:
本题就是个并查集模型,题目中的“组”就是集合,好朋友关系就是并查集节点的边。我们在输入每队好朋友关系时,用并查集的Union函数将其合并在一个集合内。这样就将所有输入数据建立成并查集模型,该并查集中有若干个集合,每个集合由它的根节点确定。因此我们将根节点作为一个集合的标识,设立一个数组flag来表示每一个数码宝贝是否是根节点,当i号数码宝贝是根节点时令flag[i]=true。最后遍历flag中所有元素,将它们累加即可得到集合数。
有一点需要注意:要实现union函数,必定要实现查找根节点findFather函数。在路径压缩小节中,我们讲解过优化findFather函数来提高查找效率。本题数据量很小,故可以不进行路径压缩,当然也可以进行压缩,这样时间效率更高。
参考代码:
#include<iostream>
using namespace std;
const int maxn=110;
int father[maxn];
bool isRoot[maxn]={0};
int findFather(int x) { //查找x的根节点
int a=x;
if(x!=father[x]) {
x=father[x];
}
while(a!=father[a]) { //路径压缩
int z=a;
a=father[a];
father[z]=x;
}
return x;
}
void Union(int a,int b) { //合并a和b所在集合
int faA=findFather(a);
int faB=findFather(b);
if(faA!=faB) {
father[faA]=faB;
}
}
int main() {
int n,m,a,b;
cin>>n>>m;
for(int i=1;i<=n;i++) { //将n个节点初始化
father[i]=i;
}
for(int i=1;i<=m;i++) { //将m对朋友关系合并成若干个集合
cin>>a>>b;
Union(a,b);
}
for(int i=1;i<=n;i++) { //i的根节点时father[i]
isRoot[father[i]]=1;
}
int ans=0;
for(int i=1;i<=n;i++) { //统计根节点数目
ans+=isRoot[i];
}
cout<<ans<<endl;
return 0;
}