以前经常听人说“离线莫队搞一搞”这种十分dalao的话,但是从来没有学习过这种奇妙的算法
说到底还是一种乱搞的方法,采用了平方分割的技巧
不过和块状链表那样的平方分割后维护块内内容不一样,莫队算法是通过某种顺序计算所有查询,使得均摊复杂度为$O(Nsqrt{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} imes 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{xcdot (x-1)}{2}$变为$frac{(x+1)cdot x}{2}$,相当于增加了一个$x$
这样,我们每次移动的计算就是$O(1)$的了,接下来就是用上面莫队的思路按顺序处理所有询问

#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 ",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$序拍平成了一个序列,剩下的就是序列上的明显的莫队了

#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}} imes 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}} imes n^{frac{1}{3}}$次$O(N)$的跳转
这样一来每部分都是$O(N^{frac{5}{3}})$
扔一道UVa的裸题:UVa 12345
就是单纯的带修改莫队而已,跟上面分析的一样做就行了
好像自增自减纠缠在语句里会WA...以后要注意了

#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 ",ans[i]); return 0; }
以后应该还能用上,如果遇到的话再补一些题目上来
树上莫队:CF 375D($Tree$ $and$ $Queries$)
(完)