带权并查集 & 种类并查集
文章目录
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.合并:并查集合并的本质就是一棵树认另一棵树做父亲,把树根相连即可,但是能否也把权值直接赋值,还有考虑各自与树根的关系,之后就可以实现树根权值的连接了。
我们设 fx
与 fy
分别为 x
与 y
的根,两者权值关系为 分别为re[x] re[y]
, 这时,我们让 x
与 y
的关系为 k
, 则 re[fx]
则为根 fx
与 fy
的关系。
我们来考虑 fx
和 x
, fy
和 y
都是同类,x
和 y
也是同类时,则 : re[x] = 0
, re[y] = 0
主观上fx
与 fy
的关系为同类,考虑向量,则有 re[x] + re[fx] = k + re[y]
等式成立
化简后 re[fx] = k + re[y] - re[x] = 0 + 0 + 0 = 0
, fx
与 fy
是同类。
我们来考虑 fx
和 x
是 捕食, fy
和 y
都是同类,x
和 y
也是同类时,则 : re[x] = 1
, re[y] = 0
主观上fx
与 fy
的关系 fy
吃 fx
,即为 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
条指令,每条指令格式为以下两种之一:
M i j
,表示让第i
号战舰所在列的全部战舰保持原有顺序,接在第j
号战舰所在列的尾部。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种:
- 权值体现在集合上,一般开一个size数组来统计集合的大小
- find函数不影响
- merge函数
x --> y
,size[y] += size[x]
- 权值体现在边上
- 代表原d数组表示当前节点到父节点的距离
- 权值体现在边上,不过是要 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
次询问,每次问两个人,然后又两个操作:
- 操作
A
,输出编号X
和编号Y
是否在同一阵营。 - 操作
D
,编号X
和编号Y
不是在同一个阵营。
保证数据的合法性。
因为是有两个阵营,所以我们可以将原来的编号长度N
扩展成为长度2 * N
。我们将1
- N
长度内的集合设置为“朋友”关系, N + 1
- 2 * N
的长度内的集合设置成“敌人”关系。如果是“敌人”关系,我们就将X
与Y + N
和 Y与X + N
合并在一起。
询问的时候X
和Y
在一个阵营就表示是“朋友”关系,X
和Y + N
或者Y
和X + 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 | 银河英雄传说 | ⭐⭐ |
4 | Parity game | ⭐⭐⭐ |
5 | Find them, Catch them | ⭐⭐ |
6 | Rochambeau | ⭐⭐⭐ |
7 | A Bug’s Life | ⭐⭐⭐ |
8 | Ubiquitous Religions | ⭐⭐⭐ |
9 | Almost Union-Find | ⭐⭐⭐ |
10 | Building Block | ⭐⭐⭐ |
11 | Exclusive-OR | ⭐⭐⭐⭐ |
12 | Zjnu Stadium | ⭐⭐⭐ |
13 | Code Lock | ⭐⭐⭐ |