Mo's Algorithm
Mo's Algorithm——莫队算法
<一个优雅的暴力算法>
主要题型分为三种,不带修改莫队,带修改莫队,树形莫队。
<最最最最最普通的莫队>
莫队算法实质是离线情况下对所有即将要查询的区间进行一次美妙的sort(以L为第一关键字,R为第二关键字),然后用
两个指针L,R,在区间上用看似暴力的方法跳来跳去,最终输出答案。
详解sort:有人会问,为什么不直接按L排序呢,但你想想,若L<L1<L2,且R1<R<R2,那么如果直接按L排序,就会
浪费很多时间,所以这里我们引入了一个分块思想,每个块的大小为sqrt(N),分块使得两个询问之间的差异平均到了L,
R上,理论复杂度为O(N*sqrt(N))。
我们应如何状态转移区间?当前状态下,询问区间为[a,b],下一个询问区间为[p,q],当前已知区间[a,b]内每种颜色
的出现次数cnt[],已知询问区间[a,b]的答案ans1,怎么求出ans2呢?
每一次移动l,r,我们都需要维护cnt数组和ans。
如图,我们损失了一个绿色方块,那么我们需对cnt[绿]减一,并判断是否为零,若为零,则表示当前区间内已经没有绿色了。
所以ans减一,否则不变。
再如图,我们得到了一个橙色方块,那么cnt[橙]+1,我们还需判断cnt[橙]加后是否为一,若是的话,则表示在当前区间新增了
一种颜色,那么ans+1,否则不变。
当然,以上只列举了两种情况,其实有四种情况不过本蒟蒻比较懒。
懂了的话可以去完成luogu2709,这里给出代码
#include<cstdio> #include<algorithm> #include<cmath> #define maxn 50050 using namespace std; struct Query{ int near,l,r,id; }Q[maxn]; int cnt[maxn],where[maxn],ans=0,a[maxn],Qnum,Cnum,print[maxn],k,m; int read(){ int s=0;char c=getchar(); while(c<'0'||c>'9') c=getchar(); while(c>='0'&&c<='9') s=s*10+c-48,c=getchar(); return s; } bool cmp(Query a,Query b){ if(where[a.l]!=where[b.l]) return where[a.l]<where[b.l]; return a.r<b.r; } void Add(int val){ ans+=(cnt[val]++)<<1|1; } void Delet(int val){ ans-=(--cnt[val])<<1|1; } void MoQueue(){ int l=1,r=0,now=0; for(int i=1;i<=Qnum;i++){ while(l<Q[i].l) Delet(a[l++]); while(l>Q[i].l) Add(a[--l]); while(r<Q[i].r) Add(a[++r]); while(r>Q[i].r) Delet(a[r--]); print[Q[i].id]=ans; } for(int i=1;i<=m;i++){ printf("%d\n",print[i]); } } int main(){ int n,base; char opt; n=read();m=read();k=read(); base=sqrt(n); for(int i=1;i<=n;i++) a[i]=read(),where[i]=i/base+1; int mm=m; while(mm--){ Q[++Qnum].l=read(); Q[Qnum].r=read(); Q[Qnum].near=Cnum; Q[Qnum].id=Qnum; } sort(Q+1,Q+1+Qnum,cmp); MoQueue(); return 0; }
<带修改的莫队>
也称可持久化莫队。
在这里我们引入第三个关键字——修改时间,即当前查询是在经过第几次修改之后的,也就是说,在查询时,
看看当前询问和时间指针,进行时间倒流或时间推移。所以sort的构造也要改变,若还是按原来,可能会因时间的影响
而移动n次,导致时间复杂度爆炸级增长,所以我们引入了第三个关键字——时间。
bool cmp(Query a,Query b){ if(where[a.l]!=where[b.l]) return where[a.l]<where[b.l]; if(a.r!=b.r) return a.r<b.r; return a.near<b.near; }
我们用个time来记录当前是第几次修改了,若当前要查询的区间的最近的修改位置比time还要后,我们则需把
中间从time+1到里查询区间最近的一次补上,若比time还要前,我们需把查询区间最近的一次+1到time还原。
luogu1903:
#include<cstdio> #include<algorithm> #include<cmath> #define maxn 10050 using namespace std; struct Query{ int near,l,r,id; }Q[maxn]; struct Change{ int pos,val; }C[1050]; int cnt[maxn],where[maxn],ans=0,a[maxn],Qnum,Cnum,print[maxn]; int read(){ int s=0;char c=getchar(); while(c<'0'||c>'9') c=getchar(); while(c>='0'&&c<='9') s=s*10+c-48,c=getchar(); return s; } bool cmp(Query a,Query b){ if(where[a.l]!=where[b.l]) return where[a.l]<where[b.l]; if(a.r!=b.r) return a.r<b.r; return a.near<b.near; } void Add(int val){ if(++cnt[val]==1) ans++; } void Delet(int val){ if(--cnt[val]==0) ans--; } void Work(int now,int i){ if(C[now].pos>=Q[i].l&&C[now].pos<=Q[i].r){ if(--cnt[a[C[now].pos]]==0) ans--; if(++cnt[C[now].val]==1) ans++; } swap(C[now].val,a[C[now].pos]); } void MoQueue(){ int l=1,r=0,now=0; for(int i=1;i<=Qnum;i++){ while(l<Q[i].l) Delet(a[l++]); while(l>Q[i].l) Add(a[--l]); while(r<Q[i].r) Add(a[++r]); while(r>Q[i].r) Delet(a[r--]); while(now<Q[i].near) Work(++now,i); while(now>Q[i].near) Work(now--,i); print[Q[i].id]=ans; } for(int i=1;i<=Qnum;i++){ printf("%d\n",print[i]); } } int main(){ int n,m,base; char opt; n=read();m=read(); base=sqrt(n); for(int i=1;i<=n;i++) a[i]=read(),where[i]=i/base+1; while(m--){ scanf(" %c",&opt); if(opt=='Q'){ Q[++Qnum].l=read(); Q[Qnum].r=read(); Q[Qnum].near=Cnum; Q[Qnum].id=Qnum; } else{ C[++Cnum].pos=read(); C[Cnum].val=read(); } } sort(Q+1,Q+1+Qnum,cmp); MoQueue(); return 0; }
至于树形莫队嘛,本蒟蒻还没学,敬请期待!