子段异或和化为两个前缀异或的异或,再按位统计前缀异或中 0/1 的个数,用 cnt0×cnt1 累加每位贡献。
OJ: luogu
题目 ID: P9236
难度:普及+/提高-
标签:异或位运算前缀和计数
创建: 2026-10-02 15:07
更新: 2026-10-03 11:01
形式化题目
给定长度为
即所有连续子段的按位异或和之总和。其中
子段一共有
暴力解法
思路
最直接的做法就是照着定义做:枚举左端点
关键是这样向右扩展时不需要重新算整段:异或满足
所以每往右多吞一个数,只要把现在的异或和再异或上
代码
/**
* 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-03 11:01
* update_at: 2026-10-03 11:01
*/
// brute.cpp:小数据暴力解,直接枚举所有子段求异或和,用来对拍 main.cpp。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 5;
int n;
int a[maxn];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
// 枚举左端点 L,向右扩展右端点 R,边走边维护当前子段的异或和
ll ans = 0;
for (int L = 1; L <= n; L++) {
int now = 0;
for (int R = L; R <= n; R++) {
now ^= a[R]; // [L, R] 的异或和
ans += now;
}
}
cout << ans << "\n";
return 0;
}复杂度
外层
瓶颈
可是题目并不要求我们输出每个子段的异或值,只要求它们的和。那就有机会绕开“逐个算异或值”这一步——只要能直接数出“有多少个子段在某一位上是
正解
思路
第一步:把子段异或换成两个前缀异或的异或。
令
于是“子段
第二步:拆位。
我们关心的不是
这是合法的,因为二进制表示下每一位的贡献可以单独相加,不会互相干扰。
第三步:
- 第
位是 的有 个; - 第
位是 的有 个。
那么从 0 组里挑一个下标、从 1 组里挑一个下标,得到的下标对异或后第
到这里,原来看起来必须逐个枚举的
第四步:按位循环统计。
用样例检验一遍。 样例
| 0 | 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|---|
| 0 | 1 | 3 | 0 | 4 | 1 |
逐位统计:
| 位 |
贡献 |
||||
|---|---|---|---|---|---|
| 0 | 0 1 1 0 0 1 | 3 | 3 | 9 | 9 |
| 1 | 0 0 1 0 0 0 | 5 | 1 | 5 | 10 |
| 2 | 0 0 0 0 1 0 | 5 | 1 | 5 | 20 |
| 全 0 | 6 | 0 | 0 | 0 |
合计
正确性说明。 三处推导各自成立:前缀异或恒等式由消去律保证;拆位相加由二进制表示的唯一性保证;
溢出提醒。 最大可能答案是
int 装不下,必须用 long long。另外单看乘积 (1LL << k) * cnt[0] * cnt[1] 因为最左边是 long long,整个表达式会自动提升,这样写是安全的)。
代码
/**
* 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-03 11:01
* update_at: 2026-10-03 11:01
*/
// P9236 [蓝桥杯 2023 省 A] 异或和之和
// 正解:按位统计前缀异或的 0/1 个数。
// 第 k 位对答案的贡献 = 2^k * (该位上 c[0] * c[1])
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int maxn = 1e5 + 5;
int n;
int a[maxn];
int pre[maxn]; // pre[i] = a[1] ^ a[2] ^ ... ^ a[i]
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
// 预异或:pre[0] = 0,pre[i] = pre[i-1] ^ a[i]
pre[0] = 0;
for (int i = 1; i <= n; i++) {
pre[i] = pre[i - 1] ^ a[i];
}
ll ans = 0;
// 逐位统计。A_i <= 2^20,再加上可能出现的进位,到 20 位足够;
// 多算几位也不会错,这里写到 30 位纯粹为了保险。
for (int k = 0; k <= 30; k++) {
ll cnt[2] = {0, 0};
// 统计前缀异或数组 pre[0..n] 中第 k 位为 0 / 1 的个数
for (int i = 0; i <= n; i++) {
cnt[(pre[i] >> k) & 1]++;
}
// 一 0 一 1 的下标对 (L-1, R) 恰好满足 pre[L-1] != pre[R],
// 也就是区间 [L, R] 的异或和第 k 位为 1,共 cnt[0] * cnt[1] 个区间
ans += (1LL << k) * cnt[0] * cnt[1];
}
cout << ans << "\n";
return 0;
}复杂度
预处理前缀异或
总结
本题的推导链条是:
- 换对象:子段异或不好直接算,用前缀异或换成两个下标值的异或(别忘了
); - 换问题:不需要知道每个异或值,只要求总和,于是拆位,把“求和”变成“数个数”;
- 换计数:某一位为
的异或值个数,正好是“该位为 0 的前缀个数 × 该位为 1 的前缀个数”。
三步的共同点是:每一步都在把更“贵”的对象替换成更“便宜”的等价的量——先去掉区间、再去掉具体数值、最后去掉枚举。这也是处理异或类统计问题最常用的套路:异或的每一位互不干扰,遇到求和就拆位,遇到区间异或就转前缀。
需要留心两个坑: