并查集及模板

并查集及模板

1. 并查集的定义及支持的操作

    并查集是一种数据结构,可以高效地(近似为O(1)的时间复杂度)执行下述操作:
        1. 将两个集合合并
        2. 询问两个元素是否在同一个集合当中。

2. 并查集的类型

    并查集根据用途的不同,可以分为三大类型:
        1. 朴素并查集(路径压缩)
        2. 维护size的并查集
        3. 维护到祖宗节点距离的并查集。
    2和3实际上都是在1的基础上维护一些额外信息而得到的并查集。

3. 朴素并查集的原理

img
img

    并查集实际上是用树形结构来维护每一个集合。树根的编号就是每个集合的编号。树中的每一个节点都会存储它的父亲是谁。即: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;
        图示请看上述第二个图。

img
img

    如果并查集的操作真的按照上述进行的话,时间复杂度还是很大的。具体体现在第二个问题。第二个问题的操作跟树的高度是成正比的。即,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的并查集原理

img

    在朴素并查集的基础上,我们需要维护一些额外信息。例如:我们还需要维护每个并查集中的元素个数。这就引出了维护size的并查集。

img

    根据上述的操作,我们可以用一个数组size[N](N代表并查集的个数)来维护每个并查集的元素个数。添加完这个数组之后,我们在朴素并查集的基础上进行修改即可。主要注意的是:在每个集合中,只有根节点所对应的size元素大小是有意义的。
    1.  在初始化的时候,将size数组中的每一个元素的值初始化为12.  将两个并查集进行合并时,假设将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。其余内容均为作者原创。
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted @   夏目^_^  阅读(286)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示