题目地址:
https://www.acwing.com/problem/content/description/258/
给定一个非负整数序列
a
a
a,初始长度为
N
N
N。有
M
M
M个操作,有以下两种操作类型:
A x
:添加操作,表示在序列末尾添加一个数
x
x
x,序列的长度
N
N
N增大
1
1
1。
Q l r x
:询问操作,你需要找到一个位置
p
p
p,满足
l
≤
p
≤
r
l≤p≤r
l≤p≤r,使得:
a
[
p
]
∧
a
[
p
+
1
]
∧
…
∧
a
[
N
]
∧
x
a[p] \wedge a[p+1] \wedge … \wedge a[N] \wedge x
a[p]∧a[p+1]∧…∧a[N]∧x最大,输出这个最大值。这里的
N
N
N是当前序列长度。
输入格式:
第一行包含两个整数
N
N
N,
M
M
M,含义如问题描述所示。第二行包含
N
N
N个非负整数,表示初始的序列
A
A
A。接下来
M
M
M行,每行描述一个操作,格式如题面所述。
输出格式:
每个询问操作输出一个整数,表示询问的答案。每个答案占一行。
数据范围:
N
,
M
≤
3
×
1
0
5
,
0
≤
a
[
i
]
≤
1
0
7
N,M≤3×10^5,0≤a[i]≤10^7
N,M≤3×105,0≤a[i]≤107。
开一个前缀异或数组 s s s,其中 s [ 0 ] = 0 , s [ i + 1 ] = s [ i ] ∧ a [ i + 1 ] s[0]=0,s[i+1]=s[i]\wedge a[i+1] s[0]=0,s[i+1]=s[i]∧a[i+1],即 s [ i ] = ⋀ k ≤ i a [ k ] s[i]=\bigwedge_{k\le i} a[k] s[i]=⋀k≤ia[k],那么 a [ p ] ∧ a [ p + 1 ] ∧ … ∧ a [ N ] ∧ x = s [ p − 1 ] ∧ s [ N ] ∧ x a[p] \wedge a[p+1] \wedge … \wedge a[N] \wedge x=s[p-1]\wedge s[N]\wedge x a[p]∧a[p+1]∧…∧a[N]∧x=s[p−1]∧s[N]∧x。而 s [ N ] ∧ x s[N]\wedge x s[N]∧x是可以实时计算出来的,所以本质上我们就是要求一个 s [ p − 1 ] s[p-1] s[p−1]使得 s [ p − 1 ] ∧ ( s [ N ] ∧ x ) s[p-1]\wedge (s[N]\wedge x) s[p−1]∧(s[N]∧x)最大,其中 l − 1 ≤ p − 1 ≤ r − 1 l-1\le p-1\le r-1 l−1≤p−1≤r−1。如果没有 p p p的取值范围的限制的话,就是仅仅找到一个数使得一个异或值最大,这可以用 0 − 1 0-1 0−1Trie来做,对于某个数 x x x的二进制表示从高位到低位遍历,同时在Trie上面走,尽量走不同的路径即可(尽量让最高位不同,这样异或出来的 1 1 1就尽可能在高位)。然而这里有取哪些值的限制,我们可以用持久化Trie来做。
先介绍一下持久化Trie是什么数据结构。首先它本质上还是一棵Trie树,但是它可以保存历史上各个版本的信息。具体来说,它提供每一个版本的“入口”,从这个入口进去可以得到历史上某次保存的“快照”的信息。例如,考虑
0
−
1
0-1
0−1Trie,在添加了数字
a
,
b
a,b
a,b之后,我们可以选择保存一下快照作为版本
1
1
1,然后接着又添加了数字
c
,
d
c,d
c,d,然后又保存一下快照作为版本
2
2
2,然后接着又添加了
e
e
e。普通的Trie能告诉我们现在Trie里存了
a
,
b
,
c
,
d
,
e
a,b,c,d,e
a,b,c,d,e这
5
5
5个数字,而可持久化Trie可以让我们回到版本
2
2
2,告诉我们在那个历史时刻,Trie里存了
a
,
b
,
c
,
d
a,b,c,d
a,b,c,d这
4
4
4个数;也可以让我们回到版本
1
1
1,告诉我们那个时候Trie里存了
a
,
b
a,b
a,b这
2
2
2个数。所以说可持久化Trie就是一种可以像普通Trie一样可以添加数,而且还能支持看一下历史上某个版本的Trie是什么样的数据结构(普通的Trie是不支持修改和删除的,这里的可持久化Trie里我们也不支持这两个操作)。朴素的做法就是每当快照的时候,我们就完全拷贝一个一模一样的Trie,然后再在新的Trie上继续操作。显然这样做时间空间都是非常浪费的。于是我们产生了这样一种想法,即,对于没有被修改的部分,我们直接重用上一个版本的,对于修改的部分,我们new出新的空间。举个例子:
如上图,我们考虑一棵Trie,它先后添加了"cat",“rat”,"cab"和"fry"这
4
4
4个单词,并且在每次添加的时候都去做快照。我们用
p
p
p指向上一个版本的树根,并且当添加了第
k
k
k个单词完成的时候,我们就说那个时候的Trie的版本就是
k
k
k。考虑一个普通的情形,比如:
第
1
1
1个版本已经存了“cat”,
p
p
p指向了第
1
1
1个版本的树根,然后来了一个单词“rat”,首先复制出一个新的树根
q
q
q作为版本
2
2
2的入口,接下来要插入’r’,那么除了’r’之外的路径都是不会被修改的,所以我们就看一下
p
p
p下面不等于‘r’的路径有哪些,把那些路径直接接到
q
q
q下面(对应的就是第二个小图里右边那个树根直接伸出一个‘c’树枝连到了左边那个节点上),接着把’r’树枝接到
q
q
q下面,然后将
p
p
p和
q
q
q都沿着各自的’r’树枝向下走,
p
p
p走到了空节点上,而
q
q
q走到了new出来的新节点上;接着要插入‘a’,也是先看一下
p
p
p有哪些不会被修改的树枝可以接在
q
q
q下面的,然而现在
p
p
p已经空了,所以没东西可以接,就直接在
q
q
q下面再new出‘a’树枝,以此类推再new出‘t’树枝。这样就完成了‘rat’的插入。我们看到如果只提供版本
2
2
2的树根的话,整个Trie就像是存了“cat”和“rat”的Trie一样,和普通Trie没有任何区别;同时我们也能回到版本
1
1
1那个Trie。但是版本
2
2
2的Trie和版本
1
1
1的共用了一条没被修改过的路径,从而节省了很多空间。
再比如,第
2
2
2个版本已经存了“cat”和“rat”,
p
p
p指向了第
2
2
2个版本的树根,然后来了一个单词“cab”。首先复制出一个新的树根
q
q
q作为版本
3
3
3的入口,接下来要插入’c’,同样的,我们看一下
p
p
p里不会被修改的路径有哪些,只有’r’路径不会被修改,于是就把‘r’那个节点接到
q
q
q的下面,然后new出‘c’树枝。注意,虽然版本
2
2
2里确实有‘c’树枝,但是我们不能用,因为‘c’树枝是将要插入新单词“cab”的,如果用了版本
2
2
2里的’c‘树枝,就把版本
2
2
2给修改了,将来如果要提供版本
2
2
2的入口的时候,就会得到“cab”这个单词,这是不对的(想象一下如果插入的不是“cab”而是“cata”,如果重用了版本
2
2
2里面的“cat”路径,最后的’a‘是没法处理的,这就是为什么对于要被修改的路径,即使之前的版本可以重用也不能用)。接着将
p
p
p和
q
q
q都沿着’c’树枝走,要插入字符’a‘了,此时
p
p
p里只有’a‘一条树枝,所以要在
q
q
q下面再new一个’a‘树枝,接着继续将
p
p
p和
q
q
q都沿着’a’树枝走,要插入字符’b‘了,此时
p
p
p下面的’t‘树枝是不会被修改的,于是把’t’树枝指向的节点接到
q
q
q下面,然后
q
q
q下面再new出’b‘树枝,然后继续将
p
p
p和
q
q
q都沿着’b’树枝走,插入完成。
我们可以将版本
0
0
0看成是一棵空树,空树里添加一个字符串和普通Trie里添加是一样的。
如上图所示,左图是第
3
3
3个版本,右图是第
4
4
4个版本。
总结来说,在可持久化Trie里保存快照后添加字符串的过程可以这样描述:
先new一个树根,然后将沿着字符串里的字符向下走,凡是不可能被修改的分叉,我们就把分叉指向的节点直接接到新树的对应节点下面;有可能被修改的分叉,就直接new出来。要获得哪个版本的信息,就直接给出那个版本的树根即可。
我们回到这道题。要找到 s [ p − 1 ] s[p-1] s[p−1]使得 s [ p − 1 ] ∧ ( s [ N ] ∧ x ) s[p-1]\wedge (s[N]\wedge x) s[p−1]∧(s[N]∧x)最大,其中 l − 1 ≤ p − 1 ≤ r − 1 l-1\le p-1\le r-1 l−1≤p−1≤r−1。我们可以每次插入 s [ k ] s[k] s[k]的时候,就开一个新的版本 k k k,这样一来,如果在询问的时候,我们总在版本 r − 1 r-1 r−1里去找最优的 s [ p − 1 ] s[p-1] s[p−1],这样找到的数的下标就都小于等于 r − 1 r-1 r−1了,就解决了右边界的问题。对于左边界,我们只需要维护每个节点子树存的数字的最大下标,如果这个最大下标大于等于 l − 1 l-1 l−1,就说明这个节点和它父亲的这个分叉是可以走的,因为下面能找到某个数,其下标大于等于 l − 1 l-1 l−1。因为最大化异或值是个贪心的思想,每一步只要能走 ∼ v \sim v ∼v就一定要走。
算法如下:
1、维护一个前缀异或数组
s
s
s,
s
[
0
]
=
0
s[0]=0
s[0]=0,我们先建立版本
0
0
0的Trie,里面只含
0
0
0这个数。同时维护一个
m
i
d
m_{id}
mid这个数组,
m
i
d
[
p
]
m_{id}[p]
mid[p]表示的是从
p
p
p的父亲走到
p
p
p这个分叉之后,能走到的下标最大的那个数
s
s
s的下标是多少。注意
t
r
[
0
]
tr[0]
tr[0]这个位置存的是空节点,空节点对应的
m
i
d
[
0
]
m_{id}[0]
mid[0]应该使得这条路不能走,而
s
s
s里的数的下标最小是
0
0
0,所以令
m
i
d
[
0
]
=
−
1
m_{id}[0]=-1
mid[0]=−1就行了;
2、接着将序列
a
a
a里的
N
N
N个数依次插入Trie中,每次插入
a
[
k
]
a[k]
a[k]的时候就开一个新的版本
k
k
k,同时维护数组
m
i
d
m_{id}
mid;
3、然后回答询问,如果是添加操作,那么就得到新的
s
[
N
]
s[N]
s[N],接着将这个
s
[
N
]
s[N]
s[N]加入Trie中,并且开一个新版本
N
N
N;如果是询问操作,则先算出当前
s
[
N
]
∧
x
s[N]\wedge x
s[N]∧x,然后找到
s
[
p
−
1
]
s[p-1]
s[p−1]使得
s
[
p
−
1
]
∧
(
s
[
N
]
∧
x
)
s[p-1]\wedge (s[N]\wedge x)
s[p−1]∧(s[N]∧x)最大,其中
l
−
1
≤
p
−
1
≤
r
−
1
l-1\le p-1\le r-1
l−1≤p−1≤r−1,所以我们去找第
r
−
1
r-1
r−1个版本的Trie,然后沿着
s
[
N
]
∧
x
s[N]\wedge x
s[N]∧x尽可能沿着“另一条路”走,能走“另一条路”当且仅当另一条路走下去能走到下标大于等于
l
−
1
l-1
l−1的节点,否则走不了另一条路,则沿着相同路走。走到最后一个节点的时候,选出的数字在
s
s
s里的下标可以由
m
i
d
m_{id}
mid数组立即得到,将其取出并与
s
[
N
]
∧
x
s[N]\wedge x
s[N]∧x做异或即可。
关于数据范围,前缀和一共有 N = 2 ∗ 3 ∗ 1 0 5 = 6 ∗ 1 0 5 N=2* 3*10^5=6*10^5 N=2∗3∗105=6∗105个, a [ i ] a[i] a[i]的范围是 1 0 7 10^7 107,而 2 23 < 1 0 7 < 2 24 2^{23}<10^7<2^{24} 223<107<224,所以每个数字加上新的树根一共会占 25 25 25个节点,所以节点数要开 25 N 25N 25N这么多。但是很多节点事实上是共用的,实际用不了这么大。程序的空间限制是516MB,足够了。代码如下:
# include<iostream>
# include<cstring>
using namespace std;
const int N = 600010, M = N * 25, POS = 23;
int n, m;
int s[N];
int tr[M][2], max_id[M];
// root[i]存的是第i个版本的树根下标
int root[N], idx;
// 将s[k]这个数插入到Trie里,上一个版本的树根下标是p,新版本的树根下标是q
void insert(int k, int p, int q) {
for (int i = POS; i >= 0; i--) {
// 取出二进制位
int v = s[k] >> i & 1;
// 如果p不空,那么把用不到的那条路复用,即接到q下面
if (p) tr[q][v ^ 1] = tr[p][v ^ 1];
// 发生修改的路径开一个新节点
tr[q][v] = ++idx;
// 同时向下沿着v挪一步
p = tr[p][v], q = tr[q][v];
// 记录一下走到q这个位置能走到的数在s里的下标最大值
max_id[q] = k;
}
}
// 从下标为root的那个版本的Trie搜索与C异或最大的路径,L是左边界,返回最大异或
int query(int root, int C, int L) {
int p = root;
for (int i = POS; i >= 0; i--) {
int v = C >> i & 1;
// 尽量走和v不同的路,贪心思想。能走当且仅当v ^ 1那条路存在下标大于等于L的数;
// 不能走的话那就走v这条路
if (max_id[tr[p][v ^ 1]] >= L) p = tr[p][v ^ 1];
else p = tr[p][v];
}
return C ^ s[max_id[p]];
}
int main() {
scanf("%d%d", &n, &m);
// 为了防止走到空节点上,将空节点的下标设为-1
max_id[0] = -1;
// 把0前缀加入Trie,作为版本1
root[0] = ++idx;
insert(0, 0, root[0]);
for (int i = 1; i <= n; i++) {
int x;
scanf("%d", &x);
s[i] = s[i - 1] ^ x;
// 给新版本的树根开辟个空间
root[i] = ++idx;
insert(i, root[i - 1], root[i]);
}
char op[2];
int l, r, x;
while (m--) {
scanf("%s", op);
if (*op == 'A') {
scanf("%d", &x);
n++;
s[n] = s[n - 1] ^ x;
root[n] = ++idx;
insert(n, root[n - 1], root[n]);
} else {
scanf("%d%d%d", &l, &r, &x);
printf("%d\n", query(root[r - 1], s[n] ^ x, l - 1));
}
}
return 0;
}
每次插入和查询时间复杂度 O ( 1 ) O(1) O(1),因为树高是个定值,空间 O ( N ) O(N) O(N)。