并查集及模板
并查集及模板
1. 并查集的定义及支持的操作
并查集是一种数据结构,可以高效地(近似为O(1)的时间复杂度)执行下述操作:
1. 将两个集合合并
2. 询问两个元素是否在同一个集合当中。
2. 并查集的类型
并查集根据用途的不同,可以分为三大类型:
1. 朴素并查集(路径压缩)
2. 维护size的并查集
3. 维护到祖宗节点距离的并查集。
2和3实际上都是在1的基础上维护一些额外信息而得到的并查集。
3. 朴素并查集的原理
并查集实际上是用树形结构来维护每一个集合。树根的编号就是每个集合的编号。树中的每一个节点都会存储它的父亲是谁。即:p[x]代表当前节点x的父亲节点编号。介绍完上述内容后,我们来解答一下下面的问题:
1. 如何判断树根?
if(p[x] == x) 那么p[x]就是集合编号(树根),否则就不是。
2. 如何查询当前节点x所在的集合编号?
首先,查询x的父亲节点编号p[x],看看它的父亲节点是否为树根。如果是,那么p[x]就是集合编号。如果不是,则从它的父亲节点开始继续往上寻找,直到找到树根为止。这样就找到了当前节点x所在的集合编号。
上述操作用代码表示:
while(p[x] != x)
x = p[x];
3. 询问两个元素是否在一个集合当中?
假设有两个元素x和y,我们可以先求一下x的集合编号,再求一下y的集合编号,最后判断二者是否相等即可。
4. 如何合并两个集合?
我们假设p[x]是x的集合编号,p[y]是y的集合编号。现在需要将x合并到y集合中,那么操作如下:
p[x] = y;
图示请看上述第二个图。
如果并查集的操作真的按照上述进行的话,时间复杂度还是很大的。具体体现在第二个问题。第二个问题的操作跟树的高度是成正比的。即,O(h)。其中,h为树的高度。
那么,对于上述的问题,我们如何来进行优化呢?
假设,当前节点x找到了树的根节点。那么,我们可以让x以及跟x同一路径上的所有节点直接指向根节点。这样的话,当前节点x寻找根节点,需要找h次。但是,当通过x以及跟x同一路径上的所有节点再次寻找根节点的话,只需要寻找1次。因此,时间复杂度就由原先的O(h)简化为了近似O(1)的效果。请见上图的图示。
以上的优化就是并查集的路径压缩优化。
并查集的优化还有一种叫做按秩合并。但是这种优化方式的效果不明显。因此,在这里就不再赘述。
4. 朴素并查集模板
//(1)朴素并查集:
int p[N]; //存储每个点的祖宗节点
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ ) p[i] = i;
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
5. 例题
https://www.acwing.com/problem/content/838/
#include <iostream>
#include <cstdio>
using namespace std;
int p[1000010];
int find(int x){
if(p[x] != x){
p[x] = find(p[x]);
}
return p[x];
}
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
p[i] = i;
}
while(m--){
char op[2];
int a,b;
scanf("%s%d%d",op,&a,&b);
if(op[0] == 'M'){
p[find(a)] = find(b);
}else{
if(find(a) == find(b)){
printf("Yes\n");
}else{
printf("No\n");
}
}
}
return 0;
}
6. 维护size的并查集原理
在朴素并查集的基础上,我们需要维护一些额外信息。例如:我们还需要维护每个并查集中的元素个数。这就引出了维护size的并查集。
根据上述的操作,我们可以用一个数组size[N](N代表并查集的个数)来维护每个并查集的元素个数。添加完这个数组之后,我们在朴素并查集的基础上进行修改即可。主要注意的是:在每个集合中,只有根节点所对应的size元素大小是有意义的。
1. 在初始化的时候,将size数组中的每一个元素的值初始化为1。
2. 将两个并查集进行合并时,假设将a元素所在集合并往b元素所在集合中。除了进行合并操作外,我们还需要:
size[find(b)] += size[find(a)]
7. 维护size的并查集模板
//(2)维护size的并查集:
int p[N], size[N];
//p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
size[i] = 1;
}
// 合并a和b所在的两个集合:
size[find(b)] += size[find(a)];
p[find(a)] = find(b);
8. 例题
https://www.acwing.com/problem/content/839/
在这里,我们只需要把连通块看成并查集即可。这道题实际上就是维护size的并查集的模板题。
#include <iostream>
#include <cstdio>
using namespace std;
int p[1000010];
int pSize[1000010];
int find(int x){
if(p[x] != x){
p[x] = find(p[x]);
}
return p[x];
}
int main(){
int n,m;
char op[3];
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++){
p[i] = i;
pSize[i] = 1;
}
while(m--){
int a,b;
scanf("%s%d%d",op,&a,&b);
if(op[0] == 'C'){
if(find(a) == find(b)){
continue;
}
//这里一定要有先后顺序
pSize[find(b)] += pSize[find(a)];
p[find(a)] = find(b);
}else if(op[1] == '1'){
if(find(a) == find(b)){
printf("Yes\n");
}else{
printf("No\n");
}
}else{
printf("%d\n",pSize[find(a)]);
}
}
return 0;
}
作者:gao79138
链接:https://www.acwing.com/
来源:本博客中的截图、代码模板及题目地址均来自于Acwing。其余内容均为作者原创。
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!