本题目是一道非常好的并查集练习题,有两种并查集的解法,分别是扩展域并查集、带权并查集,下面分别进行介绍:
一、扩展域并查集
1、三点认知
-
两个同类元素的天敌集合是同一个集合,猎物集合也是同一个集合。
-
天敌的天敌是猎物 (因为是食物环嘛)
-
猎物的猎物是天敌 (因为是食物环嘛)
2、扩展域思想
\(1\sim n\)个元素扩大为\(1\sim 3n\)个元素,使用\([1,3n]\)个并查集(每一个并查集中的所有元素都具有同一种特性,不同并查集中不存在相同元素)来维护\(3n\)元素彼此的关系。
在这里\(x\)元素,\(x+n\)元素,\(x+2n\)元素三者的关系被定义为:
\(x\)元素所在集合中所有\(∈[1,n]\)的元素都是\(x\)元素的同类
\(x+n\)元素所在集合中所有\(∈[1,n]\)的元素都是\(x\)元素的天敌
\(x+2n\)元素所在集合中所有\(∈[1,n]\)的元素都是\(x\)元素的猎物
\(x+n\)元素所在的集合中所有\(∈[1,n]\)的元素都是\(x+2n\)元素的猎物
当得到一句关于两个元素\(x,y\)彼此关系的描述时,如果已知目前\(x,y\)它们各自的猎物和天敌,以及它们是否是同类,就可以判断这句描述的真假。
可以通过\(x+n\)元素来确定\(x\)元素目前已知的天敌,也可以通过\(x+2n\)元素来确定\(x\)元素目前的猎物,还可以通过\(x\)元素本身来确定\(x\)的同类。
于是就能够进行语言真假的判断了!
对于一句真话:
当\(x\)元素,\(y\)元素是同类时,将他们两者的天敌集合(\(x+n\)元素与\(y+n\)元素所在集合)和猎物集合(\(x+2n\)元素与\(y+2n\)元素所在集合)以及自身所在的集合分别合并。
当\(x\)元素是\(y\)元素的天敌时,将\(x\)元素所在集合与\(y\)元素的天敌集合合并,将\(y\)元素所在集合和\(x\)元素的猎物集合合并,将\(x\)元素的天敌集合和\(y\)元素的猎物集合合并
3、实现代码
#include<bits/stdc++.h>
using namespace std;
//扩展域并查集
const int N = 150010;
int p[N];
int ans;
/**
* 功能:寻找祖先
* @param x
* @return
*/
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
//加入家族集合中
void join(int x, int y) {
int f1 = find(x), f2 = find(y);
if (f1 != f2)p[f1] = f2;
}
int main() {
int n, m;
cin >> n >> m;
//开三个并查集,合用一个数组表示
//对于每种生物:设x为本身,x+n为猎物,x+2*n为天敌
for (int i = 1; i <= 3 * n; i++) p[i] = i; //初始化
while (m--) {
int x, y, t;
cin >> t >> x >> y;
// 太大了,越界了,肯定是假话
if (x > n || y > n) {
ans++;
continue;
}
//xy同类
if (t == 1) {
//如果y是x的天敌或猎物,为谎言
if (find(x + n) == find(y) || find(x + 2 * n) == find(y)) {
ans++;
continue;
}
//x的同类和y的同类,x的猎物是y的猎物,x的天敌是y的天敌
join(x, y);
join(x + n, y + n);
join(x + 2 * n, y + 2 * n);
}
//y是x的猎物
if (t == 2) {
//如果x是y的同类或猎物,为谎言
if (find(x) == find(y) || find(x + 2 * n) == find(y)) {
ans++;
continue;
}
join(x, y + 2 * n);//x的同类是y的天敌
join(x + n, y);//x的猎物是y的同类
join(x + 2 * n, y + n); //x的天敌是y的猎物
}
}
printf("%d\n", ans);
return 0;
}
二、带权并查集
功能:查询祖先+修改父节点为祖先+更新节点到根的距离(通过到父节点的距离累加和)
\(d[i]\)的含义:第 \(i\) 个节点到其父节点距离。
C++ 代码
#include <bits/stdc++.h>
using namespace std;
const int N = 50010;
const int M = 3;
/**
带权并查集:
(1)不管是同类,还是吃与被吃的关系,都把它们放到同一个集合中去
(2)关系用距离根结点的长度来描述: 0:同类,1:猎物, 2:天敌
(3)通过上面的记录关系,就可以推理出任何两个节点的关系。
*/
int n, m;
int p[N]; //父亲是谁
int d[N]; //i结点到父结点的距离
int res; //假话的个数
/**
* p是指节点i的父节点是谁
* d是指节点i到自己父亲的距离
* d[x]=1 : x吃根结点
* d[x]=2 : x被根结点吃
* d[x]=0 : x与根结点是同类
* @param x
* @return
*/
int find(int x) {
if (p[x] != x) {
//这里将最祼并查集的代码拆开了
int t = find(p[x]);
//在代码中间加了一个维护距离的代码,之所以这样做,是因为这里需要使用p[x]更新压缩路径后d[x]的值,不用破坏
//下面更新距离的代码,只能加在两者中间的位置,因为现在的d[p[x]]其实是指父结点到根的距离(因为路径压缩的原因)
//而不是原来意义上父结点到爷爷结点的距离
d[x] = (d[p[x]] + d[x]) % M;
//修改p[x]
p[x] = t;
}
return p[x];
}
int main() {
cin >> n >> m;
//并查集初始化
for (int i = 1; i <= n; i++) p[i] = i;
//m个条件
while (m--) {
int t, x, y;
cin >> t >> x >> y;
//如果出现x,y的序号,大于最大号,那么肯定是有问题,是假话
if (x > n || y > n) {
res++;
continue;
}
//找出祖先
int px = find(x), py = find(y);
//t==1,同类
if (t == 1) {
//同类,要合并并查集
if (px != py) {
p[px] = py;
d[px] = (d[y] - d[x] + M) % M;
} else if ((d[x] - d[y] + M) % M > 0) res++;//不是同类,结果值++
} else {
//t==2,表示X吃Y
//不在同一个家族内,描述刚录入进来的信息,肯定是真话,记录这个信息
if (px != py) {
//不同类,也要合并并查集
p[px] = py;
d[px] = (d[y] + 1 - d[x] + M) % M;
} else if ((d[y] + 1 - d[x] + M) % M > 0) res++; //同一个家族,但距离并不是1,那就是错误答案
}
}
//输出
printf("%d\n", res);
return 0;
}
代码解释: