莫队算法
以前经常听人说“离线莫队搞一搞”这种十分dalao的话,但是从来没有学习过这种奇妙的算法
说到底还是一种乱搞的方法,采用了平方分割的技巧
不过和块状链表那样的平方分割后维护块内内容不一样,莫队算法是通过某种顺序计算所有查询,使得均摊复杂度为$O(N\sqrt{N})$
比如,有一个长度为$n$的序列$a_i$,其中的每一个元素有一个颜色$c_i$
我们有$m$个查询$[l_j,r_j]$,即查询这段区间内相同颜色最多出现多少次、一共有多少种颜色出现次数最多,这两个询问
我们显然能够用暴力的方法$O(N^2)$地解决这个问题:对于每个$[l_j,r_j]$,一个$for$循环跑一遍就行了
那么两个查询$[l_j,r_j]$、$[l_{j'},r_{j'}]$之间是否存在某些关系,来帮助我们减少枚举次数呢?
事实上,如果我们已经知道$[l_j,r_j]$的结果,只需要将左端点$l_j$一点点移到$l_{j'}$、将右端点$r_j$一点点移到$r_{j'}$,我们就能得到查询$[l_{j'},r_{j'}]$的结果
问题在于,从一个查询变为另一个,左右端点的移动可能是$O(N)$级别的
现在考虑将整个序列分成$\sqrt{N}$个区间,则每个区间的长度是$\sqrt{N}$
我们对于所有询问,处理的顺序是:将所有询问排序,第一关键字是$l_j$所在块的编号,第二关键字是$r_j$所在块的编号
为什么这样做能够保证均摊复杂度呢?
对于每组$l_j$所在块编号和$r_j$所在块编号对应相等的查询,因为所有左端点都在同一块内,所以最多移动$\sqrt{N}$次;右端点同理
而从一组跳到另一组,对于$l_j$所在块编号确定的时候,右端点从第$1$块一直跳到第$\sqrt{N}$块,而相邻块之间的跳转跟块的长度有关;$l_j$所在块编号也要跳$\sqrt{N}$次,所以总体上进行了$\sqrt{N}\times \sqrt{N}$次$O(\sqrt{N})$的跳转
总而言之,就是通过减少相邻询问间的跳转来降低复杂度,而真正的计算仍然是暴力
给一道具体的题目吧:BZOJ 2038
在这道题目中,每次查询$[l_j,r_j]$的分母就是$C(r_j-l_j+1,2)$;而分子则是计算当前区间内每个数字$i$出现的次数$cnt_i$,并对每个$i$将$C(cnt_i,2)$求和
想使用莫队算法,我们先得考虑如何暴力:即,怎么从一个查询移动到另一个
假设一次移动以后,我们加入了数字$a$,从而使其在现有区间内的出现次数从$x$变为$x+1$
那么数字$a$对分子的贡献由$C(x,2)$变为$C(x+1,2)$,即由$\frac{x\cdot (x-1)}{2}$变为$\frac{(x+1)\cdot x}{2}$,相当于增加了一个$x$
这样,我们每次移动的计算就是$O(1)$的了,接下来就是用上面莫队的思路按顺序处理所有询问
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <cstdio> #include <cstring> #include <vector> #include <cmath> #include <algorithm> using namespace std; struct Query { int x,y,id; Query(int a,int b,int c) { x=a,y=b,id=c; } }; typedef long long ll; const int MAX=50005; const int SQ=240; int sz; inline int Index(int x) { return x/sz; } inline bool operator < (Query a,Query b) { if(Index(a.x)!=Index(b.x)) return Index(a.x)<Index(b.x); return Index(a.y)<Index(b.y); } int n,m; int a[MAX]; vector<Query> v; int cnt[MAX]; ll ans1[MAX],ans2[MAX]; inline ll gcd(ll x,ll y) { if(y==0) return x; return gcd(y,x%y); } int main() { // freopen("input.txt","r",stdin); scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&a[i]); sz=(int)sqrt(n*1.0)+1; for(int i=1;i<=m;i++) { int x,y; scanf("%d%d",&x,&y); v.push_back(Query(x,y,i)); } sort(v.begin(),v.end()); int left=v[0].x,right=left-1; ll val=0; for(int i=0;i<v.size();i++) { int x=v[i].x,y=v[i].y,id=v[i].id; while(left<x) --cnt[a[left]],val-=cnt[a[left]],left++; while(left>x) val+=cnt[a[--left]],cnt[a[left]]++; while(right<y) val+=cnt[a[++right]],cnt[a[right]]++; while(right>y) --cnt[a[right]],val-=cnt[a[right]],right--; ans1[id]=val,ans2[id]=(ll)(y-x+1)*(y-x+0)/2; } for(int i=1;i<=m;i++) { ll div=gcd(ans2[i],ans1[i]); printf("%lld/%lld\n",ans1[i]/div,ans2[i]/div); } return 0; }
树上莫队
一般的莫队只能处理序列上的问题,而树上的问题(特别是查询子树)一般会通过$dfs$序将树上问题转化成序列上问题
再给一道题目:CF 600E
在这题中,我们要对每个点进行一次查询
怎么转化成序列上问题呢?如果对这颗树从根开始进行一次$dfs$,并且记录访问每个点的顺序$l_i$和离开每个点的顺序$r_i$,就能够发现:以某个点$x$为根的子树中,所有点的访问顺序都介于$l_x$和$r_x$之间
inline void dfs(int x,int fa) { l[x]=++tot; for(int i=0;i<e[x].size();i++) { int next=e[x][i]; if(next!=fa) dfs(next,x); } r[x]=tot; }
这样,我们就把一棵树通过$dfs$序拍平成了一个序列,剩下的就是序列上的明显的莫队了
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <cstdio> #include <cstring> #include <cmath> #include <vector> #include <algorithm> using namespace std; struct Query { int x,y,id; Query(int a=0,int b=0,int c=0) { x=a,y=b,id=c; } }; inline bool operator < (Query a,Query b) { return a.y<b.y; } typedef long long ll; const int MAX=100005; const int SQ=350; int n; int c[MAX]; vector<int> e[MAX]; int tot; int l[MAX],r[MAX]; inline void dfs(int x,int fa) { l[x]=++tot; for(int i=0;i<e[x].size();i++) { int next=e[x][i]; if(next!=fa) dfs(next,x); } r[x]=tot; } int sz; int a[MAX]; vector<Query> v[SQ]; ll sum[MAX]; int cnt[MAX]; inline int Index(int x) { return x/sz+1; } ll ans[MAX]; int main() { // freopen("input.txt","r",stdin); scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",&c[i]); for(int i=1;i<n;i++) { int x,y; scanf("%d%d",&x,&y); e[x].push_back(y); e[y].push_back(x); } dfs(1,0); for(int i=1;i<=n;i++) a[l[i]]=c[i]; sz=(int)sqrt(n*1.0)+1; for(int i=1;i<=n;i++) v[Index(l[i])].push_back(Query(l[i],r[i],i)); for(int i=1;i<=Index(n);i++) sort(v[i].begin(),v[i].end()); for(int i=1;i<=Index(n);i++) { if(!v[i].size()) continue; memset(sum,0LL,sizeof(sum)); memset(cnt,0,sizeof(cnt)); int left=v[i][0].x,right=left-1,top=0; for(int j=0;j<v[i].size();j++) { int x=v[i][j].x,y=v[i][j].y,id=v[i][j].id; while(left<x) { int color=a[left]; sum[cnt[color]]-=color; cnt[color]--; sum[cnt[color]]+=color; left++; if(!sum[top]) top--; } while(left>x) { left--; int color=a[left]; sum[cnt[color]]-=color; cnt[color]++; sum[cnt[color]]+=color; top=(cnt[color]>top?cnt[color]:top); } while(right<y) { right++; int color=a[right]; sum[cnt[color]]-=color; cnt[color]++; sum[cnt[color]]+=color; top=(cnt[color]>top?cnt[color]:top); } ans[id]=sum[top]; } } for(int i=1;i<=n;i++) printf("%I64d ",ans[i]); return 0; }
带修改莫队(三维莫队)
上面的所有莫队都是不带修改的,如果有修改存在,能否用莫队处理?
可以!(不过更加奇妙了)
我们的每次查询相当于带了一个时间维度$t_i$,表示$t_i$前的所有修改必须到位
那么我们重新考虑一下查询间的关系
从$[l_i,r_i,t_i]$到$[l_{i'},r_{i'},t_{i'}]$,我们不仅需要移动左右端点,还必须考虑到时间上的移动
我们先可以将左右端点先移到$[l_{i'},r_{i'}]$(方法跟上面是一样的),问题在于时间如何移动:如果在时间$t_x$上有一个修改
- 向后移:将颜色序列上的对应位置修改成新颜色,如果这个位置在当前区间内,就减去原颜色,加上新颜色
- 向前移:将颜色序列上的对应位置恢复成原颜色,如果这个位置在当前区间内,就减去新颜色,加上原颜色
而我们要做的就是一直移动时间,将$t_{i'}$前的所有修改全部安排上,同时$t_{i'}$后的一点也不修改
这样,查询之间可以通过端点每次$O(1)$的移动来慢慢到达
而且处理查询的顺序也出来了:将所有查询排序,第一关键字是$l_i$所在块的编号,第二关键字是$r_i$所在块的编号,第三关键字是$t_i$
不过最玄学(其实很有道理)的地方来了:一共分为$N^{\frac{1}{3}}$块,每块长度为$N^{\frac{2}{3}}$,均摊复杂度为$O(N^{\frac{5}{3}})$
第一眼看上去是不是有点吓人,现在我们来分析为什么要这样划分
如果左右端点所在块确定了,左右端点在块内的移动是$O(N^{\frac{2}{3}})$的,$n$次查询都需要移动
时间的移动是单调的,在$n^{\frac{1}{3}}\times n^{\frac{1}{3}}$个可能的左右端点所在块的分布中,都需要$O(N)$的扫描
对于左端点所在块确定的情况,右端点要跳$n^{\frac{1}{3}}$次,每次跳转要花费$O(N^{\frac{1}{3}})$的右端点移动时间和$O(N)$的时间维移动时间;左端点一共跳$n^{\frac{1}{3}}$次,一共是$n^{\frac{1}{3}}\times n^{\frac{1}{3}}$次$O(N)$的跳转
这样一来每部分都是$O(N^{\frac{5}{3}})$
扔一道UVa的裸题:UVa 12345
就是单纯的带修改莫队而已,跟上面分析的一样做就行了
好像自增自减纠缠在语句里会WA...以后要注意了
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <cstdio> #include <cstring> #include <algorithm> #include <vector> #include <cmath> using namespace std; struct Query { int x,y,id; Query(int a,int b,int c) { x=a,y=b,id=c; } }; struct Modify { int x,y,prev,id; Modify(int a,int b,int c,int d) { x=a,y=b,prev=c,id=d; } }; const int MAX=50005; const int SQ=250; int sz; inline int Index(int x) { return x/sz; } inline bool operator < (Query a,Query b) { if(Index(a.x)!=Index(b.x)) return Index(a.x)<Index(b.x); if(Index(a.y)!=Index(b.y)) return Index(a.y)<Index(b.y); return a.id<b.id; } int n,m; int c[MAX]; int a[MAX]; vector<Query> q; vector<Modify> v; int tot; int cnt[MAX*20]; inline void Del(int x) { cnt[a[x]]--; if(cnt[a[x]]==0) tot--; } inline void Add(int x) { cnt[a[x]]++; if(cnt[a[x]]==1) tot++; } int ans[MAX]; int main() { // freopen("input.txt","r",stdin); scanf("%d%d",&n,&m); for(int i=0;i<n;i++) scanf("%d",&c[i]),a[i]=c[i]; for(int i=1;i<=m;i++) { char op=getchar(); while(op<'A' || op>'Z') op=getchar(); int x,y; scanf("%d%d",&x,&y); if(op=='M') v.push_back(Modify(x,y,a[x],i)),a[x]=y; else q.push_back(Query(x,--y,i)); } for(int i=1;i<=n;i++) if(i*i*i>=n) { sz=i*i; break; } for(int i=0;i<n;i++)//#2 a[i]=c[i]; sort(q.begin(),q.end()); memset(ans,-1,sizeof(ans)); int left=q[0].x,right=left-1,j=-1; for(int i=0;i<q.size();i++) { int x=q[i].x,y=q[i].y,id=q[i].id; while(left<x) Del(left++); while(left>x) Add(--left); while(right<y)//#1 Add(++right); while(right>y) Del(right--); while(j+1<v.size() && v[j+1].id<id) if(v[++j].x>=x && v[j].x<=y) Del(v[j].x),a[v[j].x]=v[j].y,Add(v[j].x); else a[v[j].x]=v[j].y; while(j>=0 && v[j].id>id) if(v[j].x>=x && v[j].x<=y) Del(v[j].x),a[v[j].x]=v[j].prev,Add(v[j].x),j--; else a[v[j].x]=v[j].prev,j--; ans[id]=tot; } for(int i=1;i<=m;i++) if(ans[i]!=-1) printf("%d\n",ans[i]); return 0; }
以后应该还能用上,如果遇到的话再补一些题目上来
树上莫队:CF 375D($Tree$ $and$ $Queries$)
带修改莫队:Codeforces 940F (Machine Learning)
区间中每个数出现数量的mex是很难用数据结构来维护的,因为相当于两层关系的叠加。所以考虑采用离线做法,就能够想到莫队了。
由于存在修改操作,所以应该采用有时间维度的带修改莫队,复杂度为$O(n^{\frac{5}{3}})$。我们有当前区间左右端点$l,r$和当前时间$t$,同时我们一定也维护了一个当前时间下的数组$a[i]$,不妨记为$cur[i]$。对于所有修改这样处理:对于一次将位置$p$改为$x$的修改,我们先看$p$是否在$l,r$中(带修改莫队先移动$l,r$再移动$t$),如果不在的话令$cur[p]=x$就可以了,如果在的话需要先将$cnt[cur[p]]$减小、再令$cur[p]=x$、最后将$cnt[x]$增加。
一开始一直在想怎么$O(1)$动态维护mex,不过需要注意到一点,上面的$O(n^{\frac{5}{3}})$是所有$l,r,t$指针移动的复杂度,而实际的询问数仍是$q$。由于一共只有$n$个数,所以mex一定不超过$\sqrt{n}$,于是对于每个查询都$O(\sqrt{n})$求一遍即可。总复杂度$O(n^{\frac{5}{3}}+n\sqrt{n})=O(n^{\frac{5}{3}})$。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//18:54-19:20 //submits: //19:20 (-1) TLE for index(id) //19:23 (-2) n^2/3 for block size #pragma GCC optimize(3) #include <cstdio> #include <vector> #include <cstring> #include <algorithm> using namespace std; const int N=100005; const int sz=2500; inline int index(int x) { return x/sz; } struct Query { int l,r,id; Query(int _l=0,int _r=0,int _id=0) { l=_l,r=_r,id=_id; } }; inline bool operator <(const Query &X,const Query &Y) { if(index(X.l)!=index(Y.l)) return index(X.l)<index(Y.l); if(index(X.r)!=index(Y.r)) if(index(X.l)%2==0) return index(X.r)<index(Y.r); else return index(X.r)>index(Y.r); return X.id<Y.id; } struct Modify { int pos,val,prev,id; Modify(int _pos,int _val,int _prev,int _id) { pos=_pos,val=_val,prev=_prev,id=_id; } }; int n,q; int a[N],cur[N]; int op[N],x[N],y[N]; vector<int> vec; inline int getpos(int x) { return lower_bound(vec.begin(),vec.end(),x)-vec.begin()+1; } vector<Query> vq; vector<Modify> vm; int cnt[N<<1],mex[N<<1]; inline int getmex() { int i=1; while(mex[i]>0) i++; return i; } inline void add(int x) { if(cnt[x]>0) mex[cnt[x]]--; cnt[x]++; if(cnt[x]>0) mex[cnt[x]]++; } inline void del(int x) { if(cnt[x]>0) mex[cnt[x]]--; cnt[x]--; if(cnt[x]>0) mex[cnt[x]]++; } int ans[N]; inline bool within(int L,int R,int pos) { return (pos>=L && pos<=R); } int main() { scanf("%d%d",&n,&q); for(int i=1;i<=n;i++) scanf("%d",&a[i]),vec.emplace_back(a[i]); for(int i=1;i<=q;i++) { scanf("%d%d%d",&op[i],&x[i],&y[i]); if(op[i]==2) vec.emplace_back(y[i]); } sort(vec.begin(),vec.end()); vec.resize(unique(vec.begin(),vec.end())-vec.begin()); for(int i=1;i<=n;i++) a[i]=getpos(a[i]),cur[i]=a[i]; for(int i=1;i<=q;i++) if(op[i]==1) vq.emplace_back(Query(x[i],y[i],i)); else { y[i]=getpos(y[i]); vm.emplace_back(Modify(x[i],y[i],cur[x[i]],i)); cur[x[i]]=y[i]; } sort(vq.begin(),vq.end()); for(int i=1;i<=n;i++) cur[i]=a[i]; int l=1,r=0,t=0; for(Query curq: vq) { int L=curq.l,R=curq.r,T=curq.id; while(l<L) del(cur[l]),l++; while(l>L) --l,add(cur[l]); while(r<R) ++r,add(cur[r]); while(r>R) del(cur[r]),r--; while(t<vm.size() && vm[t].id<T) { int pos=vm[t].pos; if(within(L,R,pos)) del(cur[pos]),add(vm[t].val); cur[pos]=vm[t].val; t++; } while(t>0 && vm[t-1].id>T) { int pos=vm[t-1].pos; if(within(L,R,pos)) del(cur[pos]),add(vm[t-1].prev); cur[pos]=vm[t-1].prev; t--; } ans[T]=getmex(); } for(int i=1;i<=q;i++) if(op[i]==1) printf("%d\n",ans[i]); return 0; }
(完)