A Daily Topic # 1 最大异或和(字典树/贪心)

A Daily Topic # 1

最大异或和

给定一个非负整数数列 a,初始长度为 N。

请在所有长度不超过 M 的连续子数组中,找出子数组异或和的最大值。

子数组的异或和即为子数组中所有元素按位异或得到的结果。

注意:子数组可以为空。

输入格式

第一行包含两个整数 N,M。

第二行包含 N 个整数,其中第 i 个为 ai。

输出格式

输出可以得到的子数组异或和的最大值。

数据范围

对于 20% 的数据,1≤M≤N≤100
对于 50% 的数据,1≤M≤N≤1000
对于 100% 的数据,1≤M≤N≤1e5,0≤ai≤2e31 − 1

输入样例:

3 2
1 2 4

输出样例:

6

++++

思路:

一. 首先看到求一段区间的(异或)和,我们就想到前缀和,然后这题问的是区间异或和的最大值,所以我们就想到构造一个异或前缀和数组(之前没接触过这个玩意儿) 然后有的同学应该做过143题,就是求某两个数的最大异或值,所以我们可以将这一题转化成143题的内容。也就是求区间[l, r]的异或和就是求s[r]与s[l - 1]的异或值,证明如下:

s[l - 1] = a[1] ^ a[2] ^ a[3] ^ ... ^ a[l - 1];

s[r] = a[1] ^ a[2] ^ a[3] ^ ... ^ a[l] ^ ... ^ a[r];

s[l] ^ s[r] = a[1] ^ a[1] ^ a[2] ^ a[2] ^ ... ^ a[l - 1] ^ a[l - 1] ^ ... ^ a[r];

我们知道一个数异或它本身结果是0,所以上式变为s[l] ^ s[r] = 0 ^ 0 ^ ... ^ s[l] ^ s[l + 1] ^ ... ^ s[r];

然后又因为任何数异或0都是它本身,所以又可以转换成s[l] ^ s[r] = s[l] ^ s[l + 1] ^ ... ^ s[r];

所以得证。

二. 通过步骤一我们将这一题转化成了我们做过的143题,然后问题就变得简单了。但是因为本题对连续区间的长度有长度限制:length <= m,所以接着我们就要考虑这个区间限制。同样的我们可以容易想到移动区间的问题其实就是类似滑动窗口的问题,或者说是双指针问题,所以我们每次在枚举区间右端点时判断区间左端点在不在区间中即可,如果不在区间中就删掉区间左端点,正因为有了这个操作,所以我们不可以套用trie树模板,需要对其做一些改动。
当m == n的时候是有可能全部选择的这时候需要在trie树中加入0这个分支,当m < n时一定不会需要与0这个分支做异或。所以要添加一个m == n时候的特判。
三. 大致思路和伪代码都考虑好了,最后就是代码实现了,贴出代码并看着代码分析:

ac code
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010 * 31;

int son[N][2], cnt[N], idx;//trie树模板
int n, m;
int s[100010];//异或前缀和数组
int a[100010];//储存输入的n个数

void insert(int u, int v)//插入函数
{
    int p = 0;
    for (int i = 30; i >= 0; i -- )
    {
        int t = u >> i & 1;
        if (!cnt[son[p][t]]) son[p][t] = ++ idx;
        p = son[p][t];
        cnt[p] += v; //v等于1表示有新数据加入字典树,-1表示删除区间左端点
    }
}

int query(int u)//询问函数
{
    int p = 0, res = 0;
    for (int i = 30; i >= 0; i -- )
    {
        int t = u >> i & 1;
        if (cnt[son[p][!t]])//尽量往位异或结果为1的分支上靠
        {
            res = res * 2 + 1;
            p = son[p][!t];
        }
        else
        {
            res *= 2;
            p = son[p][t];
        }
    }
    
    return res;
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ )
    {
        scanf("%d", &a[i]);
        s[i] = s[i - 1] ^ a[i];//异或前缀和
    }
    
    if (m == n) insert(0, 1);//特判m == n的情况,增加0这个选择
    int ans = 0;
    for (int i = 1; i <= n; i ++ )//枚举区间右端点
    {
        ans = max(ans, query(s[i]));
        insert(s[i], 1);//插入字典树
        
        if (i > m) insert(s[i - m], -1);//删除不在区间中的左端点
    }
    
    printf("%d\n", ans);
    
    return 0;
}
posted @ 2021-05-10 23:03  sunnyday0725  阅读(69)  评论(0编辑  收藏  举报