普通莫队的简介
莫队是一种基于分块思想的离线算法,用于解决区间问题,适用范围如下:
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]); }
——时间划过风的轨迹,那个少年,还在等你