最大异或和
给定一个非负整数数列 a,初始长度为 N。
请在所有长度不超过 M 的连续子数组中,找出子数组异或和的最大值。
子数组的异或和即为子数组中所有元素按位异或得到的结果。
X O R XOR XOR异或前缀和
X O R XOR XOR的性质:
- a ⊕ a = 0 a \oplus a=0 a⊕a=0
- a ⊕ b = b ⊕ a a \oplus b=b \oplus a a⊕b=b⊕a
- a ⊕ b ⊕ c = a ⊕ ( b ⊕ c ) = ( a ⊕ b ) ⊕ c a \oplus b \oplus c=a \oplus(b \oplus c)=(a \oplus b) \oplus c a⊕b⊕c=a⊕(b⊕c)=(a⊕b)⊕c;
- a ⊕ b ⊕ a = b a \oplus b \oplus a=b a⊕b⊕a=b
- a ⊕ b = ! a ⊕ ! b a \oplus b=! a \oplus ! b a⊕b=!a⊕!b
- ! a ⊕ b = a ⊕ ! b = ! ( a ⊕ b ) ! a \oplus b=a \oplus ! b=!(a \oplus b) !a⊕b=a⊕!b=!(a⊕b)
我们可以将前缀和的定义推广:
在 a [ 0 ] ∼ a [ n ] \mathrm{a}\left[ 0 \right] \sim \mathrm{a}\left[ \mathrm{n} \right] a[0]∼a[n]序列上定义运算 ⋅ \cdot ⋅,如果 ⋅ \cdot ⋅有逆则可以使用前缀和的技巧。 0 0 0到 k k k位上的前缀和可以表示为 a [ 0 ] ⋅ a [ 1 ] ⋅ a [ 2 ] ⋯ a [ k ] \mathrm{a}\left[ 0 \right] \cdot \mathrm{a}\left[ 1 \right] \cdot \mathrm{a}\left[ 2 \right] \cdots \mathrm{a}\left[ \mathrm{k} \right] a[0]⋅a[1]⋅a[2]⋯a[k]。
我们让 ⋅ \cdot ⋅为异或 ⊕ \oplus ⊕, ( i ⊕ j ) ⊕ j = i \left( i\oplus j \right) \oplus j=\,\,i (i⊕j)⊕j=i满足逆运算,所以可以使用前缀和计算部分区间的异或和。如对于 X O R XOR XOR前缀和数组 S S S 有 S [ l ∼ r ] = S [ r ] ⊕ S [ l − 1 ] \mathrm{S}\left[ l\sim r \right] \,\,=\,\,\mathrm{S}\left[ r \right] \oplus \mathrm{S}\left[ l-1 \right] S[l∼r]=S[r]⊕S[l−1]。
- 另, 0 0 0到 4 n − 1 4n-1 4n−1的所有数一起异或的和为 0 0 0,如 3 7 11 3\ 7\ 11 3 7 11 …,原理是每两位会不断出现 00 , 01 , 10 , 11 00,01,10,11 00,01,10,11,对应位上两个 1 1 1异或为 0 0 0
解法1 暴力 O ( n 2 ) O(n^2) O(n2)
我们可以枚举 i , j i,j i,j作为起点和终点,预处理异或前缀和后,每次计算区间长度小于 m m m的最大异或值,更新 m a x max max。
#include <iostream>
using namespace std;
const int N = 1e6 + 10, M = 31 * N;
int a[N];
int s[N];
int get(int l, int r) {
//由于n^a^a等价于n自身,故前缀和计算时可以通过异或l-1部分来把r中l-1的部分去掉
return s[r] ^ s[l - 1];
}
int main() {
int n, m;
cin >> n >> m;
int x, maxn = 0;
for (int i = 1; i <= n; ++i) {
scanf("%d", &a[i]);
s[i] = s[i - 1] ^ a[i];//前缀异或和
}
int res = 0;
for (int i = 1; i <= n; ++i) {
for (int j = i; j <= n && j < i + m; ++j) {
res = max(res, get(i, j));//
}
}
cout << res << endl;
return 0;
}
这样可以过 80 80% 80数据。
解法2 Tire树 O ( n l o g n ) O(nlogn) O(nlogn)
加入 X O R XOR XOR前缀和后问题就变成就最大异或对了,这个问题可以由 T r i e Trie Trie树优化。
将数字变成二进制,在 T i r e Tire Tire树中存储(二叉树)。假设前面已经存储了 k k k个值,第 k + 1 k+1 k+1个数与前 k k k个数之间的最大异或值怎么找呢?只需要对k从高位到低位遍历,每次尝试找当前这一位的反向(是 0 0 0就找 1 1 1,是 1 1 1就找 0 0 0),让异或值最大。如果没有反向就顺着走。所以 T r i e Trie Trie树可以一次查询一个值和一个区间中最大的异或对。
故本题树中维护 m m m长度的区间,每次查询准备新加入的值,更新 m a x max max。如果区间长度 > m >m >m就删除区间的第一个数。删除只需要将插入的值改成 − 1 -1 −1。
总复杂度就是遍历 ∗ * ∗查询,查询只要看数字的位数, O ( n l o g n ) O(nlogn) O(nlogn)文章来源:https://uudwc.com/A/NZ6XX
详细代码
注意看推的样例!文章来源地址https://uudwc.com/A/NZ6XX
/*
* @Author: ACCXavier
* @Date: 2021-05-10 22:07:21
* @LastEditTime: 2022-03-18 10:32:38
* Bilibili:https://space.bilibili.com/7469540
* 题目地址:https://www.acwing.com/problem/content/3488/
* @keywords:
*/
#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 100010 * 31, M = 100010;//每一个数字最多31个分叉,故开31*1e5
int n, m;
int s[M];
int son[N][2], cnt[N], idx;
void insert(int x, int v) {//插入/删除x 取决于v是1还是-1
int p = 0;
for (int i = 30; i >= 0; i--) {
int u = x >> i & 1;
if (!son[p][u]) son[p][u] = ++idx;
//!接下来两行顺序不能反,因为此时p
p = son[p][u];
cnt[p] += v; // 每一位都记录
}
}
int query(int x) {//查询最大值
int res = 0, p = 0;
for (int i = 30; i >= 0; i--) {
int u = x >> i & 1;
if (cnt[son[p][!u]]) // 看反向点是否存在
p = son[p][!u], res = res * 2 + 1;
else // 没有反向就顺着走,这条路肯定是存在的。最坏就是000...0
p = son[p][u], res = res * 2;
}
return res;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
int x;
scanf("%d", &x);
s[i] = s[i - 1] ^ x;
}
int res = 0;
insert(s[0], 1);//至少要插入0,与0异或是自己
for (int i = 1; i <= n; i++) {
if (i > m) insert(s[i - m - 1], -1);//剔除前面的
// 遍历到i 只会查询i - m ~ i - 1共m长度
res = max(res, query(s[i]));//意会一下
//要求的是长度<=m的最大异或值,,每次i更新,都相当于走到当前m区间的最后一个数字,前面的数字都已经插入trie集合,且前面的最大值已经更新,然后当前的数字只需要进入trie集合找最大值即可 这个是从1开始递推的 比如m=3
//i为1 trie集合只有0,最大就是1处的值 0101
//i为2 trie集合0101,当前1000,异或得1100,1100为最大值,同时1000存入trie
//i为3 trie 0101,1000 ,当前1110,按照trie树走第一位取0,故按照0101异或,得1011 最大值不更新
//i为4 trie 1000,1110(0101已经被删除),当前0001,按照trie树走1110,得1111,更新为最大值
/*
上述例子输入测试
4 3
5
8
14
1
结果为15 也就是1111
*/
insert(s[i], 1);//插入当前序列
}
printf("%d\n", res);
return 0;
}