题解:AcWing 239 奇偶游戏
【题目来源】
AcWing:239. 奇偶游戏 - AcWing题库
【题目描述】
小 \(A\) 和小 \(B\) 在玩一个游戏。
首先,小 \(A\) 写了一个由 \(0\) 和 \(1\) 组成的序列 \(S\),长度为 \(N\)。
然后,小 \(B\) 向小 \(A\) 提出了 \(M\) 个问题。
在每个问题中,小 \(B\) 指定两个数 \(l\) 和 \(r\),小 \(A\) 回答 \(S[l∼r]\) 中有奇数个 \(1\) 还是偶数个 \(1\)。
机智的小 \(B\) 发现小 \(A\) 有可能在撒谎。
例如,小 \(A\) 曾经回答过 \(S[1∼3]\) 中有奇数个 \(1\),\(S[4∼6]\) 中有偶数个 \(1\),现在又回答 \(S[1∼6]\) 中有偶数个 \(1\),显然这是自相矛盾的。
请你帮助小 \(B\) 检查这 \(M\) 个答案,并指出在至少多少个回答之后可以确定小 \(A\) 一定在撒谎。
即求出一个最小的 \(k\),使得 \(01\) 序列 \(S\) 满足第 \(1∼k\) 个回答,但不满足第 \(1∼k+1\) 个回答。
【输入】
第一行包含一个整数 \(N\),表示 \(01\) 序列长度。
第二行包含一个整数 \(M\),表示问题数量。
接下来 \(M\) 行,每行包含一组问答:两个整数 \(l\) 和 \(r\),以及回答 even 或 odd,用以描述 \(S[l∼r]\) 中有偶数个 \(1\) 还是奇数个 \(1\)。
【输出】
输出一个整数 \(k\),表示 \(01\) 序列满足第 \(1∼k\) 个回答,但不满足第 \(1∼k+1\) 个回答,如果 \(01\) 序列满足所有回答,则输出问题总数量。
【输入样例】
10
5
1 2 even
3 4 odd
5 6 even
1 6 even
7 10 odd
【输出样例】
3
【核心思想】
-
问题分析:给定长度为 \(N\) 的 01 序列和 \(M\) 个区间奇偶性回答(区间 \([l, r]\) 中 1 的个数为偶数或奇数),求前多少个回答自洽,第几个回答开始出现矛盾。这是一个带权并查集问题,关键在于将区间奇偶性约束转化为前缀和节点之间的异或关系。
-
算法选择:
- 带权并查集(Extended Union-Find):维护每个节点到根节点的异或值(奇偶性),支持 \(O(\alpha(n))\) 的合并与查询
- 前缀和转化:区间 \([l, r]\) 中 1 的个数的奇偶性等价于前缀和 \(sum[r]\) 与 \(sum[l-1]\) 的奇偶性差异,即 \(sum[r] \oplus sum[l-1] = t\)
- 离散化(Hash/Map):下标范围大,需要映射为连续编号
-
关键步骤:
- 前缀和建模:设 \(sum[i]\) 为前 \(i\) 项中 1 的个数的前缀和,则区间 \([l, r]\) 中 1 的个数的奇偶性为 \(sum[r] \oplus sum[l-1]\)
- 离散化:使用
unordered_map将 \(l-1\) 和 \(r\) 映射为连续编号(因为 \(l-1\) 和 \(r\) 的范围可能达到 \(10^9\)) - 初始化带权并查集:
p[i] = i,`d[i] = 0$(每个节点独立,到自身的异或值为 0) - 带权查找(路径压缩):
- 递归查找根节点
u = find(p[x]) - 更新
d[x] ^= d[p[x]],使d[x]表示 \(x\) 到根节点的异或值 p[x] = u路径压缩
- 递归查找根节点
- 处理每个回答 \((a, b, t)\),其中 \(a = l-1, b = r\):
- 若 \(a\) 和 \(b\) 在同一集合:
- 检查
d[a] ^ d[b] == t,若不等则前 \(i-1\) 个回答自洽,第 \(i\) 个矛盾,记录答案并退出
- 检查
- 若不在同一集合:
- 合并:
p[pa] = pb - 设置权值:
d[pa] = d[a] ^ d[b] ^ t,保证合并后 \(a\) 到 \(b\) 的异或值为 \(t\)
- 合并:
- 若 \(a\) 和 \(b\) 在同一集合:
-
时间/空间复杂度:
- 时间复杂度:\(O(M \cdot \alpha(N))\),其中 \(\alpha\) 为阿克曼函数反函数,近似常数。每次查找或合并操作均摊 \(O(\alpha(N))\)
- 空间复杂度:\(O(N)\),并查集父节点数组、权值数组和离散化映射表
-
带权并查集的核心思想:
- 异或关系传递:通过维护每个节点到根节点的异或值,快速查询任意两节点间的奇偶性关系
- 路径压缩更新权值:在路径压缩时,利用异或的传递性
d[x] ^= d[p[x]]更新到根的距离 - 合并时设置权值:合并两个集合时,通过
d[pa] = d[a] ^ d[b] ^ t保证新集合内所有异或关系一致 - 前缀和降维:将区间问题转化为两个端点的前缀和关系,是处理区间奇偶性/异或约束的经典技巧
- 适用于带奇偶性约束的连通性判断、异或方程组、博弈公平性问题
【算法标签】
并查集
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 200005; // 最大节点数(离散化后最多 2*M+5 个节点)
int n, m; // n: 序列长度(输入值,但随后被重置用于离散化计数), m: 问题数量
int p[N], d[N]; // p: 并查集父节点数组; d[x]: 节点 x 到父节点路径上的奇偶性(异或值)
unordered_map<int, int> S; // 离散化映射:原始下标 -> 连续编号
// 离散化:将原始下标映射为连续编号(从1开始)
int get(int x)
{
if (S.count(x) == 0) S[x] = ++n; // 若该值未出现过,分配新编号(n 从0递增)
return S[x]; // 返回该下标对应的编号
}
// 带权并查集查找(路径压缩),同时维护到根节点的奇偶性关系
int find(int x)
{
if (p[x] != x)
{
int u = find(p[x]); // 递归查找根节点
d[x] ^= d[p[x]]; // 更新 x 到根节点的奇偶性:d[x] = d[x] ^ d[父节点]
p[x] = u; // 路径压缩:将 x 直接指向根节点
}
return p[x]; // 返回根节点
}
int main()
{
cin >> n >> m; // 读入序列长度 N 和问题数量 M
for (int i = 1; i <= N; i++) p[i] = i; // 初始化并查集:每个节点自成一个集合(注意用输入的N而非n)
n = 0; // 重置 n,用于离散化计数
int res = m; // 默认答案:若所有回答都自洽,则输出 m
for (int i = 1; i <= m; i++)
{
int a, b;
string type;
cin >> a >> b >> type; // 读入区间 [a, b] 和回答类型
a = get(a - 1), b = get(b); // 离散化:将前缀和节点 (a-1) 和 b 映射为编号
int t = 0; // t = 0 表示 even(偶数个1),t = 1 表示 odd(奇数个1)
if (type == "odd") t = 1;
int pa = find(a), pb = find(b); // 查找 a 和 b 的根节点
if (pa == pb) // 若 a 和 b 已在同一集合
{
// 检查奇偶性是否矛盾:d[a]^d[b] 表示区间 [a,b] 中1的个数的奇偶性
if ((d[a] ^ d[b]) != t)
{
res = i - 1; // 前 i-1 个回答自洽,第 i 个回答导致矛盾
break; // 找到矛盾,提前退出
}
}
else // 若 a 和 b 不在同一集合,合并
{
p[pa] = pb; // 将 pa 的根指向 pb 的根
// 设置 pa 到 pb 的奇偶性关系:d[pa] = d[a] ^ d[b] ^ t
// 保证合并后:d[a] ^ d[b] = t(即区间 [a,b] 中1的个数奇偶性为 t)
d[pa] = d[a] ^ d[b] ^ t;
}
}
cout << res << endl; // 输出最多自洽的回答数量
return 0;
}
【运行结果】
10
5
1 2 even
3 4 odd
5 6 even
1 6 even
7 10 odd
3
浙公网安备 33010602011771号