[算法与数据结构]——并查集

news2025/1/20 16:29:53

                目录

        1. 概论

        定义:

        主要构成:

        作用:

  2. 并查集的现实意义

        故事引入:

        数据结构的角度来看:

  3. find( )函数的定义与实现

        故事引入:

        实现:

  4. join( )函数的定义与实现

        故事引入:

        实现:

  5. 路径压缩算法之一(优化find( )函数)

        问题引入:

        实现:

  6. 路径压缩算法之二(加权标记法)

        备注:

主要思路:

实现:

  7. 总结


1. 概论

        定义:

        并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题(即所谓的并、查)。比如说,我们可以用并查集来判断一个森林中有几棵树、某个节点是否属于某棵树等。

        主要构成:

        并查集主要由一个整型数组pre[ ]和两个函数find( )、join( )构成。
数组 pre[ ] 记录了每个点的前驱节点是谁,函数 find(x) 用于查找指定节点 x 属于哪个集合,函数 join(x,y) 用于合并两个节点 x 和 y 。

        作用:

        并查集的主要作用是求连通分支数(如果一个图中所有点都存在可达关系(直接或间接相连),则此图的连通分支数为1;如果此图有两大子图各自全部可达,则此图的连通分支数为2……)


2. 并查集的现实意义

        故事引入:

        话说在江湖中散落着各式各样的大侠,他们怀揣着各自的理想和信仰在江湖中奔波。或是追求武林至尊,或是远离红尘,或是居庙堂之高,或是处江湖之远。尽管大多数人都安分地在做自己,但总有些人会因为彼此的信仰不同而聚众斗殴。因此,江湖上常年乱作一团,纷纷扰扰。
        这样长期的混战,难免会打错人,说不定一刀就把拥有和自己相同信仰的队友给杀了。这该如何是好呢?于是,那些有着相同信仰的人们便聚在一起,进而形成了各种各样的门派,比如我们所熟知的“华山派”、“峨嵋派”、“,崆峒派”、“少林寺”、“明教”……这样一来,那些有着相同信仰的人们便聚在一起成为了朋友。以后再遇到要打架的事时,就不会打错人了。

        但是新的问题又来了,原本互不相识的两个人如何辨别是否共属同一门派呢?
        这好办!我们可以先在门派中选举一个“大哥”作为话事人(也就是掌门人,或称教主等)。这样一来,每当要打架的时候,决斗双方先自报家门,说出自己所在门派的教主名称,如果名称相同,就说明是自己人,就不必自相残杀了,否则才能进行决斗。于是,教主下令将整个门派划分为三六九等,使得整个门派内部形成一个严格的等级制度(即树形结构)。教主就是根节点,下面分别是二级、三级、……、N级队员。每个人只需要记住自己的上级名称,以后遇到需要辨别敌友的情况时,只需要一层层往上询问(网上询问)就能知道是否是同道中人了。

        数据结构的角度来看:

        由于我们的重点是在关注两个人是否连通,因此他们具体是如何连通的,内部结构是怎样的,甚至根节点是哪个(即教主是谁),都不重要。所以并查集在初始化时,教主可以随意选择(就不必再搞什么武林大会了),只要能分清敌友关系就行。

        备注:上面所说的“教主”在教材中被称为“代表元”。
        即:用集合中的某个元素来代表这个集合,则该元素称为此集合的代表元。


3. find( )函数的定义与实现

        故事引入:

        子夜,小昭于骊山下快马送信,发现一头戴竹笠之人立于前方,其形似黑蝠,倒挂于树前,甚惧,正系拔剑之时,只听四周悠悠传来:“如此夜深,姑凉竟敢擅闯明教,何不下坐陪我喝上一盅?”。小昭听闻,后觉此人乃明教四大护法之一的青翼蝠王韦一笑,答道:“在下小昭,乃紫衫龙王之女”。蝠王轻惕,急问道:“尔等既为龙王之女,故同为明教中人。敢问阁下教主大名,若非本教中人,于明教之地肆意走动那可是死罪!”。小昭吓得赶紧打了个电话问龙王:“龙王啊,咱教主叫啥名字来着?”,龙王答道:“吾教主乃张无忌也!”,小昭遂答蝠王:“张无忌!”。蝠王听后,抱拳请礼以让之。
        在上面的情境中,小昭向他的上级(紫衫龙王)请示教主名称,龙王在得到申请后也向他的上级(张无忌)请示教主名称,此时由于张无忌就是教主,因此他直接反馈给龙王教主名称是“张无忌”。同理,青翼蝠王也经历了这一请示过程。
        在并查集中,用于查询各自的教主名字的函数就是我们的find()函数。find(x)的作用是用于查找某个人所在门派的教主,换言之就是用于对某个给定的点x,返回其所属集合的代表。

        实现:

        首先我们需要定义一个数组:int pre[1000]; (数组长度依题意而定)。这个数组记录了每个人的上级是谁。这些人从0或1开始编号(依题意而定)。比如说pre[16]=6就表示16号的上级是6号。如果一个人的上级就是他自己,那说明他就是教主了,查找到此结束。也有孤家寡人自成一派的,比如欧阳锋,那么他的上级就是他自己。
        每个人都只认自己的上级。比如小昭只知道自己的上级是紫衫龙王。教主是谁?不认识!要想知道自己教主的名称,只能一级级查上去。因此你可以视find(x)这个函数就是找教主用的。
下面给出这个函数的具体实现:

int find(int x)					//查找x的教主
{
	while(pre[x] != x)			//如果x的上级不是自己(则说明找到的人不是教主)
		x = pre[x];				//x继续找他的上级,直到找到教主为止
	return x;					//教主驾到~~~
}

4. join( )函数的定义与实现

        故事引入:

        虚竹和周芷若是我个人非常喜欢的两个人物,他们的教主分别是玄慈方丈和灭绝师太,但是显然这两个人属于不同门派,但是我又不想看到他们打架。于是,我就去问了下玄慈和灭绝:“你看你们俩都没头发,要不就做朋友吧”。他们俩看在我的面子上同意了,这一同意非同小可,它直接换来了少林和峨眉的永世和平。

        实现:

        在上面的情景中,两个已存的不同门派就这样完成了合并。这么重大的变化,要如何实现?要改动多少地方?其实很简单,我对玄慈方丈说:“大师,麻烦你把你的上级改为灭绝师太吧。这样一来,两派原先所有人员的教主就都变成了师太,于是下面的人们也就不会打起来了!反正我们关心的只是连通性,门派内部的结构不要紧的”。玄慈听后立刻就不乐意了:“我靠,凭什么是我变成她手下呀,怎么不反过来?我抗议!”。抗议无效,我安排的,最大。反正谁加入谁效果是一样的,我就随手指定了一个,join()函数的作用就是用来实现这个的。

join(x,y)的执行逻辑如下:
1、寻找 x 的代表元(即教主);
2、寻找 y 的代表元(即教主);
3、如果 x 和 y 不相等,则随便选一个人作为另一个人的上级,如此一来就完成了 x 和 y 的合并。
下面给出这个函数的具体实现:

void join(int x,int y)                     //我想让虚竹和周芷若做朋友
{
    int fx=find(x), fy=find(y);            //虚竹的老大是玄慈,芷若MM的老大是灭绝
    if(fx != fy)                           //玄慈和灭绝显然不是同一个人
        pre[fx]=fy;                        //方丈只好委委屈屈地当了师太的手下啦
}

5. 路径压缩算法之一(优化find( )函数)

        问题引入:

        前面介绍的 join(x,y) 实际上为我们提供了一个将不同节点进行合并的方法。通常情况下,我们可以结合着循环来将给定的大量数据合并成为若干个更大的集合(即并查集)。但是问题也随之产生,我们来看这段代码:

if(fx != fy)  
    pre[fx]=fy;

        这里并没有明确谁是谁的前驱(上级)的规则,而是我直接指定后面的数据作为前面数据的前驱(上级)。那么这样就导致了最终的树状结构无法预计,即有可能是良好的 n 叉树,也有可能是单支树结构(一字长蛇形)。试想,如果最后真的形成单支树结构,那么它的效率就会及其低下(树的深度过深,那么查询过程就必然耗时)。
        而我们最理想的情况就是所有人的直接上级都是教主,这样一来整个树的结构就只有两级,此时查询教主只需要一次。因此,这就产生了路径压缩算法。
        设想这样一个场景:两个互不相识的大将夏侯惇和许褚碰面了,他们都互相看不惯,想揍对方。于是按照江湖规矩,先打电话问自己的上级:“你是不是教主?” 上级说:“我不是呀,我的上级是***,我帮你问一下。” 就这样两个人一路问下去,直到最终发现他们的教主都是曹操。具体结构如下:

找掌门人

        “失礼失礼,原来是自己人,在下军机处前将军夏侯惇!”
        “幸会幸会,不打不相识嘛,在下军情处上将许褚!”
        紧接着,两人便高高兴兴地手拉手喝酒去了。
        “等等等等,两位同学请留步,还有事情没完成呢!”我叫住他俩:“还要做路径压缩!”
        两人醒悟, 夏侯惇赶紧打电话给他的上级郭嘉: “军师啊, 我查过了, 我们的教主都是曹丞相。 不如我们直接拜在丞相手下吧, 省得级别太低, 以后找起来太麻烦。 ”郭嘉答道: “所言甚是” 。
        许褚接着也打电话给刚才拜访过的荀彧,做了同样的事。于是此时,整个曹操阵营的结构如下所示:

路径压缩

        这样一来,在刚才查询过程中涉及到的人物就都聚集在了曹操的直接领导下,以后再需要查询教主名称的时候,就只需要询问一级便可得到。所以,在经过一次查询后,整个门派树的高度都将大大降低,路径压缩所实现的功能就是这么个意思。

        实现:

        从上面的查询过程中不难看出,当从某个节点出发去寻找它的根节点时,我们会途径一系列的节点(比如上面的例子中,从夏侯惇→郭嘉→曹植→曹操),在这些节点中,除了根节点外(即曹操),其余所有节点(即夏侯惇、郭嘉、曹植)都需要更改直接前驱为曹操。
因此,基于这样的思路,我们可以通过递归的方法来逐层修改返回时的某个节点的直接前驱(即pre[x]的值)。简单说来就是将x到根节点路径上的所有点的pre(上级)都设为根节点。下面给出具体的实现代码:

int find(int x)                     //查找结点 x的根结点 
{
    if(pre[x] == x) return x;        //递归出口:x的上级为 x本身,即 x为根结点        
    return pre[x] = find(pre[x]);    //此代码相当于先找到根结点 rootx,然后pre[x]=rootx 
}

        该算法存在一个缺陷:只有当查找了某个节点的代表元(教主)后,才能对该查找路径上的各节点进行路径压缩。换言之,第一次执行查找操作的时候是实现没有压缩效果的,只有在之后才有效。


6. 路径压缩算法之二(加权标记法)

        备注:

        不要一看到“加权标记”这种比较高大上的名词就害怕,其实他也是属于路径压缩算法,不同的是其思想更加高级(更不容易想到)罢了。不过我还是说一下,掌握了前面几点内容就能应对绝大多数ACM问题,下面的“加权标记法”供有兴趣的同学学习交流。

主要思路:

        加权标记法需要将树中所有节点都增设一个权值,用以表示该节点所在树中的高度(比如用rank[x]=3表示 x 节点所在树的高度为3)。这样一来,在合并操作的时候就能通过这个权值的大小来决定谁当谁的上级(玄慈哭了:“正义终会来到,但永不会缺席”)。
        在合并操作的时候,假设需要合并的两个集合的代表元(教主)分别为 x 和 y,则只需要令pre[x] = y 或者pre[y] = x 即可。但我们为了使合并后的树不产生退化(即:使树中左右子树的深度差尽可能小),那么对于每一个元素 x ,增设一个rank[x]数组,用以表达子树 x 的高度。在合并时,如果rank[x] < rank[y],则令pre[x] = y;否则令pre[y] = x。
        举个例子,我们对以A,F为代表元的集合进行合并操作(如下图所示):

路径压缩

         由于rank(A) > rank(F) ,因此令pre[F]= A。合并后的图形如下图所示:

路径压缩         可以看出,合并前两个树的最大高度为3,合并后依然是3,这也就达到了我们的目的。但如果令pre[A]= F,那么就会使得合并后的树的总高度增加,这里我就不上图了,同学们不信可以自己画出来看看。
        我们常说,鱼和熊掌不可兼得,同理,时间复杂度和空间复杂度也很难兼得。由于给每个节点都增加了一个权值来标记其在树中的高度,这也就意味着需要额外的数据结构来存放权重信息,所以这将导致额外的空间开销。

实现:

        加权标记法的核心在于对rank数组的逻辑控制,其主要的情况有:
        1、如果rank[x] < rank[y],则令pre[x] = y;
        2、如果rank[x] == rank[y],则可任意指定上级;
        3、如果rank[x] > rank[y],则令pre[y] = x;
        在实际写代码时,为了使代码尽可能简洁,我们可以将第1点单独作为一个逻辑选择,然后将2、3点作为另一个选择(反正第2点任意指定上级嘛),所以具体的代码如下:

void union(int x,int y)
{
    x=find(x);                            //寻找 x的代表元
    y=find(y);                            //寻找 y的代表元
    if(x==y) return ;                    //如果 x和 y的代表元一致,说明他们共属同一集合,则不需要合并,直接返回;否则,执行下面的逻辑
    if(rank[x]>rank[y]) pre[y]=x;        //如果 x的高度大于 y,则令 y的上级为 x
    else                                //否则
    {
        if(rank[x]==rank[y]) rank[y]++;    //如果 x的高度和 y的高度相同,则令 y的高度加1
        pre[x]=y;                        //让 x的上级为 y
    }
}

7. 总结

        1、用集合中的某个元素来代表这个集合,则该元素称为此集合的代表元;
        2 、一个集合内的所有元素组织成以代表元为根的树形结构;
        3 、对于每一个元素 x,pre[x] 存放 x 在树形结构中的父亲节点(如果 x 是根节点,则令pre[x] = x);
        4 、对于查找操作,假设需要确定 x 所在的的集合,也就是确定集合的代表元。可以沿着pre[x]不断在树形结构中向上移动,直到到达根节点。
        因此,基于这样的特性,并查集的主要用途有以下两点:
        1、维护无向图的连通性(判断两个点是否在同一连通块内,或增加一条边后是否会产生环);
        2、用在求解最小生成树的Kruskal算法里。


一般来说,一个并查集对应三个操作:
1、初始化( Init()函数 )
2、查找函数( Find()函数 )
3、合并集合函数( Join()函数 )


下面给出上述所有内容的代码汇总:

const int  N=1005                    //指定并查集所能包含元素的个数(由题意决定)
int pre[N];                         //存储每个结点的前驱结点 
int rank[N];                        //树的高度 
void init(int n)                     //初始化函数,对录入的 n个结点进行初始化 
{
    for(int i = 0; i < n; i++){
        pre[i] = i;                 //每个结点的上级都是自己 
        rank[i] = 1;                //每个结点构成的树的高度为 1 
    } 
}
int find(int x)                      //查找结点 x的根结点 
{
    if(pre[x] == x) return x;          //递归出口:x的上级为 x本身,则 x为根结点 
    return find(pre[x]);             //递归查找 
} 
 
int find(int x)                     //改进查找算法:完成路径压缩,将 x的上级直接变为根结点,那么树的高度就会大大降低 
{
    if(pre[x] == x) return x;        //递归出口:x的上级为 x本身,即 x为根结点 
    return pre[x] = find(pre[x]);   //此代码相当于先找到根结点 rootx,然后 pre[x]=rootx 
} 

bool isSame(int x, int y)              //判断两个结点是否连通 
{
    return find(x) == find(y);      //判断两个结点的根结点(即代表元)是否相同 
}

bool join(int x,int y)
{
    x = find(x);                        //寻找 x的代表元
    y = find(y);                        //寻找 y的代表元
    if(x == y) return false;            //如果 x和 y的代表元一致,说明他们共属同一集合,则不需要合并,返回 false,表示合并失败;否则,执行下面的逻辑
    if(rank[x] > rank[y]) pre[y]=x;        //如果 x的高度大于 y,则令 y的上级为 x
    else                                //否则
    {
        if(rank[x]==rank[y]) rank[y]++;    //如果 x的高度和 y的高度相同,则令 y的高度加1
        pre[x]=y;                        //让 x的上级为 y
    }
    return true;                        //返回 true,表示合并成功
}

趁热打铁!!!

去做一下练习题吧!!!

首页 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

本文转载自:
【算法与数据结构】—— 并查集_酱懵静的博客-CSDN博客_并查集

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/152069.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

c++11 标准模板(STL)(std::forward_list)(三)

定义于头文件 <forward_list> template< class T, class Allocator std::allocator<T> > class forward_list;(1)(C11 起)namespace pmr { template <class T> using forward_list std::forward_list<T, std::pmr::polymorphic_…

Apollo本地快速部署

GitHub项目地址 Gitee项目地址 Apollo&#xff08;阿波罗&#xff09;是携程框架部门研发的分布式配置中心&#xff0c;能够集中化管理应用不同环境、不同集群的配置&#xff0c;配置修改后能够实时推送到应用端&#xff0c;并且具备规范的权限、流程治理等特性&#xff0c;适…

【计算机网络-数据链路层】介质访问控制协议(MAC协议)

文章目录1 静态划分信道——信道划分 MAC 协议1.1 频分多路复用&#xff08;FDM&#xff09;——“并行”1.2 时分多路复用&#xff08;TDM&#xff09;——“并发”1.2.1 同步时分多路复用1.2.2 异步时分多路复用1.3 波分多路复用&#xff08;WDM&#xff09;1.4 码分多路复用…

数据结构进阶 AVL树

作者&#xff1a;小萌新 专栏&#xff1a;数据结构进阶 作者简介&#xff1a;大二学生 希望能和大家一起进步&#xff01; 本篇博客简介&#xff1a;介绍高阶数据结构:AVL树 AVL树AVL树的概念AVL树节点类的定义AVL树的插入AVL树的旋转左单旋右单旋左右双旋右左单旋AVL树的验证…

多线程适用接口及常见类

日升时奋斗&#xff0c;日落时自省 目录 1、Callable接口 1.1、Callable方式 1.2、非Callable方式 2、JUC&#xff08;java.util.concurrent&#xff09;的常见类 2.1、ReentrantLock 2.2、信号量Semaphore 2.3、CountDownLatch 3、线程安全的集合类 3.1、多线程使用A…

【SpringMVC】使用SpringMVC处理JSON格式的数据

目录 一、前言 二、ResponseBody 三、RequestBody 四、HttpMessageConverter 相关文章&#xff08;可以关注我的SpringMVC专栏&#xff09; SpingMVC专栏SpingMVC专栏一、前言我们在使用Servlet处理前端请求&#xff0c;使用Json格式的数据&#xff0c;通常引入外部提供的一些…

拉伯证券|利好来了,145万手封涨停!低位+低市值“无主”股揭秘

二三四五或将完毕多年“无主”局势。 周末大消息不断。蚂蚁集团1月7日在官网发布关于持续完善公司管理的公告。公告显现&#xff0c;蚂蚁集团调整首要股东投票权&#xff0c;强化与阿里巴巴集团的隔离&#xff0c;阿里巴巴开创人马云抛弃了对蚂蚁集团的实践操控权&#xff0c;蚂…

JavaScript 原型链

文章目录原型链本质 - 对象间的关联关系instanceof 和 isPrototypeOf__proto__的大致实现委托原型链 原型链就是一系列对象的链接。通常来说&#xff0c;这个链接的作用是&#xff1a;如果在对象上没有找到需要的属性或者方法引用&#xff0c;引擎就会继续在[[Prototype]]关联的…

餐饮业数字化提速,OceanBase助海底捞变身“云上捞”

在海底捞火锅智慧餐厅&#xff0c;你会看到忙得团团转的机械臂和传菜机器人&#xff0c;顾客在智能点餐系统上下单&#xff0c;机械臂和传菜机器人就会着手备菜、传菜、上菜&#xff0c;服务人员则有更多时间专心应答顾客的各种询问。这种新模式&#xff0c;也为海底捞家喻户晓…

Springboot新手开发 基本总结

前言&#xff1a; &#x1f44f;作者简介&#xff1a;我是笑霸final&#xff0c;一名热爱技术的在校学生。 &#x1f4dd;个人主页&#xff1a;个人主页1 || 笑霸final的主页2 &#x1f4d5;系列专栏&#xff1a;后端专栏 &#x1f4e7;如果文章知识点有错误的地方&#xff0c;…

网络实验之HSRP协议

一、HSRP协议简介 HSRP&#xff1a;热备份路由器协议&#xff08;HSRP&#xff1a;Hot Standby Router Protocol&#xff09;&#xff0c;是cisco平台一种特有的技术&#xff0c;是cisco的私有协议。该协议中含有多台路由器&#xff0c;对应一个HSRP组。该组中只有一个路由器承…

mac自定义环境变量,mac自定义python变量,自定义通用变量(任意名字)

文章目录几个常用的命令工作原理知道原理后已经有了环境变量没有该环境变量几个常用的命令 功能命令查看当前环境变量echo $PATH编辑文件&#xff08;注&#xff1a;这里是直接编辑bash_profile&#xff09;vi ~/.bash_profile编辑i退出编辑esc回到命令&#xff08;用命令来保…

Apollo实现cron语句的热配置

GitHub项目地址 Gitee项目地址 Apollo&#xff08;阿波罗&#xff09;是携程框架部门研发的分布式配置中心&#xff0c;能够集中化管理应用不同环境、不同集群的配置&#xff0c;配置修改后能够实时推送到应用端&#xff0c;并且具备规范的权限、流程治理等特性&#xff0c;适…

stl algorithms 算法

所有泛型算法&#xff0c;除了少数例外&#xff0c;前两个参数均为一组iterator&#xff0c;用来标识欲遍历容器元素的范围&#xff0c;范围从第一个iterator所指位置开始&#xff0c;至第二个iterator所指位置&#xff08;并不包括&#xff09;结束 如 int arr[3]{1,2,3} ve…

管理客户信息并非易事

客户信息是企业的重要资产&#xff0c;是企业日积累月的价值沉淀&#xff0c;管理客户信息对于企业来说并不是一件容易的事&#xff0c;只有妥善管理客户信息&#xff0c;才能为企业创造更多价值。前言众所周知&#xff0c;客户信息是企业的重要资产&#xff0c;是企业日积累月…

rtu遥测终端机应用及安装介绍

1、设备介绍 设备集遥测终端机功能和视频录像机功能为一体&#xff0c;融合先进的3G/4G/WIFI通信技术、实现水文/水资源/环保212/TCP Modbus/MQTT等数据的采集、视频、图像存储、显示、控制、报警及传输等智能值守功能。 2、设备应用方向 本设备可广泛适用于带视频监测的水…

Jenkins 构建过程中提示 GPG 错误

错误信息如下&#xff1a;[INFO] --- maven-gpg-plugin:3.0.1:sign (sign-artifacts) rets-io --- gpg: no default secret key: No secret keygpg: signing failed: No secret key这个问题的原因应该是我们最近把我们的项目发布到中央 Maven 仓库中&#xff0c;但是发布项目到…

社区发现系列05:图的构建

想要挖掘作弊团伙首先先要构建社交网络图&#xff0c;然后用算法或者策略挖掘作弊团伙&#xff0c;那么如何构建社交网络图呢&#xff1f;下面给大家介绍一些实战经验&#xff0c;主要从电商和互金小额贷款两个业务场景来说&#xff1a; 1、电商业务 由于电商业务涉及的业务场…

《Spring揭秘》读书笔记1:IoC和AOP

1 Spring框架的由来 Spring框架的本质&#xff1a;提供各种服务&#xff0c;以帮助我们简化基于POJO的Java应用程序开发。 各种服务实现被划分到了多个相互独立却又相互依赖的模块当中&#xff1a; Core核心模块&#xff1a;IoC容器、Framework工具类。 AOP模块&#xff1a;S…

万物皆可灵活用工?灵活用工模式最契合的行业是哪些?

灵活用工VS传统用工 1&#xff09;传统用工 企业与员工签订劳动合同&#xff0c;双方出现问题纠纷适用于劳动法&#xff0c;关系固定&#xff0c;企业责任大。养着你&#xff0c;难&#xff0c;辞了你&#xff0c;更难。企业成本相对较高、负担较重&#xff08;薪资、福利&am…