本题本质上是:找最多个互不重叠的连续区间,并且每个区间里的所有数异或起来都等于 k。
后面的前缀异或和贪心,都是为了更快地找到这些合法区间。
课堂目标:从异或基础出发,推导前缀异或公式,再用贪心选最多个不相交区间。
本题本质上是:找最多个互不重叠的连续区间,并且每个区间里的所有数异或起来都等于 k。
后面的前缀异或和贪心,都是为了更快地找到这些合法区间。
先复习异或,再讲前缀异或为什么能“抵消”,接着手推样例,最后读代码里的 pre、need、pos、lastEnd。
如果选择区间 [l,r],它必须满足:
并且两个被选中的区间不能共用任何下标。比如选了 [1,3],下一个区间就只能从 4 或更后面开始。
异或符号是 ^。两个二进制位相同得 0,不同得 1。
重点记住:重复的一段异或两次会消失,所以前缀异或可以像前缀和一样求区间。
枚举所有 [l,r],再计算每段异或和。区间数量是 n² 级别,而本题 n ≤ 5×10⁵,不能这样做。
固定右端点 r,不要再枚举全部左端点。用前缀异或把“这一段的值”变成“找以前有没有某个前缀值”。
先不要急着优化。设 dp[r] 表示只看前 r 个数,最多能选多少个不相交的合法区间。
原因是:选了 [l,r] 后,它前面的区间只能放在 1..l-1,这样才不会重叠。
for (int r = 1; r <= n; r++) {
dp[r] = dp[r - 1]; // 不选以 r 结尾的区间
int xr = 0;
for (int l = r; l >= 1; l--) {
xr ^= a[l]; // 当前区间 [l, r] 的异或和
if (xr == k) {
dp[r] = max(dp[r], dp[l - 1] + 1);
}
}
}
这段代码的价值是帮助学生理解“不相交”如何进入状态转移;问题是两层循环,复杂度是 O(n²),本题数据过不了。
设 pre[i] = a[1] ^ a[2] ^ ... ^ a[i],那么:
题目要求区间异或和等于 k:
已知当前的 pre[r],要反推以前的 pre[l-1]:
所以扫描到 r 时,查找 need = pre[r] ^ k 是否出现过即可。
暴力慢在:每个右端点 r 都要向左枚举很多个 l。前缀异或把问题改成:以前有没有出现过 pre[r] ^ k?
于是 need = pre ^ k 后,直接查 pos[need]。如果查到位置 j,合法区间就是 [j+1, r]。
这一步把每个右端点的向左查找从 O(n) 变成数组查询 O(1)。本题值域小于 2²⁰,所以用数组比 map 更直接。
这题是在所有合法区间里选最多个不重叠区间。经典选择方式是:每次选右端点最早的合法区间。
区间越早结束,后面剩下的位置越多。我们从左到右扫描,第一次遇到能结束的合法区间时,它就是当前最早结束的选择,所以可以立刻选。
注意:pos[need] 取最近位置,会让当前区间尽量短;但贪心成立的关键是“右端点最早”,不是单纯因为“区间最短”。
pre[0] = 0 表示“一个数都不选”的空前缀,pos[0] = 0 表示这个空前缀出现在位置 0。
它专门用来发现从第 1 个数开始的合法区间。比如 a[1] = k 时,pre[1] = k,于是 need = pre[1] ^ k = 0,这时必须能找到 pre[0],才能选出区间 [1,1]。
样例:a = [2, 1, 0, 3],k = 2。我们从左到右扫描,每次维护 pre 和 pos。
如果当前 pre = 3,k = 2,应该去找哪个以前出现过的前缀异或值?
为什么要检查 pos[need] >= lastEnd?
如果先执行 pos[pre] = i,再判断 pos[need],当 k = 0 时会发生什么?
提示:这时 need = pre,当前位置可能被自己当成“以前出现过的位置”。
| i | a[i] | pre | need = pre ^ k | 查 pos[need] | 判断与动作 |
|---|---|---|---|---|---|
| 0 | 空 | 0 | - | pos[0] = 0 | 空前缀,给从 1 开始的区间做准备 |
| 1 | 2 | 2 | 0 | pos[0] = 0 | 0 ≥ lastEnd,选 [1,1],ans = 1 |
| 2 | 1 | 3 | 1 | pos[1] 不存在 | 不能选,记录 pos[3] = 2 |
| 3 | 0 | 3 | 1 | pos[1] 不存在 | 不能选,更新 pos[3] = 3 |
| 4 | 3 | 0 | 2 | pos[2] = 1 | 1 ≥ lastEnd,选 [2,4],ans = 2 |
#include <bits/stdc++.h>
using namespace std;
const int MAXV = 1 << 20;
int pos[MAXV];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
cin >> n >> k;
for (int i = 0; i < MAXV; i++) pos[i] = -1;
int pre = 0;
int lastEnd = 0;
int ans = 0;
pos[0] = 0; // 空前缀
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
pre ^= x;
int need = pre ^ k;
if (pos[need] >= lastEnd) {
ans++;
lastEnd = i;
}
// 必须在判断之后更新,避免 k=0 时选到空区间
pos[pre] = i;
}
cout << ans << '\n';
return 0;
}
pos[v] 记录前缀异或值 v 最近一次出现的位置。lastEnd 是上一个被选区间的右端点,用来防止重叠。pos[pre],这是处理 k = 0 的关键边界。初始化 pos 需要 2²⁰ 次,扫描数组需要 n 次,所以时间复杂度是 O(n + 2²⁰)。
前缀异或值范围在 0..2²⁰-1,所以空间复杂度是 O(2²⁰)。
不要把区间异或写成普通减法;不要忘记 pos[0] = 0;不要先更新再判断;不要忘记检查 lastEnd。
pre[l-1] = pre[r] ^ k。lastEnd。pos 当成出现次数,而不是位置。