[啊哈!算法] 零基础彻底弄懂"并查集"
今天是算法数据结构专题的第5篇文章,我们一起来学习一下「并查集」。
并查集被很多ACMer认为是最简洁而优雅的数据结构之一,主要用于解决一些元素分组的问题。并支持两种操作:
- 合并(Union):把两个不相交的集合合并为一个集合。
- 查询(Find):查询两个元素是否在同一个集合中。
不多废话开始文章。
注:文章转载自啊哈磊,感谢啊哈磊学长允许的转载授权
咱们从一个故事说起——解密犯罪团伙。

快过年了,犯罪分子们也开始为年终奖“奋斗”了,小哼的家乡出现了多次抢劫事件。由于强盗人数过于庞大,作案频繁,警方想查清楚到底有几个犯罪团伙实在是太不容易了,不过警察叔叔还是搜集到了一些线索,需要咱们帮忙分析一下。
现在有11个强盗。
1号强盗与2号强盗是同伙。
3号强盗与4号强盗是同伙。
5号强盗与2号强盗是同伙。
4号强盗与6号强盗是同伙。
2号强盗与6号强盗是同伙。
7号强盗与11号强盗是同伙。
8号强盗与7号强盗是同伙。
9号强盗与7号强盗是同伙。
9号强盗与11号强盗是同伙。
1号强盗与6号强盗是同伙。
有一点需要注意:强盗同伙的同伙也是同伙。你能帮助警方查出有多少个独立的犯罪团伙吗?
要想解决这个问题,首先我们假设这11个强盗相互是不认识的,他们各自为政,每个人都是首领,他们只听从自己的。之后我们将通过警方提供的线索,一步步地来“合并同伙”。
第一步:我们申请一个一维数组f,我们用f[1] ~ f[11]分别存储1~11号强盗中每个强盗的首领“BOSS”是谁。
第二步:初始化。根据我们之前的约定,这11个强盗最开始是各自为政的,每个强盗的BOSS就是自己。“1号强盗”的BOSS就是“1号强盗”自己,因此f[1]的值为1。以此类推,“11号强盗”的BOSS是“11号强盗”,即f[11]的值为11。请注意,这是很重要的一步。


我们用数组下标来表示强盗的编号每个单元格中存储的是每个强盗的“BOSS”是谁
第三步:开始“合并同伙”,即如果发现目前两个强盗是同伙,则这两个强盗是同一个犯罪团伙。现在有一个问题:合并之后谁才是这个犯罪团伙的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。现在“2号强盗”的BOSS是“1号强盗”,而“1号强盗”的BOSS变成了“5号强盗”,即“1号强盗”带领手下“2号强盗”归顺了“5号强盗”,这样所有的关系信息就都保留下来了。如下。

警方得到的第4条线索是“4号强盗”与“6号强盗”是同伙。f[4]的值是3,f[6]的值是6。根据“靠左”原则,让“6号强盗”加入“3号犯罪团伙”。我们需要将f[6]的值改为3。原理和处理第1条和第2条线索相同。

警方得到的第5条线索是“2号强盗”与“6号强盗”是同伙。f[2]的值是1,f[1]的值是5,即“2号强盗”的大BOSS(首领)是“5号强盗”。f[6]的值是3,即“6号强盗”的BOSS是“3号强盗”。根据“靠左”原则和“擒贼先擒王”原则,让“6号强盗”的BOSS“3号强盗”归顺“2号强盗”的大BOSS(首领)“5号强盗”。因此我们需要将f[3]的值改为5,即让“3号强盗”带领其手下归顺“5号强盗”。
需要特别注意的是,此时,“5号强盗”团伙内部发生了一些变动。我们在寻找“2号强盗”的大BOSS(首领)是谁时,顺带将f[2]从1改成了5,也就是说现在“2号强盗”也变成大BOSS(首领)“5号强盗”的直属手下了。
这就是强盗团伙的江湖规矩,谁能找到自己帮派的大BOSS(首领),谁就会被大BOSS(首领)提拔,升职加薪,成为大BOSS(首领)的直属手下。这种扁平化管理的方式可以有效地提高强盗团伙找大BOSS的效率,在“并查集”算法中有一个专门的术语,叫作“路径压缩”,具体代码在后面展示。

细心的同学会问了,刚才不是说如果直接把f[2]改成5,“2号强盗”和“1号强盗”之间的关系就断了吗?此一时,彼一时。在得到第3条线索的时候,那时候“1号强盗”和“5号强盗”的关系还没有建立起来,如果把f[2]改为5,“2号强盗”想要找 “1号强盗”就找不到了。但到了第5条线索的时候,“2号强盗”和“1号强盗”已经都在大BOSS(首领)“5号强盗”手下工作了,这时候将f[2]改为5,“2号强盗”想找大BOSS(首领)“5号强盗”变得更加方便,而“1号强盗”和“2号强盗”之间的关系也没有丢失,因此整体上效率变得更高了。
警方得到的第6条线索是“7号强盗”与“11号强盗”是同伙。f[11]的值是11,f[7]的值是7。根据“靠左”原则,让“11号强盗”归顺“7号强盗”。我们需要将f[11]的值改为7。


警方得到的第7条线索是“8号强盗”与“7号强盗”是同伙。f[8]的值是8,f[7]的值是7。根据“靠左”原则,让“7号强盗”归顺“8号强盗”。我们需要将f[7]的值改为8。


警方得到的第8条线索是“9号强盗”与“7号强盗”是同伙。f[9]的值是9,f[7]的值是8。根据“靠左”原则和“擒贼先擒王”原则,我们需要将f[8]的值改为9。


警方得到的第9条线索是“9号强盗”与“11号强盗”是同伙。f[9]的值是9,f[11]的值是7。什么?他们竟然不在同一个犯罪团伙中?这貌似不对吧,通过上图可以很显然地看出来“11号强盗”和“9号强盗”都在同一个犯罪团伙中。不过“11号强盗”并不直属于大BOSS(首领)“9号强盗”,而是归顺在“7号强盗”的手下。现在来看看“7号强盗”又归顺了谁呢?我们发现f[7]=8,也就是说“7号强盗”归顺了“8号强盗”。而f[8]=9,也就是说“8号强盗”归顺了“9号强盗”。我们再来看看“9号强盗”有没有归顺于别的人。发现f[9]的值还是9,太牛了!说明“9号强盗”的BOSS仍然是自己,他就是所在团伙的大BOSS(首领)。
我们刚才模拟的过程其实就是递归的过程。从“11号强盗”顺藤摸瓜一直找到他所在团伙的大BOSS(首领)“9号强盗”。刚才说了,强盗团伙的江湖规矩是,谁能找到自己帮派的大BOSS(首领),就会被提拔为首领的直属手下。经过这一次“路径压缩”,一路上“11号强盗”“7号强盗”和“8号强盗”,都找到了自己的大BOSS“9号强盗”。下次再问他们的BOSS是谁的时候,他们就能马上回答出是“9号强盗”啦。


警方得到的最后一条线索是“1号强盗”与“6号强盗”是同伙。这又是一次“路径压缩”的过程。f[1]是5,“1号强盗”的BOSS是“5号强盗”。f[6]是3,“6号强盗”的BOSS是“3号强盗”。f[3]是5,“3号强盗”的BOSS是“5号强盗”。说明“6号强盗”和“1号强盗”是在一个团伙中的,但他现在并不是直接跟着团伙的大BOSS(首领)“5号强盗”,而是跟着“3号强盗”。而经过这次“路径压缩”,他的BOSS就改成了“5号强盗”。但是注意,这一次的“路径压缩”只发生在“6号强盗”“3号强盗”“5号强盗”这条路径上,团伙中的“4号强盗”不在被压缩的路径上,所以他的BOSS暂时不会改变,还是“3号强盗”。


好了,所有的线索分析完毕,那么究竟有多少个犯罪团伙呢?我想你从上面的图中一眼就可以看出来了,一共有3个犯罪团伙,分别是5号犯罪团伙(由5、1、2、3、4、6号强盗组成),9号犯罪团伙(由9、8、7、11号强盗组成)以及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,sum=0;
//这里是初始化,非常地重要,数组里面存的是自己数组下标的编号就好了。
void init()
{
int i;
for(i=1;i<=n;i++)
f[i]=i;
return;
}
//这是找爹的递归函数,不停地去找爹,直到找到祖宗为止,其实就是去找犯罪团伙的最高领导人,
//“擒贼先擒王”原则。
int getf(int v)
{
//return f[v] == v ? v : (f[v] = getf(f[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、t2分别为v和u的大BOSS(首领),每次双方的会谈都必须是各自最高领导人才行
t1=getf(v);
t2=getf(u);
if( t1!=t2 ) //判断两个结点是否在同一个集合中,即是否为同一个祖先。
{
f[t2]=t1;
//“靠左”原则,左边变成右边的BOSS。即把右边的集合,作为左边集合的子集合。
}
return;
}
//请从此处开始阅读程序,从主函数开始阅读程序是一个好习惯。
int main()
{
int i,x,y;
scanf("%d %d",&n,&m);
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);
getchar();getchar();
return 0;
}
可以输入以下数据进行验证。第一行n m,n表示强盗的人数,m表示警方搜集到的m条线索。接下来的m行每一行有两个数a和b,表示强盗a和强盗b是同伙。
11 10
1 2
3 4
5 2
4 6
2 6
7 11
8 7
9 7
9 11
1 6
运行结果是:
3
并查集也称为不相交集数据结构。此算法的发展经历了十多年,研究它的人也很多,其中Robert E. Tarjan做出了很大的贡献。在此之前John E. Hopcroft和Jeffrey D. Ullman也进行了大量的分析。你是不是又感觉Robert E. Tarjan和John E. Hopcroft很熟悉?没错,就是发明了深度优先搜索的两个人——1986年的图灵奖得主。你看牛人们从来都不闲着的。他们到处交流,寻找合作伙伴,一起改变世界。
[啊哈!算法] 零基础彻底弄懂"并查集"的更多相关文章
- 【零基础】搞懂GPU为什么比CPU“快”
一.前言 近几年深度学习在各领域大显神威,而”GPU加速"也得到了越来越多的篇幅,似乎任何程序只要放到GPU上运行那速度就是杠杠的.GPU代替CPU计算已成了大势所趋?我先告诉你结论”那是不 ...
- 笔试算法题(38):并查集(Union-Find Sets)
议题:并查集(Union-Find Sets) 分析: 一种树型数据结构,用于处理不相交集合(Disjoint Sets)的合并以及查询:一开始让所有元素独立成树,也就是只有根节点的树:然后根据需要将 ...
- (转)零基础入门深度学习(6) - 长短时记忆网络(LSTM)
无论即将到来的是大数据时代还是人工智能时代,亦或是传统行业使用人工智能在云上处理大数据的时代,作为一个有理想有追求的程序员,不懂深度学习(Deep Learning)这个超热的技术,会不会感觉马上就o ...
- 【雕爷学编程】零基础Python(01)---“投机取巧”的三条途径
从3月13日报名尝试上网课学习(4天课8.9元),开始接触Python(中文发音“派森”),到今天有一星期了.这两天广泛搜索了一下相关的学习途径,本着“投机取巧”的出发点,居然小有心得,这里一并分享出 ...
- 彻底弄懂LSH之simHash算法
马克·吐温曾经说过,所谓经典小说,就是指很多人希望读过,但很少人真正花时间去读的小说.这种说法同样适用于“经典”的计算机书籍. 最近一直在看LSH,不过由于matlab基础比较差,一直没搞懂.最近看的 ...
- 2018-02-03-PY3下经典数据集iris的机器学习算法举例-零基础
---layout: posttitle: 2018-02-03-PY3下经典数据集iris的机器学习算法举例-零基础key: 20180203tags: 机器学习 ML IRIS python3mo ...
- 一文带你弄懂 JVM 三色标记算法!
大家好,我是树哥. 最近和一个朋友聊天,他问了我 JVM 的三色标记算法.我脑袋一愣发现竟然完全不知道!于是我带着疑问去网上看了几天的资料,终于搞清楚啥事三色标记算法,它是用来干嘛的,以及它和 CMS ...
- 【零基础】一文读懂CPU(从二极管到超大规模集成电路)
一.前言 我们都知道芯片,也知道芯片技术在21世纪是最重要的技术之一,但很少有人能知道芯片技术的一些细节,如芯片是如何构造的.为什么它可以运行程序.芯片又是如何被设计制造出来的等等.本文就尝试从最底层 ...
- 【零基础】彻底搞懂51单片机各种型号(ATMEL系列)
零.前言 初学者开始学习51单片机时往往先是一愣,说好51单片机啊,咋个型号是AT89C52,这个S52又是咋回事?上学的时候大都懵懵懂懂就这么用着,但始终没整明白,所以今天我们就彻底搞明白这些“51 ...
- 普通程序员转型AI免费教程整合,零基础也可自学
普通程序员转型AI免费教程整合,零基础也可自学 本文告诉通过什么样的顺序进行学习以及在哪儿可以找到他们.可以通过自学的方式掌握机器学习科学家的基础技能,并在论文.工作甚至日常生活中快速应用. 可以先看 ...
随机推荐
- 【JSOI2008】火星人 (哈希+Splay)
题目 这种含有修改操作的就难以用后缀数组实现了,求LCP这种区间相等的类型可以想到用hash判断,同时LCP的答案大小符合二分条件可以二分求出,如果只有修改可以用线段树维护,因为还有有插入操作所以想到 ...
- CON2 工单重估 效率提升
CON2 工单重估 效率提升 业务背景:月结CON2 每次只能允许一个进程操作 集团公司较多的话,很影响月结效率. SAP提供了专家模式程序 RKAZCON2 ,可以选平行运行 平行处理 需要选服 ...
- MINA框架
一.小程序MINA框架分为三个部分: 有 View(视图层).App Service(逻辑层)和 Natice(系统层). 1.View(视图层) 视图层包含了小程序多个页面.每个页面都有WXML文件 ...
- VScode 中利用virtualenv建立 Python 虚拟环境
! https://zhuanlan.zhihu.com/p/638114885 VScode 建立 Python 虚拟环境 主要目的:创建一个与默认 python 版本不同的 python 虚拟环境 ...
- HDU 1108
Big Number Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)Total ...
- 数据库系列:业内主流MySQL数据中间件梳理
数据库系列:MySQL慢查询分析和性能优化 数据库系列:MySQL索引优化总结(综合版) 数据库系列:高并发下的数据字段变更 数据库系列:覆盖索引和规避回表 数据库系列:数据库高可用及无损扩容 数据库 ...
- Linux-LVM 磁盘扩容
LVM技术详解:视频1.视频2.视频3 安装lvm2后才支持如下命令 yum install -y lvm2 序号 功能 PV物理卷命令 VG卷组命令 LV逻辑卷命令 01 扫描功能 pvscan v ...
- BlockCanary原理解析
一.背景 为了解决应卡顿,分析耗时. 二.原理 Looper中的loop方法: public static void loop() { ... for (;;) { ... // This must ...
- 斯坦福 UE4 C++ ActionRoguelike游戏实例教程 07.在C++中使用UMG
斯坦福 UE4 C++ ActionRoguelike游戏实例教程 07.在C++中使用UMG 斯坦福课程 UE4 C++ ActionRoguelike游戏实例教程 0.绪论 概述 本篇文章的目标是 ...
- 斯坦福 UE4 C++ ActionRoguelike游戏实例教程 06.敲定AI——游戏框架拓展和细节优化
斯坦福课程 UE4 C++ ActionRoguelike游戏实例教程 0.绪论 概述 这篇文章对应课程13课, 50~54节.虽然标题是敲定AI,实际内容和AI关联并不大,主要工作是对游戏内各种细节 ...