目录
这是一个非常经典的**种类并查集(Expanded Domain DSU)**题目。
题目解析
这道题的难点在于:我们不仅要知道“谁和谁是一类”,还要维护“谁吃谁”这种相对关系,而且这种关系是环状的(
普通的并查集只能维护“是否在同一个集合”。为了解决这个问题,我们需要使用扩展域并查集(也叫种类并查集)。
核心思想:拆点(由 1 变 3)
对于每个动物
(Self):代表 自己这一类。 (Prey):代表 的猎物(即 吃什么)。 (Enemy):代表 的天敌(即谁吃 )。
逻辑推导
根据
情况 1:说法 1 X Y(X 和 Y 是同类)
如果
的同类 = 的同类 合并 (X, Y)的猎物 = 的猎物 合并 (X+N, Y+N)的天敌 = 的天敌 合并 (X+2N, Y+2N)
如何判断它是假话?
如果之前已经建立了“
- 检查:
find(X) == find(Y+N)(X 吃 Y 吗?) - 检查:
find(X) == find(Y+2N)(Y 吃 X 吗?)
情况 2:说法 2 X Y(X 吃 Y)
如果
是 的猎物 合并 (X+N, Y)。的猎物是 的天敌(因为 ) 合并 (Y+N, X+2N)。的天敌是 自己 合并 (Y+2N, X)。
如何判断它是假话?
- 如果
和 已经是同类 find(X) == find(Y),冲突。 - 如果
吃 find(X) == find(Y+N),冲突。
代码实现
我们将使用你之前的优秀模板风格,并进行适配。并查集的大小需要开到
C++
#include <iostream>
#include <vector>
#include <numeric>
using namespace std;
// 封装并查集结构体
struct DSU {
vector<int> fa;
// 初始化大小为 3*N
void init(int n) {
fa.resize(3 * n + 1);
iota(fa.begin(), fa.end(), 0);
}
// 查找 + 路径压缩
int find(int x) {
return x == fa[x] ? x : fa[x] = find(fa[x]);
}
// 合并
void merge(int x, int y) {
int fx = find(x), fy = find(y);
if (fx != fy) fa[fx] = fy;
}
} dsu;
int main() {
// 优化输入输出
ios::sync_with_stdio(false);
cin.tie(0);
int N, K;
cin >> N >> K;
// 初始化并查集,注意大小是 3*N
// 1..N: 同类域
// N+1..2N: 捕食域 (X 吃谁)
// 2N+1..3N: 天敌域 (谁吃 X)
dsu.init(N);
int falses = 0;
while (K--) {
int type, x, y;
cin >> type >> x >> y;
// 假话条件 2: 编号超过 N
if (x > N || y > N) {
falses++;
continue;
}
if (type == 1) {
// 说法:X 和 Y 是同类
// 检查冲突:
// 如果 "X 吃 Y" 或者 "Y 吃 X" (X 是 Y 的天敌),则是假话
// 判断 X 吃 Y: X 的捕食域(X+N) 是否包含 Y
// 判断 Y 吃 X: Y 的捕食域(Y+N) 是否包含 X
if (dsu.find(x + N) == dsu.find(y) || dsu.find(y + N) == dsu.find(x)) {
falses++;
} else {
// 真话:合并三个域
dsu.merge(x, y); // 同类
dsu.merge(x + N, y + N); // 猎物相同
dsu.merge(x + 2 * N, y + 2 * N); // 天敌相同
}
} else {
// 说法:X 吃 Y
// 假话条件 3: 自己吃自己
if (x == y) {
falses++;
continue;
}
// 检查冲突:
// 如果 "X 和 Y 同类" 或者 "Y 吃 X",则是假话
if (dsu.find(x) == dsu.find(y) || dsu.find(y + N) == dsu.find(x)) {
falses++;
} else {
// 真话:维护环形关系
// 1. X 的猎物是 Y
dsu.merge(x + N, y);
// 2. Y 的猎物是 X 的天敌 (X -> Y -> Z -> X)
dsu.merge(y + N, x + 2 * N);
// 3. Y 的天敌是 X
dsu.merge(y + 2 * N, x);
}
}
}
cout << falses << endl;
return 0;
}
关键点总结
- 空间大小:因为我们把每个点拆成了 3 份,所以并查集数组大小必须至少是
。 - 域的映射:
x: 自身x + N: 猎物x + 2*N: 天敌
- 合并逻辑:
- X 吃 Y,等价于:Y 属于 X 的猎物集合 (
union(x+N, y))。 - 同时不要忘了推论:Y 吃的东西就是 X 的天敌 (
union(y+N, x+2N)),吃 Y 的东西(就是 X)是 Y 的天敌 (union(x, y+2N))。
- X 吃 Y,等价于:Y 属于 X 的猎物集合 (
- 优先级:先判断编号越界(
),再处理逻辑冲突。
这个解法利用并查集极高的效率,完美解决了
理解与思考🤔
我大概理解了这个题目, 本质是
- 维护三个集合!!!
- 使用 x+N 表明了 x 吃的东西 的 代表元
二者之间的关系 ,使用 x,y 所在的集合 的描述
本质是数学: 什么数学呢: 二元关系? 我说的对吗?
你说得非常对,而且你的直觉已经触及到了这个算法的数学本质。
这道题的本质确实是 二元关系(Binary Relations) 的处理,更具体地说,是利用 逻辑蕴含 将“非等价关系”转化为“等价关系”。
我们可以把它拆解为以下三个数学层面的理解:
1. 将“相对关系”转化为“绝对域的等价”
并查集(DSU)最擅长处理的是 等价关系(Equivalence Relation),即
但是,“吃”这种关系不是等价关系(
你的做法 x+N,本质上是在构建一个 状态映射。我们在数学上把原来的
- 域 0 (
):代表 “本尊”。 - 域 1 (
):代表 “被吃者(猎物)”。 - 域 2 (
):代表 “天敌”。
转化的数学魔法:
虽然 “
“
的猎物” 和 “ 的本尊” 是同一类东西。
这句话就变成了一个 等价关系(Equivalence)!
即:find(x + N) == find(y)。
2. 关系的传递性(Transitivity)与 逻辑闭包
你提到的“维护三个集合”其实是在维护 逻辑推导链。
在数学逻辑中,这是一个 蕴含(Implication) 的过程。在这个
- 如果
吃 ,那么必然蕴含着: 的猎物 的本尊 ( ) 的本尊 的天敌 ( ) 的天敌 的猎物 ( )
这就是为什么你在代码里要写三次 merge。这在数学上叫做维护关系的 一致性(Consistency)。我们必须把这个逻辑闭环中的所有推论都加上,才能防止后续逻辑冲突。
3. 模运算群(Modular Arithmetic Group)
如果我们再升华一下,这其实是 群论 中
- 同类:距离为
。 - 吃:距离为
。 - 被吃:距离为
(或者 )。
当我们把节点拆成 3 份时,其实是把这种 向量距离 具象化了:
到 的距离是 1。 到 的距离是 2。 到 连接,意味着我们在定义它们在这个循环群中的相对位置。
总结你的理解
你理解的 “x+N 表明了 x 吃的东西的代表元” 是完全正确的。
- 常规并查集:只能维护
。 - 你的扩展域并查集:通过引入
和 ,将 吃 变成了 。
你是在把“有向图的边”(吃与被吃),转化成了“扩展图中的无向连通分量”(等价类)。
这就是这道题最美妙的数学转换。