【暖*墟】#数据结构# 莫队算法的学习与练习

普通莫队的简介

莫队是一种基于分块思想的离线算法,用于解决区间问题,适用范围如下:

    1. 只有询问没有修改。     2. 允许离线。

    3. 在已知询问 [l,r] 答案的情况下可以 O(1) 得到 [l,r−1],[l,r+1],[l−1,r],[l+1,r] 的答案。

满足以上三个条件就可以在O(n√m+mlogm)的时间复杂度下得到每个询问的解。

普通莫队的算法思想

莫队的精髓就在于:通过对询问进行排序,并把询问的结果作为下一个询问求解的基础

“在已知询问 [l,r] 答案的情况下可以 O(1) 得到 [l,r−1],[l,r+1],[l−1,r],[l+1,r]的答案”

  • 即:把询问的结果作为下一个询问求解的基础,便于按顺序求值。

例:【p1494】[国家集训队]小Z的袜子

  • N只袜子从1到N编号,然后从编号L到R中任选两只。
  • 求抽到两只颜色相同的袜子的概率。询问多个(L,R)。
  • 然而数据中有L=R的情况,请特判这种情况,输出0/1。

用cnt[i]表示当前处理的区间内颜色为i的袜子出现的次数,用len表示当前处理的区间的长度,

以已知区间[l,r]的答案求解区间[l,r+1]为例,用x表示新增的那只袜子的颜色。

  • 分别处理分子和分母:

分母 为任选两只袜子的组合总数,原先是len∗(len−1)/2,

   现在是len∗(len+1)/2,增加了len。

分子 为两只袜子颜色相同的组合总数,比原来增加了cnt[x],

    即:新增的这只袜子和原区间内的同色袜子的组合(出现次数)。

//将一只颜色为x的袜子加入区间:(fz代表分子,fm代表分母)

void add(int x){ fm+=len,len++,fz+=cnt[x],cnt[x]++; }


//将一只颜色为x的袜子移出区间:(fz代表分子,fm代表分母)

void del(int x){ len--,fm-=len,cnt[x]--,fz-=cnt[x]; }

莫队的具体实现方案

用 l、r 分别记录当前区间的两个端点,用下方代码来更新答案:

while(l>q[i].l) add(col[--l]);

while(r<q[i].r) add(col[++r]);

while(l<q[i].l) del(col[l++]);

while(r>q[i].r) del(col[r--]);

//↑↑q[i].l/r即正在处理的询问区间的两个端点,col[p]即第p只袜子的颜色

而莫队的精髓就在于通过对询问进行‘排序’,使得暴力求解的复杂度得到保证。

为了让l、r两个指针走过的总距离尽量小,就要运用分块的思想了。

  • 把整个区间[1,n]分成若干块,以询问的‘左端点所在块’为第一关键字,
  • 以询问的‘右端点’为第二关键字,对询问进行排序,那么:

对于同一块的询问,l指针每次最多移动块的大小,r指针最多移动n次。

对于不同块的询问,l每次换块时最多移动两倍块的大小,r每次换块时最多移动n次。

总结:(用B表示块的大小)l指针每次移动O(B),r指针每块移动O(n)。

        所以l的移动次数最多为询问数×块的大小,即O(mB)。

        r的移动次数最多为块的个数×总区间大小,即O(n^2/B)。

             因此,总移动次数为O(mB+n^2/B)。

初始的当前区间(任意空区间均可): l=1,r=0 。

整个莫队算法可以概括为:

1.记录询问; 2.以n/√m为块的大小,按规则对询问进行排序(分块只用于排序);

3.双指针暴力处理每个询问; 4.总的复杂度为 O(n√m+mlogm)。

普通莫队的各种例题

例1:【p1494】小Z的袜子

  • N只袜子从1到N编号,然后从编号L到R中任选两只。
  • 求抽到两只颜色相同的袜子的概率。询问多个(L,R)。
  • 然而数据中有L=R的情况,请特判这种情况,输出0/1。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;

// 【p1494】 小Z的袜子 
// N只袜子从1到N编号,然后从编号L到R中任选两只。
// 求抽到两只颜色相同的袜子的概率。询问多个(L,R)。
// 然而数据中有L=R的情况,请特判这种情况,输出0/1。

void reads(int &x){ //读入优化(正负整数)
    int fx=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
    x*=fx; //正负号
}

const int N=50019;

int n,m,pos[N],col[N]; ll cnt[N],ans;

ll gcd(ll a,ll b){return b==0?a:gcd(b,a%b);}

ll sqr(ll x){return x*x;}

struct node{int l,r,id;ll fz,fm;}q[N];

bool cmp(node a,node b)
 { if(pos[a.l]==pos[b.l]) return a.r<b.r; return a.l<b.l; }

bool cmp_id(node a,node b){return a.id<b.id;}

void update(int p,int add)
 { ans-=sqr(cnt[col[p]]),cnt[col[p]]+=add,ans+=sqr(cnt[col[p]]); }

void solve(){
    for(int i=1,l=1,r=0;i<=m;i++){
        for(;r<q[i].r;r++) update(r+1,1);
        for(;r>q[i].r;r--) update(r,-1);
        for(;l<q[i].l;l++) update(l,-1);
        for(;l>q[i].l;l--) update(l-1,1);
        if(q[i].l==q[i].r){ q[i].fm=0,q[i].fz=1; continue; }
        q[i].fm=ans-(q[i].r-q[i].l+1);
        q[i].fz=(ll)(q[i].r-q[i].l+1)*(q[i].r-q[i].l);
        ll gcds_=gcd(q[i].fm,q[i].fz);
        q[i].fm/=gcds_,q[i].fz/=gcds_; //分数约分
    }
}

int main(){
    reads(n),reads(m); for(int i=1;i<=n;i++) reads(col[i]);
    int block=(int)sqrt(n); for(int i=1;i<=n;i++) pos[i]=(i-1)/block+1;
    for(int i=1;i<=m;i++) reads(q[i].l),reads(q[i].r),q[i].id=i;
    sort(q+1,q+m+1,cmp); solve(); //离线,进行莫队算法
    sort(q+1,q+m+1,cmp_id); for(int i=1;i<=m;i++)
        printf("%lld/%lld
",q[i].fm,q[i].fz);
}

例2:【p2709】小B的询问

  • 查询区间(l,r)中的∑(cnt[i]^2),cnt[i]表示i在区间内出现的次数。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;

// 【p2709】小B的询问
// 查询区间(l,r)中的∑(cnt[i]^2),cnt[i]表示i在区间内出现的次数。

void reads(ll &x){ //读入优化(正负整数)
    ll fx=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
    x*=fx; //正负号
}

const ll N=50019;

ll n,m,k,pos[N],col[N]; ll cnt[N],ans=0;
struct node{ll l,r,id;ll sum;}q[N];

bool cmp(node a,node b)
 { if(pos[a.l]==pos[b.l]) return a.r<b.r; return a.l<b.l; }

bool cmp_id(node a,node b){return a.id<b.id;}

void update(ll p,ll add) //↓↓相邻数的平方差
 { cnt[col[p]]+=add,ans+=add*(2*cnt[col[p]]-add); }

void solve(){
    for(ll i=1,l=1,r=0;i<=m;i++){
        for(;r<q[i].r;r++) update(r+1,1);
        for(;r>q[i].r;r--) update(r,-1);
        for(;l<q[i].l;l++) update(l,-1);
        for(;l>q[i].l;l--) update(l-1,1);
        q[i].sum=ans; //记录此区间的sum值
    }
}

int main(){
    reads(n),reads(m),reads(k); for(ll i=1;i<=n;i++) reads(col[i]);
    ll block=(ll)sqrt(n); for(ll i=1;i<=n;i++) pos[i]=(i-1)/block+1;
    for(ll i=1;i<=m;i++) reads(q[i].l),reads(q[i].r),q[i].id=i;
    sort(q+1,q+m+1,cmp); solve(); //离线,进行莫队算法
    sort(q+1,q+m+1,cmp_id); for(ll i=1;i<=m;i++) cout<<q[i].sum<<endl;
}

 例3:【p3245】大数

  • 一个数字串,m个询问,每次询问串[l,r]中 “ %P=0的子串个数 ” 。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;

// 【p3245】大数
// 一个数字串,m个询问,每次询问串[l,r]中‘%P=0的子串个数’ 。

/*【莫队】存数字串的后缀:rs[i]表示i到n形成的数%p​的值。
那么数字[l,r]=num(l,r)≡ rs[l]−rs[r+1] / 10^(r−l+1) (mod p)​。
当10^(r−l+1)%p≠0时:num(l,r)∗10^(r−l+1)≡rs[l]−rs[r+1](mod p)。
所以如果rs[l]=rs[r+1],num(l,r)就是p的倍数,条件是gcd(10,p)=1,p不能是2或5。*/

void reads(int &x){ //读入优化(正负整数)
    int fx=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
    x*=fx; //正负号
}

const int N=500019;

int p,n,m,pos[N],a[N],rs[N],mi[N]; ll cnt[N],sum=0,cntt=0,l,r;struct number{ int z,id; }b[N];
struct node{int l,r,id;ll ans;}q[N];

bool cmp(node a,node b) //【分块优化排序】同块中按r排序,不同块时按l排序
 { if(pos[a.l]==pos[b.l]) return a.r<b.r; return a.l<b.l; }

bool cmp2(number a,number b){return a.z<b.z;}

bool cmp_id(node a,node b){return a.id<b.id;}

inline void addl(int x){cntt+=(a[x]%p==0),sum+=cntt;}
inline void addr(int x){sum+=(r-l+1)*(a[x]%p==0),cntt+=(a[x]%p==0);}
inline void dell(int x){sum-=cntt,cntt-=(a[x]%p==0);}
inline void delr(int x){sum-=(r-l+1)*(a[x]%p==0),cntt-=(a[x]%p==0);}

inline void add(int x){sum+=cnt[rs[x]],cnt[rs[x]]++;}
inline void del(int x){cnt[rs[x]]--,sum-=cnt[rs[x]];}

void get_25(){
    l=1; for(int i=1;i<=m;i++){
        while(l<q[i].l) dell(l),l++;
        while(l>q[i].l) l--,addl(l);
        while(r>q[i].r) delr(r),r--;
        while(r<q[i].r) r++,addr(r); q[i].ans=sum;
    }
}

void solve(){
    l=1; for(int i=1;i<=m;i++){
        while(l<q[i].l) del(l),l++;
        while(l>q[i].l) l--,add(l);
        while(r>q[i].r+1) del(r),r--;
        while(r<q[i].r+1) r++,add(r);q[i].ans=sum;
    }
}

char ss[100019];

int main(){
    reads(p),scanf("%s",ss+1); n=strlen(ss+1);
    int block=(int)sqrt(n); for(int i=1;i<=n;i++) 
        a[i]=ss[i]-'0',pos[i]=(i-1)/block+1;

    mi[1]=1; for(int i=2;i<=n;i++) mi[i]=mi[i-1]*10%p;
    if(p!=2&&p!=5){ for(int i=n;i>=1;i--){
            b[i].z=(b[i+1].z+a[i]*mi[n-i+1]%p)%p,b[i].id=i;
        } b[n+1].id=n+1; b[n+1].z=0; //z记录后缀%p得到的值(未离散化)
        sort(b+1,b+n+2,cmp2); //排序,这样就可以将rs数组离散化
        for(int i=1;i<=n+1;i++) rs[b[i].id]=rs[b[i-1].id]+(b[i].z!=b[i-1].z);
    } //↑↑计算后缀数%p之后得到的值(rs[]),离散化rs数组
    
    reads(m); for(int i=1;i<=m;i++) reads(q[i].l),reads(q[i].r),q[i].id=i;
    sort(q+1,q+m+1,cmp); if(p!=2&&p!=5) solve(); //离线,进行莫队算法
    else get_25(); //特殊情况:重开一个莫队处理2和5的情况
    sort(q+1,q+m+1,cmp_id); for(int i=1;i<=m;i++) printf("%lld
",q[i].ans);
}

 例4:【p4396】作业

  • 长度为n的数列和m个询问,统计区间[l,r]中 满足a<=x<=b的x 的 个数 和 种类数 。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;

// 【p4396】作业 
// 长度为n的数列和m个询问,统计区间[l,r]中‘满足a<=x<=b的x’的 个数 和 种类数 。

//莫队+分块的做法肯定有问题...但我感觉数据就是这样设计的(输入的max值<n)
//因为出题人想设计的是[莫队+分块]做法,但没有说清楚↑这一点
//所以 if(pos[l]!=0&&pos[r]==0) for(int i=pos[l]+1;i<=num;i++) 
//        ans1[id]+=skuai[i]... 这种全盘皆加的处理方法也是能过的

const int N=100010;

int n,m,L,R,block,num,pos[N],lll[N],rrr[N],cnt[N];

int skuai[N],vary[N],col[N],ans1[N],ans2[N]; //skuai[]即每个块的大小

struct node{ int l,r,x,y,id; }a[N];

bool cmp(node aa,node bb) //分块优化莫队
 { if(pos[aa.l]==pos[bb.l]) return aa.r<bb.r; return aa.l<bb.l; }

void build(){ //预处理出每个块的边界
    block=sqrt(n); num=n/block; if(n%block) num++; //分块其实维护的是具体数字
    for(int i=1;i<=num;i++) lll[i]=(i-1)*block+1,rrr[i]=i*block;
    rrr[num]=n; for(int i=1;i<=n;i++) pos[i]=(i-1)/block+1; }

void add(int col_){ cnt[col_]++; skuai[pos[col_]]++;
    if(cnt[col_]==1) vary[pos[col_]]++; } //此块中数的种类++

void del(int col_){ cnt[col_]--; skuai[pos[col_]]--;
    if(cnt[col_]==0) vary[pos[col_]]--; } //减完了,此块种类--

void find(int l,int r,int id){ //分块查找具体数字[l,r]
    if(pos[l]==pos[r]) //同一块中,直接+每个数的出现次数
     { for(int i=l;i<=r;i++) if(cnt[i]) ans1[id]+=cnt[i],ans2[id]++; return; }
    if(pos[l]!=0) for(int i=l;i<=rrr[pos[l]];i++) //零散块暴力维护
        if(cnt[i]) ans1[id]+=cnt[i],ans2[id]++;
    if(pos[r]!=0) for(int i=lll[pos[r]];i<=r;i++) //零散块暴力维护 
        if(cnt[i]) ans1[id]+=cnt[i],ans2[id]++;
    if(pos[l]!=0&&pos[r]!=0) //维护整块
        for(int i=pos[l]+1;i<pos[r];i++) ans1[id]+=skuai[i],ans2[id]+=vary[i];
    if(pos[l]!=0&&pos[r]==0) //r>n,直接++每个块的大小(所有数)
    // [这里的意思就是输入的每个数<n,这题有解释不清楚的地方......]
        for(int i=pos[l]+1;i<=num;i++) ans1[id]+=skuai[i],ans2[id]+=vary[i]; 
    //每块中维护的种类数都是按顺序的,比如[1,2,3,4][5,6,7,8]...所以vary不会重复
}

int main(){
    scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&col[i]);
    for(int i=1;i<=m;i++) scanf("%d%d%d%d",&a[i].l,&a[i].r,&a[i].x,&a[i].y),a[i].id=i;
    build(); sort(a+1,a+1+m,cmp); L=1,R=0; //分块并将询问排序
    for(int i=1;i<=m;i++){
        while(R<a[i].r) add(col[++R]); while(R>a[i].r) del(col[R--]);
        while(L<a[i].l) del(col[L++]); while(L>a[i].l) add(col[--L]);
        find(a[i].x,a[i].y,a[i].id); //在数值(x,y)的块中
        //寻找,当前区间中出现的数x<=i<=y(用cnt[i]记录次数)
    } for(int i=1;i<=m;i++) printf("%d %d
",ans1[i],ans2[i]);
}

 例5:【p4462】异或序列

  • 求区间中,子段异或和=k的有多少组。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;

//【p4462】异或序列 //求区间中,子段异或和=k的有多少组

// 把每个位置的数值记成异或前缀和的形式,问题变成:
// 区间[l−1,r]中,有多少对数异或为k。用cnt[]记录异或值,莫队。

const int N=100019; struct node{ int l,r,id; }q[N];

int n,m,k,block,sum=0,pos[N],cnt[N],a[N],ans[N];

bool cmp(node aa,node bb) //分块优化莫队
 { if(pos[aa.l]==pos[bb.l]) return aa.r<bb.r; return aa.l<bb.l; }

void add(int x){ sum+=cnt[a[x]^k],cnt[a[x]]++; }

void del(int x){ cnt[a[x]]--,sum-=cnt[a[x]^k]; }

int main(){
    scanf("%d%d%d",&n,&m,&k); block=(int)sqrt(n); // a[] 变成储存前缀的异或序列↓↓
    for(int i=1;i<=n;i++) scanf("%d",&a[i]),pos[i]=(i-1)/block+1,a[i]=a[i-1]^a[i];
    for(int i=1;i<=m;i++) scanf("%d%d",&q[i].l,&q[i].r),q[i].l--,q[i].id=i; //注意l--
    sort(q+1,q+m+1,cmp); for(int i=1,L=1,R=0;i<=m;i++){ //【莫队】 
        while(R<q[i].r) add(++R); while(R>q[i].r) del(R--);
        while(L<q[i].l) del(L++); while(L>q[i].l) add(--L); ans[q[i].id]=sum;
    } for(int i=1;i<=m;i++) printf("%d
",ans[i]);
}

                                         ——时间划过风的轨迹,那个少年,还在等你

原文地址:https://www.cnblogs.com/FloraLOVERyuuji/p/10428197.html