莫队

莫队

用于处理一类区间查询问题,需要满足可以在极少的时间内从区间 \([L,R]\) 的答案转移出区间 \([L\pm 1,R\pm 1]\) 的答案。

莫队的操作:

现在有一个已知答案的区间 \([L,R]\),通过一个一个移动左右端点来找到下一个区间的答案。

但是这样的时间复杂度最坏是 \(O(N^2)\) 的。

考虑将所有询问离线下来,将原序列分块,按照左端点所在块的编号排序,再按照右端点的编号排序。

这样,左端点的移动只能在一个长度为 \(\sqrt N\) 的块中。同一个块中的右端点最多只会移动 \(N\) 次。

总时间复杂度 \(O((M+N)\sqrt N)\)

端点的移动:

//要先扩展,再收缩
while(L>q[i].l-1)add(--L);
while(R<q[i].r)add(++R);
while(L<q[i].l-1)del(L++);
while(R>q[i].r)del(R--);

例题:P2709 【模板】莫队 / 小 B 的询问

#include<bits/stdc++.h>
#define int long long
//#define lc p<<1
//#define rc p<<1|1
#define endl putchar('\n')
#define psp putchar(' ')
using namespace std;
typedef unsigned long long ull;
typedef long long ll;
const int N=1e5+5;
int read(){
	int x=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+c-'0',c=getchar();
	return x*f;
}
void print(int x){
	if(x<0)putchar('-'),x=-x;
	if(x<10){putchar(x+'0');return;}
	print(x/10);
	putchar(x%10+'0');
}
void putstr(string s){
	for(int i=0;i<s.size();i++)putchar(s[i]);
}
int lowbit(int x){
	return x&-x;
}
int n,m,k;
int T;
int ans;
int a[N];
int belong[N];
int con[N];
struct que{
	int l,r,id;
}q[N];
bool cmp(que a,que b){
	return belong[a.l]<belong[b.l]||(belong[a.l]==belong[b.l]&&a.r<b.r);
}
int len;
int cnt;
void init(){
	for(int i=1;i<=n;i+=len){
		cnt++;
		for(int j=i;j<=i+len-1&&j<=n;j++){
			belong[j]=cnt;
		}
	}
}
void add(int x){//(c+1)^2=c^2+1+2*c
	ans+=2*con[x]+1;
	con[x]++;
}
void del(int x){//(c-1)^2=c^2+1-2*c
	ans+=1-2*con[x];
	con[x]--;
}
int res[N];
signed main(){
	//ios::sync_with_stdio(0);
	n=read(),m=read(),k=read();
	for(int i=1;i<=n;i++)a[i]=read();
	len=sqrt(n);
	init();
	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);
	int l=1,r=0;
	for(int i=1;i<=m;i++){
		while(l>q[i].l)add(a[l-1]),l--;
		while(r<q[i].r)add(a[r+1]),r++;
		while(l<q[i].l)del(a[l]),l++;
		while(r>q[i].r)del(a[r]),r--;
		res[q[i].id]=ans;
	}
	for(int i=1;i<=m;i++)print(res[i]),endl;
}

带修莫队

普通莫队无法维护修改,因为询问排序之后无法判断其在修改之前还是在修改之后。

于是可以再加一维:时间。

对于每个查询,再增加一个 \(t\),表示在这个操作之前有多少个修改操作。

这个时候,先处理的是左右端点,再处理时间,所以需要将查询的右端点也按照所在块的编号排序,再按照 \(t\) 排序。

在将当前区间 \([L,R]\) 靠向询问的区间 \([l_i,r_i]\) 之后,也要把当前已经处理的修改数量 \(T\)\(t_i\) 靠齐。

while(t<q[i].t)update(i,++t);
while(t>q[i].t)update(i,t--);

update 中,我们需要判断修改的下标在不在当前这个区间里面,如果在则需要删除原来的值,增添新的值。

对于修改原数组,直接将修改的值和原数组的值交换:swap(a[p[t].l],p[t].r),这样 \(T\) 向更小的值走的时候需要清楚这次修改操作,就只需要再次交换,就能换回原数组的值。

void update(int x,int t){
	if(q[x].l<=p[t].l&&p[t].l<=q[x].r){
		del(a[p[t].l]);
		add(p[t].r);
	}
	swap(a[p[t].l],p[t].r);
}

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

块的大小取 \(N^{\frac{2}{3}}\) 最优。

#include<bits/stdc++.h>
//#define lc p<<1
//#define rc p<<1|1
#define endl putchar('\n')
#define psp putchar(' ')
using namespace std;
typedef unsigned long long ull;
typedef long long ll;
const int N=1e6+5;
int read(){
	int x=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+c-'0',c=getchar();
	return x*f;
}
void print(int x){
	if(x<0)putchar('-'),x=-x;
	if(x<10){putchar(x+'0');return;}
	print(x/10);
	putchar(x%10+'0');
}
void putstr(string s){
	for(int i=0;i<s.size();i++)putchar(s[i]);
}
int lowbit(int x){
	return x&-x;
}
int n,m,k;
int T;
int ans;
int a[N];
int con[N];
struct que{
	int ll,rr,l,r,id,t;
}q[N],p[N];
int cntq,cntp;
bool cmp(que a,que b){
	return a.ll<b.ll||(a.ll==b.ll&&a.rr<b.rr)||(a.ll==b.ll&&a.rr==b.rr&&a.t<b.t);
}
int len;
void add(int x){
	ans+=(++con[x]==1);
}
void del(int x){
	ans-=(--con[x]==0);
}
int res[N];
void update(int x,int t){
	if(q[x].l<=p[t].l&&p[t].l<=q[x].r){
		del(a[p[t].l]);
		add(p[t].r);
	}
	swap(a[p[t].l],p[t].r);
}
signed main(){
	ios::sync_with_stdio(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>a[i];
	len=max(1,(int)pow(n,2.0/3.0));
	for(int i=1;i<=m;i++){
		char op;
		cin>>op;
		if(op=='Q'){
			cntq++;
			cin>>q[cntq].l>>q[cntq].r;
			q[cntq].id=cntq;
			q[cntq].t=cntp;
			q[cntq].ll=q[cntq].l/len;
			q[cntq].rr=q[cntq].r/len;
		}
		else{
			cntp++;
			cin>>p[cntp].l>>p[cntp].r;
		}
	}
	sort(q+1,q+1+cntq,cmp);
	int l=1,r=0,t=0;
	for(int i=1;i<=cntq;i++){
		while(l>q[i].l)add(a[l-1]),l--;
		while(l<q[i].l)del(a[l]),l++;
		while(r>q[i].r)del(a[r]),r--;
		while(r<q[i].r)add(a[r+1]),r++;
		while(t<q[i].t)update(i,++t);
		while(t>q[i].t)update(i,t--);
		res[q[i].id]=ans;
	}
	for(int i=1;i<=cntq;i++)cout<<res[i]<<'\n';
}

回滚莫队

前提条件和莫队一样,需要相邻的区间可以转移。

但是回滚莫队解决的问题只需要满足几段时间内转移 \([L,R]\to [L,R+1]\)\([L,R]\to[L+1,R]\) 即可。即只需要满足在极短时间内删除一个元素/添加一个元素即可。

常见的问题如:询问区间中最大的数字出现次数乘上值。

以只能增加元素不能删除元素为例:

我们不能保证所有询问都可以由一个区间不断扩展依次得到,所以将所有询问分块处理。

实现回滚莫队的排序不能像普通莫队那样奇偶排序,因为不支持删除,需要保证 \(l\) 在同一个块中时 \(r\) 是递增的。

struct que{
	int l,r,ll,rr,id;
	friend bool operator<(que a,que b){
		return a.ll<b.ll||(a.ll==b.ll&&a.r<b.r);//先按左端点所在块排序,再按右端点排序。
	}
}q[N];

若询问的两个端点在同一个块中,直接暴力,时间复杂度 \(O(\sqrt N)\)

否则考虑左端点都在一个块中如何查询。

首先需要明确,此时查询的 \(r\) 全都不在左端点所在的块中,因为这种情况前面已经处理过。

为了只通过扩展就能得到所有区间,应该将 \(l\) 的起点设置在块的最右端 \(+1\) 的位置,\(r\) 的起点设置在块的最右端。

右端点一定是能通过扩展依次得到块中所有询问的答案的,因为块中右端点升序排序。

但是左端点不一定是递减排序的,所以每处理完一个询问,就要将左端点回到起点位置,右端点由于递增,不用归位。

归位的之后不能直接把 \(l\) 赋值了,需要一步一步挪到起点,更新状态。(虽然答案不能极短时间内计算,但是状态可以)

看到时间复杂度:右端点毋庸置疑是 \(O(N)\) 的,左端点每次最多移动 \(\sqrt N\) 个。总共有 \(\sqrt N\) 个块,总时间复杂度:\(N\sqrt N\)

若当前查询所在的块与上一次不在同一个块,则需要更新起点,并且清空上一次的状态。

if(q[id].ll!=last){
	for(int i=min(n,last*len)+1;i<=r;i++)cnt[a[i]]--;//左端点到起点的状态每个查询计算完就会清空,这里无需清空。
	last=q[id].ll;
	l=min(n,q[id].ll*len)+1;
	r=min(n,q[id].ll*len);
	mx=0;
}

例题:P14420 [JOISC 2014] 历史的研究 / Historical Research

#include<bits/stdc++.h>
#define int long long
#define endl putchar('\n')
#define psp putchar(' ')
using namespace std;
const int N=1e5+5;
int read(){
	int x=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+c-'0',c=getchar();
	return x*f;
}
void print(int x){
	if(x<0)putchar('-'),x=-x;
	if(x<10){putchar(x+'0');return;}
	print(x/10);
	putchar(x%10+'0');
}
int n,m,k;
int a[N];
int b[N];
int deepseek(int x){
	int l=1,r=m;
	while(l<r){
		int mid=l+r>>1;
		if(b[mid]>=x)r=mid;
		else l=mid+1;
	}
	return r;
}
int ccnt[N];
int cnt[N];
struct que{
	int l,r,ll,rr,id;
	friend bool operator<(que a,que b){
		return a.ll<b.ll||(a.ll==b.ll&&a.r<b.r);
	}
}q[N];
int ans[N];
int mx;
signed main(){
	//ios::sync_with_stdio(0);
	n=read(),k=read();
	for(int i=1;i<=n;i++)b[i]=a[i]=read();
	sort(b+1,b+1+n);
	b[0]=b[1]-1;
	for(int i=1;i<=n;i++)if(b[i]!=b[i-1])b[++m]=b[i];
	for(int i=1;i<=n;i++)a[i]=deepseek(a[i]);
	int len=sqrt(n);
	for(int i=1;i<=k;i++)q[i].l=read(),q[i].r=read(),q[i].ll=(q[i].l-1)/len+1,q[i].rr=(q[i].r-1)/len+1,q[i].id=i;
	sort(q+1,q+1+k);
	int l=1,r=0;
	int last=0;
	for(int id=1;id<=k;id++){
		if(q[id].ll==q[id].rr){
			for(int i=q[id].l;i<=q[id].r;i++)ans[q[id].id]=max(ans[q[id].id],b[a[i]]*(++ccnt[a[i]]));
			for(int i=q[id].l;i<=q[id].r;i++)ccnt[a[i]]--;
			continue;
		}
		if(q[id].ll!=last){
			for(int i=min(n,last*len)+1;i<=r;i++)cnt[a[i]]--;
			last=q[id].ll;
			l=min(n,q[id].ll*len)+1;
			r=min(n,q[id].ll*len);
			mx=0;
		}
		while(r<q[id].r){
			r++;
			mx=max(mx,b[a[r]]*(++cnt[a[r]]));
		}
		ans[q[id].id]=mx;
		while(l>q[id].l){
			l--;
			ans[q[id].id]=max(ans[q[id].id],b[a[l]]*(++cnt[a[l]]));
		}
		while(l<min(n,q[id].ll*len)+1){
			cnt[a[l++]]--;
		}
	}
	for(int i=1;i<=k;i++)print(ans[i]),endl;
}

树上莫队

所有普通莫队能解决的询问,只是变成了在树上。

莫队处理的是一段区间的询问,所以应该把树变成一维的数组。

对树求欧拉序(进入记录依次,结束记录一次),树上的路径就变成了欧拉序中的一个区间。

对于树上的点 \(x\),记 \(st_x\) 为其在欧拉序中第一次出现的下标,\(ed_x\) 为其在欧拉序中第二次出现的下标。

则对于路径 \(u\to v(st_u<st_v)\),分两种情况:

  1. \(lca_{u,v}=u\),则两点路径中的点为欧拉序区间 \([st_u,st_v]\)只出现一次的点。
  2. \(lca_{u,v}\neq u\),则两点路径中的点为欧拉序区间 \(ed_u,st_u\)只出现一次的点\(lca_{u,v}\)

然后树上的路径就被转换成了一个区间的查询。

为了处理那些在区间中出现了两次的点(不需要的点),用一个数组记录其出现次数,第二次出现删除即可。这样 adddel 就可以写成一个函数。

void update(int x){
	x=e[x];
	vis[x]^=1;
	if(vis[x])res+=(cnt[a[x]]++==0);
	else res-=(--cnt[a[x]]==0);
}

例题:Count on a tree II

#include<bits/stdc++.h>
#define endl putchar('\n')
#define psp putchar(' ')
using namespace std;
typedef unsigned long long ull;
typedef long long ll;
const int N=2e5+5;
int read(){
	int x=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+c-'0',c=getchar();
	return x*f;
}
void print(int x){
	if(x<0)putchar('-'),x=-x;
	if(x<10){putchar(x+'0');return;}
	print(x/10);
	putchar(x%10+'0');
}
void putstr(string s){
	for(int i=0;i<s.size();i++)putchar(s[i]);
}
int lowbit(int x){
	return x&-x;
}
int n,m,k;
int T;
int a[N];
int b[N];
int deepseek(int x){
	int l=1,r=m;
	while(l<r){
		int mid=l+r>>1;
		if(b[mid]>=x)r=mid;
		else l=mid+1;
	}
	return r;
}
int e[N];
int st[N];
int ed[N];
vector<int>g[N];
int idx;//
int f[21][N];
int dep[N];
void dfs(int fa,int x){
	dep[x]=dep[fa]+1;
	e[++idx]=x;
	st[x]=idx;
	f[0][x]=fa;
	for(int i=0;i<g[x].size();i++){
		int y=g[x][i];
		if(y==fa)continue;
		dfs(x,y);
	}
	e[++idx]=x;
	ed[x]=idx;
}
int LCA(int x,int y){
	if(dep[x]<dep[y])swap(x,y);
	for(int i=20;i>=0;i--)if(dep[f[i][x]]>=dep[y])x=f[i][x];
	if(x==y)return x;
	for(int i=20;i>=0;i--)if(f[i][x]!=f[i][y])x=f[i][x],y=f[i][y];
	return f[0][x];
}
struct que{
	int l,r,ll,id,lca;
	friend bool operator<(que a,que b){
		return a.ll<b.ll||(a.ll==b.ll&&a.r<b.r);
	}
}q[N];
int vis[N];
int cnt[N];
int ans[N];
int res;
void update(int x){
	x=e[x];
	vis[x]^=1;
	if(vis[x])res+=(cnt[a[x]]++==0);
	else res-=(--cnt[a[x]]==0);
}
signed main(){
	//ios::sync_with_stdio(0);
	n=read(),k=read();
	for(int i=1;i<=n;i++)b[i]=a[i]=read();
	sort(b+1,b+1+n);
	b[0]=b[1]-1;
	for(int i=1;i<=n;i++)if(b[i]!=b[i-1])b[++m]=b[i];
	for(int i=1;i<=n;i++)a[i]=deepseek(a[i]);
	for(int i=1;i<n;i++){
		int u=read(),v=read();
		g[u].push_back(v);
		g[v].push_back(u);
	}
	dfs(1,1);
	for(int len=1;len<=20;len++){
		for(int i=1;i<=n;i++){
			f[len][i]=f[len-1][f[len-1][i]];
		}
	}
	int len=sqrt(2*n);
	for(int i=1;i<=k;i++){
		q[i].id=i;
		int x=read(),y=read();
		if(st[x]>st[y])swap(x,y);
		int lca=LCA(x,y);
		if(lca==x){
			q[i].l=st[x],q[i].r=st[y];
			q[i].lca=0;
		}
		else{
			q[i].l=ed[x],q[i].r=st[y];
			q[i].lca=st[lca];
		}
		q[i].ll=(q[i].l-1)/len+1;
	}
	sort(q+1,q+1+k);
	int l=1,r=0;
	for(int i=1;i<=k;i++){
		while(l>q[i].l)update(--l);
		while(r<q[i].r)update(++r);
		while(l<q[i].l)update(l++);
		while(r>q[i].r)update(r--);
		if(q[i].lca)update(q[i].lca);
		ans[q[i].id]=res;
		if(q[i].lca)update(q[i].lca);
	}
	for(int i=1;i<=k;i++){
		print(ans[i]),endl;
	}
}

二次离线

处理单次转移不能 \(O(1)\) 解决而需要 \(O(\log N)\)\(O(\sqrt N)\) 的问题,但是需要保证操作满足可差分性。

\(f(x,[L,R])\) 表示第 \(x\) 个元素对区间 \([L,R]\) 的贡献,可差分性即满足 \(f(x,[L,R])=f(x,[1,R])-f(x,[1,L-1])\)

以左端点需要向左移(扩展)为例。

对于区间 \([L,R]\) 的答案,要转移到区间 \([L-1,R]\),就需要加上一个 \(f(L-1,[L,R])\)

那么,现在的区间 \([L,R]\),要扩展到目标区间 \([l,R](l<L)\),就需要加上 \(\sum\limits_{i=l}^{L-1}f(i,[i+1,R])\)

对于 \(f(i,[i+1,R])\),可以拆解为 \(f(i,[1,R])-f(i,[1,i])\)

加上的数就变成了 \(\sum\limits_{i=l}^{L-1}f(i,[1,R])-f(i,[1,i])\)

对于 \(f(i,[1,i])\),可以预处理,因为区间的右端点和当前下标一样。所以也可以求出前缀和,快速计算 \(\sum\limits_{i=l}^{L-1}f(i,[1,i])\)

现在要解决的就是 \(\sum\limits_{i=l}^{L-1}f(i,[1,R])\)

考虑朴素的思路,遍历 \(1\sim R\),标记所有合法的,再在区间 \([l,L-1]\) 中找。

但是每个块执行的时候都会做这件事,有很多点都被重复遍历导致时间过大。考虑用扫描线的思想,二次离线,统一遍历一遍,解决所有问题。

例题 莫队二次离线 / 第十四分块(前体)

#include<bits/stdc++.h>
#define int long long
//#define lc p<<1
//#define rc p<<1|1
#define endl putchar('\n')
#define psp putchar(' ')
using namespace std;
typedef unsigned long long ull;
typedef long long ll;
const int N=1e5+5;
int read(){
	int x=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+c-'0',c=getchar();
	return x*f;
}
void print(int x){
	if(x<0)putchar('-'),x=-x;
	if(x<10){putchar(x+'0');return;}
	print(x/10);
	putchar(x%10+'0');
}
void putstr(string s){
	for(int i=0;i<s.size();i++)putchar(s[i]);
}
int lowbit(int x){
	return x&-x;
}
int n,m,k;
int T;
int a[N];
struct que{
	int l,r,ll,id;
	friend bool operator<(que a,que b){
		return a.ll<b.ll||(a.ll==b.ll&&a.r<b.r);
	}
}q[N];
int len;
int con[N];
vector<int>ok;
int s1[N];
int s2[N];
struct line{
	int l,r,id,tag;
};
vector<line>has[N];
int diff[N];
int state[N];
int ans[N];
signed main(){
	//ios::sync_with_stdio(0);
	n=read(),m=read(),k=read();
	for(int i=1;i<=n;i++)a[i]=read();
	len=sqrt(n);
	if(k>14){
		while(m--){
			read(),read(),print(0),endl;
		}
		return 0;
	}
	for(int i=0;i<16384;i++){
		if(__builtin_popcount(i)==k){
			ok.push_back(i);
		}
	}
	for(int i=1;i<=n;i++){
		s1[i]=s1[i-1]+con[a[i]];
		for(int j=0;j<ok.size();j++){
			int num=ok[j];
			con[a[i]^num]++;
		}
		s2[i]=s2[i-1]+con[a[i]];
	}
	for(int i=1;i<=m;i++)q[i].l=read(),q[i].r=read(),q[i].ll=(q[i].l-1)/len+1,q[i].id=i;
	sort(q+1,q+1+m);
	int l=1,r=0;
	for(int i=1;i<=m;i++){
		if(l>q[i].l){
			diff[i]-=(s2[l-1]-s2[q[i].l-1]);
			has[r].push_back({q[i].l,l-1,q[i].id,1});
			l=q[i].l;
		}
		if(r<q[i].r){
			diff[i]+=(s1[q[i].r]-s1[r]);
			has[l-1].push_back({r+1,q[i].r,q[i].id,-1});
			r=q[i].r;
		}
		if(l<q[i].l){
			diff[i]+=(s2[q[i].l-1]-s2[l-1]);
			has[r].push_back({l,q[i].l-1,q[i].id,-1});
			l=q[i].l;
		}
		if(r>q[i].r){
			diff[i]-=(s1[r]-s1[q[i].r]);
			has[l-1].push_back({q[i].r+1,r,q[i].id,1});
			r=q[i].r;
		}
	}
	memset(con,0,sizeof(con));
	for(int i=1;i<=n;i++){
		for(int j=0;j<ok.size();j++){
			int num=ok[j];
			con[a[i]^num]++;
		}
		for(int j=0;j<has[i].size();j++){
			int res=0;
			for(int o=has[i][j].l;o<=has[i][j].r;o++){
				res+=con[a[o]];
			}
			state[has[i][j].id]+=res*has[i][j].tag;
		}
	}
	int st=0;
	for(int i=1;i<=m;i++){
		st+=diff[i]+state[q[i].id];
		ans[q[i].id]=st;
	}
	for(int i=1;i<=m;i++){
		print(ans[i]),endl;
	}
}
posted on 2026-07-22 20:34  fish2012  阅读(8)  评论(0)    收藏  举报