• 并查集_模版


    并查集

      并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。

      并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。常常在使用中以森林来表示。

    例题:

    输入格式:

    第一行包含两个整数N、M,表示共有N个元素和M个操作。
    接下来M行,每行包含三个整数Zi、Xi、Yi
    当Zi=1时,将Xi与Yi所在的集合合并
    当Zi=2时,输出Xi与Yi是否在同一集合内,是的话输出Y;否则话输出N

    输出格式:

    如上,对于每一个Zi=2的操作,都有一行输出,每行包含一个大写字母,为Y或者N

    模版代码:

    #include<cstdio>
    #include<iostream>
    using namespace std;
    int a1,a2,a3,f[200001],n,m;
    
    int getf(int o) {
        if (f[o]==o) return o;
        else return f[o]=getf(f[o]);
    }
    
    void make(int v, int u) {
        int t1,t2;
        t1=getf(v);
        t2=getf(u);
        if (t1!=t2) f[t2]=t1;
        return; 
    }
    void find(int v,int u) {
        int t1,t2;
        t1=getf(v);
        t2=getf(u);
        if (t1==t2) printf("Y
    ");
        else printf("N
    ");
    }
    int main() {
        scanf("%d%d", &n, &m);
        for (int i=1; i<=n; i++) f[i]=i;
        for (int i=1; i<=m; i++) {
            scanf("%d%d%d", &a1, &a2, &a3);
            if (a1==1) make(a2, a3);
            else find(a2, a3);
        }
        return 0;
    }

    主要应用类型:
    1.团伙问题(搭桥问题,修路问题):求连通块
    2.最小生成树(kruskal)
    3.L最近公共祖先(LCA)中的Tarjan
    并查集的合并过程:(路径压缩)

    int  find(int x) {
        if(fa[x]!=x)  fa[x]=find(fa[x]);
        return fa[x]; 
         //或  return x==fa[x] ? x : fa[x]=find(fa[x]); 
    }

    因此,并查集在查询是否在同一集合时十分快捷

    if(fa[a]==fa[b]) {
        ...
    }

    与并查集相对的,可以有一种拆分集:

    例题:

    题目描述:
    维护一个数据结构支持下列两个操作:
    -删除某条边,表示为”D x”,即为删除第x条边
    -查询两点是否属于一个集合,表示为”Q a b”,即为查询节点a与节点b是否在一个集合内,若在同一个集合内,输出”Yes”,否则输出”No”(输出不包括引号)

    输入:
    第一行包括三个正整数数据结构中节点的个数n,边数m,操作数q
    接下来的m行每行包括两个正整数u,v,表示节点u与节点v在同一个集合里
    再接下来的q行每行包括一个格式如题描述操作
    输出:
    对于每一个查询,输出包括一行,表示查询结果,即如果两点属于同一个集合输出”Yes”,否则输出”No”。

    实际上,将删除操作存下来,反向就可以建立并查集,将答案倒序输出即可

    代码:

    #include<cstdio>
    #include<cstring>
    #include<iostream>
    #define CL(X,N) memset(X, N, sizeof(X))
    #define LL long long
    using namespace std;
    const int maxn=1e5+10;
    int n, m, q;
    int a[maxn], b[maxn];
    int u[maxn], v[maxn], f[maxn];
    char op[maxn];
    bool del_state[maxn];
    
    int _find(int x) {
        return x==f[x] ? x : f[x]=_find(f[x]);
    }
    
    void merge(int x, int y) {
        f[_find(y)]=_find(x);
        return ;
    }
    
    int main() {
        scanf("%d%d%d", &n, &m, &q);
        for(int i=1; i<=n; i++) f[i]=i;
        for(int i=1; i<=m; i++) scanf("%d%d", a+i, b+i);
        for(int i=1; i<=q; i++) {
            char cmd[2];
            scanf("%s", cmd);
            op[i]=cmd[0];/*存操作符*/ 
            switch(cmd[0]) {
                case 'D' : {
                    scanf("%d", u+i);
                    del_state[u[i]]=1;/*是否删除*/
                    break;
                }
                case 'Q' : {
                    scanf("%d%d", u+i, v+i);
                    break;
                }
                default : break;
            }
        }
        for(int i=1; i<=m; i++) if(!del_state[i]) merge(a[i], b[i]);
        /*将没被删除的边加入集合*/
        int ans[maxn], cnt=0;
        for(int i=q; i>0; i--) {
            if(op[i]=='Q') {
                if(_find(u[i])==_find(v[i])) ans[cnt++]=1;
                else ans[cnt++]=0;/*正向存答案*/
            }
            else merge(a[u[i]], b[u[i]]);
        }
        for(int i=cnt-1; i>=0; i--) printf(ans[i] ? "Yes
    " : "No
    ");/*倒序输出答案*/
        return 0;
    }

    如果有多种归属的情况,可以考虑建立多倍并查集

    例题:

    洛谷P2024 食物链

    洛谷上 Sooke (dalao)的讲解很详细,此处就不再多说(才不是我懒得写了)这道题比较巧妙的地方在于判断相关性,建议一做。

    至于Tarjan,这个算法主要还是与LCA有关,并查集只是中途用来判断两点是否在同一棵子树,不过对并查集的利用还是很巧妙的

    下面给出部分Tarjan的模版代码:

    void work(int fa,int son) {    //这里是Tarjan部分
        f[son]=son;
        for(int i=head[son];i;i=edge[i].net) {
            int to=edge[i].to;
            if(to==fa) continue;
            dis[to]=dis[son]+edge[i].c;    /*更新深度*/
            work(son,to);
            f[to]=son;
        }
        ...
    return
    ; }

    由于递归过程中,work(son, to) 比 f[to]=son先执行,因此可以看做这是在树上自底向上将节点加入并查集

    ". . ." 部分可以是对深度或距离的更新查询,但由于查询顺序不一定于答案顺序相同(work(fa, son) 函数像先序遍历),就需要先存再输出,不过整个算法的时间复杂度是十分优秀的(因为只查询了一遍)

    对于答案顺序的具体原因这里不做更多说明,希望了解的还请浏览其他博客

    (最后好像水了点)

  • 相关阅读:
    理清一下JavaScript面向对象思路
    IE的CSS渲染跟其它浏览器有什么不同
    页面元素的CSS渲染优先级
    push与createElement性能比较
    关于JavaScript的push()函数
    关于JavaScript的沙箱模式
    JavaScript SandBox沙箱设计模式
    用live()方法给新增节点绑定事件
    深入JavaScript对象创建的细节
    Keras class_weight和sample_weight用法
  • 原文地址:https://www.cnblogs.com/normaldisk/p/9282719.html
Copyright © 2020-2023  润新知