判断奇偶回答是否矛盾:把区间前缀异或化为带权并查集约束 kind[y]^kind[x]=parity,按出现顺序二分第一个矛盾位置(离散化+可撤销并查集),O(m log m)。
OJ: roj
题目 ID: 3107
难度:省选/NOI-
标签:并查集带权并查集离散化二分答案
创建: 2026-10-01 17:10
更新: 2026-10-07 13:50
形式化题目
01 序列 OK 类答案,按题目约定输出)。
正解
思路
1. 前缀异或化
令
每个回答变成一条异或约束:
2. 带权并查集
维护 kind[x] =
- 查询:
同根时, ;与新约束矛盾则该回答撒谎。 - 合并:异根时按
挂接,按秩合并控制树高,路径压缩时用「从根往回」顺序修正 kind。
3. 找第一个矛盾 + 离散化
- 端点离散化(
个回答至多 个不同端点); - 回答矛盾具有单调性:前
个回答若已矛盾,之后必然持续矛盾。按回答顺序在线增量维护也可直接得到第一个矛盾(本题即按序加入、遇矛盾即报);若需要「前 k 个是否矛盾」则二分 + 可撤销并查集。
本实现按回答顺序在线加入,第一个使约束冲突的回答即为答案。
代码
Python 版:
python
#!/usr/bin/env python3
# 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-01 17:28
# update_at: 2026-10-01 17:28
import sys
ODD = 1 # 回答编码:1 = 奇数个 1(前缀异或不同),0 = 偶数个 1(前缀异或相同)
def find(kind: dict[int, int], parent: dict[int, int], x: int) -> int:
"""路径压缩地返回 x 的根,并把 kind[x] 修正为 x 到根的异或。"""
path: list[int] = []
while parent[x] != x:
path.append(x)
x = parent[x]
root, acc = x, 0
for node in reversed(path): # 从靠近根的一端往回走,先修正父亲再修正儿子
acc ^= kind[node]
kind[node] = acc # node 到根的异或
parent[node] = root # 顺带把这条路径压平
return root
def add_constraint(
kind: dict[int, int],
parent: dict[int, int],
size: dict[int, int],
x: int,
y: int,
parity: int,
) -> bool:
"""加入约束 `kind[y] ^ kind[x] = parity`;与已有约束矛盾时返回 False。"""
rx, ry = find(kind, parent, x), find(kind, parent, y)
if rx == ry:
return kind[x] ^ kind[y] == parity # 同集合:已有约束的推论必须与本次回答一致
if size[rx] < size[ry]: # 小树挂到大树,控制树高
rx, ry, x, y = ry, rx, y, x
parent[ry] = rx
kind[ry] = kind[x] ^ kind[y] ^ parity # 使 y 到 x 的新路径异或恰好是 parity
size[rx] += size[ry]
return True
def solve() -> None:
data = sys.stdin.buffer.read().split()
it = iter(data)
length = int(next(it)) # 序列长度(只界定前缀下标范围,判定过程不用它)
m = int(next(it)) # 回答数量
kind: dict[int, int] = {} # kind[v] = 点 v 到其父亲的异或(根恒为 0)
parent: dict[int, int] = {}
size: dict[int, int] = {}
answer = m # 所有回答都不矛盾时,能撑过的回答数就是 M
for i in range(m):
left = next(it)
right = next(it)
word = next(it)
x, y = int(left) - 1, int(right) # 前缀坐标:S[l..r] 对应点 l-1 与点 r
for v in (x, y): # 坐标只有 2M 个且 N 可达 1e9,按需开点
parent.setdefault(v, v)
kind.setdefault(v, 0)
size.setdefault(v, 1)
parity = ODD if word == b"odd" else 0
if not add_constraint(kind, parent, size, x, y, parity):
answer = i # 第 i 条回答与前面矛盾,说明序列能同时满足的是前 i 条
break
print(answer) # length 只界定前缀下标范围,判定过程与它无关
if __name__ == "__main__":
solve()C++ 版(同一算法):
cpp
/**
* 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-06 18:57
* update_at: 2026-10-06 18:57
*/
#include <cstdio>
#include <cstring>
#include <algorithm>
typedef long long ll;
// 带权并查集:kind[x] = x 到其所在集合根的异或值
const int MAXM = 20005; // M <= 10000,每个回答两个端点,至多 2M 个坐标
int fa[MAXM]; // 并查集父亲
int kind[MAXM]; // kind[x] = 前缀异或值 P[x] ^ P[fa[x]](到父亲的异或),find 后修正为到根
int rnk[MAXM]; // 按秩合并用的树高
// 离散化后的坐标点
int xs[MAXM]; // 所有出现过的端点(l-1 与 r)
ll tot; // 离散化后点的个数
struct Query {
ll l, r; // 存的是前缀坐标 l-1 与 r(原始值,未离散化)
int parity; // 0 = 偶数个 1,1 = 奇数个 1
};
Query qs[MAXM]; // qs[i] = 第 i 个回答(下标从 1 开始)
int m; // 回答数量
// 朴素 find:从 x 一路向上,同时累加异或值,再统一路径压缩
// 注意必须「先靠根的先修正」:递归返回时自根向叶累加 kind
int find_root(int x) {
if (fa[x] == x) return x;
int root = find_root(fa[x]);
kind[x] ^= kind[fa[x]]; // 父亲的 kind 此时已是「父亲到根」的异或
fa[x] = root;
return root;
}
// 加入约束 kind[y] ^ kind[x] = parity;与已有约束矛盾返回 false
bool add_constraint(int x, int y, int parity) {
int rx = find_root(x), ry = find_root(y);
if (rx == ry) {
// 同一集合:P[x] ^ P[y] 应等于 kind[x] ^ kind[y]
return (kind[x] ^ kind[y]) == parity;
}
// 异根:按秩合并,把矮的挂到高的下面
if (rnk[rx] < rnk[ry]) {
int t = rx; rx = ry; ry = t;
t = x; x = y; y = t;
}
fa[ry] = rx;
kind[ry] = kind[x] ^ kind[y] ^ parity; // 保证 y 到 x 的路径异或恰为 parity
if (rnk[rx] == rnk[ry]) rnk[rx]++;
return true;
}
// 离散化:返回原始值 v 在 xs 中的下标(下标从 1 开始)
int id(ll v) {
int low = 1, high = tot;
while (low < high) {
int mid = (low + high) / 2;
if (xs[mid] < v) low = mid + 1;
else high = mid;
}
return low;
}
int main() {
ll n;
scanf("%lld %d", &n, &m); // n 只界定前缀下标范围,判定过程与它无关
for (int i = 1; i <= m; i++) {
char word[10];
scanf("%lld %lld %s", &qs[i].l, &qs[i].r, word);
// S[l..r] 的 1 的个数奇偶 = P[l-1] ^ P[r],前缀坐标取 l-1 与 r
qs[i].l = qs[i].l - 1;
qs[i].parity = (word[0] == 'o') ? 1 : 0;
}
// 收集所有端点做离散化(N <= 1e9,坐标不能直接开数组)
tot = 0;
for (int i = 1; i <= m; i++) {
xs[++tot] = qs[i].l;
xs[++tot] = qs[i].r;
}
std::sort(xs + 1, xs + tot + 1);
tot = std::unique(xs + 1, xs + tot + 1) - (xs + 1);
// 并查集初始化:每个离散化坐标是一个独立点
for (int i = 1; i <= tot; i++) {
fa[i] = i;
kind[i] = 0;
rnk[i] = 0;
}
// 按回答顺序逐条加入,第一条与前面矛盾的回答就是答案
for (int i = 1; i <= m; i++) {
int x = id(qs[i].l), y = id(qs[i].r);
if (!add_constraint(x, y, qs[i].parity)) {
printf("%d\n", i - 1); // 前 i-1 条回答可以满足,第 i 条矛盾
return 0;
}
}
printf("%d\n", m); // 所有回答都自洽
return 0;
}复杂度
- 时间复杂度:
(离散化排序)。 - 空间复杂度:
。
总结
- 「奇偶个数」类问题的标准变形:前缀异或 → 异或约束 → 带权并查集。
- 带权并查集的关键:
kind存「到根的异或」,路径压缩时按靠近根的顺序累加修正。 - 回答矛盾具有单调性,配合离散化与可撤销并查集可以二分出精确的「第一个谎言」。