并查集详解 —— 简单且易懂 (C语言实现)

树的神奇用法——并查集

下面我将用一道题,一步一步地让你学会并查集。

题目:解密犯罪团伙
题目大意:给出强盗总数和警方线索,输出共有几个独立的犯罪团伙

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

要想解决这个问题,首先我们假设这10个强盗相互是不认识的,他们各自为政,每个人都是首领,他们只听从自己的。之后我们将通过警方提供的线索,一步步地来“合并同伙”。

第一步:我们申请一个一维数组f,我们用数组下标1~10来表示这10个强盗,用每个下标所对应的单元格来存储每个强盗的BOSS是谁。

第二步:初始化。根据我们之前的约定,这10个强盗最开始是各自为政的,每个强盗的BOSS就是自己。“1号强盗”的 BOSS就是“1号强盗”自己,因此f[1]的值为1。以此类推,“10号强盗”的BOSS是“10号强盗”,即f[10]的值为10。请注意这是很重要的一步。

在这里插入图片描述
第三步:开始“合并同伙”,即如果发现目前两个强盗是同伙,则这两个强盗是同一个犯罪团伙。现在有一个问题:合并之后谁才是这个犯罪团伙的大BOSS(首领)呢?

例如警方得到的第1条线索是“1号强盗与2号强盗是同伙”。“1号强盗”和“2号强盗”原来的 BOSS都是自己,如今发现“1号强盗”和“2号强盗”其实是同一个犯罪团伙,那么究竟是让“1号强盗”变成“2号强盗”的 BOSS,还是让“2号强盗”变成“1号强盗”的BOSS 呢?一个犯罪团伙只能有一个首领。其实无所谓,都可以。我们这里假定左边的强盗更厉害一些,给这个规定起个名字叫做“靠左”法则。也就是说“2号强盗”的 BOSS将变成“1号强盗”。因此我们将f[2]中的数改为1,表明“2号强盗”归顺了“1号强盗”。其实准确地说应该是原本归顺“2号强盗”的所有人都归顺了“1号强盗”才对,只不过此时“2号强盗”只有孤身一人,因此只需要将f[2]的值改为1。不要着急,继续往后面看你就知道我为什么这样说了,如下。

在这里插入图片描述
警方得到的第2条线索是“3号强盗与4号强盗是同伙”,说明“3号强盗”和“4号强盗”也是同一个犯罪团伙。根据“靠左”原则“4号强盗”归顺了“3号强盗”,所以f[4]中的值要改为3,原理和刚才处理第1条线索是一样的,如下。

在这里插入图片描述
警方得到的第3条线索是“5号强盗”与“2号强盗”是同伙。f[5]的值是5,说明“5号强盗”的BOSS仍然是自己。f[2]的值是1,说明“2号强盗”的BOSS是“1号强盗”。根据“靠左”法则,右边的强盗必须归顺于左边的强盗。此时你可能会将f[2]的值改为5。注意啦!!!此时如果你将f[2]的值改为5,就是说让“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。原理和处理第1条和第2条线索相同。

在这里插入图片描述
警方得到的第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号强盗”。我们需要将f[7]的值改为8,原理和处理第1、第2和第4条线索相同。

在这里插入图片描述
警方得到的第7条线索是“9号强盗”与“7号强盗”是同伙。f[9]的值是9,f[7]的值是8。根据“靠左”原则和“擒贼先擒王”原则,我们需要将f[8]的值改为9。同时f[7]的值也将改为9,此步骤通过递归返回时实现,不会增加算法的时间复杂度。

在这里插入图片描述
警方得到的第8条线索是“1号强盗”与“6号强盗”是同伙。f[1]的值是5,f[6]的值是5。“1号强盗”和“6号强盗”的BOSS都是“5号强盗”。因此这条线索是冗余线索。警方得到的最后一条线索是“2号强盗”与“4号强盗”是同伙。f[2]的值是5,f[4]的值是3。他们竟然不在同一个犯罪团伙中?这貌似不对吧,通过上图可以很显然地看出来“2号强盗”和“4号强盗”都在同一个犯罪团伙中。其实这是我们刚才在处理警方的第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个犯罪团伙,分别是5号犯罪团伙(由5、2、1、3、4、6号强盗组成),9号犯罪团伙(由9、8、7号强盗组成)以及10号犯罪团伙(只有10号强盗一个人)。从下面这张图我们就可以清晰地看出,如果f[i]=i,就表示此人是一个犯罪团伙的最高领导人,有多少个最高领导人其实就是有多少个“独立的犯罪团伙”。最后数组中f[5]=5、f[9]=9、f[10]=10,因此有3个独立的犯罪团伙。

在这里插入图片描述
我们刚才模拟的过程其实就是并查集的算法。并查集通过一个一维数组来实现,其本质是维护一个森林。刚开始的时候,森林的每个点都是孤立的,也可以理解为每个点就是一棵只有一个结点的树,之后通过一些条件,逐渐将这些树合并成一棵大树。其实合并的过程就是“认爹”的过程。在“认爹”的过程中,要遵守“靠左”原则和“擒贼先擒王”原则。在每次判断两个结点是否已经在同一棵树中的时候(一棵树其实就是一个集合),也要注意必须求其根源,中间父亲结点(“小 BOSS")是不能说明问题的,必须找到其祖宗(树的根结点),判断两个结点的祖宗是否是同一个根结点才行。下面我将“解密犯罪团伙”这个问题模型化,并给出代码和注释:

#include<stdio.h>
int f[1001] = {0}, n, m;
//这里是初始化,非常的重要,
//数组里面存的是自己数组下标的编号就好了。 
void init()
{
	int i;
	for(i=1; i<=n; i++)
		f[i]=i;
	return ;
}
//这是找爹的递归函数,不停的找爹,直到找到祖宗为止,
//其实就是去找犯罪团伙的最高领导人
//“擒贼先擒王”原则。 
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;//分别为v和u的首领,每次双方的会谈都必须是各自最高领导人才行 
	t1=getf(v);
	t2=getf(u);
	if(t1!=t2)//判断两个结点是否在同一个集合中,即是否为同一个祖先 
		f[t2]=t1;//靠左原则,左边变成右边的领导,即把右边的集合作为左边集合的子集合 
	return;
}

int main()
{
	int i,x,y;
	scanf("%d%d", &n, &m);
	
	int sum=0;
	init();//初始化是必须的 
	for(i=1; i<=m; i++)
	{
		//开始合并犯罪团伙 
		scanf("%d%d",&x,&y);
		merge(x,y);
	}
	//最后扫描有多少个独立的犯罪团伙 
	for(i=1; i<=n; i++)
		if(f[i]==i) sum++;
	printf("%d\n",sum);
	
	return 0; 
}
/*
11 10
1 2
3 4
5 2
4 6
2 6
7 11
8 7
9 7
9 11
1 6
*/

怎么样,是不是思路清晰,是不是跃跃欲试 ,来吧!

注:摘自《啊哈!算法》,个人认为是一本不错的算法书,非常适合初学者。

  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值