2025真题 | 异或和 luogu-P14359
CSP-J 2025真题- 异或和,动态规划考点,适合GESP六级考生练习,难度⭐⭐⭐☆☆,洛谷难度等级普及/提高−。
P14359 [CSP-J 2025] 异或和
题目要求
题目描述
小 R 有一个长度为 的非负整数序列 。定义一个区间 () 的权值为 的二进制按位异或和,即 ,其中 表示二进制按位异或。
小 X 给了小 R 一个非负整数 。小 X 希望小 R 选择序列中尽可能多的不相交的区间,使得每个区间的权值均为 。两个区间 相交当且仅当两个区间同时包含至少一个相同的下标,即存在 使得 且 。
例如,对于序列 ,若 ,则小 R 可以选择区间 和区间 ,权值分别为 和 ;若 ,则小 R 可以选择区间 和区间 ,权值分别为 和 。
你需要帮助小 R 求出他能选出的区间数量的最大值。
输入格式
输入的第一行包含两个非负整数 ,分别表示小 R 的序列长度和小 X 给小 R 的非负整数。
输入的第二行包含 个非负整数 ,表示小 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 可以选择区间 和区间 ,异或和分别为 和 。可以证明,小 R 能选出的区间数量的最大值为 。
【样例 2 解释】
小 R 可以选择区间 和区间 ,异或和分别为 和 。可以证明,小 R 能选出的区间数量的最大值为 。
【样例 3 解释】
小 R 可以选择区间 ,异或和为 。可以证明,小 R 能选出的区间数量的最大值为 。注意:小 R 不能同时选择区间 和区间 ,因为这两个区间同时包含下标 。
【样例 4】
见选手目录下的 与 。
该样例满足测试点 的约束条件。
【样例 5】
见选手目录下的 与 。
该样例满足测试点 的约束条件。
【样例 6】
见选手目录下的 与 。
该样例满足测试点 的约束条件。
【数据范围】
对于所有测试数据,保证:
- , ;
- 对于所有 ,均有 。
| 测试点编号 | 特殊性质 | ||
|---|---|---|---|
| A | |||
| B | |||
| A | |||
| B | |||
| C | |||
| C | |||
| 无 | |||
| B | |||
| C | |||
| 无 | |||
| C | |||
| 无 |
特殊性质 A: 对于所有 ,均有 。
特殊性质 B: 对于所有 ,均有 。
特殊性质 C: 对于所有 ,均有 。
题目分析
本题考查 异或性质、前缀和思想 以及 动态规划 (DP)。
1. 核心数学性质:前缀异或和
题目关心的区间 的异或和。对于区间求和问题,最常用的技巧是 前缀和。同样地,对于区间异或,我们可以使用 前缀异或和。
定义 为序列前 个元素的异或和:
特别地,。
根据异或的性质,特别是 归零律()和 结合律,我们可以推导出区间异或和的公式:
推导过程如下:
展开前缀异或和定义:
- 表示从第 个到第 个元素的异或和:
- 表示从第 个到第 个元素的异或和:
进行异或运算:我们将 和 进行异或:
利用结合律和交换律:将相同的项放在一起(即 对 ,…, 对 )。由于异或运算满足交换律和结合律,我们可以随意调整运算顺序:
利用归零律消去:因为任何数异或自己都等于 0(),前面的公共部分全部抵消变为 0:
这正是区间 的异或和。这个性质非常重要,它让我们能够在 的时间内通过两个前缀值的异或,快速求出任意子区间的异或和,类似于前缀和求子区间和()。
题目要求找到最多的不相交区间,使得每个区间的异或和都为 。 也就是说,如果我们要选一个以 结尾的区间 ,必须满足:
根据异或的交换律,这等价于:
这意味着,当我们遍历到位置 时,我们只需要在它前面找到一个位置 (即 ),满足 ,那么区间 就是一个符合条件的区间。
2. 动态规划 (DP) 设计
这是一个典型的线性 DP 问题。设 表示在前 个数中,最多能选出多少个符合条件的不相交区间。
对于当前位置 ,我们有两种选择:
不选以 结尾的区间:此时我们只需继承前面的最优结果,即 。
选一个以 结尾的区间 : 这就要求区间异或和为 ,即 (其中 )。 为了让总区间数最大,我们应该选择一个满足条件的 ,使得 尽可能大。 此时状态转移方程为:
综合起来,状态转移方程为:
为了更直观地理解上述 DP 转移过程,我们以 样例 1 为例进行手动模拟:
场景设定:
- 序列:
a = [2, 1, 0, 3](下标 ) - 目标异或值:
- 前缀异或数组
pre:
DP 模拟过程:
(数值 2):
- 继承: 至少为 。
- 尝试选区间:需找 使得 。
- 发现 ,符合条件!构成区间 。
- 决策:。
(数值 1):
- 继承: 至少为 。
- 尝试选区间:需找 使得 。
- 此前 值为 ,无 。无法构成新区间。
- 决策:。
(数值 0):
- 继承: 至少为 。
- 尝试选区间:需找 使得 。
- 此前 值为 ,无 。
- 决策:。
(数值 3):
- 继承: 至少为 。
- 尝试选区间:需找 使得 。
- 发现 ,符合条件!这意味着区间 (即 ) 的异或和为 。
- 决策:。
最终答案: 。
通过这个过程可以看到,DP 的核心在于:在每一个位置 i,我们既要“守成”(继承 dp[i-1]),也要“进取”(查找是否存在 pre[j] 满足条件,从而接上一个新的区间)。
🤔 核心疑问解答:如何保证区间不重叠?
很多同学可能会问:“为什么直接用 就可以?怎么保证 里统计的那些区间,和当前新选的区间 不冲突?”
答案就在下标上:
- 的含义是:在下标 的范围内,最多能选多少个不相交区间。也就是说,这里面所有被选中的区间,结束位置都 。
- 我们新选的区间是 ,它的起始位置是 。
- 因为 ,所以新区间 和 范围内的任何区间在物理位置上都是完全错开的,绝对不可能有重叠。这就是线性 DP 的妙处,通过下标的自然分割保证了“无后效性”和“无重叠”。
3. 优化 DP (贪心 + 桶/哈希表)
直接套用上面的 DP 方程,对于每个 都要回头遍历找 ,时间复杂度是 ,这对于 的数据规模是会超时的。我们需要优化查找过程。
观察转移方程:。 我们需要快速找到满足 的所有 中, 最大的那个值。
我们可以维护一个数组(桶)max_val[v],用来记录:在前缀异或和为 v 的所有位置中,最大的 DP 值是多少。
即 max_val[v] = max(dp[j]),其中 pre[j] == v。
这样,当我们处理到位置 时:
- 计算当前前缀异或和
curr = pre[i]。 - 计算我们需要寻找的目标前缀异或值
target = curr ^ k。 - 查找
max_val[target]。如果max_val[target]存在(即之前出现过满足条件的前缀和),说明我们可以形成一个新区间,此时候选答案为max_val[target] + 1。 - 同时,不要忘了 至少可以继承 (即代码中的
dp变量在循环开始时就保留了上一轮的值)。 - 更新桶:计算出 后,用它来更新
max_val[curr],供后面使用。
4. 算法流程总结
- 初始化数组
max_val为 -1(表示该异或值从未出现过),特别地max_val[0] = 0(表示还未开始时,前缀异或为 0,区间数为 0)。 - 维护一个变量
dp,表示当前位置的最大区间数。 - 遍历序列,维护当前的前缀异或和
curr_xor。 - 对于每个位置:
- 计算需要寻找的目标前缀值
target = curr_xor ^ k。 - 如果
max_val[target]有效,尝试更新dp = max(dp, max_val[target] + 1)。 - 更新当前前缀异或值的记录:
max_val[curr_xor] = max(max_val[curr_xor], dp)。
- 计算需要寻找的目标前缀值
- 最后输出
dp即为答案。
这种做法的时间复杂度是 ,空间复杂度是 (取决于数值范围,题目中 )。
示例代码
#include <algorithm>
#include <iostream>
#include <vector>
const int MAX_VAL = 1 << 20; // 2^20 = 1048576, 题目限制 a_i < 2^20, k < 2^20
int max_dp[MAX_VAL];
int main() {
// 优化 I/O 速度,避免大量输入输出导致超时
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int n;
int k;
std::cin >> n >> k;
// 初始化 max_dp 数组
// max_dp[v] 存储的是:当某个位置的前缀异或和为 v
// 时,该位置及之前能得到的最大区间数
for (int i = 0; i < MAX_VAL; ++i) {
max_dp[i] = -1;
}
// 初始状态:还没有读取任何数时,前缀异或和为 0,区间数为 0
max_dp[0] = 0;
int current_xor = 0; // 当前的前缀异或和 (pre[i])
int dp = 0; // 当前位置的最大区间数 (dp[i])
for (int i = 1; i <= n; ++i) {
int a;
std::cin >> a;
current_xor ^= a; // 计算到当前位置 i 的前缀异或和
// 步骤 A: 尝试以当前位置 i 作为区间的结尾
// 我们需要找一个之前的断点 j-1,使得 pre[j-1] ^ pre[i] == k
// 即 pre[j-1] == pre[i] ^ k
int target = current_xor ^ k;
// 如果之前存在某个位置的前缀异或和等于 target
if (max_dp[target] != -1) {
// 尝试以当前位置为结尾构成一个新区间
// dp[i] = max(dp[i-1], max_dp[target] + 1)
// 注意:这里的 dp 变量实际上在迭代过程中一方面代表
// dp[i-1],更新后代表 dp[i] 因为 dp[i] 至少是
// dp[i-1],所以直接比较即可
dp = std::max(dp, max_dp[target] + 1);
}
// 更新当前前缀异或值的最大 dp 值
// 如果当前 dp 值比之前记录的更大,则更新
if (dp > max_dp[current_xor]) {
max_dp[current_xor] = dp;
}
}
std::cout << dp << std::endl;
return 0;
}本文由coderli.com原创,按照CC BY-NC-SA 4.0 进行授权
所有代码已上传至Github:https://github.com/lihongzheshuai/yummy-code
“luogu-”系列题目可在 洛谷题库 在线评测。
“bcqm-”系列题目可在 编程启蒙题库 在线评测。
GESP/CSP认证交流QQ群: 688906745
