P4135 作诗

传送门

分块

设sum[ i ] [ j ] 存从左边到第 i 块时,数字 j 的出现次数

 f [ i ] [ j ] 存从第 i 块,到第 j 块的一整段的答案

那么最后答案就是一段区间中几块整段的答案加上两边小段的贡献

考虑两边小段的影响,对于每一个出现的数

它可能会使答案增加(使原本大区间中出现奇数次的数变成出现偶数次)

也可能使答案减少(使原本大区间中出现偶数次的数变成出现奇数次)

有了 sum 数组我们可以很方便地求出大区间中每个数的出现次数

对小段的每个数直接计算一下它对答案的贡献

开一个 cnt[ i ] 记录一下之前每个数 i 出现的次数就好了

//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

//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;
}

猜你喜欢

转载自www.cnblogs.com/LLTYYC/p/9785521.html
今日推荐