莫队算法(普通,带修,回滚,树上)

本文详细介绍了莫队算法的四种形式:普通莫队、带修莫队、回滚莫队和树上莫队。通过案例分析和代码模板,阐述了每种莫队算法的适用场景、排序方法和时间复杂度,帮助读者深入理解这一高效的数据处理算法。
摘要由CSDN通过智能技术生成

这是用来复习的博客,不太建议想要初学普通莫队的人看这篇博客,不过要是想快速复习一遍,这个博客可能比较适合你。

莫队算法

莫队算法是一种通过对询问多关键字排序来降低时间复杂度的算法,当一些可离线题目满足 一个询问[l,r]的答案可以由[l-1,r]/[l+1,r]/[l,r-1]/[l,r+1]更新得到 时,可以考虑莫队算法。普通莫队不支持修改,不过如果是简单的修改可以用带修莫队/回滚莫队解决,至于树上问题则需要引入树上莫队。莫队实现简洁,应用面大,非常值得一学。


一、普通莫队

普通莫队需要将询问按左端点所属块第一关键字右端点位置第二关键字进行排序,这里是奇偶化排序,即编号为奇数的块和编号为偶数的块编号单调性不同,能带来很可观的优化。通常来说按照Block=sqrt(n)定义块的大小即可。代码非常简洁,calc加了点小优化,这里贴上模板的代码。
洛谷P1494 [国家集训队] 小 Z 的袜子
注意一下奇偶化排序

#include<bits/stdc++.h>
using namespace std;
#define il inline
#define re register
typedef long long LL;
il int read(){
	int s=0,w=1;char c=getchar();
	while(c<'0'||c>'9'){ if(c=='-') w=-1;c=getchar();}
	while(c>='0'&&c<='9'){ s=(s<<1)+(s<<3)+c-'0';c=getchar();}
	return s*w;
} 
il LL read_ll(){
	LL s=0,w=1;char c=getchar();
	while(c<'0'||c>'9'){ if(c=='-') w=-1;c=getchar();}
	while(c>='0'&&c<='9'){ s=(s<<1)+(s<<3)+c-'0';c=getchar();}
	return s*w;
} 
const int N=5e4+10;
int n,m,c[N],ans[N][5],now_ans;
namespace Modui{
	int Block,ID[N],t[N];
	bool vis[N];
	struct node{ int l,r,id;}Q[N]; int qt;
	il bool cmp(node c,node d){//奇偶化排序 
		if(ID[c.l]!=ID[d.l]) return ID[c.l]<ID[d.l];
		return ID[c.l]&1?c.r<d.r:c.r>d.r;
	}
	il void calc(int x){
		if(!vis[x]) now_ans+=t[c[x]],++t[c[x]];
		else --t[c[x]],now_ans-=t[c[x]];
		vis[x]^=1;
	}
} using namespace Modui;
il int gcd(int x,int y){
	if(x<y) swap(x,y);
	return y==0?x:gcd(y,x%y);
}
int main()
{
	n=read(),m=read(),Block=sqrt(n);
	for(re int i=1;i<=n;i++)
		c[i]=read(),ID[i]=(i-1)/Block+1;
	for(re int i=1;i<=m;i++){
		Q[++qt]=(node){read(),read(),i};
		if(Q[qt].l==Q[qt].r) ans[i][1]=0,ans[i][2]=1,--qt;
	}
	sort(Q+1,Q+1+qt,cmp);
	for(re int i=1,l=1,r=0;i<=qt;i++){
		while(l>Q[i].l) calc(--l);
		while(r<Q[i].r) calc(++r);
		while(l<Q[i].l) calc(l++);
		while(r>Q[i].r) calc(r--);
		ans[Q[i].id][1]=now_ans,ans[Q[i].id][2]=1LL*(Q[i].r-Q[i].l)*(Q[i].r-Q[i].l+1)>>1LL;
		int GCD=gcd(ans[Q[i].id][1],ans[Q[i].id][2]);
		ans[Q[i].id][1]/=GCD,ans[Q[i].id][2]/=GCD;
	}
	for(re int i=1;i<=m;i++) printf("%d/%d\n",ans[i][1],ans[i][2]);
}

一道练习题


二、带修莫队

带修莫队较于普通莫队有如下修改:

  • 对询问加入时间戳t(即该操作在第t个修改操作之后),并添加一个时间指针,在移动指针时优先移动时间指针。
  • 排序方式转变为:以左端点所在块为第一关键字,右端点所在块为第二关键字,时间戳为第三关键字进行排序
  • 块长定义为n2/3 ,总复杂度复杂度为O(n5/3 )级别。

由上述定义可以发现,普通莫队由于没有修改,时间戳均为0,所以我们可以将普通莫队看作带修莫队的一个特殊情况。这也就是说,只有两个询问处于同一个时间点,我们才能放心大胆地移动指向询问的指针。我们发现,在移动完时间指针后,上一个询问与当前询问便会处于同一个时间点内,即两个询问之间没有修改,于是问题被转化成了普通莫队。(希望我说明白了)
至于时间复杂度分析,大部分时候都不会需要,想了解可以看看《信息学奥赛一本通 金牌导航》里面的讲解。

洛谷P1903 [国家集训队] 数颜色 / 维护队列

#include<bits/stdc++.h>
using namespace std;
#define il inline
#define re register
typedef long long LL;
il int read(){
	int s=0,w=1;char c=getchar();
	while(c<'0'||c>'9'){ if(c=='-') w=-1;c=getchar();}
	while(c>='0'&&c<='9'){ s=(s<<1)+(s<<3)+c-'0';c=getchar();}
	return s*w;
} 
il LL read_ll(){
	LL s=0,w=1;char c=getchar();
	while(c<'0'||c>'9'){ if(c=='-') w=-1;c=getchar();}
	while(c>='0'&&c<='9'){ s=(s<<1)+(s<<3)+c-'0';c=getchar();}
	return s*w;
} 
il int read_sp(){
	char c=getchar();
	while(c!='Q'&&c!='R') c=getchar();
	return c=='Q'?1:2;
}
const int N=140000;
const int M=1e6+10;
int n,m,c[N],lst[N],ans[N],now_ans;
namespace Modui{
	int Block,ID[N],t[M];
	bool vis[N];
	struct node{ int l,r,t,id;}Q[N]; int qt;
	struct Node{ int x,val,pre;}C[N]; int ct;
	il bool cmp(node c,node d){ 
		if(ID[c.l]!=ID[d.l]) return ID[c.l]<ID[d.l];
		if(ID[c.r]!=ID[d.r]) return ID[c.r]<ID[d.r];
		return c.t<d.t;
	}
	il void calc(int x){
		if(!vis[x]){
			if(t[c[x]]==0) ++now_ans;
			++t[c[x]];
		}
		else{
			if(t[c[x]]==1) --now_ans;
			--t[c[x]];
		} vis[x]^=1;
	}
	il void upd(int x,int val){
		if(vis[x]){
			calc(x);
			c[x]=val;
			calc(x);
		}
		else c[x]=val;
	}
} using namespace Modui;
int main(){
	n=read(),m=read(),Block=pow(n,2.0/3.0);
	for(re int i=1;i<=n;i++) lst[i]=c[i]=read(),ID[i]=(i-1)/Block+1;
	for(re int i=1,op,x,val;i<=m;i++){
		op=read_sp();
		if(op==1)  ++qt,Q[qt]=(node){read(),read(),ct,qt};
		if(op==2){
			x=read(),val=read(),++ct,C[ct]=(Node){x,val,lst[x]};
			lst[x]=val;
		}
	}
	sort(Q+1,Q+1+qt,cmp);
	for(re int i=1,l=1,r=0,t=0;i<=qt;i++){
		while(t<Q[i].t) ++t,upd(C[t].x,C[t].val);
		while(t>Q[i].t) upd(C[t].x,C[t].pre),--t;
		while(l>Q[i].l) calc(--l);
		while(r<Q[i].r) calc(++r);
		while(l<Q[i].l) calc(l++);
		while(r>Q[i].r) calc(r--);
		ans[Q[i].id]=now_ans;
	}
	for(re int i=1;i<=qt;i++) printf("%d\n",ans[i]);
	return 0;
}

三、回滚莫队

回滚莫队适用于所有操作只能有一个类型(如只加不减、只减不加),复杂度同普通莫队一样,也为O(n3/2 )。当移动指针时更新答案的效果不独立(前面的修改操作对后面的修改操作有影响),或者不能简单地撤销或者叠加贡献时,回滚莫队就能派上用场了。
回滚莫队的排序方法同普通莫队一样,以左端点所属块为第一关键字,右端点为第二关键字排序,这样询问根据左端点被分成了n1/2 组。
首先,当左端点所属块改变时,设当前左端点属于ID号块,L,R分别表示该块的左右边界,l,r表示左/右指针。将l放置在R+1处,将r放置在R处,同时别忘了其他初始化操作(比如清零数组)
随后对于同一组的询问,我们分两种情况讨论:

  • 如果左、右端点均属于ID,暴力回答询问,单次复杂度O(n1/2 )
  • 否则,像普通莫队一样移动左右端点。不过注意,在回答完此次询问后,左端点对答案的贡献需要全部清除,并令左端点回到R+1处 。这也就是“回滚”的意义。这样一来,这部分就只需要考虑一种操作就行了。此时,左端点单次移动不超过O(n1/2 );而每组询问右端点移动次数不超过O(n),所以右端点总共O(n3/2 )次移动。

于是整个算法的正确性得到了保证。
下面这道题,可以用主席树、随机化、优化+O2的暴力(是的你没听错)等等各种算法通过(毕竟这是个黄题),而我们用回滚莫队解决它
这道题的取最值就属于不易于撤销的操作,于是回滚莫队
洛谷P7261 [COCI2009-2010#3] PATULJCI

#include<bits/stdc++.h>
using namespace std;
#define il inline
#define re register
il int read()
{
	int s=0,w=1;char c=getchar();
	while(c<'0'||c>'9'){ if(c=='-') w=-1;c=getchar();}
	while(c>='0'&&c<='9'){ s=(s<<1)+(s<<3)+c-'0';c=getchar();}
	return s*w;
}
const int N=3e5+10;
const int M=1e4+10;
int n,col,SZ,a[N],m,num[N],L[N],R[N],ans[N];
struct node{
	int a,b,id;
}Q[M];
il bool operator <(const node &c,const node &d){
	if(num[c.a]!=num[d.a]) return num[c.a]<num[d.a];
	else return c.b<d.b;
}
namespace Modui{
	int maxx;
	int t[N],temp[N];
	il void Bruce(int x)
	{
		int l=Q[x].a,r=Q[x].b,len=(r-l+1)>>1;
		for(re int i=l;i<=r;i++){
			temp[a[i]]++;
			if(temp[a[i]]>len){
				ans[Q[x].id]=a[i];break;
			}
		}
		for(re int i=l;i<=r;i++) temp[a[i]]=0;
		return ;
	}
	il void add(int x){
		t[x]++,maxx=(t[maxx]<t[x])?x:maxx;
	}
	il void work()
	{
		int l=1,r=0;
		for(re int i=1;i<=m;i++){
			if(num[Q[i].a]!=num[Q[i-1].a])
				memset(t,0,sizeof(t)),l=R[num[Q[i].a]]+1,r=R[num[Q[i].a]],maxx=0;
			if(num[Q[i].b]==num[Q[i].a]) Bruce(i);
			else{
				while(r<Q[i].b) add(a[++r]);
				int tmp_max=maxx,tmp_l=l;
				while(l>Q[i].a) add(a[--l]);
				ans[Q[i].id]=t[maxx]>((Q[i].b-Q[i].a+1)>>1)?maxx:0;
				maxx=tmp_max;
				while(l<tmp_l) t[a[l]]--,l++;
			}
		}
	}
}
//Modui
int main()
{
	n=read(),col=read(),SZ=sqrt(n);
	for(re int i=1;i<=n;i++) a[i]=read(),num[i]=(i-1)/SZ+1;
	for(re int i=1;i<=SZ;i++) L[i]=SZ*(i-1)+1,R[i]=SZ*i;
	if(R[SZ]!=n) SZ++,R[SZ]=n,L[SZ]=R[SZ-1]+1;
	m=read();
	for(re int i=1;i<=m;i++) Q[i].a=read(),Q[i].b=read(),Q[i].id=i;
	sort(Q+1,Q+1+m);
	Modui::work();
	for(re int i=1;i<=m;i++){
		if(ans[i]) printf("yes %d\n",ans[i]);
		else printf("no\n");
	}
	return 0;
} 

再贴上洛谷官方模板题的代码
P5906 【模板】回滚莫队&不删除莫队

#include<bits/stdc++.h>
using namespace std;
#define il inline
#define re register
typedef long long LL;
il int read(){
	int s=0,w=1;char c=getchar();
	while(c<'0'||c>'9'){ if(c=='-') w=-1;c=getchar();}
	while(c>='0'&&c<='9'){ s=(s<<1)+(s<<3)+c-'0';c=getchar();}
	return s*w;
}
const int N=2e5+10;
int n,m,a[N],b[N],bt,ans[N];
namespace Modui{
	int ID[N],L[600],R[600],lmin[N],rmax[N],temp[N],now_ans,Block;
	struct node{ int l,r,id;}Q[N];
	il bool cmp(node c,node d){
		return ID[c.l]!=ID[d.l]?ID[c.l]<ID[d.l]:c.r<d.r;
	}
	il void Bruce(int x){
		int val=0;
		for(re int i=Q[x].l;i<=Q[x].r;++i){
			if(!temp[a[i]]) temp[a[i]]=i;
			else val=max(val,i-temp[a[i]]);
		}
		for(re int i=Q[x].l;i<=Q[x].r;++i) temp[a[i]]=0;
		ans[Q[x].id]=val;
	}
	il void calc(int x,int op){
		if(op==1){
			if(!lmin[a[x]]) lmin[a[x]]=rmax[a[x]]=x;
			else rmax[a[x]]=x,now_ans=max(rmax[a[x]]-lmin[a[x]] , now_ans);
		}
		if(op==2){
			if(!rmax[a[x]]) rmax[a[x]]=x;
			else now_ans=max(now_ans , rmax[a[x]]-x);
		}
		if(op==3){
			if(rmax[a[x]]==x) rmax[a[x]]=0;
		}
	}
} using namespace Modui;
int main()
{
	//step1:input & prework
	n=read(),Block=sqrt(n);
	for(re int i=1;i<=n;i++) b[i]=a[i]=read(),ID[i]=(i-1)/Block+1;
	for(re int i=1,ed=ID[n];i<=ed;i++) L[i]=R[i-1]+1,R[i]=i*Block;
	R[ID[n]]=n;
	sort(b+1,b+1+n);
	bt=unique(b+1,b+1+n)-b-1;
	for(re int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+1+bt,a[i])-b;
	m=read();
	for(re int i=1;i<=m;++i){
		Q[i]=(node){read(),read(),i};
	}
	//step2:work
	sort(Q+1,Q+1+m,cmp);
	for(re int i=1,l=1,r=0;i<=m;i++){
		if(ID[Q[i].l] != ID[Q[i-1].l]){//the block is changed
			memset(lmin,0,sizeof(lmin)),memset(rmax,0,sizeof(rmax));
			now_ans=0,l=R[ID[Q[i].l] ]+1,r=R[ID[Q[i].l] ];
			//多个[]套在一块的时候别套懵了,要明确定义 
		}
		if(ID[Q[i].l]==ID[Q[i].r]) Bruce(i);//bruce
		else{//huigun modui
			while(r<Q[i].r) calc(++r,1); 
			int tmp_ans=now_ans;
			while(l>Q[i].l) calc(--l,2);
			ans[Q[i].id]=now_ans;
			while(l<=R[ID[Q[i].l] ]) calc(l++,3);
			now_ans=tmp_ans;
		}
	}
	for(re int i=1;i<=m;i++) printf("%d\n",ans[i]);
	return 0;
}
/*
9
1 3 2 4 2 1 4 2 3
3
1 6
2 4
3 8

5
0
5
*/

四、树上莫队

普通莫队只能支持在一段区间上移动指针,我们要做的就是把树转化一条链,不难想到欧拉序(即将每经过一个点就将其编号加入一个栈后,最终该栈中所有元素构成的序列,每个点在欧拉序中出现2次)。
具体地,我们求出一棵树的欧拉序,在移动指针的时候将路过的节点上的状态取反(0变成1,1变成0),这时我们发现,所有标记为1的点就是这段路径上的点。于是就是普通的莫队了。
还有一种分块方式,是将一个点的子树内Block个点打包在一个块内并保证每个块内元素为O(Block)级别从而保证莫队复杂度,这里不做介绍,感兴趣自己了解吧qwq
洛谷P4074 [WC2013] 糖果公园
树上带修莫队。可以考虑将询问[L,R]转化成[ ift[L] ,ift[R] ],其中ift[i]表示i第一次出现在欧拉序上时出现在哪里(即该点在欧拉序上的左侧位置)。如果ift[L]>ift[R],将L,R互换。
注意! 如果L!=lca(L,R),L,那么L和lca是不会被统计的! 拿此题样例来说,对于4–>2这条路径,在统计的时候只会统计2的信息而未统计4,3上的信息,一定要注意特判。
注意分块大小,这是一个带修莫队,所以块大小要开n2/3 。然后注意询问指针是在长度为2n的序列上移动。
(改了好长时间结果发现是本地系统栈炸了……)

#include<bits/stdc++.h>
using namespace std;
#define il inline
#define re register
typedef long long LL;
il int read(){
	int s=0,w=1;char c=getchar();
	while(c<'0'||c>'9'){ if(c=='-') w=-1;c=getchar();}
	while(c>='0'&&c<='9'){ s=(s<<1)+(s<<3)+c-'0';c=getchar();}
	return s*w;
}
const int N=1e5+10;
int n,m,K,c[N],lst[N];
int fa[N],son[N],dep[N],top[N],sz[N];
int ift[N],oft[N],ioft[N<<1],ft;
LL V[N],W[N],ans[N];
namespace Graph{
	int head[N],ver[N<<1],nxt[N<<1],Gt;
	il void addedge(int u,int v){
		ver[++Gt]=v,nxt[Gt]=head[u],head[u]=Gt;
		ver[++Gt]=u,nxt[Gt]=head[v],head[v]=Gt;
	}
} using namespace Graph;
namespace Modui{
	int ID[N<<1],Block,t[N];
	LL now_ans;
	bool vis[N];
	struct node{ int l,r,t,id; }Q[N]; int qt;
	struct Node{ int x,val,pre; }C[N]; int ct;
	il bool cmp(node c,node d){
		if(ID[c.l]!=ID[d.l]) return ID[c.l]<ID[d.l];
		if(ID[c.r]!=ID[d.r]) return ID[c.r]<ID[d.r];
		return c.t<d.t;
	}
	il void calc(int x){
		if(!vis[x]) ++t[c[x] ],now_ans+=V[c[x] ]*W[t[c[x] ] ];
		else now_ans-=V[c[x] ]*W[t[c[x] ] ],--t[c[x] ];
		vis[x]^=1;
	}
	il void Change(int x,int val){
		if(vis[x]){
			calc(x);
			c[x]=val;
			calc(x);
		}
		else c[x]=val;
	}
} using namespace Modui;
il void dfs_pre(int x,int F){
	ioft[ift[x]=++ft]=x;
	fa[x]=F,sz[x]=1,dep[x]=dep[F]+1;
	for(re int i=head[x],v;i;i=nxt[i]){
		v=ver[i];if(v==F) continue;
		dfs_pre(v,x);
		sz[x]+=sz[v];
		if(sz[son[x]]<sz[v]) son[x]=v;
	}
	ioft[++ft]=x;
}
il void dfs2(int x,int T){
	top[x]=T;
	if(son[x]) dfs2(son[x],T);
	for(re int i=head[x],v;i;i=nxt[i]){
		v=ver[i];if(v==fa[x] || v==son[x]) continue;
		dfs2(v,v);
	}
}
il int getlca(int u,int v){
	int tu=top[u],tv=top[v];
	while(tu!=tv){
		if(dep[tu]<dep[tv]) swap(tu,tv),swap(u,v);
		u=fa[tu],tu=top[u];
	}
	return dep[u]<dep[v]?u:v;
}
int main()
{
	//freopen("P4074_6.in","r",stdin);
	//freopen("LZ.out","w",stdout);
	n=read(),K=read(),m=read();
	for(re int i=1;i<=K;i++) V[i]=read();
	for(re int i=1;i<=n;i++) W[i]=read();
	for(re int i=1,u,v;i<n;i++) u=read(),v=read(),addedge(u,v);
	dfs_pre(1,0),dfs2(1,1),Block=pow(ft,2.0/3.0);//注意块大小 
	for(re int i=1;i<=ft;i++) ID[i]=(i-1)/Block+1;
	//上面这块挂过,注意要对1~ft的数计算ID值 
	for(re int i=1;i<=n;i++) lst[i]=c[i]=read();
	for(re int i=1,op,x,y;i<=m;i++){
		op=read(),x=read(),y=read();
		if(op==0) C[++ct]=(Node){x,y,lst[x]},lst[x]=y;
		if(op==1){
			if(ift[x]>ift[y]) swap(x,y);
			++qt,Q[qt]=(node){ift[x],ift[y],ct,qt};
		}
	}
	sort(Q+1,Q+1+qt,cmp);
	for(re int i=1,l=1,r=0,t=0,x,y;i<=qt;++i){
		x=ioft[Q[i].l],y=ioft[Q[i].r];
		while(t<Q[i].t) ++t,Change(C[t].x,C[t].val);
		while(t>Q[i].t) Change(C[t].x,C[t].pre),--t;
		while(l>Q[i].l) calc(ioft[--l]);
		while(r<Q[i].r) calc(ioft[++r]);
		while(l<Q[i].l) calc(ioft[l++]);
		while(r>Q[i].r) calc(ioft[r--]);
		//注意特判 
		int LCA=getlca(x,y);
		if(LCA!=x) calc(x),calc(LCA);
		ans[Q[i].id]=now_ans;
		if(LCA!=x) calc(x),calc(LCA);
	}
	for(re int i=1;i<=qt;i++) printf("%lld\n",ans[i]);
	return 0; 
}

是不是莫队也没有那么难~

end

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值