莫队算法
以前经常听人说“离线莫队搞一搞”这种十分dalao的话,但是从来没有学习过这种奇妙的算法
说到底还是一种乱搞的方法,采用了平方分割的技巧
不过和块状链表那样的平方分割后维护块内内容不一样,莫队算法是通过某种顺序计算所有查询,使得均摊复杂度为O(N√N)
比如,有一个长度为n的序列ai,其中的每一个元素有一个颜色ci
我们有m个查询[lj,rj],即查询这段区间内相同颜色最多出现多少次、一共有多少种颜色出现次数最多,这两个询问
我们显然能够用暴力的方法O(N2)地解决这个问题:对于每个[lj,rj],一个for循环跑一遍就行了
那么两个查询[lj,rj]、[lj′,rj′]之间是否存在某些关系,来帮助我们减少枚举次数呢?
事实上,如果我们已经知道[lj,rj]的结果,只需要将左端点lj一点点移到lj′、将右端点rj一点点移到rj′,我们就能得到查询[lj′,rj′]的结果
问题在于,从一个查询变为另一个,左右端点的移动可能是O(N)级别的
现在考虑将整个序列分成√N个区间,则每个区间的长度是√N
我们对于所有询问,处理的顺序是:将所有询问排序,第一关键字是lj所在块的编号,第二关键字是rj所在块的编号
为什么这样做能够保证均摊复杂度呢?
对于每组lj所在块编号和rj所在块编号对应相等的查询,因为所有左端点都在同一块内,所以最多移动√N次;右端点同理
而从一组跳到另一组,对于lj所在块编号确定的时候,右端点从第1块一直跳到第√N块,而相邻块之间的跳转跟块的长度有关;lj所在块编号也要跳√N次,所以总体上进行了√N×√N次O(√N)的跳转
总而言之,就是通过减少相邻询问间的跳转来降低复杂度,而真正的计算仍然是暴力
给一道具体的题目吧:BZOJ 2038
在这道题目中,每次查询[lj,rj]的分母就是C(rj−lj+1,2);而分子则是计算当前区间内每个数字i出现的次数cnti,并对每个i将C(cnti,2)求和
想使用莫队算法,我们先得考虑如何暴力:即,怎么从一个查询移动到另一个
假设一次移动以后,我们加入了数字a,从而使其在现有区间内的出现次数从x变为x+1
那么数字a对分子的贡献由C(x,2)变为C(x+1,2),即由x⋅(x−1)2变为(x+1)⋅x2,相当于增加了一个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\n",ans1[i]/div,ans2[i]/div); } return 0; }
树上莫队
一般的莫队只能处理序列上的问题,而树上的问题(特别是查询子树)一般会通过dfs序将树上问题转化成序列上问题
再给一道题目:CF 600E
在这题中,我们要对每个点进行一次查询
怎么转化成序列上问题呢?如果对这颗树从根开始进行一次dfs,并且记录访问每个点的顺序li和离开每个点的顺序ri,就能够发现:以某个点x为根的子树中,所有点的访问顺序都介于lx和rx之间
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; }
带修改莫队(三维莫队)
上面的所有莫队都是不带修改的,如果有修改存在,能否用莫队处理?
可以!(不过更加奇妙了)
我们的每次查询相当于带了一个时间维度ti,表示ti前的所有修改必须到位
那么我们重新考虑一下查询间的关系
从[li,ri,ti]到[li′,ri′,ti′],我们不仅需要移动左右端点,还必须考虑到时间上的移动
我们先可以将左右端点先移到[li′,ri′](方法跟上面是一样的),问题在于时间如何移动:如果在时间tx上有一个修改
- 向后移:将颜色序列上的对应位置修改成新颜色,如果这个位置在当前区间内,就减去原颜色,加上新颜色
- 向前移:将颜色序列上的对应位置恢复成原颜色,如果这个位置在当前区间内,就减去新颜色,加上原颜色
而我们要做的就是一直移动时间,将ti′前的所有修改全部安排上,同时ti′后的一点也不修改
这样,查询之间可以通过端点每次O(1)的移动来慢慢到达
而且处理查询的顺序也出来了:将所有查询排序,第一关键字是li所在块的编号,第二关键字是ri所在块的编号,第三关键字是ti
不过最玄学(其实很有道理)的地方来了:一共分为N13块,每块长度为N23,均摊复杂度为O(N53)
第一眼看上去是不是有点吓人,现在我们来分析为什么要这样划分
如果左右端点所在块确定了,左右端点在块内的移动是O(N23)的,n次查询都需要移动
时间的移动是单调的,在n13×n13个可能的左右端点所在块的分布中,都需要O(N)的扫描
对于左端点所在块确定的情况,右端点要跳n13次,每次跳转要花费O(N13)的右端点移动时间和O(N)的时间维移动时间;左端点一共跳n13次,一共是n13×n13次O(N)的跳转
这样一来每部分都是O(N53)
扔一道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\n",ans[i]); return 0; }
以后应该还能用上,如果遇到的话再补一些题目上来
树上莫队:CF 375D(Tree and Queries)
带修改莫队:Codeforces 940F (Machine Learning)
区间中每个数出现数量的mex是很难用数据结构来维护的,因为相当于两层关系的叠加。所以考虑采用离线做法,就能够想到莫队了。
由于存在修改操作,所以应该采用有时间维度的带修改莫队,复杂度为O(n53)。我们有当前区间左右端点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(n53)是所有l,r,t指针移动的复杂度,而实际的询问数仍是q。由于一共只有n个数,所以mex一定不超过√n,于是对于每个查询都O(√n)求一遍即可。总复杂度O(n53+n√n)=O(n53)。

//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; }
(完)
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 理解Rust引用及其生命周期标识(下)
· 从二进制到误差:逐行拆解C语言浮点运算中的4008175468544之谜
· .NET制作智能桌面机器人:结合BotSharp智能体框架开发语音交互
· 软件产品开发中常见的10个问题及处理方法
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
· 2025成都.NET开发者Connect圆满结束
· 后端思维之高并发处理方案
· 千万级大表的优化技巧
· 在 VS Code 中,一键安装 MCP Server!
· 10年+ .NET Coder 心语 ── 继承的思维:从思维模式到架构设计的深度解析