带权并查集 & 种类并查集

带权并查集 & 种类并查集

1. 带权并查集简介

带权并查集就是在维护集合关系的树中添加边权的并查集,这样做可以维护更多的信息。

在对并查集进行路径压缩和合并操作时,这些权值具有一定属性,即可将他们与父节点的关系,变化为与所在树的根结点关系。

也就是说,权值代表着当前节点与父节点的某种关系(即使路径压缩了也是这样),通过两者关系,也可以将同一棵树下两个节点的关系表示出来。

2. 例题深入讲解

2.1 例题1 食物链

题意:

一共有A,B,C三类动物,他们之间存在“捕食”,“被捕食”,“同类”三种关系。

我们用 fa[x] 表示编号 i与父亲节点的权值关系,由于只有三类动物,所以权值也只有三种:

0:同类1:捕食2:被捕食

转移时便可以采用对 3 取模来实现。之后我们需要实现一下两个操作:查找合并

1.查找(路径压缩):路径压缩就是在搜索的时候找到最远的祖先,然后将父亲节点赋值,对于权值而言,就是找出权值与最远祖先之前所有边权传递的过程,找出节点与父亲节点的关系,依次传递即可。
查找
我们定义3号节点父节点是2号节点,2号节点父节点是1号节点,他们与父亲节点的关系分别为re[2]re[3],路径压缩后 new re[3]的值应该是多少呢?

我们来考虑 1 和 2, 2 和 3 都是同类时,则 : re[2] = 0, re[3] = 0

显而易见的 new re[3] = re[2] + re[3] = 0 + 0 = 0, 表示 1 和 3 还是同类。

当 1 捕食 2, 2 和 3 是同类时,则 : re[2] = 1, re[3] = 0

new re[3] = re[2] + re[3] = 1 + 0 = 1, 表示 1 捕食 3。

当 1 捕食 2, 2 捕食 3,时,则 : re[2] = 1, re[3] = 1

new re[3] = re[2] + re[3] = 1 + 1 = 2, 表示 1被 捕食 3。

当 1 捕食 2, 2 被捕食 3,时,则 : re[2] = 1, re[3] = 2

new re[3] = re[2] + re[3] = 1 + 2 = 3, 1 吃 2, 3 也吃 2 ,所以1 和 3 还是同类。

当 1 被捕食 2, 2 被捕食 3,时,则 : re[2] = 2, re[3] = 2

new re[3] = re[2] + re[3] = 2 + 2 = 4, 3 吃 2 ,2 吃 1,所以 1 捕食 3 。

我们可以看到 new re[3] = 4 和 new re[3] = 1 都表示为 1 捕食 3, new re[3] = 0 和 new re[3] = 3 都表示 1 和 3 是同类(更多的大家可以自己画画试试)。

所以有了new re[3] = (re[3] + re[2] ) % 3 就是 re[3] = (re[3] + re[2] ) % 3

2.合并:并查集合并的本质就是一棵树认另一棵树做父亲,把树根相连即可,但是能否也把权值直接赋值,还有考虑各自与树根的关系,之后就可以实现树根权值的连接了。
合并

我们设 fxfy 分别为 xy 的根,两者权值关系为 分别为re[x] re[y], 这时,我们让 xy 的关系为 k, 则 re[fx]则为根 fxfy的关系。

我们来考虑 fxxfyy 都是同类,xy 也是同类时,则 : re[x] = 0, re[y] = 0

主观上fxfy 的关系为同类,考虑向量,则有 re[x] + re[fx] = k + re[y] 等式成立

化简后 re[fx] = k + re[y] - re[x] = 0 + 0 + 0 = 0, fxfy 是同类。

我们来考虑 fxx 是 捕食, fyy 都是同类,xy 也是同类时,则 : re[x] = 1, re[y] = 0

主观上fxfy 的关系 fyfx ,即为 fx 被捕食 fy, 考虑向量,则有 re[x] + re[fx] = k + re[y] 等式成立 化简后 re[fx] = k + re[y] - re[x] = 0 + 0 - 1 = -1,出现负数,我们加 3 处理一下re[fx] = k + re[y] - re[x] + 3= 0 + 0 - 1 + 3= -1 + 3 = 2 , fx 被捕食 fy

(例子大家可以自己画画看)

那么就可以得出re[fx] = k + re[y] - re[x] 这个结果。考虑到re[y] - re[x]可能会出现负数的情况,我们会将这里+ 3 后取模,则为re[fx] = (k + re[y] - re[x] +3 ) % 3, 这里的k根据题意可能是 0 或者 1,最后化简结果为 re[fx] = (0 + re[y] - re[x] +3 ) % 3 或者re[fx] = (1 + re[y] - re[x] +3 ) % 3

【代码参考】

#include <iostream>
using namespace std;
const int maxn = 1e6 + 7;
int fa[maxn], rela[maxn];
int n, m, ans = 0;
int find(int x)
{
    if (x == fa[x]) return fa[x];
    int son = fa[x];
    fa[x] = find (fa[x]);
    rela[x] = (rela[x] + rela[son]) % 3;
    return fa[x];
}
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++) fa[i] = i, rela[i] = 0;
    for (int i = 1; i <= m; i++)
    {
        int op, a, b;
        cin >> op >> a >> b;
        if ((a > n || b > n) || (op == 2 && a == b)) { ans++; continue; }
        if (op == 1) 
        {
            int fx = find(a), fy = find(b);
            if (fx == fy && rela[a] != rela[b]) { ans++; continue; }
            else if (fx != fy) {
                fa[fx] = fy;
                rela[fx] = (3 - rela[a] + rela[b]) % 3; 
            }
        }
        else
        {
            int fx = find(a), fy = find(b);
            if (fx == fy) {
                int k = (rela[a] - rela[b] + 3) % 3;
                if (k != 1) { ans++; continue; }
            }
            else {
                fa[fx] = fy;
                rela[fx] = (3 - rela[a] + rela[b] + 1) % 3;
            }
        }
    }
    cout << ans << endl;
    return 0;
}

我们不妨总结一下:

  • 【查找】:关系是传递的,可以通过累加取模来模拟过程。
  • 【合并】:就是向量运算,需要大家考虑向量关系。

这里推荐一篇博客:带权并查集,他讲的向量关系会更加细一些。

2.2 例题2 银河英雄传说

题意:

有一个划分为 N 列的星际战场,各列依次编号为 1,2,…,N

N 艘战舰,也依次编号为 1,2,…,N 其中第 i 号战舰处于第i 列。

T 条指令,每条指令格式为以下两种之一:

  1. M i j,表示让第 i 号战舰所在列的全部战舰保持原有顺序,接在第 j 号战舰所在列的尾部。
  2. C i j,表示询问第 i 号战舰与第 j 号战舰当前是否处于同一列中,如果在同一列中,它们之间间隔了多少艘战舰。

我们多维护一个数组d,用来存放每个点到父节点的距离。

这里还是并查集的两个操作

  • 【查找】:查找父节点,期间维护路径压缩之后 这个点到根节点的距离+父节点到跟节点的距离,就是d[x] += d[fa[x];
int find (int x)
{
	if (x == fa[x]) return x;
    int root = find(fa[x]); // 计算集合代表
    d[x] += d[fa[x]];       // 维护数组d,对边权求和
    return fa[x] = root;    // 路径压缩
}
  • 【合并】:在合并两列战舰时,其中一列的根节点将变为普通节点,它到另一列的根节点的距离,即为另一列的深度,所以我们还需要维护一个 数组size 来存储每个并查集的深度,以便合并时直接使用
    而在合并后,新的并查集的深度也要随之更新
int merge(int x, int y)
{
    x = find(x), y = find(y);
    fa[x] = y, d[x] = size[y];
    size[y] += size[x];
}

3. 小总结

带权并查集的题型一共就是3种:

  1. 权值体现在集合上,一般开一个size数组来统计集合的大小
    • find函数不影响
    • merge函数 x --> y , size[y] += size[x]
  2. 权值体现在
    • 代表原d数组表示当前节点到父节点的距离
  3. 权值体现在上,不过是要 d[x] % 2
    • 这一类就是关系传递

1 和 2 的综合练习是体现在 银河英雄传说 这个例题里面, 3 则是体现在食物链 这个例题里面。

种类并查集

1. 种类并查集简介

一般的并查集,维护的是具有连通性、传递性的关系,例如亲戚的亲戚是亲戚。但是,有时候,我们要维护另一种关系:敌人的敌人是朋友。种类并查集就是为了解决这个问题而诞生的。

2. 例题深入理解

2.1 例题1 食物链

题意:

一共有A,B,C三类动物,他们之间存在“捕食”,“被捕食”,“同类”三种关系。

种类并查集的写方法是,首先将长度N扩展成3 * n

1 - N 设置为X同类,N + 1 - 2 * N 设置为x可以吃, 2 * N + 1 - 3 * N设置为X的天敌。

若D = 1,则表示X和Y是同类。我们就查询X与Y是否是天敌或可以吃的关系。
若D = 2,则表示X吃Y。我们就查询Y是否可以吃X 或者 X和Y是否是同类就行。

剩下的全是并查集的基本操作,看看注释就可以懂。

【参考代码】

#include <iostream>
#define endl "\n"
using namespace std;
typedef long long ll;
const int maxn = 1e6 + 7;
const int inf = 2147483647;
int fa[maxn];
int find(int x)
{
    return x == fa[x] ? x : fa[x] = find(fa[x]);
}
void merge(int x, int y)
{
    x = find(x), y = find(y);
    if (x != y) fa[x] = y;
}
void solve()
{
    int n, k, ans = 0;
    scanf("%d %d", &n, &k);
    for (int i = 1; i <= 3 * n; i++) fa[i] = i;
    // 1 -- n x同类
    // n + 1 -- 2 * n x可以吃
    // 2 * n + 1 -- 3 * n x天敌
    while (k--)
    {
        int op, x, y;
        scanf("%d %d %d", &op, &x, &y);
        if (x > n || y > n) { ans++; continue; }
        
        if (op == 1){
            if (find(x) == find(y + n) || find(x) == find(y + 2 * n)){ans++; continue;}
            //如果1是2的天敌或猎物,显然为谎言
			else merge(x, y), merge(x + n, y + n), merge(x + 2 * n, y + 2 * n);
			//如果为真,那么1的同类和2的同类,1的猎物是2的猎物,1的天敌是2的天敌
		}
        else{
            if (x == y) { ans++; continue; }
			if (find(x + 2 * n) == find(y) || find(x) == find(y)) {ans++; continue;}
            //如果1是2的同类或猎物,显然为谎言
			else{
                merge(y, x + n),merge(x, y + 2 * n),merge(x + 2 * n,  y + n);
				//如果为真,那么1的同类是2的天敌,1的猎物是2的同类,1的天敌是2的猎物
			}
        }

    }
    printf("%d\n", ans);
}
signed main()
{
    int t = 1;
    while (t--)
    {
        solve();
    }
    return 0;
}


2.1 例题2 Find them, Catch them

题目大意

有两个帮派,有N个人,编号是1- N,有M次询问,每次问两个人,然后又两个操作:

  1. 操作A,输出编号X和编号Y是否在同一阵营。
  2. 操作D,编号X和编号Y不是在同一个阵营。

保证数据的合法性。

因为是有两个阵营,所以我们可以将原来的编号长度N扩展成为长度2 * N。我们将1 - N长度内的集合设置为“朋友”关系, N + 1 - 2 * N的长度内的集合设置成“敌人”关系。如果是“敌人”关系,我们就将XY + N 和 Y与X + N合并在一起。

询问的时候XY在一个阵营就表示是“朋友”关系,XY + N 或者YX + N在一个阵营就代表是“敌人”关系,否则就是不清楚。

【参考代码】

#include <iostream>
#define endl "\n"
using namespace std;
typedef long long ll;
const int maxn = 1e6 + 7;
const int inf = 2147483647;
int fa[maxn];
int find(int x)
{
    return x == fa[x] ? x : fa[x] = find(fa[x]);
}
void merge(int x, int y)
{
    x = find(x), y = find(y);
    if (x != y) fa[x] = y;
}
void solve()
{
    int n, m;
    scanf("%d %d", &n, &m);
    for (int i = 1; i <= 2 * n; i++) fa[i] = i;
    while (m--)
    {
        char op[2];
        int x, y;
        scanf("%s%d%d", &op, &x, &y);
        int xx = find(x), yy = find(y);
        if (op[0] == 'A'){
            if (xx == yy) cout << "In the same gang." << endl;
            else if (xx == find(y + n)) cout << "In different gangs." << endl;
            else cout << "Not sure yet." << endl;
        }
        else merge(xx, y + n), merge(yy, x + n);
    }
}
signed main()
{
    int t;
    scanf("%d", &t);
    while (t--)
    {
        solve();
    }
    return 0;
}

3. 小总结

种类并查集会比带权并查集好写,好理解,能用种类并查集就种类并查集。

题单

题目序号题目出处题目难度
1食物链⭐⭐
2程序自动分析⭐⭐
3银河英雄传说⭐⭐
4Parity game⭐⭐⭐
5Find them, Catch them⭐⭐
6Rochambeau⭐⭐⭐
7A Bug’s Life⭐⭐⭐
8Ubiquitous Religions⭐⭐⭐
9Almost Union-Find⭐⭐⭐
10Building Block⭐⭐⭐
11Exclusive-OR⭐⭐⭐⭐
12Zjnu Stadium⭐⭐⭐
13Code Lock⭐⭐⭐
  • 7
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TUStarry

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值