根号算法

· · 算法·理论

一、分块

我们发现,如果一片区域被完全覆盖,我们可以对这个整块打上标记,而如果没有完全覆盖,我们只能挨个加。设我们将序列平均分成了 B 块,对完全覆盖的整块我们最多做 O(B) 次打标记。对没有完全覆盖的部分,发现只有两端要暴力做,所以是 O(\frac{N}{B}) 级别的,最终复杂度为 O(B+\frac{N}{B}) 可以发现,当 B=\sqrt N 时最小,于是我们可以在 O(\sqrt N) 的复杂度完成区间修改,区间查询。

虽然这没有线段树快,但其能维护更复杂的信息,并且单点修改是 O(1) 级别,这将与后文的莫队很好地结合。

二、根号分治

CF797E Array Queries

给出数组 a ,对此询问 k,p,求至少经过多少次 p\gets p+a_p+k 才能使得 p>n

可以发现,如果暴力跳是 O(n) 的,等等,不要这么粗略的分析复杂度,实际是 O(\frac{n}{k}) 级别,如果我们对于 k\ge B 的暴力跳,复杂度是 O(\frac n B)。如果存储答案呢?设我们存储了 k\le B 的答案,我们的复杂度为 O(nB) 级别。模仿分块的思想,取 B=\sqrt n,分别进行两种操作,便可得到 O(n\sqrt n) 的解法。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,sqn,a[N];
int dp[N][320];
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n;sqn=sqrt(n);
    for(int i=1;i<=n;++i)
        cin>>a[i];
    for(int i=n;i>=1;--i){
        for(int j=1;j<=sqn;++j){
            if(i+a[i]+j>n)dp[i][j]=1;
            else dp[i][j]=dp[i+a[i]+j][j]+1;
        }
    }cin>>m;
    while(m--){
        int p,k,ans=0;cin>>p>>k;
        if(k<=sqn){
            cout<<dp[p][k]<<'\n';
        }
        else{
            while(p<=n)p+=a[p]+k,++ans;
            cout<<ans<<'\n';
        }
    }
    return 0;
}

CF1580C Train Maintenance

比较明显的根号分治。如果 x_i+y_i>\sqrt n,直接暴力差分添加。对于 x_i+y_i\le \sqrt n,开一个桶 f_{a,b} 表示 x_i+y_i=a,在这个长度为 a 的周期中,第 b 天有多少车在维修。那么第 i 天,f_{a,i\bmod a} 是有贡献的。值得注意的是,维护差分数组的删除时,由于有点差分数组已经经过前缀和了。对于以前的一个位置 j 的贡献如果要删除,应该算到 i 上。

#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,m,sqn,x[N],y[N];
int las[N],b[N],st[500][500];
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;sqn=sqrt(n);
    for(int i=1;i<=n;++i)
        cin>>x[i]>>y[i];
    for(int i=1,op,p,ans;i<=m;++i){
        ans=0;cin>>op>>p;
        int z=x[p]+y[p];
        if(op==1){
            las[p]=i;
            if(z<=sqn)
                for(int j=0;j<z;++j)st[z][(i+j)%z]+=(j>=x[p]);
            else
                for(int j=i+x[p];j<=m;j+=z)++b[j],--b[min(m+1,j+y[p])];
        }
        else{
            if(z<=sqn)
                for(int j=0;j<z;++j)st[z][(las[p]+j)%z]-=(j>=x[p]);
            else
                for(int j=las[p]+x[p];j<=m;j+=z)--b[max(i,j)],++b[max(i,min(m+1,j+y[p]))];
        }
        for(int j=2;j<=sqn;++j)
            ans+=st[j][i%j];
        b[i]+=b[i-1];
        cout<<ans+b[i]<<'\n';
    }
    return 0;
}

P6189 [NOI Online #1 入门组] 跑步

正整数拆分问题。

有一个很明显的 DP,f_{i,j} 表示拆分成若干不大于 i 的数,总和为 j 的方案数,有 f_{0,0}=1,f_{i,j}=f_{i-1,j}+f_{i,j-i}。复杂度为 O(n^2)

m=\lfloor\sqrt n\rfloor 上面的 DP,我们可以只求到 f_{m-1,n},下面更换 DP 方式。

g_{i,j} 表示用 i 个大于等于 m 的数,和为 j 的方案数,有 g_{0,0}=1g_{i,j}=g_{i-1,j-m}+g_{i,j-i}。转移的意思为,我们可以新添加一个数 m,或者将已经选的 i 个数全部加一,分别对应 g_{i-1,j-m}g_{i,j-i}。这一部分的 g 也只用求到 g_{m,n},因为更多的数无意义。

最终答案为 \sum_{i=1}^n(f_{m-1,i}\times\sum_{j=0}^mg_{j,n-i}),相当于把 < m 的和 \ge m 的拼起来。

这样总复杂度为 O(n\sqrt n)

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,Q=320;
int n,m,p,f[Q][N],g[Q][N];
int ans=0;
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n>>p;const int P=p;
    int m=sqrt(n);f[0][0]=g[0][0]=1;
    for(int i=1;i<m;++i){
        for(int j=0;j<=n;++j){
            f[i][j]=f[i-1][j];
            if(j>=i)(f[i][j]+=f[i][j-i])%=P;
        }
    }
    for(int i=1;i<m;++i){
        for(int j=0;j<=n;++j){
            if(j>=m)(g[i][j]+=g[i-1][j-m])%=P;
            if(j>=i)(g[i][j]+=g[i][j-i])%=P;
        }
    }
    for(int i=0;i<=n;++i){
        int s=0;for(int j=0;j<m;++j)(s+=g[j][n-i])%=P;
        ans=(ans+1ll*f[m-1][i]*s)%P;
    }cout<<ans<<'\n';
    return 0;
}

三、莫队

普通莫队——P3901 数列找不同

多次判断某个区间的数是否互不相同。

只讲莫队的方法。题目相当于判断区间内不同数字的个数是否等于区间长度。我们发现,如果我们记录了区间 [l,r] 的数字的桶,我们可以 O(1) 更新到 [l+1,r],[l,r-1],[l-1,r],[l,r+1]。一种暴力的想法是将询问存下来,先按 l 排序,再按 r 排序,每次从上一个的答案区间暴力移动过来。

但是我们可以轻松卡掉这种做法,原因是我们的 l 从小到大,只有 O(n) 级别的变化量,而 r 可能因为左右大幅度横跳达到 O(n^2) 级别。

考虑根号平衡,将长度为 n 的区间分成 \sqrt n 块,将 l 所在的块作为第一关键字从小到大,将 r 作为第二关键字从小到大。这样暴力移动区间复杂度是对的。因为相邻两个 l 变化量是 O(\sqrt n) 级别的,而对于 l 再同一个块内的 r 是递增的,共有 \sqrt n 个块,所以 r 的变化量的和是 O(n\sqrt n) 级别,于是总复杂度为 O(n\sqrt n)

不理解可以先记着排序的方式,会用就行。

#include<bits/stdc++.h>
using namespace std;
const int N=100005;
struct question{int l,r,id;}q[N];
int tong[N],a[N],p1=0,p2=0;
int cnt=0,ans[N],s;
inline bool cmp(question x,question y){
    return (x.l/s==y.l/s)?x.r<y.r:x.l<y.l;
}inline void add(int x){
    if((++tong[a[x]])==1)cnt++;
}inline void era(int x){
    if((--tong[a[x]])==0)cnt--;
}signed main(){
    std::ios::sync_with_stdio(false);
    cin.tie(NULL);cout.tie(NULL);
    register int n,m,i,j;
    cin>>n>>m;
    s=sqrt(n);
    for(i=1;i<=n;i++)
        cin>>a[i];
    for(i=1;i<=m;i++)
        cin>>q[i].l>>q[i].r,q[i].id=i;
    sort(q+1,q+m+1,cmp);
    for(i=1;i<=m;i++){
        int L=q[i].l;
        int R=q[i].r;
        while(p1>L)p1--,add(p1);
        while(p1<L)era(p1),p1++;
        while(p2>R)era(p2),p2--;
        while(p2<R)p2++,add(p2);
        if(cnt==R-L+1)
            ans[q[i].id]=1;
    }
    for(int i=1;i<=m;i++)
        if(ans[i]==1)cout<<"Yes\n";
        else cout<<"No\n";
    return 0;
}

作者若干年前的丑代码。

P1494 [国家集训队] 小 Z 的袜子

维护区间内有多少对相同的数字。

在上一题的基础上,记录一个总答案,设加入数 x 前有 st_x 个数 x,对总答案的贡献就是 st_x,并且 st_x\gets st_x+1

#include<bits/stdc++.h>
#define pb push_back
#define ll long long
using namespace std;
const int N=50005,Q=223;
int n,m,a[N],st[N];
ll ans[N],gty[N],res;
struct ques{int l,r,id;}q[N];
void add(int x){res+=st[x];++st[x];}
void era(int x){--st[x];res-=st[x];}
bool cmp(ques x,ques y){
    return x.l/Q==y.l/Q?x.r<y.r:x.l<y.l;
}signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;++i)
        cin>>a[i];
    for(int i=1,j;i<=m;++i)
        cin>>q[i].l>>q[i].r,j=q[i].r-q[i].l+1,
        gty[q[i].id=i]=1ll*j*(j-1)/2;
    sort(q+1,q+m+1,cmp);
    for(int i=1,l=1,r=0;i<=m;++i){
        int L=q[i].l,R=q[i].r;
        while(l>L)add(a[--l]);while(r<R)add(a[++r]);
        while(l<L)era(a[l++]);while(r>R)era(a[r--]);
        ans[q[i].id]=res;
    }for(int i=1;i<=m;++i){
        if(!ans[i])cout<<"0/1\n";
        else{
            int d=__gcd(ans[i],gty[i]);
            cout<<ans[i]/d<<'/'<<gty[i]/d<<'\n';
        }
    }
    return 0;
}

P3730 曼哈顿交易

多次询问区间相同数字出现次数的第 k 小。

如果只是区间第 k 小,那么可以用主席树解决。但是我们要维护区间每个数的出现次数,如果用莫队加线段树维护一个桶,复杂度为 O(n \sqrt n \log n) 的,原因是线段树单点修改是 O(\log n) 的。

我们不需要线段树的 O(\log n) 的查询,分块的 \sqrt n 也很好,并且分块的单点修改是 O(1) 的,刚好配上莫队,于是使用值域分块加莫队就可以 O(n\sqrt n) 解决。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,SN=320;
int n,m,sqn,nqs;
int a[N],lsh[N];
struct ques{int l,r,k,id;}q[N];
inline bool cmp(ques a,ques b){return(a.l/sqn==b.l/sqn?a.r<b.r:a.l<b.l);}
int block[N],le[SN],ri[SN];
int st1[N],st2[N],st3[SN];
int ans[N];
inline void add(int x){
    --st2[st1[x]];
    --st3[block[st1[x]]];
    ++st1[x];
    ++st2[st1[x]];
    ++st3[block[st1[x]]];
}
inline void del(int x){
    --st2[st1[x]];
    --st3[block[st1[x]]];
    --st1[x];
    ++st2[st1[x]];
    ++st3[block[st1[x]]];
}
inline int solve(int k){
    int i;
    for(i=1;i<=sqn;++i)
        if(k<=st3[i])break;
        else k-=st3[i];
    if(i==sqn+1)return -1;
    for(int j=le[i];j<=ri[i];++j)
        if(k<=st2[j])return j;
        else k-=st2[j];
    return -1;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;
    sqn=sqrt(n);
    for(int i=1;i<=n;++i)
        cin>>a[i],lsh[i]=a[i];
    sort(lsh+1,lsh+n+1);
    int cnt=unique(lsh+1,lsh+n+1)-lsh-1;
    for(int i=1;i<=n;++i)
        a[i]=lower_bound(lsh+1,lsh+cnt+1,a[i])-lsh;
    for(int i=1,num=-1;i<=n;++i){
        block[i]=(i-1)/sqn+1;
        if(num^block[i])
            le[++nqs]=i,num=nqs;
        ri[nqs]=i;
    }
    for(int i=1;i<=m;++i)
        cin>>q[i].l>>q[i].r>>q[i].k,q[i].id=i;
    sort(q+1,q+m+1,cmp);
    int l=1,r=0;
    for(int i=1;i<=m;++i){
        while(l>q[i].l)add(a[--l]);
        while(r<q[i].r)add(a[++r]);
        while(l<q[i].l)del(a[l++]);
        while(r>q[i].r)del(a[r--]);
        ans[q[i].id]=solve(q[i].k);
    }

    for(int i=1;i<=n;++i)
        cout<<ans[i]<<'\n';
    return 0;
}

带修莫队——P1903 [国家集训队] 数颜色 / 维护队列

除了区间的变化,我们多了一维时间线。

我们能否从状态 (l,r,t),即第 t 个时刻区间 [l,r] 的信息 O(1) 更新到 (l\pm 1,r\pm 1,t\pm 1) 呢?答案是可以的,区间变化是平常的时间的变化要考虑第 t\pm1 个时刻干了什么。如果修改位置在 [l,r] 中,相当于一次删除,一次添加。如果不在则暂时没有影响,我们可以把可能会被修改成的数加到序列末尾,这样一次时间的变化相当与做序列的交换,这样就解决了还原的问题(不然当你撤回时间上的变化时不知道原来时什么)。

那么怎么排序呢?取 B=n^{\frac2 3},如果 \lfloor \frac {l_1}B \rfloor\not=\lfloor \frac {l_2}B \rfloor 按照 l 排,如果等则按照 \lfloor \frac {r}B \rfloor 排,如果 \lfloor \frac {r_1}B \rfloor=\lfloor \frac {r_2}B \rfloor 再按 t 排,这样复杂度为 O(n^{\frac 5 3}),不会证也不想证。

#include<bits/stdc++.h>
using namespace std;
const int N=133338,M=1e6+5,Q=2600;
int n,m,k,a[N],p[N],c[N],ans[N];
struct ques{int l,r,t,id;}q[N];
bool cmp(ques x,ques y){
    return x.l/Q==y.l/Q?x.r/Q==y.r/Q?x.t<y.t:x.r<y.r:x.l<y.l;
}int st[M],cnt=0;
void add(int x){cnt+=!st[x]++;}
void del(int x){cnt-=!--st[x];}
void upd(int x,int y){
    if(q[x].l<=p[y]&&p[y]<=q[x].r)
        del(a[p[y]]),add(c[y]);
    swap(a[p[y]],c[y]);
}signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;++i)
        cin>>a[i];
    for(int i=1,j=0;i<=m;++i){
        char op;cin>>op;
        if(op=='R')++j,cin>>p[j]>>c[j];
        else ++k,cin>>q[k].l>>q[k].r,q[k].t=j,q[k].id=k;
    }sort(q+1,q+k+1,cmp);
    for(int i=1,l=1,r=0,t=0;i<=k;++i){
        int L=q[i].l,R=q[i].r,T=q[i].t;
        while(l>L)add(a[--l]);while(r<R)add(a[++r]);
        while(l<L)del(a[l++]);while(r>R)del(a[r--]);
        while(t<T)upd(i,++t);while(t>T)upd(i,t--);
        ans[q[i].id]=cnt;
    }for(int i=1;i<=k;++i)
        cout<<ans[i]<<'\n';
    return 0;
}

回滚莫队——P5906 【模板】回滚莫队

有些时候,我们会发现添加容易,但是删除很困难。

在讨论莫队复杂度时,我们发现 l 的总是在一个块里来回移动,r 总是从小到大的。对于 l 所属的块变化时,我们直接 O(n) 暴力计算,因为总共 O(\sqrt n) 次。而 l 在同一个块时,r 是递增的不用担心,而 l 我们可以直接从块的有边界重新跑一边。具体的,设 xl 所在块的右端点,我们有区间 [x,r] 的答案,每次从 [x,r] 更新到 [l,r],总结答案,再恢复成 [x,r],也就是“回滚”的操作。

对于这一题,我们维护每个数最左和最右的位置,这种信息添加是容易的。如果 l,r 在同一个块内则暴力计算,如果 l_il_{i-1} 不在同一个块也暴力计算。保存一个 [x,r] 的答案数组,暴力移动后再改回去,复杂度是 O(n\sqrt n) 的。

#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,m,sqn,a[N];
int lsh[N],len;
int block[N];
struct ques{int l,r,id;}q[N];
inline bool cmp(ques x,ques y){
    return (block[x.l]==block[y.l]?x.r<y.r:x.l<y.l);
}int ans[N];
int t[N],pre[N];
int mx1[N],mn1[N];
int mx2[N],mn2[N];
int mx3[N],mn3[N];
int res1,res2;
inline void add1(int x){
    mx2[a[x]]=mx1[a[x]]=max(mx1[a[x]],x);
    mn2[a[x]]=mn1[a[x]]=min(mn1[a[x]],x);
    res2=res1=max(res1,mx1[a[x]]-mn1[a[x]]);
}
inline void add2(int x){
    mx2[a[x]]=max(mx2[a[x]],x);
    mn2[a[x]]=min(mn2[a[x]],x);
    res2=max(res2,mx2[a[x]]-mn2[a[x]]);
}
inline void era(int x){
    mx2[a[x]]=mx1[a[x]];
    mn2[a[x]]=mn1[a[x]];
    res2=res1;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n;sqn=sqrt(n);
    for(int i=1;i<=n;++i)
        cin>>a[i],lsh[i]=a[i];
    sort(lsh+1,lsh+n+1);
    len=unique(lsh+1,lsh+n+1)-lsh-1;
    for(int i=1;i<=n;++i)
        a[i]=lower_bound(lsh+1,lsh+len+1,a[i])-lsh;
    for(int i=1;i<=n;++i)
        block[i]=(i-1)/sqn+1;
    cin>>m;
    for(int i=1;i<=m;++i)
        cin>>q[i].l>>q[i].r,q[i].id=i;
    sort(q+1,q+m+1,cmp);
    memset(mn1,0x3f,sizeof(mn1));
    memset(mn2,0x3f,sizeof(mn2));
    memset(mn3,0x3f,sizeof(mn3));
    for(int i=1,l=1,r=0,bl=0,br=0;i<=m;++i){
        int L=q[i].l,R=q[i].r;
        if(block[R]-block[L]<=1){
            int res=0;
            for(int j=L;j<=R;++j){
                mx3[a[j]]=max(mx3[a[j]],j);
                mn3[a[j]]=min(mn3[a[j]],j);
                res=max(res,mx3[a[j]]-mn3[a[j]]);
            }
            ans[q[i].id]=res;
            for(int j=L;j<=R;++j)
                mx3[a[j]]=0,mn3[a[j]]=0x3f3f3f3f;
        }
        else{
            if(block[L]!=bl){
                for(int j=1;j<=len;++j)
                    mx1[j]=mx2[j]=0,mn1[j]=mn2[j]=0x3f3f3f3f;
                res1=res2=0;
                bl=block[L];
                r=br=min(bl*sqn,n);
                l=r+1;
            }
            while(r<R)add1(++r);
            while(l>L)add2(--l);
            ans[q[i].id]=res2;
            while(l<=br)era(l++);
            res2=res1;
        }
    }
    for(int i=1;i<=m;++i)
        cout<<ans[i]<<'\n';
    return 0;
}

P3709 大爷的字符串题

求区间众数出现的次数。

这个转化比较特别,我们可以将这个区间重排成尽可能少的严格递增序列,这样是最优的,而答案就是区间众数出现次数。

我们添加是好添加的,然而删除我们不知道次大的是什么,如果用 set 则又要多个 \log,所以使用回滚莫队。

#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,m,sqn,a[N];
int lsh[N],len;
int block[N];
struct ques{int l,r,id;}q[N];
inline bool cmp(ques x,ques y){
    return (block[x.l]==block[y.l]?x.r<y.r:x.l<y.l);
}int ans[N];
int t[N],pre[N];
int st1[N],st2[N],st3[N];
int res1,res2;
inline void add1(int x){
    st2[x]=st1[x]=++st1[x];
    res2=res1=max(res1,st1[x]);
}
inline void add2(int x){
    st2[x]=++st2[x];
    res2=max(res2,st2[x]);
}
inline void era(int x){
    st2[x]=st1[x];
    res2=res1;
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;sqn=sqrt(n);
    for(int i=1;i<=n;++i)
        cin>>a[i],lsh[i]=a[i];
    sort(lsh+1,lsh+n+1);
    len=unique(lsh+1,lsh+n+1)-lsh-1;
    for(int i=1;i<=n;++i)
        a[i]=lower_bound(lsh+1,lsh+len+1,a[i])-lsh;
    for(int i=1;i<=n;++i)
        block[i]=(i-1)/sqn+1;
    for(int i=1;i<=m;++i)
        cin>>q[i].l>>q[i].r,q[i].id=i;
    sort(q+1,q+m+1,cmp);
    for(int i=1,l=1,r=0,bl=0,br=0;i<=m;++i){
        int L=q[i].l,R=q[i].r;
        if(block[R]-block[L]<=1){
            int res=0;
            for(int j=L;j<=R;++j){
                ++st3[a[j]];
                res=max(res,st3[a[j]]);
            }
            ans[q[i].id]=res;
            for(int j=L;j<=R;++j)
                st3[a[j]]=0;
        }
        else{
            if(block[L]!=bl){
                for(int j=1;j<=len;++j)
                    st1[j]=st2[j]=0;
                res1=res2=0;
                bl=block[L];
                r=br=min(bl*sqn,n);
                l=r+1;
            }
            while(r<R)add1(a[++r]);
            while(l>L)add2(a[--l]);
            ans[q[i].id]=res2;
            while(l<=br)era(a[l++]);
            res2=res1;
        }
    }
    for(int i=1;i<=m;++i)
        cout<<-ans[i]<<'\n';
    return 0;
}

莫队二离——P4887 【模板】莫队二次离线

有时,我们从 [l,r][l\pm1,r\pm1] 不能做到 O(1)。莫队的复杂度为 O(n\sqrt nk)k 为移动一次端点的复杂度,我们可以将其优化成 O(n\sqrt n+nk)

以这题为例,我们要查询若干个 f(x,l,r),表示 a_x 加入 [l,r] 这个区间的贡献。先考虑这个怎么计算,有 f(x,l,r)=f(x,1,r)-f(x,1,l-1),转化为一个前缀的计算。打标或组合计算得,二进制下有 k1 的数最多只有约 3000 个,于是 O(3000n) 处理前缀和(挺极限的),于是我们可以计算出 (x,1),(x,2)...(x,y) 合法二元组的数目。

for(int i=0;i<16384;++i)
    if(__builtin_popcount(i)==k)
        base.push_back(i);
for(int i=1;i<=n;++i){
    for(auto x:base)
        ++t[a[i]^x];
    pre[i]=t[a[i+1]];
}

这里 pre_xf(x+1,1,x) 的答案。

我们对答案差分,这样我们只用处理相邻两个答案的变化量。我们共有 n\sqrt n 个的形如 f(x,l,r) 的查询,如果直接存将是 O(n\sqrt n) 的空间,不能通过。我们发现我们的查询为四种形式,不难发现 k\not=0f(x,x,x)=0

f(r+1,l,r)=f(r+1,1,r)-f(r+1,1,l-1) f(r,l,r)=f(r,1,r-1)-f(r,1,l-1) f(l-1,l,r)=f(l-1,1,r)-f(l-1,1,l-2) f(l,l,r)=f(l,1,r)-f(l,1,l-1)

其中部分是直接能用 pre 算的。不能算的,我们以 f(r+1,1,l-1) 为例。将 (r,R,id) 挂到 l-1 上,其中 l,r 为现在的区间端点,R 为目标位置,记录其是加还是减,当前缀和做到 l-1 时,暴力从 rR 算,因为莫队的特性,这些移动长度和不超过 O(n\sqrt n)

特判 k=0,最后再作一边前缀和。代码小清新。

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,m,k,a[N];
int sqn,ans[N];
struct ques{int l,r,id;}q[N];
inline bool cmp(ques x,ques y){
    return (x.l/sqn==y.l/sqn?x.r<y.r:x.l<y.l);
}
vector<int> base;
vector<tuple<int,int,int> >v[N];
int t[N],pre[N];
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n>>m>>k;
    if(k>14){
        for(int i=1;i<=m;++i)
            cout<<"0\n";
        return 0;
    }sqn=sqrt(n);
    for(int i=1;i<=n;++i)
        cin>>a[i];
    for(int i=1;i<=m;++i)
        cin>>q[i].l>>q[i].r,q[i].id=i;
    sort(q+1,q+m+1,cmp);
    for(int i=0;i<16384;++i)
        if(__builtin_popcount(i)==k)
            base.push_back(i);
    for(int i=1;i<=n;++i){
        for(auto x:base)
            ++t[a[i]^x];
        pre[i]=t[a[i+1]];
    }memset(t,0,sizeof(t));
    for(int i=1,l=1,r=0;i<=n;++i){
        int L=q[i].l,R=q[i].r;
        if(l<L)v[r].emplace_back(l,L-1,-i);
        while(l<L)ans[q[i].id]+=pre[l-1],++l;
        if(l>L)v[r].emplace_back(L,l-1,i);
        while(l>L)ans[q[i].id]-=pre[l-2],--l;
        if(r<R)v[l-1].emplace_back(r+1,R,-i);
        while(r<R)ans[q[i].id]+=pre[r],++r;
        if(r>R)v[L-1].emplace_back(R+1,r,i);
        while(r>R)ans[q[i].id]-=pre[r-1],--r;
    }
    for(int i=1,id,l,r;i<=n;++i){
        for(auto x:base)++t[a[i]^x];
        for(const auto& x:v[i]){
            tie(l,r,id)=x;
            for(int j=l,tmp=0;j<=r;++j){
                tmp=t[a[j]];
                if(j<=i&&k==0)--tmp;
                if(id<0)ans[q[-id].id]-=tmp;
                else ans[q[id].id]+=tmp;
            }
        }
    }
    for(int i=1;i<=m;++i)
        ans[q[i].id]+=ans[q[i-1].id];
    for(int i=1;i<=m;++i)
        cout<<ans[i]<<'\n';
    return 0;
}

P5047 [Ynoi2019 模拟赛] Yuno loves sqrt technology II

我们需要在莫队的基础上求出区间 < x,>x 数的个数,如果硬做在莫队的基础上要多个 \log,于是莫队二离启动。这题与上一题十分相似,甚至处理起来更直白。逆序对用树状数组还是分块随意。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e5+5,sqn=320;
ll c[N];
void add(int x){while(x<N)++c[x],x+=x&-x;}
ll ask(int x){int k=0;while(x)k+=c[x],x-=x&-x;return k;}
int n,m;
int a[N],lsh[N],cnt;
ll pre[N],suf[N],ans[N];
struct ques{int l,r,id;}q[N];
inline bool cmp(ques x,ques y){
    return x.l/sqn==y.l/sqn?x.r<y.r:x.l<y.l;
}
struct node{int id,l,r,v;};
vector<node>v1[N],v2[N];
inline void init(){
    cin>>n>>m;
    for(int i=1;i<=n;++i)
        cin>>a[i],lsh[i]=a[i];
    sort(lsh+1,lsh+n+1);
    cnt=unique(lsh+1,lsh+n+1)-lsh-1;
    for(int i=1;i<=n;++i)
        a[i]=lower_bound(lsh+1,lsh+cnt+1,a[i])-lsh;
    for(int i=1;i<=n;++i)
        pre[i]=pre[i-1]+i-1-ask(a[i]),add(a[i]);
    memset(c,0,sizeof(c));
    for(int i=n;i>=1;--i)
        suf[i]=suf[i+1]+ask(a[i]-1),add(a[i]);
    for(int i=1;i<=m;++i)
        cin>>q[i].l>>q[i].r,q[i].id=i;
    sort(q+1,q+m+1,cmp);
}
int id[N],le[N],ri[N];
int tag[N],w[N];
inline void solve1(){
    for(int i=1,l=1,r=0;i<=m;++i){
        int L=q[i].l,R=q[i].r,id=q[i].id;
        if(r<R)ans[q[i].id]+=(pre[R]-pre[r]),
            v1[l].push_back((node){id,r+1,R,-1}),r=R;
        if(r>R)ans[q[i].id]-=(pre[r]-pre[R]),
            v1[l].push_back((node){id,R+1,r,1}),r=R;
        if(l<L)ans[q[i].id]-=(suf[l]-suf[L]),
            v2[r].push_back((node){id,l,L-1,1}),l=L;
        if(l>L)ans[q[i].id]+=(suf[L]-suf[l]),
            v2[r].push_back((node){id,L,l-1,-1}),l=L;
    }
}
inline void solve2(){
    int num=sqrt(cnt),cur=1;le[1]=1;
    if(num*num<cnt)++num;
    for(int i=1;i<=cnt;++i){
        id[i]=cur;
        if(i%num==0)ri[cur]=i,le[++cur]=i+1;
    }ri[cur]=cnt;
    for(int i=1;i<=n;++i){
        for(int j=0;j<v1[i].size();++j){
            int L=v1[i][j].l,R=v1[i][j].r,v=v1[i][j].v;
            for(int k=L;k<=R;++k)
                ans[v1[i][j].id]+=1ll*v*(tag[id[a[k]+1]]+w[a[k]+1]);
        }
        int x=a[i];
        for(int i=le[id[x]];i<=x;++i)++w[i];
        for(int i=1;i<id[x];++i)++tag[i];
    }
    memset(tag,0,sizeof(tag));
    memset(w,0,sizeof(w));
    for(int i=n;i>=1;--i){
        for(int j=0;j<v2[i].size();++j){
            int L=v2[i][j].l,R=v2[i][j].r,v=v2[i][j].v;
            for(int k=L;k<=R;++k)
                ans[v2[i][j].id]+=1ll*v*(tag[id[a[k]-1]]+w[a[k]-1]);
        }
        int x=a[i];
        for(int i=x;i<=ri[id[x]];++i)++w[i];
        for(int i=id[x]+1;i<=num;++i)++tag[i];
    }
}
inline void out(){
    for(int i=1;i<=m;++i)ans[q[i].id]+=ans[q[i-1].id];
    for(int i=1;i<=m;++i)cout<<ans[i]<<'\n';
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    init();solve1();solve2();out();
    return 0;
}

四、简单习题。

P3396 哈希冲突:根号分治

P5501 [LnOI2019] 来者不拒,去者不追:莫队二离

P4396 [AHOI2013] 作业:莫队

P7708 「Wdsr-2.7」八云蓝自动机 Ⅰ:莫队维护操作序列