[字典树] aw3485. 最大异或和(trie变种+贪心+前缀和+滑动窗口+美团2021)

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=-1insert() 时候给每一个子串都记录一下,加的时候统一加,删的时候一起删。
  • 因为题目中所有数都是大于 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;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值