莫队学习笔记
普通莫队
初遇——从暴力谈起
我们通过一道例题来讨论普通莫队。
题目链接。
观察数据范围,一个很直接的想法是:开一个数组
接下来我们考虑如何优化这个算法。
首先,对于
接下来我们看几个询问:
1 5 2 6 5 7 ...
注意到,这几个询问的区间有重复部分,解决每个询问时都会重复扫描重复部分,很浪费时间。于是,我们可以使用两个指针
于是可以写出如下代码:
#include<bits/stdc++.h> #define endl '\n' #define int long long using namespace std; int n,a[30005],m,cnt[1000005],now,l=1,r=0,lt,rt; void add(int x){ if(!cnt[a[x]]) now++; cnt[a[x]]++; } void del(int x){ cnt[a[x]]--; if(!cnt[a[x]]) now--; } signed main(){ cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; cin>>m; for(int i=1;i<=m;i++){ cin>>lt>>rt; while(l>lt) add(--l); while(r<rt) add(++r); while(l<lt) del(l++); while(r>rt) del(r--); cout<<now<<endl; } return 0; }
困境——乱跑的指针
当一个询问的左端点和右端点与上一个询问的左端点与右端点差距过大时,就会出现
例如当
1 2 29999 30000 3 4 29997 29998 ...
这时,
那么如何提高指针移动的效率呢?
观察到,指针的低效移动是由询问左右端点的无序性引起的,所以我们可以把询问离线,并将数列分块。按照左端点所处的块的位置升序排序,如果左端的所处的块位置相同,则按照右端点升序排序。这样,就在保证了左端点比较有序也保证了右端点比较有序,从而提高了指针的移动效率。
代码如下:
#include<bits/stdc++.h> #define endl '\n' #define int long long using namespace std; int n,a[30005],m,cnt[1000005],now,l=1,r=0,L[30005],R[30005],pos[30005],t,ans[200005]; struct node{ int l,r,id; }q[200005]; inline bool cmp(node x,node y){ if(pos[x.l]==pos[y.l]) return x.r<y.r; return pos[x.l]<pos[y.l]; } inline void add(int x){ if(!cnt[a[x]]) now++; cnt[a[x]]++; } inline void del(int x){ cnt[a[x]]--; if(!cnt[a[x]]) now--; } signed main(){ std::ios::sync_with_stdio(false); cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; t=sqrt(n); for(int i=1;i<=t;i++){ L[i]=(i-1)*t+1; R[i]=i*t; } if(R[t]<n){ t++; L[t]=R[t-1]+1; R[t]=n; } for(int i=1;i<=t;i++){ for(int j=L[i];j<=R[i];j++){ pos[j]=i; } } cin>>m; for(int i=1;i<=m;i++){ cin>>q[i].l>>q[i].r; q[i].id=i; } sort(q+1,q+1+m,cmp); for(int i=1;i<=m;i++){ while(l>q[i].l) add(--l); while(r<q[i].r) add(++r); while(l<q[i].l) del(l++); while(r>q[i].r) del(r--); ans[q[i].id]=now;//注意,排序只有询问的顺序可能会改变,所以应该将答案存在ans[q[i].id]而不是ans[i] //4个while循环的位置有一定的要求,详见OI-Wiki } for(int i=1;i<=m;i++) cout<<ans[i]<<endl; return 0; }
优化——顺路而为之
考虑一种排序方式:
对于左端点所处的块不同的询问,按左端点升序排序。对于左端点所处的块相同的询问,若这个块是的编号是奇数,则按右端点升序排序,否则按右端点降序排序。
按照这样的方式排序后,
代码如下:
bool cmp(node x,node y){ if(x.l/t!=y.l/t) return x.l<y.l; if((x.l/t)&1) return x.r<y.r; return x.r>y.r; }
一些习题
题意简述
求区间众数的出现次数。
解法分析
本题可以离线,于是可以考虑使用莫队解决。
add
操作比较好写,下面我们主要考虑 del
操作。
记删掉的数为
-
的出现次数小于 ,对 没有影响。 -
的出现次数等于 ,但不止 一个数出现了 次,对 没有影响。 -
的出现次数等于 ,并且只有 一个数出现了 次, 。
综上,我们需要开两个数组
代码
#include<bits/stdc++.h> using namespace std; inline int read(){ ... } inline void write(int x){ ... } // read函数与write函数为快读与快写 int n,m,a[100005],b[100005],t,l=1,r,cnt1[200005],cnt2[200005],now,ans[200005]; struct node{ int l,r,id; }q[200005]; bool cmp(node x,node y){ if(x.l/t!=y.l/t) return x.l<y.l; if((x.l/t)&1) return x.r<y.r; return x.r>y.r; }//对排序策略的优化 void add(int x){ cnt2[cnt1[x]]--; cnt1[x]++; cnt2[cnt1[x]]++; now=max(now,cnt1[x]); } void del(int x){ cnt2[cnt1[x]]--; if(!cnt2[cnt1[x]] && now==cnt1[x]) now--; cnt1[x]--; cnt2[cnt1[x]]++; } int main(){ n=read(); m=read(); for(int i=1;i<=n;i++){ a[i]=read(); b[i]=a[i]; } sort(b+1,b+1+n); t=unique(b+1,b+1+n)-b-1; for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+1+t,a[i]-b;//离散化 t=sqrt(n); for(int i=1;i<=m;i++){ q[i].l=read(); q[i].r=read(); q[i].id=i; } sort(q+1,q+1+m,cmp); for(int i=1;i<=m;i++){ while(l>q[i].l) add(a[--l]); while(r<q[i].r) add(a[++r]); while(l<q[i].l) del(a[l++]); while(r>q[i].r) del(a[r--]); ans[q[i].id]=now; } for(int i=1;i<=m;i++){ write(ans[i]); putchar('\n'); } return 0; }
带修莫队
显然,想小标题是困难的QAQ
修改——与时间旅行
我们同样通过一道例题来探究带修莫队。
题目链接。
本题要求支持修改操作,我们可以在修改操作上加上一维时间维度,它的取值是最近一次修改操作的时间,即最近一次修改操作是第几次修改操作。同样,在主函数记录一个当前时间,如果当前时间早于查询操作的时间,就说明在回答这个询问之前需要按照输入数据进行修改操作;如果当前时间晚于查询操作的时间,就说明在回答这个查询之前需要按照输入数据对修改操作进行撤销,也就是尝试向以下6个方向扩展或缩小。
另外,由于加上了时间这一维,所以我们需要改变排序策略,具体的代码如下:
#include<bits/stdc++.h> #define endl '\n' #define int long long using namespace std; int n,m,a[200005],t,cnt_c,cnt_q,cnt[1000005],now,l=1,r,last,ans[200005]; struct que{ int id,ti,l,r; }q[200005]; struct cha{ int x,dat; }c[200005]; bool cmp(que x,que y){ if(x.l/t!=y.l/t) return x.l/t<y.l/t; else if(x.r/t!=y.r/t) return x.r/t<y.r/t; else return x.ti<y.ti;//在排序时需要考虑时间 } void add(int x){ if(!cnt[x]) now++; cnt[x]++; } void del(int x){ cnt[x]--; if(!cnt[x]) now--; } signed main(){ std::ios::sync_with_stdio(false); cin>>n>>m; for(int i=1;i<=n;i++){ cin>>a[i]; } for(int i=1;i<=m;i++){ char t1; int t2,t3; cin>>t1>>t2>>t3; if(t1=='Q') q[++cnt_q]=(que){cnt_q,cnt_c,t2,t3}; else if(t1=='R') c[++cnt_c]=(cha){t2,t3}; } t=pow(n,2.0/3.0); sort(q+1,q+1+cnt_q,cmp); for(int i=1;i<=cnt_q;i++){ while(l>q[i].l) add(a[--l]); while(r<q[i].r) add(a[++r]); while(l<q[i].l) del(a[l++]); while(r>q[i].r) del(a[r--]); while(last<q[i].ti){ last++; if(c[last].x<=r && c[last].x>=l){ add(c[last].dat); del(a[c[last].x]); } swap(a[c[last].x],c[last].dat);//在下面的撤销修改中再次swap即可撤销修改,减少码量 } while(last>q[i].ti){ if(c[last].x<=r && c[last].x>=l){ add(c[last].dat); del(a[c[last].x]); } swap(a[c[last].x],c[last].dat); last--; } ans[q[i].id]=now; } for(int i=1;i<=cnt_q;i++) cout<<ans[i]<<endl; return 0; }
一些习题
【模板】树套树 。
题意简述
实现树套树。
解法分析
众所周知,实现树套树的代码一般又臭又长,但是如果题目没有强制在线,我们可以考虑使用莫队代替树套树。
单点修改可以交给带修莫队完成,其他操作则需要用到值域分块的技巧。
记离散化后的最大值为
当查询
当查询排名为
当查询
当查询
代码
#include<bits/stdc++.h> using namespace std; inline int read(){register int t1=0,t2=0;register char x=getchar();while(x<'0' ||x>'9'){if(x=='-') t2|=1;x=getchar();}while(x>='0' && x<='9'){t1=(t1<<1)+(t1<<3)+(x^48),x=getchar();}return t2?-t1:t1;} inline void write(int x){register int sta[100],top=0;if(x<0) putchar('-'),x=-x;do{sta[top++]=x%10,x/=10;}while(x);while(top) putchar(sta[--top]+48);} int n,m,cnt_q,cnt_c,temp,t,l=1,r,last; int a[500005],b[500005],L[500005],R[500005],pos[500005],sum[500005],cnt[500005],ans[500005]; struct que{ int l,r,ti,id,k,opt; }q[500005]; struct cha{ int x,d; }c[500005]; bool cmp(que x,que y){ if(x.l/t!=y.l/t) return x.l/t<y.l/t; else if(x.r/t!=y.r/t) return x.r/t<y.r/t; return x.ti<y.ti; } inline void add(int x){ cnt[x]++; sum[pos[x]]++; } inline void del(int x){ cnt[x]--; sum[pos[x]]--; } inline int get_rank(int x){ int an=1; for(int i=1;i<=t;i++){ if(x<=R[i]) for(int j=L[i];j<x;j++) an+=cnt[j]; else an+=sum[i]; } return an; } inline int get_kth(int x){ for(int i=1;i<=t;i++){ if(x<=sum[i]){ for(int j=L[i];j<=R[i];j++){ x-=cnt[j]; if(x<=0) return b[j]; } } x-=sum[i]; } return -114514; } inline int get_pre(int x){ int r=get_rank(x),an; if(r==1) return -2147483647; an=get_kth(r-1); return an; } inline int get_aft(int x){ int r=get_rank(x),an; if(!cnt[x]) an=get_kth(r);//对于 x 不存在的情况需要特殊考虑 else an=get_kth(r+1); if(an==-114514) return 2147483647; else return an; } int main(){ n=read(); m=read(); for(int i=1;i<=n;i++){ a[i]=read(); b[++temp]=a[i]; } for(int i=1;i<=m;i++){ int t1=read(),t2=read(),t3=read(); if(t1==3){ c[++cnt_c]={t2,t3}; b[++temp]=t3; } else{ int t4=read(); q[++cnt_q].opt=t1; q[cnt_q].l=t2; q[cnt_q].r=t3; q[cnt_q].k=t4; b[++temp]=t4; q[cnt_q].id=cnt_q; q[cnt_q].ti=cnt_c; } } sort(b+1,b+1+temp); temp=unique(b+1,b+1+temp)-b-1; for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+1+temp,a[i])-b; for(int i=1;i<=cnt_c;i++) c[i].d=lower_bound(b+1,b+1+temp,c[i].d)-b; for(int i=1;i<=cnt_q;i++) if(q[i].opt!=2) q[i].k=lower_bound(b+1,b+1+temp,q[i].k)-b; t=pow(n,2.0/3.0); sort(q+1,q+1+cnt_q,cmp); t=sqrt(temp); for(int i=1;i<=t;i++){ L[i]=(i-1)*t+1; R[i]=i*t; } if(R[t]<temp){ t++; L[t]=R[t-1]+1; R[t]=temp; } for(int i=1;i<=t;i++){ for(int j=L[i];j<=R[i];j++){ pos[j]=i; } } for(int i=1;i<=cnt_q;i++){ while(l>q[i].l) add(a[--l]); while(r<q[i].r) add(a[++r]); while(l<q[i].l) del(a[l++]); while(r>q[i].r) del(a[r--]); while(last<q[i].ti){ last++; if(c[last].x<=r && c[last].x>=l){ add(c[last].d); del(a[c[last].x]); } swap(a[c[last].x],c[last].d); } while(last>q[i].ti){ if(c[last].x<=r && c[last].x>=l){ add(c[last].d); del(a[c[last].x]); } swap(a[c[last].x],c[last].d); last--; } if(q[i].opt==1) ans[q[i].id]=get_rank(q[i].k); else if(q[i].opt==2) ans[q[i].id]=get_kth(q[i].k); else if(q[i].opt==4) ans[q[i].id]=get_pre(q[i].k); else ans[q[i].id]=get_aft(q[i].k); } for(int i=1;i<=cnt_q;i++){ write(ans[i]); putchar('\n'); } return 0; }
优缺点评价
优点:
- 代码相比于传统树套树较为简短,细节较少。
缺点:
- 无法在线回答询问。
回滚莫队
但是我还是决定想小标题。
回滚——我是小标题
我们依然从一道例题开始讲起。
题目链接。
在这道题目中,在莫队中插入元素比较简单,但是从莫队中删除元素较为困难,于是,我们可以只使用插入这一个操作,剩下的交给回滚解决,这就是回滚莫队的核心思想。
假设块长为
首先,我们将询问离线下来,并且将询问按照左端点所处的块为第一关键字,右端点为第二关键字升序排序,这样左端点在同一块的询问就会变得连续,而它们的右端点将递增。按排序后的顺序处理询问,若该询问的左右端点在同一个块内,可以在
具体代码如下:
#include<bits/stdc++.h> #define int long long using namespace std; inline int read(){register int t1=0,t2=0;register char x=getchar();while(x<'0' ||x>'9'){if(x=='-') t2|=1;x=getchar();}while(x>='0' && x<='9'){t1=(t1<<1)+(t1<<3)+(x^48),x=getchar();}return t2?-t1:t1;} inline void write(int x){register int sta[105],top=0;if(x<0) putchar('-'),x=-x;do{sta[top++]=x%10,x/=10;}while(x);while(top) putchar(sta[--top]+48);} int n,m,a[100005],b[100005],pos[100005],R[100005],L[100005],t,temp,l=1,r=0,now,cnt[100005],temp_cnt[100005],ans[100005],last,temp_l; struct que{ int l,r,id; }q[100005]; void build(){ t=sqrt(n); for(int i=1;i<=t;i++){ L[i]=(i-1)*t+1; R[i]=i*t; } if(R[t]<n){ t++; R[t]=n; L[t]=R[t-1]+1; } for(int i=1;i<=t;i++) for(int j=L[i];j<=R[i];j++) pos[j]=i; } bool cmp(que x,que y){ if(pos[x.l]!=pos[y.l]) return pos[x.l]<pos[y.l]; return x.r<y.r; } inline void add(int x,int& y){ cnt[a[x]]++; y=max(y,cnt[a[x]]*b[a[x]]); } inline void del(int x){cnt[a[x]]--;} signed main(){ n=read(); m=read(); build(); for(int i=1;i<=n;i++){ b[i]=read(); a[i]=b[i]; } for(int i=1;i<=m;i++){ q[i].l=read(); q[i].r=read(); q[i].id=i; } sort(q+1,q+1+m,cmp); sort(b+1,b+1+n); temp=unique(b+1,b+1+n)-b-1; for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+1+temp,a[i])-b; for(int i=1;i<=m;i++){ if(pos[q[i].l]==pos[q[i].r]){ for(int j=q[i].l;j<=q[i].r;j++){ temp_cnt[a[j]]++; ans[q[i].id]=max(ans[q[i].id],temp_cnt[a[j]]*b[a[j]]); } for(int j=q[i].l;j<=q[i].r;j++) temp_cnt[a[j]]--; } else{ if(pos[q[i].l]!=last){ while(l<R[pos[q[i].l]]+1) del(l++); while(r>R[pos[q[i].l]]) del(r--); now=0; last=pos[q[i].l]; } while(r<q[i].r) add(++r,now); temp_l=l; temp=now; while(temp_l>q[i].l) add(--temp_l,temp); ans[q[i].id]=temp; while(temp_l<l) del(temp_l++); } } for(int i=1;i<=m;i++){ write(ans[i]); putchar('\n'); } return 0; }
树上莫队
前置知识:最近公共祖先的求法。
我们还是从一道例题开始讲起。
题目链接。
我们先考虑给出的树满足链的性质的情况,在这种情况下,使用带修莫队就可以轻松地解决这个问题。
但是本题给出的是一棵树,这时就需要用到树上莫队的技术。
我们先使用 dfs 将给定的树拍成相应的括号序,记第
若使用树链剖分求最近公共祖先,则代码如下:
#include<bits/stdc++.h> #define int long long using namespace std; inline int read(){register int t1=0,t2=0;register char x=getchar();while(x<'0' ||x>'9'){if(x=='-') t2|=1;x=getchar();}while(x>='0' && x<='9'){t1=(t1<<1)+(t1<<3)+(x^48),x=getchar();}return t2?-t1:t1;} inline void write(int x){register int sta[105],top=0;if(x<0) putchar('-'),x=-x;do{sta[top++]=x%10,x/=10;}while(x);while(top) putchar(sta[--top]+48);} const int MAXN=1e6+5; int n,m,Q,tot,cnt_q,cnt_c,now,l=1,r,last; int v[MAXN],w[MAXN],a[MAXN],fa[MAXN],son[MAXN],dep[MAXN],siz[MAXN],top[MAXN],st[MAXN],ed[MAXN],rnk[MAXN],ans[MAXN],cnt[MAXN]; bool vis[MAXN]; vector<int> e[MAXN]; struct que{ int l,r,lca,id,ti; }q[MAXN]; bool cmp(que x,que y){ if(x.l/Q!=y.l/Q) return x.l/Q<y.l/Q; else if(x.r/Q!=y.r/Q) return x.r/Q<y.r/Q; return x.ti<y.ti; } struct cha{ int x,y; }c[MAXN]; inline void dfs1(int x){ //树剖顺便求括号序 son[x]=-1; siz[x]=1; st[x]=++tot; rnk[tot]=x; for(int i:e[x]){ if(!dep[i]){ dep[i]=dep[x]+1; fa[i]=x; dfs1(i); siz[x]+=siz[i]; if(son[x]==-1 || siz[i]>siz[son[x]]) son[x]=i; } } ed[x]=++tot; rnk[tot]=x; } inline void dfs2(int x,int y){ //树剖 top[x]=y; if(son[x]==-1) return; dfs2(son[x],y); for(int i:e[x]) if(i!=son[x] && i!=fa[x]) dfs2(i,i); } inline int lca(int x,int y){ //树剖求LCA while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]]) swap(x,y); x=fa[top[x]]; } if(dep[x]<dep[y]) return x; else return y; } inline void add(int x){ cnt[x]++; now+=v[x]*w[cnt[x]]; } inline void del(int x){ now-=v[x]*w[cnt[x]]; cnt[x]--; } inline void change(int x){ if(!vis[x]) add(a[x]); else del(a[x]); vis[x]^=1; } inline void change_time(int x){ //进行修改操作 if(vis[c[x].x]){ add(c[x].y); del(a[c[x].x]); } swap(c[x].y,a[c[x].x]); } signed main(){ n=read(); m=read(); Q=read(); for(int i=1;i<=m;i++) v[i]=read(); for(int i=1;i<=n;i++) w[i]=read(); for(int i=1;i<n;i++){ int t1=read(),t2=read(); e[t1].push_back(t2); e[t2].push_back(t1); } for(int i=1;i<=n;i++) a[i]=read(); dep[1]=1; dfs1(1); dfs2(1,1); while(Q--){ int t1=read(),t2=read(),t3=read(); if(t1==0){ cnt_c++; c[cnt_c]={t2,t3}; } else{ cnt_q++; if(st[t2]>st[t3]) swap(t2,t3); q[cnt_q].lca=lca(t2,t3); q[cnt_q].id=cnt_q; q[cnt_q].ti=cnt_c; if(q[cnt_q].lca==t2){ q[cnt_q].l=st[t2]; q[cnt_q].r=st[t3]; q[cnt_q].lca=0; } else{ q[cnt_q].l=ed[t2]; q[cnt_q].r=st[t3]; } } } Q=sqrt(tot); sort(q+1,q+1+cnt_q,cmp); for(int i=1;i<=cnt_q;i++){ //先计算再考虑修改也是可以的 while(last<q[i].ti) change_time(++last); while(last>q[i].ti) change_time(last--); while(l>q[i].l) change(rnk[--l]); while(r<q[i].r) change(rnk[++r]); while(l<q[i].l) change(rnk[l++]); while(r>q[i].r) change(rnk[r--]); if(q[i].lca) change(q[i].lca); ans[q[i].id]=now; if(q[i].lca) change(q[i].lca); } for(int i=1;i<=cnt_q;i++){ write(ans[i]); putchar('\n'); } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现