莫队
莫队
莫队算法可以解决一类离线区间询问问题,适用性极为广泛。同时将其加以扩展,便能轻松处理树上路径询问以及支持修改操作。 ——— OI-WIKI
莫队是一种 看起来非常暴力的暴力,能够处理离线区间查询的问题。
具体地,我们可以将询问离线下来,通过某种特殊的排序使得所有询问可以在较小的复杂度内完成。
通常,这种特殊的排序我们使用分块(更精确的可以建哈夫曼最小生成树,但是既然都打暴力了就要一以贯之),将 \([1,n]\) 分为 \(\sqrt{n}\) 块,左端点在一块内的按照右端点排序,否则按照左端点排序,这样就能在 \(\mathcal{O}(n\sqrt{n})\) 的时间复杂度内求解所有询问。注意这是建立在将 \([l,r]\) 扩展到相邻区间 \([l+1,r],[l-1,r],[l,r+1],[l,r-1]\) 的复杂度是 \(\mathcal{O}(1)\) 的情况下,否则复杂度会大大提升。
例题
例 1:P2709 小B的询问
题目大意
给定一个长度为 \(n\)、元素值域在 \([1,k]\) 的序列 \(a\),给定 \(m\) 个形如 \([l,r]\) 的询问,求出 \(\sum\limits_{i=1}^{k}{c_i^2}\),其中 \(c_i\) 指 \(i\) 在 \([l,r]\) 中出现的次数。\(1\le n,m\le 5\times 10^4\)。
思路
直接离线排序套莫队即可。注意此处的加点与删点的更新答案应该是 \(c_i^2\xrightarrow{+2c_i+1} (c_i+1)^2\) 与 \(c_i^2\xrightarrow{-2c_i+1} (c_i-1)^2\)。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#define maxn 50005
#define ll long long
using namespace std;
int n,m,k,len,a[maxn],le,ri,ans,ti[maxn],res; struct node{int id,l,r; ll ans;}q[maxn];
bool cmp(node a,node b){if((a.l-1)/len==(b.l-1)/len) return a.r<b.r; return (a.l-1)/len<(b.l-1)/len;}
bool cmp2(node a,node b){return a.id<b.id;}
void add(int p){res+=(2*ti[p]+1); ti[p]++;} void del(int p){res-=(2*ti[p]-1); ti[p]--;}
int main(){
scanf("%d%d%d",&n,&m,&k); len=sqrt(n); for(int i=1;i<=n;i++) scanf("%d",&a[i]); le=1; ri=0;
for(int i=1;i<=m;i++){q[i].id=i; scanf("%d%d",&q[i].l,&q[i].r);} sort(q+1,q+1+m,cmp);
for(int i=1;i<=m;i++){
while(le>q[i].l){le--; add(a[le]);} while(ri<q[i].r){ri++; add(a[ri]);}
while(le<q[i].l){le++; del(a[le-1]);} while(ri>q[i].r){ri--; del(a[ri+1]);} q[i].ans=res;
} sort(q+1,q+1+m,cmp2); for(int i=1;i<=m;i++) printf("%lld\n",q[i].ans);
return 0;
}
例 2:P1903 [国家集训队] 小 Z 的袜子
题目大意
给定一个长度为 \(n\) 的序列 \(a\),给定 \(m\) 个形如 \([l,r]\) 的询问,求出在 \([l,r]\) 中随机抽取两个数是相同的的概率。\(1\le n,m\le 5\times 10^4\)。
思路
将概率看作方法数除以总数,用莫队统计总数,除以总方法数即可。注意加减点时的修改建立在加之前 / 减之后的基础上。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#define maxn 50005
#define ll long long
using namespace std;
int n,m,a[maxn],len,le=1,ri=0; struct node{int l,r,id; ll ans;}q[maxn]; ll res=0,ti[maxn];
bool cmp(node a,node b){if((a.l-1)/len==(b.l-1)/len) return a.r<b.r; return (a.l-1)/len<(b.l-1)/len;}
bool cmp2(node a,node b){return a.id<b.id;} ll gcd(ll a,ll b){if(b==0) return a; return gcd(b,a%b);}
void add(int p){res+=ti[p]; ti[p]++;} void del(int p){res-=(ti[p]-1); ti[p]--;}
int main(){
scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&a[i]); len=sqrt(n);
for(int i=1;i<=m;i++){q[i].id=i; scanf("%d%d",&q[i].l,&q[i].r);} sort(q+1,q+1+m,cmp);
for(int i=1;i<=m;i++){
if(q[i].l==q[i].r){q[i].ans=0; continue;}
while(le>q[i].l){le--; add(a[le]);} while(ri<q[i].r){ri++; add(a[ri]);}
while(le<q[i].l){le++; del(a[le-1]);} while(ri>q[i].r){ri--; del(a[ri+1]);} q[i].ans=res;
} sort(q+1,q+1+m,cmp2);
for(int i=1;i<=m;i++){
if(q[i].l==q[i].r){printf("0/1\n"); continue;} ll num=1LL*(q[i].r-q[i].l+1)*(q[i].r-q[i].l)/2;
ll g=gcd(q[i].ans,num); printf("%lld/%lld\n",q[i].ans/g,num/g);
}
return 0;
}
回滚莫队
普通莫队求解问题时,会碰到区间扩展(变大或变小之一)不好实现的问题,如维护区间最大值时缩小区间不好实现。这时候就可以使用回滚莫队解决。
假设区间只能扩大,排序结束后,询问 \([l,r]\) 在同一块内的暴力处理。对于剩下的操作,左端点在同一块内的一起操作:先扩展右端点,记录当前的值,再扩展左端点,最后统计完答案以后将左端点跳回块的右端,值改回刚才记录的值,相当于把左端点重置了。
例题
例 3:AT_joisc2014_c 歴史の研究
题目大意
给定一个序列 \(a\),\(q\) 次询问 \([l,r]\) 区间内 \(i\times cnt_i\) 的最大值(\(cnt_i\) 为数字 \(i\) 出现的次数)。
思路
本题因为维护最大值,所以区间无法缩小,于是采用回滚莫队解决。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#define maxn 300005
#define ll long long
using namespace std;
int n,T,len,a[maxn],ord[maxn],cnt[maxn],l,r,num=-1; ll mmax;
struct ques{int l,r,id; ll ans;}q[maxn]; bool cmp2(ques aa,ques bb){return aa.id<bb.id;}
bool cmp1(ques aa,ques bb){return ((aa.l-1)/len==(bb.l-1)/len)?(aa.r<bb.r):((aa.l-1)/len<(bb.l-1)/len);}
ll brute(int l,int r){
int ct[maxn]={0}; ll res=0; for(int i=l;i<=r;i++){ct[a[i]]++; res=max(res,1LL*ct[a[i]]*ord[a[i]]);}
for(int i=l;i<=r;i++) ct[a[i]]--; return res;
}
int main(){
scanf("%d%d",&n,&T); len=sqrt(T); for(int i=1;i<=n;i++){scanf("%d",&a[i]); ord[i]=a[i];}
sort(ord+1,ord+1+n); int llen=unique(ord+1,ord+1+n)-1-ord;
for(int i=1;i<=n;i++) a[i]=lower_bound(ord+1,ord+1+llen,a[i])-ord;
for(int i=1;i<=T;i++){scanf("%d%d",&q[i].l,&q[i].r); q[i].id=i;} sort(q+1,q+1+T,cmp1);
for(int i=1;i<=T;i++){
if((q[i].l-1)/len!=num){num=(q[i].l-1)/len; memset(cnt,0,sizeof(cnt)); l=r=(num+1)*len; l++; mmax=0LL;}
if((q[i].l-1)/len==(q[i].r-1)/len){q[i].ans=brute(q[i].l,q[i].r); continue;}
while(r<q[i].r){r++; cnt[a[r]]++; mmax=max(mmax,1LL*cnt[a[r]]*ord[a[r]]);} ll res=mmax;
while(l>q[i].l){l--; cnt[a[l]]++; mmax=max(mmax,1LL*cnt[a[l]]*ord[a[l]]);} q[i].ans=mmax;
while(l<=(num+1)*len){cnt[a[l]]--; l++;} mmax=res;
} sort(q+1,q+1+T,cmp2); for(int i=1;i<=T;i++) printf("%lld\n",q[i].ans);
return 0;
}