首页 > 其他分享 >并查集

并查集

时间:2024-03-16 11:22:06浏览次数:16  
标签:int 查集 族长 fa query 亲戚关系

并查集是一种可以动态维护若干个不重叠的集合,并支持合并与查询的数据结构。

例:P1551 亲戚

题目描述: 如果 \(x\) 和 \(y\) 是亲戚,\(y\) 和 \(z\) 是亲戚,那么 \(x\) 和 \(z\) 也是亲戚。如果 \(x\) 和 \(y\) 是亲戚,那么 \(x\) 的亲戚都是 \(y\) 的亲戚,\(y\) 的亲戚也都是 \(x\) 的亲戚。现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
输入格式: 输入 \(3\) 个整数 \(n,m,p \ (n,m,p \le 5000)\),分别表示有 \(n\) 个人,\(m\) 对亲戚关系,\(p\) 对亲戚关系的询问。接下来 \(m\) 行,每行两个数说明这两个人是亲戚。接下来 \(p\) 行,每行询问两个人是否是亲戚。
输出格式: 对于每次查询,需要输出 Yes 或者 No 表示这次查询是否是亲戚关系。

分析: 将所有有亲戚关系的人归为同一个集合中(同一个家族)。如果想查询两个人是否有亲属关系,只需要判断这两个人是否为同一个家族内。

image

那么,怎么判断两个人是否在同一个家族内呢?可以从每个家族中选出一位“族长”来代表整个家族,这样只需要知道两个人的族长是否为同一人,就能判断出是否属于同一个家族。

规定所有的成员都有一名“负责人”,最开始的时候,所有人都“自成一族”,每个成员都有一个指向自己的箭头,意思是自己的“负责人”就是自己,这时本人就是族长。

image

假如得知 \(1\) 和 \(2\) 是亲戚关系,那么就需要将 \(1\) 和 \(2\) 合并为同一个家族,将 \(1\) 的“负责人”改成 \(2\) 即可(反过来也可以)。于是,关系就变为:

image

由于 \(1\) 号的负责人变成了 \(2\),族长换成了 \(2\),所以家族 \(1\) 不复存在。这时,\(1\) 号和 \(2\) 号在同一个家族中,他们的族长都是 \(2\) 号。

假如得知 \(1\) 和 \(5\) 也是亲戚关系。直接将 \(1\) 号的“负责人”改成 \(5\) 是不行的(不然和 \(2\) 号建立起的关系就丢失了),不过可以把 \(1\) 号的族长(也就是 \(2\) 号)的负责人变成 \(5\) 号。这样 \(1,2,5\) 三个人都成为亲戚关系了,他们的族长是 \(5\) 号。

image

接下来假如得知 \(3\) 和 \(4\) 是亲戚关系,只需要将 \(3\) 的负责人变成 \(4\),都归为家族 \(4\)。假如 \(2\) 和 \(5\) 是亲戚关系,由于发现 \(2\) 和 \(5\) 的族长都是 \(5\),已经是同一个家族了,所以可以忽略掉。

假如 \(1\) 和 \(3\) 是亲戚关系,将 \(1\) 的族长(\(5\) 号)的负责人变成 \(3\) 的族长(\(4\) 号),至此一共就只剩下两个家族了。需要注意的是,查询某位成员的族长要沿着负责人关系一层一层往上遍历,直到发现自己的负责人就是自己,这位成员就是族长。

image

如果需要查询两个人是否是同一个家族的,只需要查询这两个人的族长是否是同一个人。如果要把两个家族合并,就把其中一个家族的族长的负责人指向另外一个家族的族长即可。

这种处理不相交可合并集合关系的数据结构叫做并查集。并查集具有查询、合并这两种基本操作。使用并查集时要先初始化并查集,然后处理这 \(m\) 对亲戚关系,如果两个人是亲戚,那么就在并查集上所对应的集合合并起来,然后对于每个询问,只需要把这两个人的集合的代表元素找出来,判断是否相等即可——如果相等,那么这两个人在同一个集合中,即为亲戚,否则不是。

如果构造测试数据,在合并时将“族谱树”变成长长的一条链,这样每次查询就必须从底部开始一层一层地往上遍历所有的节点才能查询到族长是谁,效率很低。针对这个问题,可以进行路径压缩优化。可以发现一棵树上每个节点离根越近越好,因此可以在查询一个节点的族长的同时将所有访问到的节点的负责人都设为族长,这样下次从同一个节点找族长的时候经过的节点数就会大大减少。

仅使用路径压缩会使查询的时间复杂度降低至 \(O(\log n)\)。除此之外,还可以进行按秩合并优化(树的深度小的一方的族长指向深度大的一方),两个优化叠加下查询复杂度会降低至 \(O(\alpha (n))\),其中 \(\alpha(n)\) 是阿克曼函数的反函数,可以近似认为是常数级(但和常数级不同阶)。

参考代码
#include <cstdio>
const int N = 5005;
int fa[N];
int query(int x) { // 查询x所在的集合
    return fa[x] == x ? x : fa[x] = query(fa[x]); // 路径压缩
}
int main()
{
    int n, m, p; scanf("%d%d%d", &n, &m, &p);
    for (int i = 1; i <= n; i++) fa[i] = i; // 初始化
    while (m--) {
        int i, j; scanf("%d%d", &i, &j);
        int qi = query(i), qj = query(j);
        if (qi != qj) fa[qi] = qj; // 合并两个集合
    }
    while (p--) {
        int i, j; scanf("%d%d", &i, &j);
        printf("%s\n", query(i) == query(j) ? "Yes" : "No");
    }
    return 0;
}

例:P1536 村村通

现有村镇道路统计表,表中列出了每条道路直接连通的村镇(村镇从 \(1\) 到 \(N\) 编号,\(N \le 1000\))。如果要求任何两个村镇间都可以实现交通(不一定是直接的道路相连,只要相互之间可达即可),最少还需要建设多少条道路?

分析:先处理每条存在的边,即把每条存在的边所连接的两个节点用并查集合并起来,在这个过程中可以维护当前还有多少个集合,即有多少个连通块。接下来每添加一条边可以把两个连通块合并成一个,即将连通块个数减一,要实现任何两个村镇间都可以实现交通,即连通块只有一个,答案就是初始的连通块个数减一,输出即可。

参考代码
#include <cstdio>
const int N = 1005;
int fa[N];
int query(int x) {
    return fa[x] == x ? x : fa[x] = query(fa[x]);
}
int main()
{
    int n, m;
    while (scanf("%d", &n) && n != 0) {
        scanf("%d", &m);
        for (int i = 1; i <= n; i++) fa[i] = i;
        int ans = n;
        while (m--) {
            int x, y; scanf("%d%d", &x, &y);
            int qx = query(x), qy = query(y);
            if (qx != qy) {
                fa[qx] = qy; ans--;
            }
        }
        printf("%d\n", ans - 1);
    }
    return 0;
}

习:P1197 [JSOI2008] 星球大战

解题思路

按照常规的思维,这里需要在并查集上实现删点操作,这是比较麻烦的。这里我们可以采用逆向思维,将逐渐摧毁的过程倒过来考虑,变成逐渐重建的过程。

先将所有要摧毁的点摧毁,并将并查集维护好,记录此时的连通块个数,这就是整个摧毁过程完成后的答案。

从最后一次被摧毁的点开始“时光倒流”,让被摧毁的点一个个加回并查集中,在这个过程中记录连通块的个数。

参考代码
#include <cstdio>
#include <vector>
using namespace std;
const int N = 400005;
vector<int> graph[N];
int ans[N], target[N], fa[N];
bool destroy[N]; // 用于标记某点是否被摧毁
int query(int x) {
    return fa[x] == x ? x : fa[x] = query(fa[x]);
}
int main()
{
    int n, m; scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i++) fa[i] = i; // 并查集初始化
    for (int i = 1; i <= m; i++) {
        int x, y; scanf("%d%d", &x, &y);
        graph[x].push_back(y); graph[y].push_back(x); // 建图
    }
    int k; scanf("%d", &k);
    for (int i = 1; i <= k; i++) {
        scanf("%d", &target[i]);
        destroy[target[i]] = true; // 标记这个点已经被摧毁
    }
    int cnt = n - k;
    for (int i = 0; i < n; i++)
        if (!destroy[i]) {
            for (int to : graph[i]) 
                if (!destroy[to]) { // i和to都没被摧毁
                    int qi = query(i), qt = query(to);
                    if (qi != qt) {
                        fa[qi] = qt; cnt--; // 合并后连通块的数量减1
                    }
                }
        }
    ans[k + 1] = cnt; // 整个摧毁过程完成后剩下的连通块个数
    for (int i = k; i >= 1; i--) { // “时光倒流”
        int cur = target[i];
        destroy[cur] = false; cnt++; // 修复该点
        for (int to : graph[cur]) 
            if (!destroy[to]) { // 修复i<->to这条边
                int qc = query(cur), qt = query(to);
                if (qc != qt) {
                    fa[qc] = qt; cnt--; // 合并
                }
            }
        ans[i] = cnt; // 修复当前点后,连通块的个数
    }
    for (int i = 1; i <= k + 1; i++) printf("%d\n", ans[i]);
    return 0;
}

扩展域并查集

在普通的并查集中,通常只有一个维度(元素本身)。而在扩展域并查集中,会对元素进行扩展,让它拥有多个维度(通常是某些性质)。

例:P5937 [CEOI1999] Parity Game

分析:首先可以想到前缀和,可以把 \(l\) 到 \(r\) 的区间和看成前缀和之差 \(sum[r]-sum[l-1]\),而这个式子的结果要么是奇数要么是偶数,当其为偶数时,说明 \(sum[r]\) 和 \(sum[l-1]\) 的奇偶性需要相同,当其为奇数时,说明 \(sum[r]\) 和 \(sum[l-1]\) 的奇偶性需要不同。

注意到 \(2m\) 远小于 \(n\),需要对最多 \(2m\) 个位置做离散化处理。把每一个位置扩展成两个域:奇数域和偶数域。不妨令 \([1,2m]\) 表示奇数域,\([2m+1,4m]\) 表示偶数域,而 \(i\) 和 \(i+2m\) 是同一个位置分别对应到奇数域和偶数域上。则两个位置的前缀和奇偶性相同可以被表达为奇数域和奇数域合并、偶数域和偶数域合并;奇偶性不同可以被表达为奇数域和对方的偶数域合并、偶数域和对方的奇数域合并。

参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 5005;
int a[N], b[N], num[N * 2], n, m, fa[N * 4];
bool eq[N];
char q[10];
int getid(int x) {
    return lower_bound(num + 1, num + 2 * m + 1, x) - num;
}
int query(int x) {
    return fa[x] == x ? x : fa[x] = query(fa[x]);
}
int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; i++) {
        scanf("%d%d%s", &a[i], &b[i], q);
        a[i]--; num[i] = a[i]; num[i + m] = b[i];
        eq[i] = q[0] == 'e';
    }
    sort(num + 1, num + 2 * m + 1);
    for (int i = 1; i <= 4 * m; i++) fa[i] = i;
    int ans = 0;
    for (int i = 1; i <= m; i++) {
        int x = getid(a[i]), y = getid(b[i]); // 离散化后对应的值
        if (!eq[i]) { // 奇偶性不等
            // 如果之前出现过奇偶性相同的情况,说明矛盾
            if (query(x) == query(y)) break;
            fa[query(x)] = query(y + 2 * m);
            fa[query(y)] = query(x + 2 * m);
        } else { // 奇偶性相等
            // 如果之前出现过奇偶性相反的情况,说明矛盾
            if (query(x) == query(y + 2 * m) || query(y) == query(x + 2 * m)) break;
            fa[query(x)] = query(y);
            fa[query(x + 2 * m)] = query(y + 2 * m);
        }
        ans++;
    }
    printf("%d\n", ans);
    return 0;
}

习:P2024 [NOI2001] 食物链

解题思路

对于动物 \(x\) 和 \(y\),可能有 \(x\) 吃 \(y\),\(x\) 与 \(y\) 是同类,\(x\) 被 \(y\) 吃三种关系。因此考虑扩展到三倍空间,分别表示三类不同物种的域。

设有 \(n\) 只动物,则并查集的空间为 \(3n\),其中 \([1,n]\) 部分为 \(A\) 类物种域,\([n+1,2n]\) 部分为 \(B\) 类物种域,\([2n+1, 3n]\) 部分为 \(C\) 类物种域。

当 \(A\) 中的 \(x\) 与 \(B\) 中的 \(y\) 合并时,相当于关系 \(x\) 吃 \(y\);当 \(C\) 中的 \(x\) 与 \(C\) 中的 \(y\) 合并时,相当于关系 \(x\) 和 \(y\) 是同类······。

由于不知道某个动物具体属于 \(A,B\) 还是 \(C\),所以三种情况都要考虑。

也就是说,每当有一句真话时,需要做三次合并。

参考代码
#include <cstdio>
const int N = 50005;
int fa[N * 3];
int query(int x) {
    return fa[x] == x ? x : fa[x] = query(fa[x]);
}
int main()
{
    int n, k; scanf("%d%d", &n, &k);
    for (int i = 1; i <= n * 3; i++) fa[i] = i;
    int ans = 0;
    while (k--) {
        int op, x, y; scanf("%d%d%d", &op, &x, &y);
        if (x > n || y > n || op == 2 && x == y) {
            ans++; continue;
        }
        if (op == 1) {
            if (query(x) == query(y + n) || query(y) == query(x + n)) {
                ans++; continue;
            } 
            fa[query(x)] = query(y);
            fa[query(x + n)] = query(y + n);
            fa[query(x + n * 2)] = query(y + 2 * n);
        } else {
            if (query(x) == query(y) || query(y) == query(x + n)) {
                ans++; continue;
            }
            fa[query(x)] = query(y + n);
            fa[query(x + n)] = query(y + n * 2);
            fa[query(x + n * 2)] = query(y);
        }
    }
    printf("%d\n", ans);
    return 0;
}

标签:int,查集,族长,fa,query,亲戚关系
From: https://www.cnblogs.com/ronchen/p/18076665

相关文章

  • 并查集
    并查集目录并查集(1.概念:(2.详解Q1:如何表示不同的家族ans1:Q2:如何将两个人归到同一个家族中ans2:CODE:PS:(1.概念:处理不相交可合并的集合关系的数据结构叫做并查集;(2.详解例题:P1551亲戚一道并查集的板子题我们来详细解一下:Q1:如何表示不同的家族ans1:即如何表示不同的集合;再......
  • 并查集解mex_cf932_B. Informatics in MAC
    目录题目概述思路想法参考代码做题反思题目概述原题参考:B.InformaticsinMAC给出一个长度为n的数组,问是否可以将其分为k个(k>1)mex相同的数组,如果可以的话,作出一种划分思路想法假设一个数组可以被分为k(k>2)个区间,每个区间的mex相同,那么可以确定的是,该数组中一定不存在mex这......
  • 一类链式并查集问题
    链接:https://ac.nowcoder.com/acm/contest/69510/G来源:牛客网你在一个星球上,外星人amiloac想让你管理一条河流,该河流有\(x\)段,每两段之间有一个挡板隔开,每一段都有各自的颜色\(a\)。你需要管理\(q\)天,每一天你需要做以下的一种操作。\(1\l\r\)将第\(l\)至\(r\)段河流的所有未......
  • 带权并查集板子
    以一道区间和查询来说明板子如何使用1.merge的时候只需要维护两个根节点的距离,利用的是合并时题目给的信息2.find的时候更新维护是子节点到根的距离3.需要加一个查询函数,因为距离数组是开在结构体内部的。题目描述对于一个长度为\(N\)的整数数列\(A_{1},A_{2},\cdotsA_......
  • 程序自动分析—并查集
    Description在实现程序自动分析的过程中,常常需要判定一些约束条件是否能被同时满足。考虑一个约束满足问题的简化版本:假设x1,x2,x3,…代表程序中出现的变量,给定n个形如xi=xj或xi≠xj的变量相等/不等的约束条件,请判定是否可以分别为每一个变量赋予恰当的值,使得上述所有约束条件......
  • 信息传递(题解)[并查集]
    题目题目描述有n个同学(编号为1到n)正在玩一个信息传递的游戏。在游戏里每人都有一个固定的信息传递对象,其中,编号为i的同学的信息传递对象是编号为Ti同学。游戏开始时,每人都只知道自己的生日。之后每一轮中,所有人会同时将自己当前所知的生日信息告诉各自的信息传递对象(注意:可能有......
  • 并查集(模板介绍+路径压缩)
    并查集(模板介绍+路径压缩)题面P3367并查集题目描述如题,现在有一个并查集,你需要完成合并和查询操作。第一行包含两个整数N,M,表示共有N个元素和M个操作。接下来M行,每行包含三个整数Z,X,Y。当Z=1时,将X与Y所在的集合合并。当Z=2时,输出Z与Y是否在同一集合内,是的输出Y;否则输出N......
  • CF776D(并查集思想)
    难度1em还是一道比较套路的题目。观察发现,如果当\(r_{i}=1\)时他的两把钥匙状态是相同的,当\(r_{i}=0\)时他的两把钥匙状态是不同的,对于这种相同不同的问题可以考虑并差集,状态一样就一样的并在一起,否则就把不一样的并在一起所以以后看见这些问题(a=b+k,a=!b,a=b)都可以用带权并查......
  • 并查集+建图 同样是逆向思维 和星球大战类似
    L2-013红色警报分数25作者 陈越单位 浙江大学战争中保持各个城市间的连通性非常重要。本题要求你编写一个报警程序,当失去一个城市导致国家被分裂为多个无法连通的区域时,就发出红色警报。注意:若该国本来就不完全连通,是分裂的k个区域,而失去一个城市并不改......
  • 并查集
    作用:1.将两个集合合并2.询问两个元素是否在一个集合当中基本原理:每个集合用一棵树来表示,树根的编号就是整个集合的编号。每个节点存储它的父节点,p[x]表示x的父节点操作:1.判断树根:if(p[x]==x);2.求x的集合编号:while(p[x]!=x)x=p[x];3.如何合并两个集合:p[x]是x的集合编......