并查集

并查集 - 擒贼先擒王

        我们先从一个故事说起——解密犯罪团伙。

    快过年了,犯罪分子们也准备为年终奖“奋斗”了,小可可的家乡出现了多起抢劫事件。由于强盗人数过于庞大,作案频繁,警方想查清楚到底有几个犯罪团伙实在是太不容易了,不过警察叔叔还有搜集到了一些线索,需要咱们帮忙分析一下:
    现在有10个强盗。
    1号强盗与2号强盗是同伙。
    3号强盗与4号强盗是同伙。
    5号强盗与2号强盗是同伙。
    4号强盗与6号强盗是同伙。
    2号强盗与6号强盗是同伙。
    8号强盗与7号强盗是同伙。
    9号强盗与7号强盗是同伙。
    1号强盗与6号强盗是同伙。
    2号强盗与4号强盗是同伙。
    有一点需要注意:强盗同伙的同伙也是同伙。你能帮助警方查出有多少个独立的犯罪团伙吗?

    要想解决这个问题,首先我们先假设10个强盗相互是不认识的,他们各自为政,每个都是首领,他们只听自己的。之后我们将通过警方提供的线索,一步步来“合并同伙”。
步骤1:我们申请一个一维数组f,我们用下标 i(i=1->10) 表示这10个强盗,用f[i]的值代表每个强盗的boss是谁。
步骤2:初始化。根据我们之前的约定,这10个强盗都各自为政,每个强盗的boss就是自己。即f[i]=i。请注意,这是很重要的一步。

步骤3:合并。开始“合并同伙”。即如果发现母亲两个强盗是同伙,则这两个强盗是同一个犯罪团伙。现在有1个问题,合并之后,谁才是这个犯罪团伙的打boss(首领)呢?
    例如警方得到的第1条线索是“1号强盗与2号强盗是同伙”。那么究竟“2号强盗”是“1号强盗”的boss呢,还是“1号强盗”是“2号强盗”的boss呢?其实这无所谓,都可以。我们这里假定左边的强盗更厉害一点,我们给起个名字叫“靠左”法则。也就是说“2号强盗”的boss是“1号强盗”。因此我们把f[2]变成1:

    警方得到的第2条线索“3号强盗与4号强盗是同伙”。按照“靠左”法则,将f[4]的值要变成3(f[3])。如下:

    警方得到的第3条线索“5号强盗与2号强盗是同伙”。2号强盗的boss是1号,如果按照“靠左”法则,将2号归顺5号强盗,那么1号强盗可就不干了,你凭什么抢我的人。那么2号该如何选择呢?

    现在我给你支个招,嘿嘿(^_^)古语云:擒贼先擒王。你直接找2号强盗的boss和1号强盗谈,让他归顺5号强盗就OK了,也就是将f[1]的值改为5。如下:

    细心的同学可能会发现一个问题:我不但将f[1]的值改为5,还将f[2]的值也改为5了,其实此处将f[2]的值改为5,不是必须的,只不过是为了提高今后找到犯罪团伙的最高领导人(其实是树的祖先)的速度。这里我们是通过递归返回的时候实现的,并不会增加算法的时间复杂度,后面再解释。
    警方得到的第4条线索“4号强盗与6号强盗是同伙”。现在,f[4]的值是3,f[6]的值是6。根据“靠左”法则,让6号加入3号犯罪团伙。我们需要将f[6]的值改为3。

    警方得到的第5条线索“2号强盗与6号强盗是同伙”。f[2]的值是5,f[6]的值是3。根据“靠左”法则和“擒贼先擒王”原则,让6号强盗的boss“3号强盗”归顺“2号强盗”的boss“5号强盗”。因此我们需要将f[3]的值改为5.另外我们也需要将f[6]的值也改为5,此处仍然在递归返回的时候实现。

    喜欢思考的同学可能发现了,为什么f[4]还是3,f[3]和f[6]都改成5了。因为在上面一步将f[4]也改成5是需要多花费时间的,这不值得。其实f[6]的值不改成5也不会影响结果,擒贼先擒王嘛!只有f[3]的值改为5就可以了。
    警方得到的第6条线索“8号强盗与7号强盗是同伙”。f[8]=8,f[7]=7,按照靠左原则,把7号归顺8号。

    警方得到的第7条线索“9号强盗与7号强盗是同伙”。f[9]=9,f[7]=8.按照靠左法则和擒贼先擒王原则,f[8]的值改为9。同时f[7]的值也改为9,此步骤也在递归返回时实现。

    警方得到的第8条线索“1号强盗与6号强盗是同伙”。f[1]=5,f[6]=5,他们在同一犯罪团伙,因此此条线索是冗余线索。
    警方得到的最后一条线索“2号强盗与4号强盗是同伙”。f[2]=5,f[4]=3,他们竟然不在同一个犯罪团伙中?貌似不对吧!通过上图我们可以发现他们在同一个团伙中。其实这是我们再处理第5条线索的时候留下的问题,你可能要晕?
    仔细观察你会发现,f[4]=3,也是说4号强盗曾经归顺3号强盗,现在看看3号归顺谁了呢?我们发现f[3]=5,也就是说3号强盗归顺5号强盗,我们再看看5号强盗归顺谁了,发现f[5]=5,原来5号就是这个团伙的boss。
    我们刚才模拟的过程其实就是递归的过程。从4号强盗顺藤摸瓜一直到了他所在团伙的最高领导人。在递归的过程中,我们需要将中间遇到的“小boss”(强盗)都改为团伙的最高领导人5号强盗。最终我们发现f[2]=5,f[4]=5,他们属于同一个团伙,这条是冗余线索。

    好了,线索分析完毕,通过上图可以发现一共有3个犯罪团伙。通过下面这张图可以看到,如果f[i]=i,那么此人就是一个犯罪团伙的最高领导人,有多少个最高领导人其实就有多少个独立的犯罪团伙。

    上面模拟的过程就是并查集的算法。并查集是通过一个一维数组来实现,其本质是维护一个森林。刚开始的时候,森林的每个点都是独立的,之后通过一些条件,逐渐将这些树合并成一棵大树。其实合并的过程就是认爹的过程。在认爹的过程中,一定要遵守“靠左”法则和“擒贼先擒王”原则。在每次判断两个结点是否已经在同一棵树中的时候(一棵树其实就是一个集合),也要注意必须求其根源,中间“小boss”是不能说明问题的,必须要找到其祖宗(树的根结点)判断两个根结点是否在同一个根结点才行。代码如下: 

c++代码如下:
#include <iostream>
using namespace std;
int f[1000]={0}, n, m, k, ans=0; 
void init(){
	for(int i=1; i<=n; i++) f[i]=i; //这里初始化很重要 
}
//这是找爹的递归函数,不停地去找爹,直到找到祖宗为止
int getf(int v){
	if(f[v]==v)  return v;
	else{
	//这里是路径压缩,每次在函数返回的时候,顺带把路上遇到的"小boss"改为
        最后找到的祖宗的编号,这样可以提高今后找到祖宗的速度
		f[v]=getf(f[v]);
		return f[v];	
	}
}
void merge(int v, int u){ //这里是合并两个子集合的函数 
	int t1, t2;
	t1=getf(v);
	t2=getf(u);
	if(t1!=t2)//判断连个结点是否在同一个集合中 
		f[t2]=t1; //靠左法则,左边变成右边的boss,即把右边的集合,
        作为左边集合的子集合。经过路径压缩以后,将f[u]的根的值也赋值为v的祖先f[t1]。 
}
int main(){
	int i, x, y;
	cin >> n >> m;
	init();
	for(i=1; i<=m; i++){
		cin >> x >> y;
		merge(x, y); //开始合并犯罪团伙 
	}
  for(i=1; i<=n; i++) //最后扫描有多少个犯罪团伙 
     if(f[i]==i) ans++;
  cout << ans;
} 

         可以输入下列数据进行验证,第一行n和m,n表示强盗的人数,m表示警方搜集到的m条线索。接下来的m行每一行有两个数a和b,表示强盗a和强盗b是同伙。
10 9
1 2
3 4
5 2
4 6
2 6
8 7
9 7
1 6
2 4
运行结果为:3



【程序1】电影节


解题思路:
   并查集模板题,直接套用模板即可。

      
C++ code:
##include <iostream>
using namespace std;
int f[50000]={0}, n, m, k, ans=0; 
void init(){
	for(int i=1; i<=n; i++) f[i]=i; 
}
int getf(int v){
	if(f[v]==v)  return v;
	else{
		f[v]=getf(f[v]);
		return f[v];	
	}
}
void merge(int v, int u){ 
	int t1, t2;
	t1=getf(v);
	t2=getf(u);
	if(t1!=t2) f[t2]=t1;
}
int main(){
	int i, x, y;
	cin >> n >> m;
	init();
	for(i=1; i<=m; i++){
		cin >> x >> y;
		merge(x, y); 
	}
	cin >> x >> y;
    if(getf(x)==getf(y)) cout << "same\n";
    else cout << "not sure\n";
} 



【程序2】家族 

            若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。 
    规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。 
    数据输入: 
    第一行:三个整数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是否具有亲戚关系。 
    数据输出: 
    P行,每行一个’Yes’或’No’。表示第i个询问的答案为“具有”或“不具有”亲戚关系。

    样例:
    input.txt
    6 5 3
    1 2
    1 5
    3 4
    5 2
    1 3
    1 4    
    2 3
    5 6
    output.txt
    Yes
    Yes
    No
直接套模板:

C++ code:
#include<iostream> 
using namespace std;
int f[50001]={0}, n, m, k; 
void init(){
	for(int i=1; i<=n; i++) f[i]=i; 
}
int getf(int v){
	if(f[v]==v)  return v;
	else{
		f[v]=getf(f[v]);
		return f[v];	
	}
}
void merge(int v, int u){ 
	int t1, t2;
	t1=getf(v);
	t2=getf(u);
	if(t1!=t2)
		f[t2]=t1;
}
int main(){
	int i, x, y, p;
	cin >> n >> m >> p;
	init();
	for(i=1; i<=m; i++){
		cin >> x >> y;
		merge(x, y); 
	}
	for(i=1; i<=p; i++){
		cin >> x >> y;
		if(getf(x)==getf(y)) cout << "Yes\n";
    else cout << "No\n";
	}
} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值