【知识总结】关于异或(xor)区间问题的总结

最近遇到的区间异或的问题比较多,总结一下。

以下难度递进

第一类:

问题描述:给你出一个序列,询问有多少个区间异或为x。例子:1 2 3 异或为0 的只有一段 1^2^3  所以输出1

题解:

此类问题有统一解,我们需要处理异或前缀 :f(x)=f(1)xor f(2)....xorf(x)

由于异或的特殊性:a xor a = 0 所以 f(R)xorf(L-1)=f(L)xorf(L+1)...xorf(R)

所以列出式子:f(R)xor X=X\rightarrow f(R)xor X=f(L-1)

所以对于任意一个新的异或前缀,我们只需要找前面有多少个异或前缀与 f(R)xor X相等就可以了

如果a_i较大,使用map存储异或前缀数量,复杂度O(nlgn)

如果a_i较小,另开辅助标记即可,复杂度O(n)

注意初始第0个异或前缀要标记为0,剩下细节就不再多说


第二类:

问题描述:给出一段区间[L,R](连续区间),假设 区间长度为len,那么一共有 len*(len-1)/2个pair,问所有pair的异或和为多少?

样例描述:[1,3] 1^2=3 1^3=2 2^3=1 所以答案为6

题解:

这类问题统一解:

1.当L,R范围在1e6以内:

可以用for循环去遍历区间[L,R],对[L,R]内每一个数二进制拆分,那么如果当前这个数第k位是1,那么他的贡献取决于第k位为0的数量,反之如果第k位为0,那么贡献取决于第k位为1的数量,可以写一下伪代码:

for(int k = 0 ;k<=30;k++){
    int zero = 0,one = 0;
    for(int i=L;i<=R;i++){
        int x = i>>k&1;
        if(x) ans+=(1<<k)*zero,one++;
        else ans+=(1<<k)*one,zero++;
    }
}

复杂度:O(30*R)

2.当L,R范围在1~1e18

显然上面for循环的思路行不通,那么此时考虑贡献,就可以考虑总体贡献。

假设当前n个数中 ,第k位有两个数为1,那么第k位总体的贡献即为 (1<<k)*(n-2)*2  ///01组合一共有(n-2)*2种。

所以可以推出当 n个数中有 ,第k位有x个数为1的话,那么第k位总体贡献为(1<<k)*(n-k)*k

所以我们只需要知道 [L,R]范围内第k位有多少个1,这么一来就成了典型的数位dp的题目。

如果数位dp不会的话也不要紧,观察二进制的分布,k为0,2为周期,k为1 4为周期:

00 01 10 11

所以只需要求出f(x,k),f(x,k)代表[1,x]中第k位有多少个1(观察规律可以求出这个函数,不再多说)。

那么:ans = \sum _{i=0}^{i=63}{(1<<i)*(f(R,i)-f(L-1,i))}

复杂度 O(log(n))


第三类:

问题描述:给出一段序列,求出所有长度的子区间异或的和。

样例描述:1 2 3  =  1 + 2 + 3 + 1^2 + 1^3 + 2^3 + 1^2^3

有统一解。。

题解:

1.首先与还是与上面思路一致,考虑第k位的贡献,但是这时候需要对每一位进行异或前缀和记录,因为涉及到所有区间,根据第一类我们可以知道假设这一位是1,那么我们需要考虑之前异或前缀为0的数量,反之这一位为0,我们需要知道异或前缀为1的数量,答案累加就是第k位的贡献。

2.追加补充一下:对比第一类,什么时候第k位有贡献?也就说以当前i结尾的区间内第k位异或为1的区间才对第i个数的第k位有贡献,所以根据第一类我们只需要知道前面有多少个 x^1即可,因为x非0即1,所以把x^1就可以了(若有不懂,放一下伪代码:

for(int k=0;k<=30;k++){
   int zero=0,one=0,x=0;///x记录异或前缀
   for(int i=1;i<=n;i++){
        int temp=num[i]>>k&1;
        x^=temp;
        if(x) ans+=zero*(1<<k),one++;
        else ans+=one*(1<<k),zero++;
    }
}

复杂度:O(30*N)

扩展:

1.如果区间内数连续可以考虑前面异或有多少个1的规律,或者直接打表找规律  —— 2019ICPC徐州 


第四类:

题目描述:来自上面题目的扩展,序列中长度为偶数且长度在区间[L,R]内的异或和

1.首先 根据第一类确定一个区间[L,R]内的异或值为 f(R) xor f(L-1) 

2.考虑如果当R是奇数,那么此时 L-1应该也开始奇数,此时R-L+1是偶数

    同样的如果当R是偶数,那么L-1应该也是偶数。

3.所以我们比之前考虑维护一个奇数异或前缀的前缀和,与偶数异或前缀的前缀和。

   如果第i位是奇数位 ,那么应该之前奇数位置上的异或前缀为0或1的个数(类比第3类)。偶数也是同样。

   现在还要再增加一个限制,增加一个区间长度,那么我只需要考虑 (i-1,i-1-len)之间的有多少个异或为1或者异或为0即可。

   然后 用维护的长度为R的结果 减去 维护的长度为L-1的结果 就是答案乐。

   由于这题比较复杂,放一下代码和注释:

/*** keep hungry and calm CoolGuang!***/
#pragma GCC optimize(2)
#include <bits/stdc++.h>
#include<stdio.h>
#include<vector>
#include<algorithm>
#define debug(x) cout<<#x<<":"<<x<<endl;
using namespace std;
const double PI=acos(-1);
typedef long long ll;
typedef pair<ll,ll> pp;
const ll INF=1e18;
const ll maxn=2e5+18;
const int mod=1e9+7;
const double eps=1e-6;
inline bool read(ll &num)
{char in;bool IsN=false;in=getchar();if(in==EOF) return false;while(in!='-'&&(in<'0'||in>'9')) in=getchar();if(in=='-'){ IsN=true;num=0;}else num=in-'0';while(in=getchar(),in>='0'&&in<='9'){num*=10,num+=in-'0';}if(IsN) num=-num;return true;}
ll n,m,p;
ll L,R;
ll num[maxn];
ll dpe[maxn][32][2],dpo[maxn][32][2];///奇数异或前缀的前缀和偶数异或前缀的前缀和
ll cal(int f,int i,int k,ll temp,ll len){///处理长度小于等于len的区间异或第k位的贡献
    if(f){
        if(i-len-1>=0) return ((dpo[i-1][k][temp^1]-dpo[i-len-1][k][temp^1]%mod+mod)*(1<<k))%mod;
        else return (dpo[i-1][k][temp^1]*(1<<k))%mod;
    }
    else{
        if(i-len-1>=0) return ((dpe[i-1][k][temp^1]-dpe[i-len-1][k][temp^1]%mod+mod)*(1<<k))%mod;
        else return (dpe[i-1][k][temp^1]*(1<<k))%mod;
    }
}
int main()
{
    read(n);
    read(L);read(R);
    for(int i=1;i<=n;i++) read(num[i]);
    ll ans=0;

    for(int k=0;k<=30;k++) dpe[0][k][0]=1;
    /// 1.问题的子问题 所有区间异或和 solved 第三类
    /// 2.解决长度在[1,R]内的偶数
    /// 3.长度为偶数考虑 :
    ///    当前i为奇数时与之前的奇数位置的异或有关系
    ///    当前i为偶数时与之前的偶数位置的异或有关系
    /// 4.所以 两个前缀和 奇数 与 偶数
    for(int k=0;k<=30;k++){
        ll temp=0;
        for(int i=1;i<=n;i++){
            int x=(num[i]>>k&1)?1:0;
            temp^=x;
            if(i%2){///当前i为奇数
                dpe[i][k][0]=dpe[i-1][k][0];///偶数不变
                dpe[i][k][1]=dpe[i-1][k][1];/// 同上
                ans=(ans+cal(1,i,k,temp,R))%mod;
                ans=(ans-cal(1,i,k,temp,L-1)%mod+mod)%mod;
                dpo[i][k][temp]=dpo[i-1][k][temp]+1;
                dpo[i][k][temp^1]=dpo[i-1][k][temp^1];
            }
            else{
                dpo[i][k][0]=dpo[i-1][k][0];///奇数数不变
                dpo[i][k][1]=dpo[i-1][k][1];/// 同上
                ans=(ans+cal(0,i,k,temp,R))%mod;
                ans=(ans-cal(0,i,k,temp,L-1)%mod+mod)%mod;
                dpe[i][k][temp]=dpe[i-1][k][temp]+1;
                dpe[i][k][temp^1]=dpe[i-1][k][temp^1];
            }
        }
    }
    printf("%lld\n",ans);
    return 0;
}

update:2020.3.9 

第四类(不满足难度递增):

给出一段序列A,求有多少对互不重合的区间,使得两区间异或为0。(n<=1000)

首先倒求一遍后缀区间的所有异或可能,记录后缀所有的异或区间值的个数。

然后,正着枚举,每次枚举到i,就将i位置对后缀的贡献删除,然后将i位置向前枚举,也就是以i结尾的区间的贡献,可以推出以i为结尾的区间数 ,绝对不与后缀区间重合,所以直接计数相加即可。

    read(n);
    for(int i=1;i<=n;i++) read(num[i]);
    ll sum=0;
    for(int i=n;i>=1;i--){
        sum^=num[i];
        xo[i]=sum;
        for(int k=i+1;k<=n+1;k++)
            vis[xo[i]^xo[k]]++;
    }
    ll ans=0;
    for(int i=1;i<=n;i++){
        pre[i]=num[i]^pre[i-1];
        for(int k=i+1;k<=n+1;k++)
            vis[xo[i]^xo[k]]--;
        for(int k=0;k<=i-1;k++)
            ans+=vis[pre[k]^pre[i]];
    }
    printf("%lld\n",ans);

总结:

1.异或问题多是把总问题考虑到某一位上的关系,总贡献分解为每一位的贡献,最后求和。

2.异或问题,也包含位运算问题,规律性很强,如果强行推不出,可以打表找一下规律。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一只酷酷光儿( CoolGuang)

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值