• [总结]并查集


    一.基础并查集(模板)

    int fa[SIZE];
    
    int get(int x) {
        if (fa[x] == x) return x;
        return fa[x] = get(fa[x]);
    }
    void merge(int x, int y) { fa[get(x)] = fa[get(y)]; }
    
    for(int i = 1; i <= SIZE; i++) fa[i] = i;

    你也可以这样写:

    int get(int x) { return fa[x] == x ? x : fa[x] = get(fa[x]); }
    void merge(int x, int y) { fa[get(y)] = fa[get(x)]; }

    一般使用路径压缩就足以保证平均复杂度了.此时单次操作的复杂度可视为O(log n).

    并查集的每个集合内部的元素具有某种共同特征,在下面这题中,同一个集合内部的所有村庄都是互相联通的,而不属于同一个集合的两个村庄一定不联通.

    P1111 修复公路

    近乎于并查集的模板题,把将要修复的公路按照时间升序排列,每修复一条公路即合并公路两端的村庄到一个集合内,集合内的任意村庄之间是相通的.

    每次合并村庄检查是否已经联通全部村庄,联通时即为最短用时.

    #include <algorithm>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    using namespace std;
    
    struct S {
        int x, y, t;
    } s[100010];
    int n, m, fa[1010];
    
    bool cmp(S a, S b) { return a.t < b.t; }
    int get(int x) {
        if (fa[x] == x) return x;
        return fa[x] = get(fa[x]);
    }
    void merge(int x, int y) { fa[get(x)] = fa[get(y)]; }
    bool check(){
        int rt = get(1);
        for(int i = 2; i <= n; i++)
            if(get(i) != rt) return false;
        return true;
    }
    
    int main() {
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= m; i++) scanf("%d%d%d", &s[i].x, &s[i].y, &s[i].t);
        for (int i = 1; i <= n; i++) fa[i] = i;
        sort(s + 1, s + 1 + m, cmp);
    
        if(check()){puts("0"); return 0;}
        for(int i = 1; i <= m; i++){
            merge(s[i].x, s[i].y);
            if(check()){printf("%d
    ", s[i].t); break;}
        }
        if(!check()) puts("-1");
    
        return 0;
    }
    P1111

    相似的题目有P3958 [NOIP2017 提高组] 奶酪,类似于上一题,同一个集合内部的空洞一定是联通的,不属于同一个集合的两个空洞一定不连通.数据范围很小.

    #include <algorithm>
    #include <cmath>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    using namespace std;
    
    struct S {
        long long x, y, z;
    } s[1010];
    int n, fa[1010];
    long long h, r;
    
    int get(int x) {
        if (fa[x] == x) return x;
        return fa[x] = get(fa[x]);
    }
    void merge(int x, int y) { fa[get(x)] = fa[get(y)]; }
    bool iscon(S a, S b) {
        return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y) +
                   (a.z - b.z) * (a.z - b.z) <=
               4 * r * r;
    }
    void solve() {
        scanf("%d%lld%lld", &n, &h, &r);
        for (int i = 1; i <= n; i++) fa[i] = i;
        for (int i = 1; i <= n; i++) {
            scanf("%lld%lld%lld", &s[i].x, &s[i].y, &s[i].z);
            for (int j = 1; j <= i - 1; j++)
                if (iscon(s[i], s[j])) 
                    merge(i, j);
        }
    
        if (n == 1) {
            if (s[1].z - r <= 0 && s[1].z + r >= h) puts("Yes");
            else puts("No");
            return;
        } else
            for (int i = 1; i <= n - 1; i++)
                for (int j = i + 1; j <= n; j++) {
                    if (get(i) == get(j) && (s[i].z - r <= 0 && s[j].z + r >= h ||
                                             s[j].z - r <= 0 && s[i].z + r >= h)) {
                        puts("Yes");
                        return;
                    }
                }
        puts("No");
    }
    
    int main() {
        // freopen("in.txt", "r", stdin);
        // freopen("out.txt", "w", stdout);
        int t;
        scanf("%d", &t);
        while (t--) solve();
    
        return 0;
    }
    P3985

    再复杂一点的有P1525 [NOIP2010 提高组] 关押罪犯,可以用边带权的并查集,这里用的是标记和普通并查集的方法.在同一座监狱的罪犯在一个集合里.

    #include <algorithm>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    using namespace std;
    
    struct S {
        int x, y, c;
    } s[100010];
    int n, m, fa[20010], enm[20010];
    
    bool cmp(S fa, S enm) { return fa.c > enm.c; }
    int get(int x) {
        if (fa[x] == x) return x;
        return fa[x] = get(fa[x]);
    }
    void merge(int x, int y) { fa[get(x)] = fa[get(y)]; }
    bool check(int x, int y) { return get(x) == get(y); }
    
    int main() {
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= n; i++) fa[i] = i;
        for (int i = 1; i <= m; i++) scanf("%d%d%d", &s[i].x, &s[i].y, &s[i].c);
        sort(s + 1, s + m + 1, cmp);
    
        for (int i = 1; i <= m; i++) {
            if (check(s[i].x, s[i].y)) {
                printf("%d", s[i].c);
                return 0;
            } else {
                if (!enm[s[i].x])
                    enm[s[i].x] = s[i].y;
                else
                    merge(enm[s[i].x], s[i].y);
                if (!enm[s[i].y])
                    enm[s[i].y] = s[i].x;
                else
                    merge(enm[s[i].y], s[i].x);
            }
        }
    
        puts("0");
    
        return 0;
    }
    P1525

    P1955 [NOI2015] 程序自动分析,离散化后使用并查集处理.题意比较直白.

    #include <algorithm>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    using namespace std;
    #define SIZE 1000000
    
    int n, fa[SIZE + 10], dis[2 * SIZE + 10], idx;
    struct S {
        int x, y, e;
    } s[SIZE];
    
    int get(int x) {
        if (x == fa[x]) return x;
        return fa[x] = get(fa[x]);
    }
    void merge(int x, int y) { fa[get(x)] = get(y); }
    void init() {
        for (int i = 0; i <= n * 2 + 1; i++) fa[i] = i;
    }
    void discrete() {
        sort(dis + 1, dis + 1 + idx);
        idx = unique(dis + 1, dis + 1 + idx) - dis - 1;
    }
    int query(int x) { return lower_bound(dis + 1, dis + 1 + idx, x) - dis; }
    bool cmp(S &a, S &b) { return a.e > b.e; }
    
    void solve() {
        scanf("%d", &n);
        init();
        idx = 0;
        for (int i = 1; i <= n; i++) {
            scanf("%d%d%d", &s[i].x, &s[i].y, &s[i].e);
            dis[++idx] = s[i].x;
            dis[++idx] = s[i].y;
        }
        discrete();
        sort(s + 1, s + 1 + n, cmp);
    
        for (int i = 1; i <= n; i++){
            int nx = query(s[i].x), ny = query(s[i].y);
            if (s[i].e)
                merge(nx, ny);
            else if (get(nx) == get(ny)) {
                puts("NO");
                return;
            }
        }
    
        puts("YES");
    }
    
    int main() {
        // freopen("out.txt", "w", stdout);
        int t;
        scanf("%d", &t);
        while (t--) solve();
    
        return 0;
    }
    P1955

    二.拓展域并查集

    P2024 [NOI2001] 食物链中,题目明确指出有三类动物A,B,C,并且有循环的捕食关系

    设置三倍大小的并查集:

    这三个圆圈称为"域".

    那么现在,对于任一动物x(x∈[1,N]),其都有唯一确定的三个域 :同类域(x),猎物域(x+N),天敌域(x+2N).(也就是三个集合)

    若动物y是x的同类,那么其三个域相同,对应合并他们的三个域.

    若动物y是x的猎物,那么x的同类域(x)是y的天敌域(y+2N),x的猎物域(x+N)是y的同类域(y),x的天敌域(x+2N)是y的猎物域(y+N).对应合并这些域.

    每次合并之前判断一下当前条件是否满足,例如当给出关系x与y是同类时,检查y是否在x的天敌域或者捕食域.

    拓展域作为一种并查集的使用思想,相对于边带权的并查集不需要对基础并查集的实现进行任何修改.

    #include <algorithm>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    using namespace std;
    
    int n, k, ans, fa[200000];
    
    int get(int x) {
        if (x == fa[x]) return x;
        return fa[x] = get(fa[x]);
    }
    void merge(int x, int y) { fa[get(x)] = get(y); }
    
    int main() {
        scanf("%d%d", &n, &k);
        for (int i = 1; i <= 3 * n; i++) fa[i] = i;
    
        while (k--) {
            int opr, x, y;
            scanf("%d%d%d", &opr, &x, &y);
            if (x > n || y > n) {
                ans++;
                continue;
            }
    
            if (opr == 1) {
                if (get(x) == get(y + n) || get(x) == get(y + n + n))
                    ans++;
                else {
                    merge(x, y);
                    merge(x + n, y + n);
                    merge(x + n + n, y + n + n);
                }
            } else {
                if (x == y || get(x) == get(y) || get(x) == get(y + n))
                    ans++;
                else {
                    merge(x, y + 2 * n);
                    merge(x + n, y);
                    merge(x + 2 * n, y + n);
                }
            }
        }
    
        printf("%d
    ", ans);
    
        return 0;
    }
    P2024

    (本文以下所有内容都属于边带权的并查集)

    三.维护集合大小的并查集

    只需要将基础并查集的merge修改为:

    int siz[SIZE];  // siz全部元素要初始化为1
    
    void merge(int x, int y) {
        x = get(x), y = get(y);
        if (x != y) {
            fa[x] = y;
            siz[y] += siz[x];
        }
    }

    之后,元素v所在集合的大小(元素个数)表示为siz[get(v)].此时边的权即为集合的大小.

    P4185 [USACO18JAN]MooTube G 此题中在排序后处理每个合并操作时顺便也更新了集合大小,最后输出给定元素所在集合的大小减去一(该元素自身)即可.如果每次合并后都枚举所有元素计算则会提高复杂度(从而TLE).

    #include <algorithm>
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    using namespace std;
    
    struct S {
        int x, y, r;
    } s[100010];
    struct Q {
        int k, v, num;
    } q[100010];
    int n, m, fa[100010], siz[100010], ans[100010];
    
    bool cmp1(Q a, Q b) { return a.k > b.k; }
    bool cmp2(S a, S b) { return a.r > b.r; }
    int get(int x) {
        if (fa[x] == x) return x;
        return fa[x] = get(fa[x]);
    }
    void merge(int x, int y) {
        x = get(x), y = get(y);
        if (x != y) {
            fa[x] = y;
            siz[y] += siz[x];
        }
    }
    
    int main() {
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= n - 1; i++)
            scanf("%d%d%d", &s[i].x, &s[i].y, &s[i].r), fa[i] = i, siz[i] = 1;
        fa[n] = n, siz[n] = 1;
        for (int i = 1; i <= m; i++) {
            scanf("%d%d", &q[i].k, &q[i].v);
            q[i].num = i;
        }
        sort(q + 1, q + 1 + m, cmp1);
        sort(s + 1, s + 1 + n, cmp2);
    
        int p1 = 1, p2 = 1;  // p1 for question, p2 for edges
        while (p1 <= m) {
            while (s[p2].r >= q[p1].k) {
                merge(s[p2].x, s[p2].y);
                p2++;
            }
            ans[q[p1].num] = siz[get(q[p1].v)] - 1;
            p1++;
        }
    
        for (int i = 1; i <= m; i++) printf("%d
    ", ans[i]);
    
        return 0;
    }
    P4185

     四.维护到根节点距离的并查集

     根据具体问题,维护距离的方式会有所不同.

    例如P1196 [NOI2002] 银河英雄传说中,如果把每个集合都看作一条链(即使路径压缩了,单调变化的权仍然可以体现这种关系),根据题意每次合并两个集合时,需要把一条链的头部接到另一条链的尾部,所以把新接上去的链的根节点权设为原链的大小,并把其父节点设为原链的根节点.(由于涉及到集合大小,需要额外地维护sz数组)

    void merge(int x, int y) {
        x = get(x), y = get(y);
        fa[x] = y;
       d[x] = sz[y];  // 注意这里 sz[y] += sz[x]; }

    例如有两条链,1,4分别为根节点:(还没有进行路径压缩)

    (边上的权表示一个节点到其父节点的距离)

    现在把右链接到左链尾部,即将4(右链的根节点)的父节点设为3(左链的尾端),新增的边的权(距离)是1.

     如果对4进行一次路径压缩:

     可以看出来这等效于把新接上去的链的根节点权设为原链的大小,并把其父节点设为原链的根节点.

    注意,只需要更新右链的父节点权值即可,其子节点的权值会在路径压缩时正确地更新.d[x]只有在进行路径压缩(调用get(x))后才表示x与根节点之间的距离.

    d[x] = sz[y] 就是这样得来的.根据题目要求也可以设计其它的更新方式.

    get进行路径压缩时对应地要更新权.

    #include <algorithm>
    #include <cstdio>
    #include <cstring>
    #include <cmath>
    #include <iostream>
    using namespace std;
    #define SIZE 30010
    
    int fa[SIZE], d[SIZE], siz[SIZE], n;
    
    inline int read() {
        char ch = getchar();
        int x = 0, f = 1;
        while (ch > '9' || ch < '0') {
            if (ch == '-') f = -1;
            ch = getchar();
        }
        while (ch >= '0' && ch <= '9') {
            x = x * 10 + ch - '0';
            ch = getchar();
        }
        return x * f;
    }
    int get(int x) {
        if (x == fa[x]) return x;
        int root = get(fa[x]);
        d[x] += d[fa[x]];
        return fa[x] = root;
    }
    void merge(int x, int y) {
        x = get(x), y = get(y);
        fa[x] = y, d[x] = siz[y];
        siz[y] += siz[x];
    }
    void init() {
        for (int i = 1; i <= SIZE; i++) {
            fa[i] = i;
            // d[i] = 1;
            siz[i] = 1;
        }
    }
    
    int main() {
        int t = read(), x, y;
        char ch;
        init();
    
        while (t--) {
            cin >> ch;
            x = read(), y = read();
            if (ch == 'M')
                merge(x, y);
            else{
                int nx = get(x), ny = get(y);
                if(nx != ny) puts("-1");
                else printf("%d
    ", abs(d[x] - d[y]) - 1);
            }
        }
    
        return 0;
    }
    P1196

    (图片由https://csacademy.com/app/graph_editor/生成) 

    五.维护更复杂的带权边并查集

    由上文可见,边带权可以维护的信息有到根节点的距离,所在集合的大小,如果适当处理还可以维护其它信息.

    始终牢记,带权边中的边,权指的是一个点到其父节点(不一定是根节点)的关系.调用get时的路径压缩会更新边和权,这个"权"的含义完全是根据题意自行定义的,更新的方式根据其意义编写.

    再以P2024 [NOI2001] 食物链为例,如果使用带权边并查集,对应的merge和get要重写.不变的是这个"权"总是指某节点与其根节点的某种关系.

    在这题中,权代表循环食物链中的三个成员,用0,1,2表示,在此基础上进行处理.(本质上是在维护对3取模的距离,并且合并两集合时也有了特殊的限制条件:使得给定的两点间的距离有着指定的关系)

    注意这里的两种merge:

    void merge1(int x, int y) {  // same
        d[fa[x]] = (d[y] - d[x] + 3) % 3;
        fa[fa[x]] = fa[y];
    }
    void merge2(int x, int y) {  // x eat y
        d[fa[x]] = (d[y] - d[x] + 4) % 3;
        fa[fa[x]] = fa[y];
    }

    (由于merge前已经对x,y进行了路径压缩,fa[x],fa[y]就是x,y的根节点)

    这里不能像平常一样令x=get(x),因为这里仍然需要使用原先的d[x]和d[y]的关系.

  • 相关阅读:
    libcurl返回常见错误码
    NSIS控制面板中显示安装包的大小和禁止多个安装程序实例
    NSIS+Duilib 制作Windows安装包
    给自己的程序添加BugReport
    使用Http协议Post上传文件
    tp剩余未验证内容-2
    tp剩余未验证内容
    再谈 tp的 实例化 类 的自动加载
    tp框架中的一些疑点知识-8
    tp框架中的一些疑点知识--cookie和session的配置
  • 原文地址:https://www.cnblogs.com/Gaomez/p/14280570.html
Copyright © 2020-2023  润新知