【ACWing】256. 最大异或和

题目地址:

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 lpr,使得: 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,M3×105,0a[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]=kia[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[p1]s[N]x。而 s [ N ] ∧ x s[N]\wedge x s[N]x是可以实时计算出来的,所以本质上我们就是要求一个 s [ p − 1 ] s[p-1] s[p1]使得 s [ p − 1 ] ∧ ( s [ N ] ∧ x ) s[p-1]\wedge (s[N]\wedge x) s[p1](s[N]x)最大,其中 l − 1 ≤ p − 1 ≤ r − 1 l-1\le p-1\le r-1 l1p1r1。如果没有 p p p的取值范围的限制的话,就是仅仅找到一个数使得一个异或值最大,这可以用 0 − 1 0-1 01Trie来做,对于某个数 x x x的二进制表示从高位到低位遍历,同时在Trie上面走,尽量走不同的路径即可(尽量让最高位不同,这样异或出来的 1 1 1就尽可能在高位)。然而这里有取哪些值的限制,我们可以用持久化Trie来做。

先介绍一下持久化Trie是什么数据结构。首先它本质上还是一棵Trie树,但是它可以保存历史上各个版本的信息。具体来说,它提供每一个版本的“入口”,从这个入口进去可以得到历史上某次保存的“快照”的信息。例如,考虑 0 − 1 0-1 01Trie,在添加了数字 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[p1]使得 s [ p − 1 ] ∧ ( s [ N ] ∧ x ) s[p-1]\wedge (s[N]\wedge x) s[p1](s[N]x)最大,其中 l − 1 ≤ p − 1 ≤ r − 1 l-1\le p-1\le r-1 l1p1r1。我们可以每次插入 s [ k ] s[k] s[k]的时候,就开一个新的版本 k k k,这样一来,如果在询问的时候,我们总在版本 r − 1 r-1 r1里去找最优的 s [ p − 1 ] s[p-1] s[p1],这样找到的数的下标就都小于等于 r − 1 r-1 r1了,就解决了右边界的问题。对于左边界,我们只需要维护每个节点子树存的数字的最大下标,如果这个最大下标大于等于 l − 1 l-1 l1,就说明这个节点和它父亲的这个分叉是可以走的,因为下面能找到某个数,其下标大于等于 l − 1 l-1 l1。因为最大化异或值是个贪心的思想,每一步只要能走 ∼ 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[p1]使得 s [ p − 1 ] ∧ ( s [ N ] ∧ x ) s[p-1]\wedge (s[N]\wedge x) s[p1](s[N]x)最大,其中 l − 1 ≤ p − 1 ≤ r − 1 l-1\le p-1\le r-1 l1p1r1,所以我们去找第 r − 1 r-1 r1个版本的Trie,然后沿着 s [ N ] ∧ x s[N]\wedge x s[N]x尽可能沿着“另一条路”走,能走“另一条路”当且仅当另一条路走下去能走到下标大于等于 l − 1 l-1 l1的节点,否则走不了另一条路,则沿着相同路走。走到最后一个节点的时候,选出的数字在 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=23105=6105个, 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)

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值