并查集的学习

概念:在某些应用中,我们要检查两个元素是否属于同一个集合,或者将两个不同的集合合并为一个集合。这是不相交集合经常处理的两种操作:查找和合并,我们成为并查集。

基本思想:

1.标示一个集合

选择集合中某个固定的元素作为集合的代表,让它作为整个集合唯一的标识。一般来说,选取的代表是任意的。也就是说,到底选择集合中的哪个元素作为它的代表是无关紧要的。

2.树的思想

在并查集中,我们对于集合的表示利用树的思想,一个集合可以看做一棵树,树根即代表该集合的标识。如果两个集合在同一个树中,则它们是同一个集合;合并两个集合,即是对两棵树进行合并。

使用:若有一堆连续的元素0~n,则可以设置数组a[n],每一个元素的值作为数组的下标使用。而数组里存放的值,则是这个下标元素所在集合的代表。在初始化时,没有任何类似于“两个元素同属一个集合”信息,故我们在初始话时可以认为每个元素独自构成一个集合,集合的代表当然就是它自己,故初始化时代码如下:

int a[/*一个常量*/];

for(int i=0;i<n;i++)

a[i]=i;

然后我们定义两个操作:查找和合并。

1.查找find:对于一个数,若i==a[i],则说明i的代表是自己,也就是说i是一个集合的代表。若i!=a[i],则说明i不是一个集合的代表,我们继续查找a[i]的代表(i代表的代表,依然是i的代表)。举个例子来说,张三的上司是李四,李四的上司是王五,王五是整个公司的BOSS。现在要查找张三所在的公司(实际就是查找这个公司的BOSS司王五),我们发现a[张三]==李四,a[张三]!=张三,所以我们继续查找张三的上司——李四,的上司。李四的上司a[李四]==王五,然后再查找王五的上司,此时发现王五的上司就是自己,即a[王五]==王五,故我们知道王五是这个公司的顶头上司,则王五就是张三所在公司这个集合的代表。具体实现如下:

int find(int x){//有待优化

if(x!=a[x])return find(a[x]);

}

这个函数是没问题的,只是有待优化,试想:对于上述例子,若张三的上司是李四,李四的上司是王五,王五的上司是赵六,赵六的上司是刘七,刘七的上司是王八(?)……则查找张三的BOSS时,要经历一个“张三问李四,李四问王五,王五问赵六,赵六问刘七,刘七问王八,王八告诉刘七自己是BOSS,刘七告诉王五王八是BOSS,王五再往下传……直到传到张三”的过程,这样一来一回,涉及的六个人都知道了自己的BOSS是王八。但若是按照这个函数来看,下次再有人问他们谁是他们的BOSS,他们还要一层一层问上去,岂不麻烦大矣?所以我们在一层一层递归的同时,更新所涉及的a[i],方便后面再有查询的需要。具体实现如下:

int find(int x){

if(x!=a[x]return find(a[x]);//这种方式称为路径压缩

return a[x];

}

当然也有很玄学的写法如下

int find(int x){return x==a[x]?a[x]:a[x]=find(a[x]);}

  2.合并Union

在使用并查集时,我们是用某一个集合中的元素来代表整个集合的,那么在合并两个集合时,只需将其中任意一个集合的代表元素作为另一个集合代表元素的代表(例如上述公司要和洋企业合并,洋企业有个叫Alice的女BOSS,然后里面还有Bob,John两个员工,现在要合并两个公司,只需要让Alice认王八做BOSS就可,Bob和John的上司在他们下一次查询自己的BOSS时会被路径压缩自动更新(当然让王八认Alice做BOSS效果是一样的))。具体代码如下:

void Union(int x,int y){

if(find(x)!=find(y));//这个判断xy是否同属一个集合的if语句经常被人省掉,因为不会有什么不良影响

a[find(x)]=find(y);//将x的BOSS的BOSS改为y的BOSS

}

3.判断两个元素是否同属一个集合Query

这个就不必再说了吧……有了find()就有了这个,包括上面的合并操作,因为可以省去条件判断,写成一条单语句,所以很多人习惯直接合并而不是再写Union函数,这个也是。上代码:

bool Query(int x,int y){

return find(x)==find(y);

}


例题:CodeVS 1073 家族

这道题之于并查集,就如八皇后之于递归,算是模板类题目了。

题目描述 Description

若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。 规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。

输入描述 Input Description

第一行:三个整数n,m,p,(n<=5000,m<=5000,p<=5000),分别表示有n个人,m个亲戚关系,询问p对亲戚关系。 以下m行:每行两个数Mi,Mj,1<=Mi,Mj<=N,表示Ai和Bi具有亲戚关系。 接下来p行:每行两个数Pi,Pj,询问Pi和Pj是否具有亲戚关系。

输出描述 Output Description

P行,每行一个’Yes’或’No’。表示第i个询问的答案为“具有”或“不具有”亲戚关系。

样例输入 Sample Input

6 5 3

1 2

1 5

3 4

5 2

1 3

1 4

2 3

5 6

样例输出 Sample Output

Yes

Yes

No

数据范围及提示 Data Size & Hint

n<=5000,m<=5000,p<=5000

不多说,上代码:

#include<iostream>
#include<stdio.h>
using namespace std;
const int maxn=5001;
int n,m,p,par[maxn];
int find(int x){
	if(x!=par[x]) par[x]=find(par[x]) ;
	return par[x];
}
void un(int x,int y){
	par[find(y)]=find(x); 
} 

bool query(int x,int y){
	return find(x)==find(y); 
}
int main(){
 
    int i,j,x,y;
    scanf("%d%d%d",&n,&m,&p);
    for (i=1;i<=n;i++) par[i]=i;
    for (i=1;i<=m;i++){
    	scanf("%d%d",&x,&y);
        if (find(par[x])!=find(par[y]))un(x,y);
    }
    for (i=1;i<=p;i++){
        scanf("%d%d",&x,&y);
     	if (query(x,y))printf("Yes\n");
     	else printf("No\n");
    }
   return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值