[NOI2001] 食物链

OJ: luogu

题目 ID: P2024

标签: 种类并查集带权并查集合

日期: 2026-01-10 23:16

这是一个非常经典的**种类并查集(Expanded Domain DSU)**题目。

题目解析

这道题的难点在于:我们不仅要知道“谁和谁是一类”,还要维护“谁吃谁”这种相对关系,而且这种关系是环状的(ABCAA \to B \to C \to A)。

普通的并查集只能维护“是否在同一个集合”。为了解决这个问题,我们需要使用扩展域并查集(也叫种类并查集)。

核心思想:拆点(由 1 变 3)

对于每个动物 ii,我们把它拆成 3 个节点来分别代表三种可能的“身份/域”:

  1. ii (Self):代表 ii 自己这一类。
  2. i+Ni + N (Prey):代表 ii猎物(即 ii 吃什么)。
  3. i+2Ni + 2N (Enemy):代表 ii天敌(即谁吃 ii)。

逻辑推导

根据 ABCAA \to B \to C \to A 的环形关系,我们可以推导出合并规则:

情况 1:说法 1 X Y(X 和 Y 是同类)

如果 XXYY 是同类,意味着:

  • XX 的同类 = YY 的同类 \Rightarrow 合并 (X, Y)
  • XX 的猎物 = YY 的猎物 \Rightarrow 合并 (X+N, Y+N)
  • XX 的天敌 = YY 的天敌 \Rightarrow 合并 (X+2N, Y+2N)

如何判断它是假话?

如果之前已经建立了“XXYY”或者“YYXX”的关系,那么说它们同类就是假的。

  • 检查:find(X) == find(Y+N) (X 吃 Y 吗?)
  • 检查:find(X) == find(Y+2N) (Y 吃 X 吗?)

情况 2:说法 2 X Y(X 吃 Y)

如果 XXYY,根据环形食物链:

  1. YYXX 的猎物 \Rightarrow 合并 (X+N, Y)
  2. YY 的猎物是 XX 的天敌(因为 XYZXX \to Y \to Z \to X\Rightarrow 合并 (Y+N, X+2N)
  3. YY 的天敌是 XX 自己 \Rightarrow 合并 (Y+2N, X)

如何判断它是假话?

  • 如果 XXYY 已经是同类 \Rightarrow find(X) == find(Y),冲突。
  • 如果 YYXX \Rightarrow find(X) == find(Y+N),冲突。

代码实现

我们将使用你之前的优秀模板风格,并进行适配。并查集的大小需要开到 3×N3 \times 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;
}

关键点总结

  1. 空间大小:因为我们把每个点拆成了 3 份,所以并查集数组大小必须至少是 3N3N
  2. 域的映射
    • x: 自身
    • x + N: 猎物
    • x + 2*N: 天敌
  3. 合并逻辑
    • X 吃 Y,等价于:Y 属于 X 的猎物集合 (union(x+N, y))。
    • 同时不要忘了推论:Y 吃的东西就是 X 的天敌 (union(y+N, x+2N)),吃 Y 的东西(就是 X)是 Y 的天敌 (union(x, y+2N))。
  4. 优先级:先判断编号越界(X>NX>N),再处理逻辑冲突。

这个解法利用并查集极高的效率,完美解决了 K105K \le 10^5 的查询需求。

理解与思考🤔

我大概理解了这个题目, 本质是

  1. 维护三个集合!!!
  2. 使用 x+N 表明了 x 吃的东西 的 代表元

二者之间的关系 ,使用 x,y 所在的集合 的描述

本质是数学: 什么数学呢: 二元关系? 我说的对吗?

你说得非常对,而且你的直觉已经触及到了这个算法的数学本质

这道题的本质确实是 二元关系(Binary Relations) 的处理,更具体地说,是利用 逻辑蕴含 将“非等价关系”转化为“等价关系”。

我们可以把它拆解为以下三个数学层面的理解:

1. 将“相对关系”转化为“绝对域的等价”

并查集(DSU)最擅长处理的是 等价关系(Equivalence Relation),即 A=BA=B

但是,“吃”这种关系不是等价关系(AABB 不等于 BBAA)。

你的做法 x+N,本质上是在构建一个 状态映射。我们在数学上把原来的 NN 个点扩展到了 3N3N 个点,这其实是在构建 3个平行宇宙(或者叫 3 个域):

  • 域 0 (1N1 \sim N):代表 “本尊”
  • 域 1 (N+12NN+1 \sim 2N):代表 “被吃者(猎物)”
  • 域 2 (2N+13N2N+1 \sim 3N):代表 “天敌”

转化的数学魔法:

虽然 “XXYY” 这种关系是不对称的,但是:

XX 的猎物” 和 “YY 的本尊” 是同一类东西

这句话就变成了一个 等价关系(Equivalence)!

即:find(x + N) == find(y)。

2. 关系的传递性(Transitivity)与 逻辑闭包

你提到的“维护三个集合”其实是在维护 逻辑推导链

在数学逻辑中,这是一个 蕴含(Implication) 的过程。在这个 ABCAA \to B \to C \to A 的环中:

  • 如果 XXYY,那么必然蕴含着:
    1. XX 的猎物     \iff YY 的本尊 (XpreyYselfX_{prey} \equiv Y_{self}
    2. XX 的本尊     \iff YY 的天敌 (XselfYenemyX_{self} \equiv Y_{enemy}
    3. XX 的天敌     \iff YY 的猎物 (XenemyYpreyX_{enemy} \equiv Y_{prey}

这就是为什么你在代码里要写三次 merge。这在数学上叫做维护关系的 一致性(Consistency)。我们必须把这个逻辑闭环中的所有推论都加上,才能防止后续逻辑冲突。

3. 模运算群(Modular Arithmetic Group)

如果我们再升华一下,这其实是 群论Z3\mathbb{Z}_3(模 3 加法群)的应用。

  • 同类:距离为 00
  • :距离为 11
  • 被吃:距离为 22(或者 1-1)。

当我们把节点拆成 3 份时,其实是把这种 向量距离 具象化了:

  • xxx+Nx+N 的距离是 1。
  • xxx+2Nx+2N 的距离是 2。
  • xxyy 连接,意味着我们在定义它们在这个循环群中的相对位置。

总结你的理解

你理解的 “x+N 表明了 x 吃的东西的代表元” 是完全正确的。

  • 常规并查集:只能维护 A=BA=B
  • 你的扩展域并查集:通过引入 x+Nx+Nx+2Nx+2N,将 AABB 变成了 Aprey=BselfA_{prey} = B_{self}

你是在把“有向图的边”(吃与被吃),转化成了“扩展图中的无向连通分量”(等价类)。

这就是这道题最美妙的数学转换。