CSP-J 2025-T3 异或和
摘要:本题是 CSP-J 2025 的一道贪心算法题,要求在长度为 n 的序列中选出尽可能多的不相交区间,使得每个区间的异或和等于给定值 k。核心解法是利用前缀异或和的性质(br ⊕ bl-1 = k ⇔bl-1 = br ⊕ k),结合右端点优先的贪心策略,并使用哈希表记录每个前缀异或值最后出现的位置,实现 O(n)时间复杂度的求解。关键易错点包括:p0 = 0 的边界初始化、更新语句的顺序(先判断后登记)、以及数组大小需覆盖 0~2^20-1 的范围。
题目描述
小 R 有一个长度为 n n n 的非负整数序列 a 1 , a 2 , ... , a n a_1, a_2, \dots, a_n a1,a2,...,an。定义一个区间 l , r l, r l,r ( 1 ≤ l ≤ r ≤ n 1 \leq l \leq r \leq n 1≤l≤r≤n) 的权值为 a l , a l + 1 , ... , a r a_l, a_{l+1}, \dots, a_r al,al+1,...,ar 的二进制按位异或和,即 a l ⊕ a l + 1 ⊕ ⋯ ⊕ a r a_l \oplus a_{l+1} \oplus \dots \oplus a_r al⊕al+1⊕⋯⊕ar,其中 ⊕ \oplus ⊕ 表示二进制按位异或。
小 X 给了小 R 一个非负整数 k k k。小 X 希望小 R 选择序列中尽可能多的不相交 的区间,使得每个区间的权值均为 k k k。两个区间 l 1 , r 1 , l 2 , r 2 l_1, r_1, l_2, r_2 l1,r1,l2,r2 相交当且仅当两个区间同时包含至少一个相同的下标,即存在 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n 使得 l 1 ≤ i ≤ r 1 l_1 \leq i \leq r_1 l1≤i≤r1 且 l 2 ≤ i ≤ r 2 l_2 \leq i \leq r_2 l2≤i≤r2。
例如,对于序列 2 , 1 , 0 , 3 2, 1, 0, 3 2,1,0,3,若 k = 2 k = 2 k=2,则小 R 可以选择区间 1 , 1 1, 1 1,1 和区间 2 , 4 2, 4 2,4,权值分别为 2 2 2 和 1 ⊕ 0 ⊕ 3 = 2 1 \oplus 0 \oplus 3 = 2 1⊕0⊕3=2;若 k = 3 k = 3 k=3,则小 R 可以选择区间 1 , 2 1, 2 1,2 和区间 4 , 4 4, 4 4,4,权值分别为 1 ⊕ 2 = 3 1 \oplus 2 = 3 1⊕2=3 和 3 3 3。
你需要帮助小 R 求出他能选出的区间数量的最大值。
输入格式
输入的第一行包含两个非负整数 n , k n, k n,k,分别表示小 R 的序列长度和小 X 给小 R 的非负整数。
输入的第二行包含 n n n 个非负整数 a 1 , a 2 , ... , a n a_1, a_2, \dots, a_n a1,a2,...,an,表示小 R 的序列。
输出格式
输出一行一个非负整数,表示小 R 能选出的区间数量的最大值。
输入输出样例 #1
输入 #1
4 2
2 1 0 3
输出 #1
2
输入输出样例 #2
输入 #2
4 3
2 1 0 3
输出 #2
2
输入输出样例 #3
输入 #3
4 0
2 1 0 3
输出 #3
1
说明/提示
【样例 1 解释】
小 R 可以选择区间 1 , 1 1, 1 1,1 和区间 2 , 4 2, 4 2,4,异或和分别为 2 2 2 和 1 ⊕ 0 ⊕ 3 = 2 1 \oplus 0 \oplus 3 = 2 1⊕0⊕3=2。可以证明,小 R 能选出的区间数量的最大值为 2 2 2。
【样例 2 解释】
小 R 可以选择区间 1 , 2 1, 2 1,2 和区间 4 , 4 4, 4 4,4,异或和分别为 1 ⊕ 2 = 3 1 \oplus 2 = 3 1⊕2=3 和 3 3 3。可以证明,小 R 能选出的区间数量的最大值为 2 2 2。
【样例 3 解释】
小 R 可以选择区间 3 , 3 3, 3 3,3,异或和为 0 0 0。可以证明,小 R 能选出的区间数量的最大值为 1 1 1。注意:小 R 不能同时选择区间 3 , 3 3, 3 3,3 和区间 1 , 4 1, 4 1,4,因为这两个区间同时包含下标 3 3 3。
【样例 4】
见选手目录下的 xor/xor4.in \textbf{\textit{xor/xor4.in}} xor/xor4.in 与 xor/xor4.ans \textbf{\textit{xor/xor4.ans}} xor/xor4.ans。
该样例满足测试点 4 , 5 4, 5 4,5 的约束条件。
【样例 5】
见选手目录下的 xor/xor5.in \textbf{\textit{xor/xor5.in}} xor/xor5.in 与 xor/xor5.ans \textbf{\textit{xor/xor5.ans}} xor/xor5.ans。
该样例满足测试点 9 , 10 9, 10 9,10 的约束条件。
【样例 6】
见选手目录下的 xor/xor6.in \textbf{\textit{xor/xor6.in}} xor/xor6.in 与 xor/xor6.ans \textbf{\textit{xor/xor6.ans}} xor/xor6.ans。
该样例满足测试点 14 , 15 14, 15 14,15 的约束条件。
【数据范围】
对于所有测试数据,保证:
- 1 ≤ n ≤ 5 × 10 5 1 \leq n \leq 5 \times 10^5 1≤n≤5×105, 0 ≤ k < 2 20 0 \leq k < 2^{20} 0≤k<220;
- 对于所有 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n,均有 0 ≤ a i < 2 20 0 \leq a_i < 2^{20} 0≤ai<220。
| 测试点编号 | n ≤ n \leq n≤ | k k k | 特殊性质 |
|---|---|---|---|
| 1 1 1 | 2 2 2 | = 0 =0 =0 | A |
| 2 2 2 | 10 10 10 | ≤ 1 \leq 1 ≤1 | B |
| 3 3 3 | 10 2 10^2 102 | = 0 =0 =0 | A |
| 4 , 5 4, 5 4,5 | ^ | ≤ 1 \leq 1 ≤1 | B |
| 6 ∼ 8 6 \sim 8 6∼8 | ^ | ≤ 255 \leq 255 ≤255 | C |
| 9 , 10 9, 10 9,10 | 10 3 10^3 103 | ^ | ^ |
| 11 , 12 11, 12 11,12 | ^ | < 2 20 < 2^{20} <220 | 无 |
| 13 13 13 | 2 × 10 5 2 \times 10^5 2×105 | ≤ 1 \leq 1 ≤1 | B |
| 14 , 15 14, 15 14,15 | ^ | ≤ 255 \leq 255 ≤255 | C |
| 16 16 16 | ^ | < 2 20 < 2^{20} <220 | 无 |
| 17 17 17 | 5 × 10 5 5 \times 10^5 5×105 | ≤ 255 \leq 255 ≤255 | C |
| 18 ∼ 20 18 \sim 20 18∼20 | ^ | < 2 20 < 2^{20} <220 | 无 |
特殊性质 A: 对于所有 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n,均有 a i = 1 a_i = 1 ai=1。
特殊性质 B: 对于所有 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n,均有 0 ≤ a i ≤ 1 0 \leq a_i \leq 1 0≤ai≤1。
特殊性质 C: 对于所有 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n,均有 0 ≤ a i ≤ 255 0 \leq a_i \leq 255 0≤ai≤255。
思路要点
题意梳理:给你一个长度为 n n n 的数组,让你把它切成尽可能多的小段(子数组)。 每一个小段内部所有数字异或起来的结果必须正好等于 k k k,而且这些小段在数组里不能有任何重叠。求最多能切出多少段。
关键思路
三步优化升级思路:
第一步:看到"区间异或和" ⟹ \implies ⟹ 【前缀异或和】
如果用暴力方法,每选定一个区间 l , r l, r l,r,都要重头把里面的数异或一遍,时间复杂度会爆炸。我们知道普通加法有"前缀和",那异或有没有呢?有!因为异或运算有一个神奇的自反性质:
A ⊕ B = C ⟹ A ⊕ C = B A \oplus B = C \implies A \oplus C = B A⊕B=C⟹A⊕C=B
也就是说,两个相同的数异或会变成 0 0 0。基于这个特性,如果我们求出一个前缀异或和数组 b b b(其中 b i bi bi 表示从第 1 1 1 个数一直异或到第 i i i 个数的结果),那么任意区间 l , r l, r l,r 的异或和就可以瞬间退化为:
区间异或和 = b r ⊕ b l − 1 \text{区间异或和} = br \oplus bl-1 区间异或和=br⊕bl−1
题目要求这个区间的异或和等于 k k k,即:
b r ⊕ b l − 1 = k ⟹ b l − 1 = b r ⊕ k br \oplus bl-1 = k \implies bl-1 = br \oplus k br⊕bl−1=k⟹bl−1=br⊕k
第二步:看到"最多互不相交" ⟹ \implies ⟹ 【右端点优先贪心】
有了上面的数学公式,我们可以选的合法区间可能多得数不过来。怎么选才能让数量最多?这是极其经典的区间调度贪心模型。
-
思考:我们应该枚举左端点还是右端点?
-
破局 :必须枚举右端点 !因为谁结束得越早,留给后面的空白空间就越大,后面才有可能塞下更多的不相交区间。所以,我们应该让外层循环带着右端点 i i i 从 1 1 1 到 n n n 推进。一旦发现以当前 i i i 为右端点能凑出一个合法区间,且不与前面冲突,就立刻选它锁住,并把这个结束位置记下来。
第三步:看到数据范围 n ≤ 5 × 10 5 n \le 5 \times 10^5 n≤5×105 ⟹ \implies ⟹ 【空间换时间(哈希/位置数组)】
外层循环遍历右端点 i i i 已经是 O ( n ) O(n) O(n) 了,如果内层再用一个循环往回找合法的 l − 1 l-1 l−1,整体复杂度就是 O ( n 2 ) O(n^2) O(n2),面对 50 万的数据量一定会超时(TLE)。
既然我们需要在 O ( 1 ) O(1) O(1) 时间内知道"目标前缀和 t = b i ⊕ k t = bi \oplus k t=bi⊕k 上一次出现在哪里",可以开一个大数组 p(充当哈希表),p[t] 专门用来记录前缀和值 t 最后一次(最近一次)出现的下标。
- 为什么要记录最近一次? 因为右端点 i i i 已经固定,左端点前一位 p t pt pt 越靠右(越大),切出来的区间就越短、越紧凑,就越不容易和前面已经选好的区间发生重叠冲突,胜算最大。
解题步骤
我们以样例输入1为例,模拟计算机的执行过程:
Markdown
4 2
2 1 0 3
-
变量定义与初始化: 读入 n = 4 , k = 2 n = 4, k = 2 n=4,k=2。
-
执行
memset(p, -1, sizeof(p));将记录位置的数组全部初始化为 − 1 -1 −1,代表所有前缀和都还没出现过。 -
执行
p[0] = 0;。这是一个重大的隐藏边界!它代表:在什么数字都没选的时候(下标为 0),前缀异或和已经是 0 了。 -
此时:
s = 0(答案计数),rd = 0(上一个选定区间的右端点)。
-
-
循环演练 ( i i i 从 1 到 4 4 4):
-
📌 当 i = 1 i = 1 i=1 时:读入
a[1] = 2。-
计算当前前缀异或和:
b[1] = b[0] ^ a[1] = 0 ^ 2 = 2。 -
计算我们需要的历史目标值:
t = b[1] ^ k = 2 ^ 2 = 0。 -
查表校验 :去查
p[0]。发现p[0] = 0。 -
贪心判断 :执行
if(p[0] >= rd),即if(0 >= 0)。条件成立 !说明区间 1 , 1 1, 1 1,1 合法且不与前面重叠。 -
结算更新 :答案
s++变为1;上一段的右端点更新为rd = 1。 -
入库登记 :记录当前前缀和的位置:
p[b[1]] = 1⟹ \implies ⟹p[2] = 1。
-
-
📌 当 i = 2 i = 2 i=2 时:读入
a[2] = 1。-
计算当前前缀异或和:
b[2] = b[1] ^ a[2] = 2 ^ 1 = 3。 -
计算目标值:
t = b[2] ^ k = 3 ^ 2 = 1。 -
查表校验 :去查
p[1]。发现p[1] = -1(历史上没出现过)。 -
贪心判断 :
if(-1 >= 1)不成立,跳过。 -
入库登记 :
p[b[2]] = 2⟹ \implies ⟹p[3] = 2。
-
-
📌 当 i = 3 i = 3 i=3 时:读入
a[3] = 0。-
计算当前前缀异或和:
b[3] = b[2] ^ a[3] = 3 ^ 0 = 3。 -
计算目标值:
t = b[3] ^ k = 3 ^ 2 = 1。 -
查表校验 :去查
p[1]。依然是-1。 -
贪心判断:不成立,跳过。
-
入库登记 :
p[b[3]] = 3⟹ \implies ⟹p[3] = 3(注意:此时前缀和 3 的最新位置从 2 更新覆盖成了 3!体现了"贪心变短"的思想)。
-
-
📌 当 i = 4 i = 4 i=4 时:读入
a[4] = 3。-
计算当前前缀异或和:
b[4] = b[3] ^ a[4] = 3 ^ 3 = 0。 -
计算目标值:
t = b[4] ^ k = 0 ^ 2 = 2。 -
查表校验 :去查
p[2]。在 i = 1 i=1 i=1 的时候我们登记过p[2] = 1。 -
贪心判断 :执行
if(p[2] >= rd),即if(1 >= 1)。条件成立 !说明找到了一个不与上一段(结束于 1)冲突的新区间 2 , 4 2, 4 2,4。 -
结算更新 :答案
s++变为2;右端点更新为rd = 4。 -
入库登记 :
p[b[4]] = 4⟹ \implies ⟹p[0] = 4。
-
-
-
输出答案: 循环结束,输出最终的答案
s,也就是2。
本题易错点
-
坑一:
p数组的大小问题要点提醒 :
p数组的下标存的是前缀和的值 。题目明确说明 a i < 2 20 a_i < 2^{20} ai<220(十进制是 1048576 1048576 1048576)。也就是说,异或出来的数值最大可能接近 1048575 1048575 1048575。因此p数组要开到1050000左右才合理。 -
坑二:哨兵边界
p[0] = 0的遗漏**要点提醒:**如果不写
p[0] = 0,当遇到第一个元素本身就等于 k k k 的情况(即区间为 1 , 1 1, 1 1,1),此时它需要的 b 0 = 0 b0 = 0 b0=0。但由于初始化成了 − 1 -1 −1,if(-1 >= 0)失败,就会白白漏掉从开头出发的合法区间。 -
坑三:更新语句
p[b[i]] = i;的位置顺序要点提醒:这句话必须 放在
if判断的后面(先结算历史,再登记自己)。如果提到了if前面,当题目给出的 k = 0 k = 0 k=0 时,t = b[i] ^ 0 = b[i],它去查p[t]就会查到刚刚写入的"自己",导致p[t] >= rd永远成立。这在数学上相当于强行切出了一个长度为 0 的虚假空区间,导致答案严重虚高。
参考代码
cpp
#include <bits/stdc++.h>
#define maxn 500005
using namespace std;
int n, k, a[maxn], b[maxn], s;
int p[1050000], rd; // rd:上个区间右端点
int main(){
scanf("%d %d", &n, &k);
memset(p, -1, sizeof(p)); // 初始化为-1,表示该前缀异或和历史上未出现过
p[0] = 0; // 一个元素都不选时,前缀异或和为0,对应下标为0
for(int i = 1; i <= n; i++){
scanf("%d", &a[i]);
b[i] = b[i - 1] ^ a[i]; // 异或前缀和滚动计算
int t = b[i] ^ k; // 根据自反性,所需历史前缀和 b[l - 1] 的目标值是 t
// 贪心防火墙:若目标位置存在且在上一段结束位置之后,说明区间合法且不重叠
if(p[t] >= rd){
s++; // 成功收获一个合法区间,答案加 1
rd = i; // 贪心锁死:立刻将当前 i 更新为最新一轮的右端点防线
}
p[b[i]] = i; // 先结算后登记:把当前前缀和的最新出现位置记录到 p 数组中
}
printf("%d", s);
return 0;
}