P4462 [CQOI2018]异或序列
原题链接 https://www.luogu.com.cn/problem/P4462
题目大意
给你一个长度为 $n$ 的序列和 $m$ 次询问,求每次询问中有多少个子区间异或和为 $k$;
题解
这是一道区间查询的题目,所用的算法是数据结构,我这里用的是莫队算法;
回顾一下异或的神奇性质:
$1.a$ ^ $b = c$ ,则 $a$ ^ $ c = b$ ,$b$ ^ $c = a$ ;
$2. a$ ^ $a = 0$ ,$a$ ^ $0 = a$ ,则 $a$ ^ $a$ ^ $a = a$ ;
有了上面的性质,我们就可以把这一长串的异或化简一下了:
但是有些同学会问了:这不是更麻烦了嘛?怎么叫化简了呢?
我们可以用 $S_i$ 来表示区间 $[ 1,i ]$ 的异或和,即 $S_i = a_1$ ^ $a_2$ ^ $a_3$ …… ^ $a_i$ ;
这样我们就将一串连续的数异或转化为了两个数的异或!
所以我们在求有多少个子区间时就是求有多少个 $S_j$ ^ $S_{i-1}= k ( l <= i <= j <= r )$ ;
接下来我们考虑区间转移对当前答案造成的影响:
假如当前区间向右扩展了一个 $a [ i+4]$ ,那么它对答案造成的影响取决于以 $a [ i+4 ]$ 为结尾的若干区间中有多少个区间异或和为 $k$ ;
我们列举出所有以 $a [ i+4 ]$ 为结尾的区间:
$1. a [ i ]$ ^ $a [ i+1 ]$ ^ $a [ i+2 ]$ ^ $a [ i+3 ]$ ^ $ a [ i+4 ] = S_{i+4}$ ^ $S_{i-1} = k ?$
$2. a [ i+1 ]$ ^ $a [ i+2 ]$ ^ $a [ i+3 ]$ ^ $a [ i+4 ] = S_{i+4}$ ^ $S_i = k ?$
$3. a [ i+2 ]$ ^ $a [ i+3 ]$ ^ $a [ i+4 ] = S_{i+4}$ ^ $S_{i+1} = k ?$
$4. a [ i+3 ]$ ^ $a [ i+4 ] = S_{i+4}$ ^ $S_{i+1} = k ?$
$5. a [ i+4 ] = S_{i+4}$^ $S_{i+3} = k ?$
但是我们这样暴力搞的话时间复杂度不允许啊,莫队的每步转移都要控制在 $O (1)$ 的时间复杂度内;
想一想,如果有 $S_{i+4}$ ^ $S_x = k$ ,那么我们只需知道区间内有多少个异或前缀和为 $S_x$ ,答案就增加多少;
所以我们开一个数组 $times [ i ]$ 表示当前区间内有多少个异或前缀和为 $i$ ;
那么我们根据性质一可以得出:$S_x = S_{i+4}$ ^ $k$ ,所以答案会增加 $times [ S_{i+4}$ ^ $k ]$ ;
这样我们就 $O(1)$ 地解决了问题;
再看个例子:
假如当前区间的左端点向右转移了一个单位,那么我们要删除 $a [ i ]$ 对答案的影响,它对答案造成的影响取决于以 $a [ i ]$ 为开头的若干区间中有多少个区间异或和为 $k$ ;
我们列举出所有以 $a [ i ]$ 为开头的区间:
$1. a [ i ]$ ^ $a [ i+1 ]$ ^ $a [ i+2 ]$ ^ $a [ i+3 ]$ ^ $a [ i+4 ] = S_{i+4}$ ^ $S_{i-1} = k ?$
$2. a [ i ]$ ^ $a [ i+1 ]$ ^ $a [ i+2 ]$ ^ $a [ i+3 ] = S_{i+3}$ ^ $S_{i-1} = k ?$
$3. a [ i ]$ ^ $a [ i+1 ]$ ^ $a [ i+2 ] = S_{i+2}$ ^ $S_{i-1} = k ?$
$4. a [ i ]$ ^ $a [ i+1 ] = S_{i+1}$ ^ $S_{i-1} = k ?$
$5. a [ i ] = S_{i}$ ^ $S_{i-1} = k ?$
所以就是求有多少个 $S_x$ ^ $S_{i-1}= k$(注意这里是 $S_{i-1}$ 而不是 $S_i$ ),那么答案减少 $times [ S_{i-1}$ ^ $k ]$ ;
这里就是提醒大家在处理左端点时一定要注意 $-1$ 的问题,非常严重!(我就是这里卡了 $2h$
核心内容讲完了,剩下的就是一些莫队算法的内容了,建议不会莫队的童鞋先学会莫队再食用本代码哦~
$Code$:
#include<iostream> #include<algorithm> #include<cmath> #include<cstdio> using namespace std; long long read() { char ch=getchar(); long long a=0,x=1; while(ch<'0'||ch>'9') { if(ch=='-') x=-x; ch=getchar(); } while(ch>='0'&&ch<='9') { a=(a<<1)+(a<<3)+(ch-'0'); ch=getchar(); } return a*x; } const long long N=1e5+5; long long n,m,k,nl=1,nr,len,num,ans; long long a[N],L[N],R[N],S[N],Ans[N],pos[N],times[N]; //times[i]:当前区间中异或和为i的前缀和有几个 struct node { long long l,r,id; }A[N<<2]; bool cmp(node x,node y) //奇偶性排序 { if(pos[x.l]!=pos[y.l]) return pos[x.l]<pos[y.l]; //若左端点不在同一块中,则左端点所在的块编号小的优先 if(pos[x.l]&1) return x.r<y.r; //若左端点在同一块中,且这个块的编号为奇数,则右端点小的优先 return x.r>y.r; //若这个块的编号为偶数,则右端点大的优先 } void add(long long x) //统计新增点a[x]的贡献 { ans+=times[S[x]^k]; //(S[x]^k)^S[x]=k times[S[x]]++; //当前区间内异或前缀和为S[x]的又多了一个 } void del(long long x) //删除点a[x]的贡献 { times[S[x]]--; //这里要先删除贡献 ans-=times[S[x]^k]; //然后在减去对答案的影响 } int main() { n=read();m=read();k=read(); for(long long i=1;i<=n;i++) { a[i]=read(); S[i]=S[i-1]^a[i]; //求异或前缀和 } len=num=sqrt(n); //每块的长度及要分成多少块 for(long long i=1;i<=num;i++) { L[i]=(i-1)*len+1; //每块的左端点 R[i]=i*len; //每块的右端点 for(long long j=L[i];j<=R[i];j++) pos[j]=i; //表示第j个数在第i个块里 } if(R[num]<n) //如果这些块不能包含所有的数,那么就再开一个块将剩下的数全放进去 { num++; //块数加一 L[num]=R[num-1]+1; R[num]=n; //这里右端点设为n,即序列的最后一个数 for(long long i=L[num];i<=R[num];i++) pos[i]=num; } for(long long i=1;i<=m;i++) //将m次询问的左右端点存下,待会离线处理 { A[i].l=read(); A[i].r=read(); A[i].id=i; //这是第几次询问 } sort(A+1,A+1+m,cmp); //将所有的块进行奇偶性排序 times[0]=1; //一开始当前区间没有任何元素 for(long long i=1;i<=m;i++) { while(nl<A[i].l) del(nl-1),nl++; //如果当前区间的左端点小于询问区间的左端点,则要删除该点的信息并向右转移 while(nl>A[i].l) nl--,add(nl-1); //如果当前区间的左端点大于询问区间的左端点,则要向左转移并统计新增点的信息 while(nr<A[i].r) add(++nr); //如果当前区间的右端点小于询问区间的右端点,则要向右转移并统计新增点的信息 while(nr>A[i].r) del(nr--); //如果当前区间的右端点大于询问区间的右端点,则要删除该点的信息并向左转移 Ans[A[i].id]=ans; //记录每次询问的答案,方便下面按询问顺序输出 } for(long long i=1;i<=m;i++) printf("%lld\n",Ans[i]); //按照询问顺序输出答案 return 0; }