1. 题目来源
链接:3485. 最大异或和
相关题目:[字典树] 最大异或对(trie+贪心)
2. 题目解析
trie
的经典应用。
三种区分度:
- 三重暴力,维护起点
i
、终点j
、循环[i, j]
区间求异或值即可,过掉n <= 100
的数据,过20%
。 - 两重暴力,优化上述做法。固定起点,向右枚举终点,边枚举边计算当前区间的异或总和,这样就不需要在重复计算区间内的异或和了,优化掉一个
n
,过掉n<=1000
的数据,过50%
。 - 最终做法,
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),也可看作是
O
(
32
n
)
O(32n)
O(32n),跟数位长度相关。采用前缀和 +
trie
树 + 滑动窗口 + 贪心 这几个算法思想来做,笔试中中等偏上的题目了。在下面思路详细写一下。
思路:
- 预处理异或前缀和。连续子数组,对应一段区间,即区间异或和,可以直接预处理 异或前缀和,即
s[i] = s[i - 1] ^ x
。设s[i] = 0^a1^a2^..^ai, s[j]=0^a1^a2^...^aj
,那么[i, j]
区间的异或和即为s[i:j]=s[j] ^ s[i - 1]
,因为s[i - 1]
是他们的公共部分,公共部分的异或值为 0,则相当于是s[i:j]=0^ai^a(i+1)^...^aj
,虽然是多异或了一个 0,但是在整体上并不影响,因为0^x=x
,相同为 0,相异为 1。 trie
应用,维护的是异或前缀和。这个的思路和 [字典树] 最大异或对(trie+贪心) 是一致的。贪心的从高位开始,尽量找能将高位置为 1 的trie
路径,这样就能求到最大值。- 滑动窗口的应用。有连续子数组的长度限制,显然需要使用滑动窗口来维护。
- 窗口不足时,就直接将异或前缀和添加进
trie
中,添加前可以求一下当前数会不会更新最大值,就用这个数query(x)
一下即可。query()
操作就是贪心的查找相反位。 - 窗口超过时,需要将
s[i-m-1]
从trie
中删除。故在此就会使用到s[0]
,也是需要将s[0]
提前插入进去。query(s[i])
看一下会不会更新res
,再将s[i]
插入进trie
中。
- 窗口不足时,就直接将异或前缀和添加进
细节:
- 在这的
trie
不是经典的trie
操作,需要支持删除操作,p = son[p][u]; cnt[p] += v; v = 1、-1
,增加的时候就参数传v=1
,删除就参数传v=-1
。insert()
时候给每一个子串都记录一下,加的时候统一加,删的时候一起删。 - 因为题目中所有数都是大于 0 的,所以符号位可以直接忽略,比特位也就
0~30
,故循环从 30 开始递减即可。 s[0]
是无所谓的,不论s[0]
是什么都无所谓被前面公共部分抵消了。
还有些细节,代码中写了点注释。十分不错的题目,从最大异或对,到最大连续异或和,再进化到区间长度限制。第二阶段跳过了,不过这个第三阶段也并不是太难。
时间复杂度:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
空间复杂度:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5+5, M = 4e6; // 10^5 个数,每个数有 32 位,故需要开这么多,trie 节点个数为总长度
int n, m;
int s[N]; // 前缀和异或值,求区间和,一定转化为前缀和
int son[M][2], cnt[M], idx;
void insert(int x, int v) { // v=1 添加数 x, v=-1 删除数 x
int p = 0;
for (int i = 31; ~i; i -- ) {
int u = x >> i & 1;
if (!son[p][u]) son[p][u] = ++ idx;
p = son[p][u];
cnt[p] += v; // 记录 trie 中有多少个数,方便删除打标记
}
}
int query(int x) { // 贪心找相反即可
int res = 0, p = 0;
for (int i = 31; ~i; i -- ) {
int u = x >> i & 1;
if (cnt[son[p][!u]]) p = son[p][!u], res = res * 2 + 1; // 如果 !u 有值的话,res * 2 + 1,相当于二进制右移一位且最低位为 1
else p = son[p][u], res = res * 2; // !u 没有值,则只能为 0,二进制位右移一位,最低位为 0
}
return res;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) {
int x;
cin >> x;
s[i] = s[i - 1] ^ x; // 计算前缀异或和,若 a1^a2^...^a10 = s[10] ^ s[0]; 在此实际上是 0^a1^a2^...^a10=s[10]^s[0],但并不影响
}
int res = 0;
insert(s[0], 1); // 需要使用到 s[0],需要提前将其添加进去
for (int i = 1; i <= n; i ++ ) {
if (i - m > 0) insert(s[i - m - 1], -1); // 维护滑动窗口,删掉 i-m 前面一个数
res = max(res, query(s[i])); // 贪心查找 s[i] 中的最值,最高位开始贪心取反即可,先查再贪和先贪再查一样,自己进不进没影响,异或为 0
insert(s[i], 1); // 将 s[i] 插入进 trie 树中
}
cout << res << endl;
return 0;
}