分块
设sum[ i ] [ j ] 存从左边到第 i 块时,数字 j 的出现次数
f [ i ] [ j ] 存从第 i 块,到第 j 块的一整段的答案
那么最后答案就是一段区间中几块整段的答案加上两边小段的贡献
考虑两边小段的影响,对于每一个出现的数
它可能会使答案增加(使原本大区间中出现奇数次的数变成出现偶数次)
也可能使答案减少(使原本大区间中出现偶数次的数变成出现奇数次)
有了 sum 数组我们可以很方便地求出大区间中每个数的出现次数
对小段的每个数直接计算一下它对答案的贡献
开一个 cnt[ i ] 记录一下之前每个数 i 出现的次数就好了
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//bl,br是大区间的左右边界的块 ans=f[bl][br];//ans初值为大区间的答案 for(int i=l;i<L[bl];i++)//L[i]存第i块的左端点 { cnt[a[i]]++;//记录a[i]在小段出现的次数 t=cnt[a[i]]+sum[br][a[i]]-sum[bl-1/*注意是bl-1*/][a[i]];//计算此时a[i]出现的次数 if(!(t&1)) ans++;//如果a[i]出现的次数变成了偶数次,答案就加一 else if(t>1) ans--;//否则如果出现次数超过2次且使出现次数变成奇数次,答案减1 //要特殊考虑t=1的情况,t=1时不会对答案有影响,因为t=0时不算出现偶数次 } for(int i=L[br+1];i<=r;i++)//注意i的范围 { cnt[a[i]]++; t=cnt[a[i]]+sum[br][a[i]]-sum[bl-1][a[i]]; if(!(t&1)) ans++; else if(t>1) ans--; } for(int i=l;i<L[bl];i++) cnt[a[i]]--; for(int i=L[br+1];i<=r;i++) cnt[a[i]]--;//别忘记还原cnt,以后询问还要用,注意不要用memset
然后来考虑如何预处理
sum数组很好求,关键是 f
好像枚举每个块复杂度会爆炸...
我们来看看处理询问时的方法,对每个数都计算贡献
预处理就相当于询问每两个块之间的贡献
我们可以用同样的方法,cnt[ i ] 记录 i 出现了几次
从左到右扫,计算每个数对答案的贡献,如果扫到一个区间的右端点了,就记录一波 f
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//bel[i]表示点i属于第几个块 for(int i=1;i<=bel[n];i++)//枚举每个左块 { t=0; for(int j=L[i];j<=n;j++)//考虑右边所有块 { cnt[a[j]]++;//记录 if(!(cnt[a[j]]&1)) t++; else if(cnt[a[j]]>1) t--; //考虑对答案的贡献 if(bel[j]!=bel[j+1]) f[i][bel[j]]=t;//如果到了一个区间的右端点,更新f } for(int j=L[i];j<=n;j++) cnt[a[j]]--;//别忘了还原cnt }
这样我们就可以在 O( n*sqrt(n) )的时间内预处理,在 O( sqrt(n)+2*sqrt(n) ) 的时间处理询问
注意一下常数就轻松过了
细节很多的一题
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<cmath> using namespace std; inline int read() { int x=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9') { if(ch=='-') f=-1; ch=getchar(); } while(ch>='0'&&ch<='9') { x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); } return x*f; } const int N=1e5+7,M=327; int n,m,q,ans; int a[N],bel[N],L[M]; int sum[M][N],f[M][M]; int cnt[N]; inline void pre()//预处理 { int t=0; for(int i=1;i<=bel[n];i++) for(int j=1;j<=m;j++) sum[i][j]+=sum[i-1][j]; for(int i=1;i<=bel[n];i++) { t=0; for(int j=L[i];j<=n;j++) { cnt[a[j]]++; if(!(cnt[a[j]]&1)) t++; else if(cnt[a[j]]>1) t--; if(bel[j]!=bel[j+1]) f[i][bel[j]]=t; } for(int j=L[i];j<=n;j++) cnt[a[j]]--; } } inline void query(int l,int r)//处理询问 { ans=0; int t=0; if(bel[l]+1>=bel[r])//对没有大区间的情况特殊处理 { for(int i=l;i<=r;i++)//直接爆力计算每个数的贡献 { cnt[a[i]]++; if(!(cnt[a[i]]&1)) ans++; else if(cnt[a[i]]>1) ans--; } for(int i=l;i<=r;i++) cnt[a[i]]--;//清空cnt return; } int bl=bel[l-1]+1,br=bel[r+1]-1;//计算bl,br,细节 //l-1是考虑当l在一个块最左边的时候,那l在的块整块都会每计算,直接整个拿出来计算就好了,r+1同理 ans=f[bl][br];//初值 for(int i=l;i<L[bl];i++)//对左边的小段计算贡献 { cnt[a[i]]++; t=cnt[a[i]]+sum[br][a[i]]-sum[bl-1][a[i]]; if(!(t&1)) ans++; else if(t>1) ans--; } for(int i=L[br+1];i<=r;i++)//对右边的小段计算贡献 { cnt[a[i]]++; t=cnt[a[i]]+sum[br][a[i]]-sum[bl-1][a[i]]; if(!(t&1)) ans++; else if(t>1) ans--; } for(int i=l;i<L[bl];i++) cnt[a[i]]--; for(int i=L[br+1];i<=r;i++) cnt[a[i]]--;//清空 return; } int main() { n=read(); m=read(); q=read(); int t=sqrt(n)+1; for(int i=1;i<=n;i++) { a[i]=read(); bel[i]=(i-1)/t+1;//处理bel if(bel[i]!=bel[i-1]) L[bel[i]]=i;//处理L sum[bel[i]][a[i]]++;//此时sum只包括第i块的数量 } bel[n+1]=bel[n]+1; L[bel[n+1]]=n+1;//重要的细节,有时我的代码考虑边界时会访问到n+1的点 pre(); int l,r; for(int i=1;i<=q;i++) { l=read(); r=read(); l=(l+ans)%n+1; r=(r+ans)%n+1; if(l>r) swap(l,r);//处理l,r query(l,r); printf("%d\n",ans); } return 0; }