笔者的话:
最近又准备要蓝桥杯省赛了,回想起2020年不会做的那个七段码的题,我决定把这个知识点自己梳理一遍。本篇文章我尽量用小白也能听的懂得话来详细地讲解并查集,因为本蒟蒻水平有限,如有疏漏不当的地方烦请大家多多指正,希望本篇文章对大家能有所帮助。
并查集能解决哪些问题
首先我们要知道并查集能解决哪些问题:
连通性,连通性,连通性!
不讲人话就是:在树和图的操作中,按照本蒟蒻的刷题经验来看,涉及节点顶点连接判断连通的问题一般可以用并查集解决。配合bfs/dfs食用更佳!
用小白的话讲解:假如图中有abc三个顶点,如果ab连通,bc连通,那么可以得到a-b-c,使得原本不相通的ac变得连通。如果我们要确定像ac这样的顶点是否连通,就可以借助并查集来判断。
目前本蒟蒻具体遇到的例题如下:
1.[leetcode:1202. 交换字符串中的元素]https://leetcode-cn.com/problems/smallest-string-with-swaps/
2.[leetcode:684. 冗余连接]https://leetcode-cn.com/problems/redundant-connection/
3.[leetcode:684. 冗余连接2]https://leetcode-cn.com/problems/redundant-connection-ii/description/
4.[2020蓝桥杯C/C++b组省赛第二场:七段码](当时没做出来,痛定思痛)
并查集简介
好懒啊以后再介绍了–。
引用B站大佬的讲解:https://www.bilibili.com/video/BV13t411v7Fs?p=1
构建并查集
- 我们现在已经了解了并查集(不了解的在把上面简介看明白= =,很重要),现在我们的问题是用什么数据结构去构造并查集?(人话:怎么实现并查集功能?)
聪明的大佬们想到了用树来模拟并查集,用数组来储存每个节点的双亲节点(以下图为例子):
图中包含下标0到5共六个节点,边集如上图左上角所示,图中左下为由边集求得的双亲(如(a,b),则a节点是b节点的双亲节点),根据这些数据创建图中右边的并查集。
这样构造并查集的好处就是,更方便的对集合进行合并、查找的操作。
说人话就是(个人理解):
1.查找该顶点是否在并查集时,直接用树代替集合,不需要真的一个一个地遍历集合去查找。
2.合并的时候,不需要真正的进行旧集合中元素的增添和无用集合的删除。使得合并效率更高效。
初始化
假定我们有vex_nums=6个节点,由于每个节点都没有双亲节点和子节点,我们可以认为他们各自为自己的双亲。故初始化数组如下:
这样,我们可以简单理解成现在已经创建了6个集合,每个节点有且仅有一个元素。
有的初始化的值会为-1,其实都行,这只是用来判断查找算法判断根节点的条件。
基本操作
有关并查集的操作有一下两种:
查找(Find):确定某个元素处于哪个子集;
合并(Union):将两个子集合并成一个集合。
查找
查找就是寻找当前节点的双亲节点。
既然我们是以树的结构储存的并查集,那么我们现在的问题是查找并查集,那么查找操作就排上用场了。
我们对比一下树的dfs中序遍历(以树 0-1-2-3 为例,从0遍历到3):
(1)中序遍历的顺序就是根-左-右顺序遍历,假设1.2.3全为其双亲节点的左叶节点,那么每次遍历都会遍历到当前节点的左叶子节点,直到遍历到3结束。(不会吧不会吧不会还有人不会dfs中序遍历吧- -)
(2)那么查找算法有点像把(只遍历左叶子的)中序遍历反过来遍历,我们可以根据叶子节点,一直查找其双亲直到找到这棵树的根节点。
我看了下资料,资料里面这么给查找算法打比方(用自己的话描述了一遍==):
查找算法就像是“追根溯源”,可以寻找自己的双亲,也可以一直寻找双亲的双亲的双亲的双亲…直到把自己家族的第一代祖宗找到为止。
浅谈合并
还是以上图为例,初始化后并查集如下图所示:
我们首先浏览到(0,2)这条边,说明顶点0和顶点2是连通的,这时我们把顶点0和顶点2连接起来:
同时我们发现,数组下标2的值变为了0。这时因为我们规定:每条(a,b)边中,左边的a为右边的b的双亲。(右边的b为左边的a的双亲也可以,这里先按本文的规定来创建并查集。)
不过这样合并并不可取,除了不好判断是否在不在同一集合中以外,还会有两个很大的问题(一般不用这种合并方法):
1.当我们要创建一个0-1-2-3-4-5-6-7-8-9-10的并查集的时候,这时会创建一条很长的链,回顾一下查找算法的实现方法,我们会发现这样创建并查集的效率会非常低。
2.如图所示:
对于这两棵树,当有(3,7)这条边时(也就是合并这两棵树时),无论3和右边的树的任意哪个节点连接,合并后的新树都会变得特别冗长,同样不利于查找操作。
针对上面两个问题,大佬们又想到了对应的解决方法↓
(按秩)合并
我们现在有一个问题(如图所示):
对于这两棵树,当有(3,7)这条边时(也就是合并这两棵树时),无论3和右边的树的任意节点连接,合并后的新树都会变得特别冗长,非常不利于查找操作。
那我们优化之前的合并算法呢?
有的大佬一下就想到了:反正我们的目的是让这两个集合合并,那我们就让这两个集合的根节点连接上,那不就好了吗?(大佬就是大佬orz)
那么现在新的问题来了,节点0和节点5谁是根节点?(到底谁来做双亲节点)
要回答这个问题,首先我们要了解秩的概念:
秩
还能怎么理解–,先姑且理解为树的高度吧!(什么?你不会算树的高度?汗- -… )
从上面的图片中我们可以很轻松看出,左边的树高度为4,右边的树高度为3。
回到我们刚刚的问题,如果让节点0作为节点5的双亲节点,合并后新树的高度仍为4,如图所示:
如果让节点5作为节点5的双亲节点,合并后新树的高度就变为5了,比原来的树最大高度还大,如图所示:
回到我们讲到合并时候的问题:如果树的路径太长,查找算法的效率会变得非常低。
那么大佬很快又想到了:我只要把秩高的树的根节点作为秩低的树的根节点的双亲不就好了吗?
(大佬就是大佬,一下就想出了问题的解决方法orz)
按秩合并方法总结就是:
1.把秩高的树的根节点作为秩低的树的根节点的双亲(怎么说的这么绕- -)
2.当两棵树的秩一样时,连接两棵树的根节点,从两个根节点中抽取一个幸运根节点作为新的根节点,并且新的根节点所在的树秩加一
压缩路径
还是同样的问题(如图所示):
即使我们使用了按秩合并,我们使用查找算法的效率仍然比较低,到底有什么更好的方法能提高查找效率呢?
我们先看看下面这个并查集:
我们可以发现一个神奇的事情,当我们用查找算法查找双亲节点的时候,他们的算亲节点同时也是这颗树的根节点。
那岂不美哉?每次查找双亲直接找到根节点,这不就省去很多不必要的查找过程了吗?
这就是压缩路径的方法了,当我们合并路径时,让一个节点的双亲直接指向根节点,这样我们在查找的时候效率就非常高了!
下面引用力扣题解的一段话:
下面是一个经验,并不绝对,仅供大家参考:在实际解决问题的时候,一般只用「路径压缩」。如果「路径压缩」的结果不太理想,再考虑使用「按秩合并」。虽然「路径压缩」和「按秩合并」同时使用在理论上会使得时间复杂度降低,但在数据规模有限的情况下,这种优化可能不能加快程序的执行时间。具体情况需要具体分析。
作者:LeetCode
链接:https://leetcode-cn.com/problems/smallest-string-with-swaps/solution/1202-jiao-huan-zi-fu-chuan-zhong-de-yuan-wgab/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
代码示例
class Disjiont{
public:
vector<int>parents;
vector<int>rank;
Disjiont(int n)
{
for(int i=0;i<n;i++)
{
parents.push_back(i);
rank.push_back(0);
}
}
//查找(压缩算法)
int find_parent(int i)
{
if(parents[i]!=i)
parents[i]=find_parent(parents[i]);
return parents[i];
}
//合并(按秩合并)
void merge(int a,int b)
{
a=find_parent(a);
b=find_parent(b);
if(rank[a]>rank[b])
parents[b]=a;
else if(rank[b]>rank[a])
parents[a]=b;
else
{
parents[a]=b;
rank[b]++;
}
}
};
经典例题
- 判断成环问题
题目:请问(0,1),(1,2),(0,2)是否成环?
如图所示:
当我们遍历到(0,2)时,我们发现:0的根节点是0、2的根节点也是0,所以0和2此时在同一棵树上,如果再连接0和2,这棵树成环了。故我们可以判断两个节点的根节点是否为同一节点来判断是否成环。
(- - 在找代码吗?我好像没写过这道题的代码- -…)
以上就是并查集的所有内容了-.-,如果觉得有帮助的话欢迎点赞收藏哦~如果有不足之处还烦请帮忙指正,感谢大佬萌帮助本蒟蒻一起成长呢QwQ。
参考资料:
[1]bilibili-【算法】并查集(Disjoint Set)[共3讲]
[2]OI Wiki-并查集
[3]leetcode-1202. 交换字符串中的元素
[4]leetcode-并查集 生成连通图