莫队算法学习笔记

其实莫涛据说是 xqz 的学生,可以算是我的学长。

以下记 \(n\) 为序列 \(a_1,a_2,\cdots,a_n\) 的长度,\(q\) 为询问次数(与修改次数之和)。

对序列以 \(B\) 为块长分块,记 \(\operatorname{pos}(i),\operatorname{edgeL}(i),\operatorname{edgeR}(i)\) 分别为 \(a_i\) 的块编号(即 \(\left\lfloor\dfrac iB\right\rfloor\)),块 \(i\) 的左端点(即 \((i-1)B+1\)),块 \(i\) 的右端点(即 \(iB\))。

普通莫队算法

莫队算法是一种用于处理序列上的区间询问问题离线算法

对序列以 \(B\) 为块长分块

若区间 \([l,r]\) 的答案能够 \(\mathcal O(k)\) 扩展到 \([l-1,r],[l+1,r],[l,r-1],[l,r+1]\),视询问次数 \(q\)\(n\) 同阶,则可以在 \(\mathcal O\left(nk\sqrt n\right)\) 的复杂度内解决。通常情况下,可以取 \(\mathcal O(k)=\mathcal O(1)\),但是有的时候带 log 也是可以接受的。

以下分析时视为 \(\mathcal O(1)\) 扩展区间,记从区间 \([l,r]\) 扩展到 \([l',r]\)\([l,r]\rightarrow[l',r']\)

而莫队算法的思想也很简单,离线后排序,对于 \([l,r]\) 按照 \(\operatorname{pos}(l)\) 升序、\(r\) 直接升序排序。


设当前维护的区间为 \([l,r]\)。则 \([l_1,r_1]\rightarrow[l_2,r_2]\) 过程中一定要保证 \(l\leq r\),否则答案会错误。(也可以用 \(l=r+1\) 来表示空区间

具体而言,先将 \([l,r]\) 向左扩展 \(l\),再向右扩展 \(r\),之后才是向右扩展 \(l\) 和向左扩展 \(r\);区间扩展的顺序不能随意修改。

\([l,r]\rightarrow[l-1,r]\)addLeft 操作,\([l,r]\rightarrow[l,r+1]\)addRight 操作,\([l,r]\rightarrow[l+1,r]\)delLeft 操作,\([l,r]\rightarrow[l,r-1]\)delRight 操作。同时,在 \([l_1,r_1]\rightarrow[l_2,r_2]\) 过程中 addLeftdelLeft 至多有一个会发生,原因显然;addRightdelRight 同理。[1]

那么操作顺序为:addLeftaddRightdelLeftdelRight


对于初始状态 \([l_0,r_0]\),可以直接取 \([1,1]\) 并手动计算答案,也可以取 \([2,1]\) 表示空区间

普通莫队算法模板代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
constexpr const int N=27000,M=27000;
int n,m,a[N+1];
struct question{
	int l,r,id;
}q[M+1];
int ans[N+1];
int B,pos[N+1],edgeL[N+1],edgeR[N+1];
void pre(){
	B=sqrt(n);
	for(int i=1;edgeR[i-1]+1<=n;i++){
		edgeL[i]=edgeR[i-1]+1;
		edgeR[i]=min(edgeL[i]+B-1,n);
		for(int j=edgeL[i];j<=edgeR[i];j++){
			pos[j]=i;
		}
	}
	sort(q+1,q+m+1,[](question a,question b){
		if(pos[a.l]!=pos[b.l]){
			return pos[a.l]<pos[b.l];
		}else{
			return a.r<b.r;
		}
	});
}
void addLeft(int &l,int &r,int &cnt){
	l--;
	//do something.
}
void addRight(int &l,int &r,int &cnt){
	r++;
	//do something.
}
void delLeft(int &l,int &r,int &cnt){
	//do something.
	l++;
}
void delRight(int &l,int &r,int &cnt){
	//do something.
	r--;
}
int main(){
	/*freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);*/
	
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	for(int i=1;i<=m;i++){
		cin>>q[i].l>>q[i].r;
		q[i].id=i;
	}
	pre();
	//cnt:记录答案 
	int l=2,r=1,cnt=0;
	for(int i=1;i<=m;i++){
		while(q[i].l<l){
			addLeft(l,r,cnt); 
		}
		while(r<q[i].r){
			addRight(l,r,cnt);
		}
		while(l<q[i].l){
			delLeft(l,r,cnt); 
		}
		while(q[i].r<r){
			delRight(l,r,cnt);
		}
		//do something.
		ans[q[i].id]=cnt;
	}
	for(int i=1;i<=m;i++){
		cout<<ans[i]<<'\n';
	}
	
	cout.flush();
	
	/*fclose(stdin);
	fclose(stdout);*/
	return 0;
}
奇偶化排序

常数优化方案。考虑 $r$ 扫到最右边后,$l$ 的块编号更新时,$r$ 又要扫到最左边,再扫回最右边。

扫回左边的过程中,其实已经可以得到一些区间的答案,但是这些答案在之后第二次扫到右边的时候才被统计。因此可以对于奇数块编号 $l$,$r$ 按照升序排序,偶数块编号 $l$,$r$ 按照降序排序。

据说常数可以提升约 $30\%$,但是我觉得没什么用。


关于复杂度。

可以发现,\([l_1,r_1]\rightarrow[l_2,r_2]\) 的复杂度是 \(\mathcal O(l_2-l_1+r_2-r_1)\)

  • 考虑 \(l_1,l_2\) 按照块编号排序,有单次移动代价 \(\mathcal O(l_2-l_1)=\mathcal O(B)\),总代价为 \(\mathcal O(qB)\)。(当 \(l_1,l_2\) 的块编号相隔过大时,这一部分的总复杂度为 \(\mathcal O(n)\),可忽略)
  • 考虑 \(r_1,r_2\) 从小到大排序,因此当当前区间 \(l\) 块编号不变时,移动 \(r\)总代价\(\mathcal O(n)\)\(l\) 的块编号至多移动 \(\mathcal O\left(\dfrac nB\right)\) 次,总代价 \(\mathcal O\left(\dfrac{n^2}B\right)\)

因此总时间复杂度为 \(\mathcal O\left(qB+\dfrac{n^2}B\right)\)。取 \(B=\dfrac{n}{\sqrt q}\) 可得最优复杂度 \(\mathcal O\left(n\sqrt q\right)\)。视 \(n,q\) 同阶,即 \(\mathcal O\left(n\sqrt n\right)\)

同时,\(B\) 的取值会影响莫队算法的复杂度,例如当 \(q=\sqrt n\) 时,取 \(B=n^{\frac 34}\) 可得最优复杂度 \(\mathcal O\left(n^{\frac 54}\right)\),而直接取 \(B=\sqrt n\) 不优。


luogu P2709 小B的询问 /【模板】莫队

给定值域 \([1,k]\) 的序列 \(a_1,a_2,\cdots,a_n\),有 \(m\) 个区间 \([l_1,r_1],\cdots,[l_m,r_m]\),对于每一个区间 \([l_i,r_i]\),求:

\[\sum_{j=1}^kc_j^2 \]

\(c_j\) 表示 \(a_{l_i},a_{l_i+1},\cdots,a_{r_i}\)\(j\) 的出现次数,\(1\leq n,m,k\leq5\times10^4\)

可离线区间询问,考虑莫队。

区间扩展是简单的,维护桶 \(c\) 即可。

参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
typedef long long ll;
constexpr const int N=5e4,M=5e4,V=5e4;
int n,m,k,a[N+1],c[V+1];
struct question{
	int l,r,id;
}q[M+1];
int ans[N+1];
int B,pos[N+1],edgeL[N+1],edgeR[N+1];
void pre(){
	B=sqrt(n);
	for(int i=1;edgeR[i-1]+1<=n;i++){
		edgeL[i]=edgeR[i-1]+1;
		edgeR[i]=min(edgeL[i]+B-1,n);
		for(int j=edgeL[i];j<=edgeR[i];j++){
			pos[j]=i;
		}
	}
	sort(q+1,q+m+1,[](question a,question b){
		if(pos[a.l]!=pos[b.l]){
			return pos[a.l]<pos[b.l];
		}else{
			return a.r<b.r;
		}
	});
}
void addLeft(int &l,int &r,ll &cnt){
	l--;
	cnt-=1ll*c[a[l]]*c[a[l]];
	c[a[l]]++;
	cnt+=1ll*c[a[l]]*c[a[l]];
}
void addRight(int &l,int &r,ll &cnt){
	r++;
	cnt-=1ll*c[a[r]]*c[a[r]];
	c[a[r]]++;
	cnt+=1ll*c[a[r]]*c[a[r]];
}
void delLeft(int &l,int &r,ll &cnt){
	cnt-=1ll*c[a[l]]*c[a[l]];
	c[a[l]]--;
	cnt+=1ll*c[a[l]]*c[a[l]];
	l++;
}
void delRight(int &l,int &r,ll &cnt){
	cnt-=1ll*c[a[r]]*c[a[r]];
	c[a[r]]--;
	cnt+=1ll*c[a[r]]*c[a[r]];
	r--;
}
int main(){
	/*freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);*/
	
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	
	cin>>n>>m>>k;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	for(int i=1;i<=m;i++){
		cin>>q[i].l>>q[i].r;
		q[i].id=i;
	}
	pre();
	//cnt:记录答案 
	int l=2,r=1;
	ll cnt=0;
	for(int i=1;i<=m;i++){
		while(q[i].l<l){
			addLeft(l,r,cnt); 
		}
		while(r<q[i].r){
			addRight(l,r,cnt);
		}
		while(l<q[i].l){
			delLeft(l,r,cnt); 
		}
		while(q[i].r<r){
			delRight(l,r,cnt);
		}
		ans[q[i].id]=cnt;
	}
	for(int i=1;i<=m;i++){
		cout<<ans[i]<<'\n';
	}
	
	cout.flush();
	
	/*fclose(stdin);
	fclose(stdout);*/
	return 0;
}

带修莫队

如果可以 \(\mathcal O(1)\) 扩展区间,\(\mathcal O(1)\) 得到修改操作对于区间答案的影响,则可以在 \(\mathcal O\left(n^{\frac53}\right)\) 的复杂度内完成。

考虑当序列存在修改操作时,普通莫队便无法处理了——因为区间离线后,无法判断当前区间内的序列到底是什么。

考虑升维,将区间 \([l,r]\) 扩展到 \([l,r,t]\)\(t\)时间维。那么原本需要离线的 \([l_i,r_i,t_i]\)\(t_i\) 便表示这是第 \(t_i\) 次修改操作之后的询问区间。

对序列 \(a\) 以块长 \(B\) 分块。

将区间 \([l_i,r_i,t_i]\) 排序时,先后按照 \(\operatorname{pos}(l_i),\operatorname{pos}(r_i),t_i\) 升序排序即可。

同样存在普通莫队中的 addLeftaddRightdelLeftdelRight,用于在 \(t\) 一定时「横向」扩展区间。

\([l_1,r_1,t_1]\rightarrow[l_2,r_2,t_1]\) 之后,便可以通过 moveUpmoveDown 操作来「纵向」扩展区间,从而使 \([l_2,r_2,t_2]\rightarrow[l_2,r_2,t_2]\)。称 \([l,r,t]\rightarrow[l,r,t+1]\)moveUp 操作,\([l,r,t]\rightarrow[l,r,t-1]\)moveDown 操作,这两种操作过程中只需要注意修改操作对于区间 \([l,r]\) 的答案的贡献即可。


关于复杂度。

考虑 \([l_1,r_1,t_1]\rightarrow[l_2,r_2,t_2]\)

  • \([l_1,r_1]\rightarrow[l_2,r_2]\)\(\mathcal O(B)\) 的,总复杂度 \(\mathcal O(qB)\)
  • 考虑 \(l,r\) 所在块编号不变时,\(t_1\rightarrow t_2\) 的总复杂度是 \(\mathcal O(q)\) 的。那么 \(t_1\rightarrow t_2\) 的总复杂度便为 \(\mathcal O\left(\dfrac{qn^2}{B^2}\right)\)

总复杂度便为 \(\mathcal O\left(qB+\dfrac{qn^2}{B^2}\right)\),取 \(B=\sqrt[3]{n^2}=n^{\frac 23}\) 最优,\(\mathcal O\left(qn^{\frac 23}\right)\)。视 \(n,q\) 同阶,则有最优复杂度 \(\mathcal O\left(n^{\frac 53}\right)\)


luogu P1903 [国家集训队] 数颜色 / 维护队列 /【模板】带修莫队

给定序列 \(a_1,a_2,\cdots,a_n\)\(m\) 次操作:

  • 询问 \(a_l,a_{l+1},\cdots,a_r\) 内不同的数的个数。
  • \(a_i\) 修改为 \(c\)

\(1\leq n,m\leq133333,1\leq a_i,c\leq10^6\)

考虑带修莫队,维护一个桶记录是否出现在区间内,以及维护好时间维上的偏移即可。

参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
constexpr const int N=133333,M=133333,V=1e6;
int n,m,sizeQ,sizeOp,a[N+1];
struct question{
	int l,r,t,id;
}q[M+1];
struct operation{
	int pos,color,backup;
}op[M+1];
int ans[M+1];
int B,size,pos[N+1],edgeL[N+1],edgeR[N+1];
void pre(){
	B=pow(n,2/3.0);
	for(int i=1;edgeR[i-1]+1<=n;i++){
		edgeL[i]=edgeR[i-1]+1;
		edgeR[i]=min(edgeL[i]+B-1,n);
		for(int j=edgeL[i];j<=edgeR[i];j++){
			pos[j]=i;
		}
	}
	sort(q+1,q+sizeQ+1,[](question a,question b){
		if(pos[a.l]!=pos[b.l]){
			return pos[a.l]<pos[b.l];
		}else if(pos[a.r]!=pos[b.r]){
			return pos[a.r]<pos[b.r];
		}else{
			return a.t<b.t;
		}
	});
}
void addLeft(int &l,int &r,int &t,int cnt[],int &pl){
	l--;
	cnt[a[l]]++;
	if(cnt[a[l]]==1){
		pl++;
	}
}
void addRight(int &l,int &r,int &t,int cnt[],int &pl){
	r++;
	cnt[a[r]]++;
	if(cnt[a[r]]==1){
		pl++;
	}
}
void delLeft(int &l,int &r,int &t,int cnt[],int &pl){
	cnt[a[l]]--;
	if(!cnt[a[l]]){
		pl--;
	}
	l++;
}
void delRight(int &l,int &r,int &t,int cnt[],int &pl){
	cnt[a[r]]--;
	if(!cnt[a[r]]){
		pl--;
	}
	r--;
}
void moveUp(int &l,int &r,int &t,int cnt[],int &pl){
	t++;
	if(l<=op[t].pos&&op[t].pos<=r){
		cnt[a[op[t].pos]]--;
		if(!cnt[a[op[t].pos]]){
			pl--;
		}
	}
	a[op[t].pos]=op[t].color; 
	if(l<=op[t].pos&&op[t].pos<=r){
		cnt[a[op[t].pos]]++;
		if(cnt[a[op[t].pos]]==1){
			pl++;
		}
	}
	
}
void moveDown(int &l,int &r,int &t,int cnt[],int &pl){
	if(l<=op[t].pos&&op[t].pos<=r){
		cnt[a[op[t].pos]]--;
		if(!cnt[a[op[t].pos]]){
			pl--;
		}
	}
	a[op[t].pos]=op[t].backup;
	if(l<=op[t].pos&&op[t].pos<=r){
		cnt[a[op[t].pos]]++;
		if(cnt[a[op[t].pos]]==1){
			pl++;
		}
	}
	t--;
}
int main(){
	/*freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);*/
	
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	
	cin>>n>>m;
	static int real[N+1];
	for(int i=1;i<=n;i++){
		cin>>a[i];
		real[i]=a[i];
	}
	while(m--){
		char ch;
		cin>>ch;
		switch(ch){
			case 'Q':
				sizeQ++;
				cin>>q[sizeQ].l>>q[sizeQ].r;
				q[sizeQ].t=sizeOp;
				q[sizeQ].id=sizeQ;
				break;
			case 'R':
				sizeOp++;
				cin>>op[sizeOp].pos>>op[sizeOp].color;
				op[sizeOp].backup=real[op[sizeOp].pos];
				real[op[sizeOp].pos]=op[sizeOp].color;
				break; 
		}
	}
	pre();
	int l=1,r=1,t=0,pl=1;
	static int cnt[V+1];
	cnt[a[1]]=1;
	for(int i=1;i<=sizeQ;i++){
		while(q[i].l<l){
			addLeft(l,r,t,cnt,pl);
		}
		while(r<q[i].r){
			addRight(l,r,t,cnt,pl);
		}
		while(l<q[i].l){
			delLeft(l,r,t,cnt,pl);
		}
		while(q[i].r<r){
			delRight(l,r,t,cnt,pl);
		}
		while(t<q[i].t){
			moveUp(l,r,t,cnt,pl);
		}
		while(q[i].t<t){
			moveDown(l,r,t,cnt,pl); 
		}
		ans[q[i].id]=pl;
	}
	for(int i=1;i<=sizeQ;i++){
		cout<<ans[i]<<'\n';
	}
	
	cout.flush();
	
	/*fclose(stdin);
	fclose(stdout);*/
	return 0;
}

回滚莫队

luogu P4137 Rmq Problem / mex

有一个长度为 \(n\) 的序列 \(a_1,a_2,\cdots,a_n\)\(m\) 次询问,第 \(i\) 次询问区间 \([l_i,r_i]\) 内最小的未出现的自然数。(即 \(\operatorname{mex}\)

\(1\leq n,m,a_i\leq2\times10^5\)


如果使用普通莫队,则需要维护 \(\operatorname{mex}\) 的增加/删除。增加、删除数都很好用桶维护,但是无法快速得出答案。

如果使用堆等数据结构存储下来,则会带上一个 log,会被卡。想要 \(\mathcal O(1)\) 得到答案,只能开桶记录是否出现,区间删除时可以直接更新 \(\operatorname{mex}\),但是区间增加时想要更新 \(\operatorname{mex}\),就只能暴力跳,复杂度会假。

普通莫队配合 bitset 维护

普通莫队配合 bitset 其实是可以维护 $\operatorname{mex}$ 的。bitset 有一个成员函数 _Find_first(),可以在 $\mathcal O\left(\dfrac{\textit{size}}{w}\right)$ 的复杂度内查询第一个 $1$ 的下标

这样可以做到 $\mathcal O\left(n\sqrt n+\dfrac{nq}w\right)$ 维护,跑不满。

放一份普通莫队配合 bitset、奇偶化排序的 AC 代码。

参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
#include<bitset> 
using namespace std;
constexpr const int N=2e5,M=2e5,V=2e5;
int n,m,a[N+1];
struct question{
	int l,r,id;
}q[M+1];
int ans[M+1];
int B,size,pos[N+1],edgeL[N+1],edgeR[N+1];
void pre(){
	B=sqrt(n);
	for(int i=1;edgeR[i-1]+1<=n;i++){
		edgeL[i]=edgeR[i-1]+1;
		edgeR[i]=min(edgeL[i]+B-1,n);
		for(int j=edgeL[i];j<=edgeR[i];j++){
			pos[j]=i;
		}
	}
	sort(q+1,q+m+1,[](question a,question b){
		if(pos[a.l]!=pos[b.l]){
			return pos[a.l]<pos[b.l];
		}else{
			if(pos[a.l]&1){
				return a.r<b.r;
			}else{
				return b.r<a.r;
			}
		}
	});
}
void addLeft(int &l,int &r,int cnt[],bitset<V+1+1> &mex){
	l--;
	cnt[a[l]]++;
	mex[a[l]]=0;
}
void addRight(int &l,int &r,int cnt[],bitset<V+1+1> &mex){
	r++;
	cnt[a[r]]++;
	mex[a[r]]=0;
}
void delLeft(int &l,int &r,int cnt[],bitset<V+1+1> &mex){
	cnt[a[l]]--;
	if(!cnt[a[l]]){
		mex[a[l]]=1;
	}
	l++;
}
void delRight(int &l,int &r,int cnt[],bitset<V+1+1> &mex){
	cnt[a[r]]--;
	if(!cnt[a[r]]){
		mex[a[r]]=1;
	}
	r--;
}
int main(){
	/*freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);*/

	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	for(int i=1;i<=m;i++){
		cin>>q[i].l>>q[i].r;
		q[i].id=i;
	}
	pre();
	int l=1,r=1;
	static int cnt[V+1+1];
	cnt[a[1]]++;
	bitset<V+1+1>mex;
	mex.set();
	mex[a[1]]=0;
	for(int i=1;i<=m;i++){
		while(q[i].l<l){
			addLeft(l,r,cnt,mex);
		}
		while(r<q[i].r){
			addRight(l,r,cnt,mex);
		}
		while(l<q[i].l){
			delLeft(l,r,cnt,mex);
		}
		while(q[i].r<r){
			delRight(l,r,cnt,mex);
		}
		ans[q[i].id]=mex._Find_first();
	}
	for(int i=1;i<=m;i++){
		cout<<ans[i]<<'\n';
	}

	/*fclose(stdin);
	fclose(stdout);*/
	return 0;
}

莫队算法的区间扩展分两种,增加(addLeftaddRight)和删除(delLeftdelRight)。

当增加、删除只有一种操作能够较为高效地(\(\mathcal O(1)\))维护时,便可以使用回滚莫队来解决。

回滚莫队的思想很简单——既然只能用一种操作,那就只用一种操作废话。

具体而言,回滚莫队分为两种:只加不减的回滚莫队[2]和只减不加的回滚莫队[3]

不增加莫队

对于询问区间,按照左端点块编号升序排序,右端点降序排序。

维护区间 \([l,r]\),当 \(l\) 块编号一定时,\(r\) 显然只需要从右往左不断删点 delRight 即可。但是此时 \(l\) 的顺序是乱序的,可能还是需要 addLeft 操作和 delLeft 操作。

\(\textit{ans}\) 为所维护区间 \([l,r]\) 的答案。

考虑确定块编号,初始化区间 \([l,r]=[\operatorname{edgeL}(\operatorname{pos}(l)),n]\),并记录 \(\textit{ans}=\textit{ans}_{\text n}\)

那么维护 \([l,r]\rightarrow[l_1,r_1]\) 时,\(r\rightarrow r_1\) 是很好通过 delRight 操作维护的。\([l,r]\rightarrow[l,r_1]\) 后,记录 \(\textit{ans}_0=\textit{ans}\)

此时对于左端点,每次都不断 delLeft 使得 \(l\rightarrow l_1\),最终使得 \([l,r]\rightarrow[l_1,r_1]\),得到答案 \(\textit{ans}\)。记录下来答案后,将状态 \([l,r]\) 直接回滚到 \([\operatorname{edgeL}(\operatorname{pos}(l)),r_1]\)\(\textit{ans}\) 回滚到 \(\textit{ans}_0\) 即可。

同时 \(\operatorname{edgeL}(\operatorname{pos}(l))\) 变化时,也可以回滚到状态 \([l,r]=[\operatorname{edgeL}(\operatorname{pos}(l)),n]\)\(\textit{ans}=\textit{ans}_{\text n}\),再不断 delLeft \([l,n]\rightarrow[\operatorname{edgeL}(\operatorname{pos}(l)+1),n]\) 即可。


关于复杂度。

甚至于从某种意义上来说,回滚莫队比普通莫队更为简单。

显然对于 \(q\) 次询问,每一次询问移动 \(l\) 都是 \(\mathcal O(B)\),总复杂度为 \(\mathcal O(qB)\)。块编号改变时还有 \(\mathcal O(n)\) 的总复杂度。

考虑移动 \(r\),单个块内的总复杂度为 \(\mathcal O(n)\),则所有块的总复杂度为 \(\mathcal O\left(\dfrac {n^2}B\right)\)

故总复杂度为 \(\mathcal O\left(qB+\dfrac{n^2}B\right)\),取 \(B=\dfrac{n}{\sqrt q}\) 可得最优复杂度 \(\mathcal O\left(n\sqrt q\right)\)。视 \(n,q\) 同阶,即 \(\mathcal O\left(n\sqrt n\right)\)


不删除莫队

类似地,反过来即可。


  1. 这四个操作名称为本人自己所想,下文 moveUpmoveDown 同理。 ↩︎

  2. 又称「只使用增加操作的回滚莫队」「不删除莫队」。 ↩︎

  3. 又称「只使用删除操作的回滚莫队」「不增加莫队」。 ↩︎

posted @ 2025-11-15 12:19  TH911  阅读(4)  评论(0)    收藏  举报