给定一个长n的数组,m个询问
每个询问给l和r,询问数组区间[l,r]中出现次数为偶数的数的异或和。
我们先从异或计算的方面思考,,也就是奇数次的异或本身还是本身,同时也有偶数次异或本身是0的结论
那么我们想,在线段树维护区间上能否利用这一性质,维护出某种偶数次异或还是本身的效果呢,那么在计算前我们先把答案设成区间内出现过的数的异或和(出现了只算一次,多次只算一次),假如说区间的数为2,2,答案应该是2,但是这个区间总异或是0,我们在计算前把答案设置成出现过的数的异或,也就是2,三次异或得到2。答案就是区间总异或和 异或上 出现的数的异或和(出现的数多少次都只算一次),简而言之就是让答案异或上出现过的所有不同数字,使原来异或本身次数为奇数的变为偶数,异或和是0,消除贡献;使异或本身次数是偶数的变为奇数,异或和就是其本身,最后答案就是所有出现次数为偶数的数的异或和。
同时区间总异或和还可以用异或版本的前缀和优化。
那么要点就在计算给定区间出现的不同的数的异或和。
先考虑一个问题,给定一个数组,同样的询问,计算区间有多少不同的数字?
题目链接:P1972 [SDOI2009] HH的项链 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
我们考虑离线处理,按照询问的右端点顺序依次加入数组的值,先按照右端点排序询问,按顺序考虑询问,每次右移询问的右端点时考虑移动区间的数,对于每个数,如果有多次出现,应该靠右记录贡献,例如:
此时这样的查询就记录不到最右的3了
每次查询都是区间求和,缩短时间用树状数组或者线段树都可以。
所以洛谷这题的代码 :
#include<bits/stdc++.h>
using namespace std;
int read()
{
int ret=0,base=1;
char ch=getchar();
while(!isdigit(ch))
{
if(ch=='-') base=-1;
ch=getchar();
}
while(isdigit(ch))
{
ret=(ret<<3)+(ret<<1)+ch-48;
ch=getchar();
}
return ret*base;
}
inline int lowbit(int x){return -x&x;}
struct question
{
int l,r,id;
bool operator<(const question &a) const {return r<a.r;}
}q[1000005];
int n,m,ans[1000005],a[1000005],kind[1000005],last[1000005],tree[1000005];
void add(int x,int k)
{
while(x<=n)
{
tree[x]+=k;
x+=lowbit(x);
}
}
int getsum(int x)
{
int ret=0;
while(x)
{
ret+=tree[x];
x-=lowbit(x);
}
return ret;
}
int query(int l,int r)
{
return getsum(r)-getsum(l-1);
}
int main()
{
n=read();
for(int i=1;i<=n;i++) a[i]=read();
m=read();
for(int i=1;i<=m;i++) q[i]=(question){read(),read(),i};
sort(q+1,q+1+m);//反复贡献到最右边
int r=0;
for(int i=1;i<=m;i++)
{
while(r<q[i].r)
{
r++;
if(last[a[r]]) add(last[a[r]],-1);
add(r,1);
last[a[r]]=r;
}
ans[q[i].id]=query(q[i].l,q[i].r);
}
for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
return 0;
}
CF这题因为数组数字太大,不能直接用数组记录上一次出现的位置,所以需要离散化,并且此时树状数组维护的数组计算不是加法,而是异或。
#include<bits/stdc++.h>
using namespace std;
int read()
{
int ret=0,base=1;
char ch=getchar();
while(!isdigit(ch))
{
if(ch=='-') base=-1;
ch=getchar();
}
while(isdigit(ch))
{
ret=(ret<<3)+(ret<<1)+ch-48;
ch=getchar();
}
return ret*base;
}
inline int lowbit(int x){return -x&x;}
struct question
{
int l,r,id;
bool operator<(const question &a) const {return r<a.r;}
}q[1000005];
int n,m,ans[1000005],a[1000005],kind[1000005],v[1000005];
int b[1000005],sum_xor[1000005],last[1000005],tree[1000005];
void add(int x,int k)
{
while(x<=n)
{
tree[x]^=k;
x+=lowbit(x);
}
}
int getsum(int x)
{
int ret=0;
while(x)
{
ret^=tree[x];
x-=lowbit(x);
}
return ret;
}
int query(int l,int r)
{
return getsum(r)^getsum(l-1);
}
int main()
{
n=read();//出现次数是偶数的数 == 不同种类的数 xor 出现奇数次数的数
for(int i=1;i<=n;i++) sum_xor[i]=sum_xor[i-1]^(a[i]=b[i]=read());
sort(b+1,b+1+n);
for(int i=1;i<=n;i++)
{
int pos=lower_bound(b+1,b+1+n,a[i])-b;
v[pos]=a[i];a[i]=pos;
}
m=read();
for(int i=1;i<=m;i++) q[i]=(question){read(),read(),i};
sort(q+1,q+1+m);
for(int i=1,r=0;i<=m;i++)
{
while(r<q[i].r)
{
r++;
if(last[a[r]]) add(last[a[r]],v[a[r]]);
add(r,v[a[r]]);
last[a[r]]=r;
}
ans[q[i].id]=query(q[i].l,q[i].r)^(sum_xor[q[i].r]^sum_xor[q[i].l-1]);
}
for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
return 0;
}
这种方法求区间不同元素的问题一般都是保存询问,离线回答,而回答的时候,是按照区间右端点顺序在线回答的,是一种半在线的做法。
另外,这题的思路也可以这样考虑
对于区间里的数分两种:出现次数为偶数,出现次数为奇数,他们总体的集合就是这个区间的所有不同数,因此 出现次数为偶数的所有数的异或和 异或 出现次数为奇数的所有数的异或和 = 出现的所有不同的数的异或和
移项就是 出现次数为偶数的所有数的异或和 = 出现的所有不同的数的异或和 异或 出现次数为奇数的所有数的异或和
师兄这个思路更简洁一点