把 A/B/C 环形食物链编码为模 3 关系值,用带权并查集维护每个动物到根的关系,逐句 O(α) 判定真假。
OJ: roj
题目 ID: 3108
难度:提高
标签:并查集食物链python
创建: 2026-10-01 17:27
更新: 2026-10-10 11:30
形式化题目
有
1 X Y:X 与 Y 状态相同;2 X Y:X 的状态是 Y 的"食物"(即 X 吃 Y)。
按顺序处理每条断言,若它与之前所有被判定为真的断言推出的约束矛盾,则该断言为假。求假断言的总数。
正解
思路
朴素做法是枚举每只动物的种族(
给三种状态编号,定义关系值
这正符合带权并查集的形状。维护 d[x] =
X > N或Y > N:假话(越界)。- 语句映射成关系值
: D=1(同类)时; D=2(X 吃 Y)时。 同根:核对 是否等于 ,不等则与前面真话冲突,是假话。注意 “X 吃 X” 会走这条分支: ,自动判假,无需特判。 - 不同根:
之间原本没有任何约束,此句必真,合并两块——设 fa[fx] = fy(、 为各自的根),利用 d[X]、d[Y]分别是、 的关系,把三段路拼起来:
下面用样例的前几句展示关系链如何生长(边上的数字是关系值:
flowchart TD
subgraph 处理「2 1 2」「2 2 3」后
A["1"] --"1(1吃2)"--> B["2"]
B --"1(2吃3)"--> C["3"]
end
随后读入「1 1 3」(1 与 3 同类)。d 值一减即得任意两点关系。
实现上最容易出错的是路径压缩的顺序:d[x] 要累加"父节点对根的关系",必须先压缩父节点。写法上先找根、再收集整条路径,然后从靠近根的一端开始逐点向上累加:
| 累加顺序 | 结果 |
|---|---|
| 自叶向根(错误) | 父的 d 还是旧值(对爷爷的关系),少加一截,链长 |
| 自根向叶(正确) | acc 沿途累加,每点写入的 d 都是对根的完整关系 |
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
// 食物链:带权并查集(模 3 关系)一次扫描判定每句话真假。
#include <cstdio>
typedef long long ll;
const ll MAXN = 50005;
ll fa[MAXN]; // fa[i]: i 的父节点
ll d[MAXN]; // d[i]: i 对根的关系,0同类 / 1 i吃根 / 2 根吃i(模 3 环)
ll path[MAXN];
ll n, k;
// 两遍迭代:先找根,再自根向叶重压路径上的 d 为对根的关系
ll find(ll x) {
ll r = x;
while (fa[r] != r) {
r = fa[r];
}
ll cnt = 0;
ll u = x;
while (u != r) {
path[cnt++] = u;
u = fa[u];
}
ll acc = 0;
for (ll i = cnt - 1; i >= 0; i--) { // 根的子节点先算,边向上边累加
ll v = path[i];
acc = (acc + d[v]) % 3;
d[v] = acc;
fa[v] = r;
}
return r;
}
int main() {
if (scanf("%lld %lld", &n, &k) != 2) return 0; // 空输入安全返回
for (ll i = 0; i <= n; i++) {
fa[i] = i;
d[i] = 0;
}
ll ans = 0;
for (ll i = 0; i < k; i++) {
ll op, x, y;
scanf("%lld %lld %lld", &op, &x, &y);
ll r = op - 1; // op=1 同类 r=0;op=2 x吃y r=1
if (x > n || y > n) { // 假话条件 2:编号越界
ans++;
continue;
}
ll fx = find(x), fy = find(y);
if (fx == fy) { // 已有关系:核对 (d[x]-d[y]) mod 3 是否等于 r
if (((d[x] - d[y] - r) % 3 + 3) % 3) {
ans++; // 与前面真话冲突(含 x 吃 x)
}
} else { // 无矛盾则合并:x→根 = x→y + y→根
fa[fx] = fy;
d[fx] = ((r + d[y] - d[x]) % 3 + 3) % 3;
}
}
printf("%lld\n", ans);
return 0;
}"""食物链:带权并查集(模 3 关系)一次扫描判定每句话真假。"""
import sys
def main() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, k = next(data), next(data)
fa = list(range(n + 1)) # fa[i]: i 的父节点
d = [0] * (n + 1) # d[i]: i 对根的关系,0同类 / 1 i吃根 / 2 根吃i(模 3 环)
ans = 0
def find(x: int) -> int:
"""两遍迭代:先找根,再自根向叶重压路径上的 d 为对根的关系。"""
r = x
while fa[r] != r:
r = fa[r]
path = []
while x != r:
path.append(x)
x = fa[x]
acc = 0
for v in reversed(path): # 根的子节点先算,边向上边累加
acc = (acc + d[v]) % 3
d[v], fa[v] = acc, r
return r
for _ in range(k):
op, x, y = next(data), next(data), next(data)
r = op - 1 # op=1 同类 r=0;op=2 x吃y r=1(关系环上 x→y 的距离)
if x > n or y > n: # 假话条件 2:编号越界
ans += 1
continue
fx, fy = find(x), find(y)
if fx == fy: # 已有关系:核对 (d[x]-d[y]) mod 3 是否等于 r
if (d[x] - d[y] - r) % 3:
ans += 1 # 与前面真话冲突(含 x 吃 x)
else: # 无矛盾则合并:x→根 = x→y + y→根
fa[fx] = fy
d[fx] = (r + d[y] - d[x]) % 3
print(ans)
main()复杂度
并查集带路径压缩,单次操作均摊
总结
本题是带权并查集处理"环形关系"的模板题。核心两步:一是把 A/B/C 环编码成模 3 关系值,使"同类 / 吃 / 被吃"统一为 d 数组与合并公式
图示解析
关系链与假话判定
下图重演样例第 5 句判假的完整状态,节点标注 d 值(对根的关系,根为 1):
flowchart LR
subgraph S1["「2 1 2」「2 2 3」合并后的链,根为 1"]
N1["1<br/>d=0"] -."1".-> N2["2<br/>d=1"] -."1".-> N3["3<br/>d=2"]
end
subgraph S2["读入「1 1 3」,断言 rel(1,3)=0"]
N4["沿链推出 rel(1,3) = 2 - 0 = 2(3 吃 1)"] --> N5["与断言 r=0 不符 → 假话"]
end
样例 7 句中判假的三句分别是:第 1 句(Y = 101 > N 越界)、第 4 句(3 吃 3)、第 5 句(与「2 1 2」「2 2 3」推出的 3 吃 1 冲突);第 6 句「2 3 1」断言 3 吃 1,恰好与链一致,是真话。观察这条链可见:每加入一条约束只需一次合并,而每次判定只依赖两点的 d 值之差——这正是并查集把"关系传递"摊薄到近