Loading

详解莫队

基础莫队

题目

翻译:给出一个序列,对于每次询问,回答询问区间内不同数字的个数。

如果用暴力,只需要创建一个 cnt 数组,记录当前每个数出现了多少次,cnt[i] 表示 \(i\) 这个数出现的次数,然后循环统计,如果这个数是第一次出现,就加一。


现在我们先将所有询问读入,再将询问按右端点小到大排序,然后暴力求出第一个询问的 cnt,那么就可以得到第一个询问的 res。对于下一个询问,我们将第一个询问的右端点一个个后移,直到等于下一个询问的右端点。如果现在移到的数是 \(x\),则分两种情况:

  • cnt[x]==0,那么 cnt[x]++,res++

  • cnt[x]>0,那么 cnt[x]++

这样右端点最坏只会移 \(n\) 次,但是左端点呢?


我们可以用分块的思想,将整个序列分成 \(\sqrt n\) 块,现在把排序改为:以左端点所在的块的编号为第一关键字,右端点下标为第二关键字排序。这样就可以把时间复杂度降到 \(O(m\sqrt n)\)


注:由于莫队不是本题的“正解”,所以需要的优化极多,且需要开启 O2 优化。

参考代码:

#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=1000010,M=1000010,S=1000010;
int n,m,len;
int w[N],ans[M];
int tmp[S],idx;
int res;
struct query{//询问
	int id,l,r;
}q[M];
int cnt[S];
inline int read(){//快读
	register int k=0,f=1;
	register char c=getchar_unlocked();
	while(c<'0' || c>'9'){
		if(c=='-') f=-1;
		c=getchar_unlocked();
	}
	while(c>='0' && c<='9'){
		k=k*10+c-'0';
		c=getchar_unlocked();
	}
	return k*f;
}
void write(register int x){//快写
	if(x<10) putchar(x+'0');
	else{
		write(x/10);
		putchar(x%10+'0');
	}
}
inline bool cmp(register query &a,register query &b){
	if(a.l/len!=b.l/len) return a.l/len<b.l/len;//按左端点所在块的编号排
	if(a.l/len&1) return a.r<b.r;//玄学优化:奇数块小到大排,偶数块大到小排
	return a.r>b.r;
}
/*void add(int x,int &res){//加入一个数
    if(cnt[x]==0) res++;//一开始没有
    cnt[x]++;
}
void del(int x,int &res){//删除一个数
    cnt[x]--;
    if(cnt[x]==0) res--;//删完后没了
}*/
int main(){
	n=read();
	for(register int i=1;i<=n;i++){
		w[i]=read();
		if(!tmp[w[i]]) tmp[w[i]]=++idx;//使数组访问更连续
		w[i]=tmp[w[i]];
	}
	m=read();
	len=max(1,int(n/sqrt(m)));//一定要取max,否则可能RE
	for(register int i=0;i<m;i++){
		q[i].l=read();
		q[i].r=read();
		q[i].id=i;
	}
	sort(q,q+m,cmp);
	for(register int k=0,i=0,j=1;k<m;k++){
		while(i<q[k].r) res+=(!cnt[w[++i]]++);//add(w[++i],res),右指针右移,扩充
		while(i>q[k].r) res-=(!--cnt[w[i--]]);//del(e[i--],res),右指针左移,减少
		while(j<q[k].l) res-=(!--cnt[w[j++]]);//del(w[j++],res),左指针右移,减少
		while(j>q[k].l) res+=(!cnt[w[--j]]++);//add(w[--j],res),左指针左移,扩充
		ans[q[k].id]=res;
	}
	for(register int i=0;i<m;i++){
		write(ans[i]);
		putchar('\n');
	}
	return 0;
}

带修莫队

题目

相较于上一个题,此题多了修改操作。

我们可以加一个时间戳 \(t\),表示进行完第 \(t\) 次修改后,序列的情况。这样移指针的时候,就要移三个。移 \(l,r\) 不变,如果移时间戳 \(t\),就会分两种情况:

  • 修改的在区间 \([l,r]\) 内,则删除原来的数,添加新修改的数。时间复杂度 \(O(1)\)

  • 修改的不在区间 \([l,r]\) 内,则不发生变化。

完成后,由于 \(t\) 有可能再返回,所以我们将两个数交换一下。比如说要将 \(x\) 修改成 \(x'\),就交换 \(t\pm 1\)\(x'\)\(t\)\(x\)

综上,移 \(t\) 指针也是 \(O(1)\) 的。

对于排序,我们将 \(l\) 所在块的编号作为第一关键字,\(r\) 所在块的编号作为第二关键字,\(t\) 作为第三关键字。

推荐使用 \(n^{\frac{2}{3}}\),也就是 \(\sqrt[3]{n^2}\) 作为块长。

参考代码:

#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=200010,S=1000010;
int n,m;
int len;
int w[N],cnt[S],ans[N];
int mq,mc;//操作数量
struct query{
	int id;
	int l,r;
	int t;//时间戳
}q[N];
struct modify{//修改操作
	int p,c;
}c[N];
int get(int x){
	return x/len;
}
bool cmp(query a,query b){
	int al=get(a.l),ar=get(a.r),bl=get(b.l),br=get(b.r);
	if(al!=bl) return al<bl;
	if(ar!=br) return ar<br;
	return a.t<b.t;
}
void add(int x,int &res){
	if(cnt[x]==0) res++;
	cnt[x]++;
}
void del(int x,int &res){
	cnt[x]--;
	if(cnt[x]==0) res--;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) scanf("%d",&w[i]);
	for(int i=0;i<m;i++){
		char op[2];
		int a,b;
		scanf("%s%d%d",op,&a,&b);
		if(*op=='Q') q[++mq]={mq,a,b,mc};
		else c[++mc]={a,b};
	}
	len=max(pow(n,0.666),1.0);
	sort(q+1,q+mq+1,cmp);
	for(int i=0,j=1,t=0,k=1,res=0;k<=mq;k++){
		int id=q[k].id,l=q[k].l,r=q[k].r,tm=q[k].t;
		while(i<r) add(w[++i],res);
		while(i>r) del(w[i--],res);
		while(j<l) del(w[j++],res);
		while(j>l) add(w[--j],res);
		while(t<tm){//执行下一个操作
			t++;
			if(c[t].p>=j && c[t].p<=i){//在范围内
				del(w[c[t].p],res);
				add(c[t].c,res);
			}
			swap(w[c[t].p],c[t].c);//交换以支持逆操作
		}
		while(t>tm){//撤销当前操作
			if(c[t].p>=j && c[t].p<=i){
				del(w[c[t].p],res);
				add(c[t].c,res);
			}
			swap(w[c[t].p],c[t].c);
			t--;
		}
		ans[id]=res;
	}
	for(int i=1;i<=mq;i++) printf("%d\n",ans[i]);
	return 0;
}

回滚莫队

如果莫队在维护时,插入操作很好维护,但是删除操作不好维护,那么就需要回滚莫队。

题目

题目大意:每次给出一个区间,区间里每种数都会又一个重要度:数本身乘这个数在区间内的出现次数。现在要求区间内重要度的最大值。

如果插入一个数 \(x\),就将重要度加 \(x\),再去更新最大值即可。但是如果删除一个数 \(x\),重要度减去 \(x\),最大值就不容易维护了(大佬们可能直到做法,但是本蒟蒻不会)。

询问排序和基础莫队一样。

我们先暴力处理左端点与右端点在一个块内的询问,再处理其他询问。比如现在查询的区间为 \([l,r]\)\(l\) 所在块为 \(B\)\(B\) 的下一个块为 \(B_n\)。对于 \(B\) 内的询问,直接暴力即可;对于跨块询问,先将左端点移到 \(B\) 的最后,右端点移到 \(B_n\) 的开头,下面再正常移左右端点即可,这样就能回避删除操作。

上面可能比较难懂,而且细节不多,所以直接看代码,我会在代码中详细注释:

#include<bits/stdc++.h>
#define LL long long
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=100010;
int n,m;
int len;
int w[N],cnt[N];
LL ans[N];
struct query{
	int id,l,r;
}q[N];
vector<int> nums;//离散化
int get(int x){
	return x/len;
}
bool cmp(query a,query b){
	int i=get(a.l),j=get(b.l);
	if(i!=j) return i<j;
	return a.r<b.r;
}
void add(int x,LL &res){
	cnt[x]++;
	res=max(res,(LL)cnt[x]*nums[x]);//一定注意离散化
}
int main(){
	cin>>n>>m;
	len=sqrt(n);
	for(int i=1;i<=n;i++){
		scanf("%d",&w[i]);
		nums.push_back(w[i]);
	}
	sort(nums.begin(),nums.end());//离散化排序
	nums.erase(unique(nums.begin(),nums.end()),nums.end());//离散化去重
	for(int i=1;i<=n;i++)
		w[i]=lower_bound(nums.begin(),nums.end(),w[i])-nums.begin();//将每个数换成离散化后的值
	for(int i=0;i<m;i++){
		scanf("%d%d",&q[i].l,&q[i].r);
		q[i].id=i;
	}
	sort(q,q+m,cmp);//正常排序
	for(int x=0;x<m;){
		int y=x;
		while(y<m && get(q[y].l)==get(q[x].l)) y++;//将所有左端点在一个块内的询问处理完
		int right=get(q[x].l)*len+len-1;//左端点所在块的最后一个数
		while(x<y && q[x].r<=right){//右端点在块内,即块内查询(由于排序方法,块内询问一定连续)
			LL res=0;
			int id=q[x].id,l=q[x].l,r=q[x].r;
			for(int k=l;k<=r;k++) add(w[k],res);//暴力
			ans[id]=res;
			for(int k=l;k<=r;k++) cnt[w[k]]--;//还原,以确保每次操作的cnt都是原状态
			x++;
		}
		LL res=0;
		int i=right,j=right+1;//左端点没变,所以right不用更新
		while(x<y){//剩下的都是跨块的,且右端点递增
			int id=q[x].id,l=q[x].l,r=q[x].r;
			while(i<r) add(w[++i],res);//移右端点
			LL backup=res;//备份,以便下次处理询问(左端点只是在一个块内,但是块内顺序时乱的)
			while(j>l) add(w[--j],res);
			ans[id]=res;
			while(j<right+1) cnt[w[j++]]--;
			res=backup;
			x++;
		}
		mems(cnt,0);
	}
	for(int i=0;i<m;i++) printf("%lld\n",ans[i]);
	return 0;
}

树上莫队

题目

样例树:

我们将整棵树变成一个欧拉序列,就是深度优先遍历序列,从上面到这个点时记录一遍,从这个点离开时再记录一遍。比如样例的欧拉序列就是:1 2 2 3 5 5 6 6 7 7 3 4 8 8 4 1

定义两个数组 firstlastfirst[u] 表示 \(u\) 第一次出现的位置,last[u] 表示 \(u\) 最后一次(也就是第二次)出现的位置。如果询问从 \(x\)\(y\) 的路径,先判断 first[x] 是不是小于 first[y],如果不是,就互换 \(x,y\)。然后分两种情况讨论:

  • \(\text{lca}(x,y)=x\),则 first[x]first[y]只出现一次的点,就是 \(x\)\(y\) 的路径。

  • \(\text{lca}(x,y)\ne x\),则路径对应欧拉序列中从 last[x]first[y] 中只出现一次的点,以及 \(\text{lca}(x,y)\)


现在问题就转化为:区间中只出现一次的数有多少种不同的权值。其实只需要再加入一个 st 数组,表示每个点是否出现了偶数次。现在小改一下 add 函数:

void add(int x,int &res){
	st[x]=!st[x];
	if(!st[x]){//删除一个数
		cnt[w[x]]--;
		if(cnt[w[x]]==0) res--;
	}
	else{//添加一个数
		if(cnt[w[x]]==0) res++;
		cnt[w[x]]++;
	}
}

不难发现,在这个问题中,adddel 其实是一样的操作。后面直接套基础莫队即可。

总结步骤:

  1. 离散化。

  2. DFS 求欧拉序列。

  3. 倍增求 LCA。

  4. 将树上询问变成序列中询问。

  5. 莫队处理。

参考代码:

#include<bits/stdc++.h>
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=100010;
int n,m;
int len;
int w[N];
int h[N],e[N],ne[N],idx;
int top;
int dep[N],f[N][20];
int seq[N],first[N],last[N];
int cnt[N],ans[N];
bool st[N];
int que[N];
struct query{
	int id,l,r,p=0;
}q[N];
vector<int> nums;
void addedge(int a,int b){
	e[idx]=b;
	ne[idx]=h[a];
	h[a]=idx++;
}
void dfs(int u,int fa){
	seq[++top]=u;//从上面到这个点
	first[u]=top;
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j!=fa) dfs(j,u);
	}
	seq[++top]=u;//从这个点离开
	last[u]=top;
}
void bfs(){
	mems(dep,0x3f);
	dep[0]=0;
	dep[1]=1;
	int hh=0,tt=0;
	que[0]=1;
	while(hh<=tt){
		int t=que[hh++];
		for(int i=h[t];~i;i=ne[i]){
			int j=e[i];
			if(dep[j]>dep[t]+1){
				dep[j]=dep[t]+1;
				f[j][0]=t;
				for(int k=1;k<=15;k++) f[j][k]=f[f[j][k-1]][k-1];
				que[++tt]=j;
			}
		}
	}
}
int lca(int a,int b){
	if(dep[a]<dep[b]) swap(a,b);
	for(int k=15;k>=0;k--)
		if(dep[f[a][k]]>=dep[b]) a=f[a][k];
	if(a==b) return a;
	for(int k=15;k>=0;k--)
		if(f[a][k]!=f[b][k]){
			a=f[a][k];
			b=f[b][k];
		}
	return f[a][0];
}
int get(int x){
	return x/len;
}
bool cmp(query a,query b){
	int i=get(a.l),j=get(b.l);
	if(i!=j) return i<j;
	return a.r<b.r;
}
void add(int x,int &res){
	st[x]=!st[x];
	if(!st[x]){
		cnt[w[x]]--;
		if(cnt[w[x]]==0) res--;
	}
	else{
		if(cnt[w[x]]==0) res++;
		cnt[w[x]]++;
	}
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		scanf("%d",&w[i]);
		nums.push_back(w[i]);
	}
	sort(nums.begin(),nums.end());
	nums.erase(unique(nums.begin(),nums.end()),nums.end());
	for(int i=1;i<=n;i++)
		w[i]=lower_bound(nums.begin(),nums.end(),w[i])-nums.begin();
	mems(h,-1);
	for(int i=0;i<n-1;i++){
		int a,b;
		scanf("%d%d",&a,&b);
		addedge(a,b);
		addedge(b,a);
	}
	dfs(1,-1);
	bfs();//预处理
	for(int i=0;i<m;i++){
		int a,b;
		scanf("%d%d",&a,&b);
		if(first[a]>first[b]) swap(a,b);//保证a在前,b在后
		int p=lca(a,b);
		if(a==p) q[i]={i,first[a],first[b]};
		else q[i]={i,last[a],first[b],p};
	}
	len=sqrt(top);
	sort(q,q+m,cmp);
	for(int k=0,i=1,j=0,res=0;k<m;k++){
		int id=q[k].id,l=q[k].l,r=q[k].r,p=q[k].p;
		while(j<r) add(seq[++j],res);
		while(j>r) add(seq[j--],res);//加入、删除都一样
		while(i<l) add(seq[i++],res);
		while(i>l) add(seq[--i],res);
		if(p!=0) add(p,res);
		ans[id]=res;
		if(p!=0) add(p,res);//删掉p
	}
	for(int i=0;i<m;i++) printf("%d\n",ans[i]);
	return 0;
}

二次离线莫队

题目

定义配对:指符合题意的数对。

\(L,R\) 为现在的左右端点,\(r\) 为右端点目标位置,那么现在就应该将 \(R+1\) 加入区间。假设 \([L,R]\) 这个区间的答案已经维护好了,那么影响的数对必定包含 \(R+1\) 这个点。所以只需要求 \([L,R]\) 这个区间中有多少个数和 \(R+1\) 配对。

可以用前缀和解决这个问题:定义 \(S_i\)\([1,i]\) 中有多少个数与 \(w_{R+1}\) 配对,现在就可以将新增的答案转化为 \(S_R-S_{L-1}\)。但是显然 \([1,L-1]\) 这个区间和 \(R+1\) 没有什么关系,所以我们将两个 \(S\) 看成两个问题:

  • 解决 \(S_R\):定义 \(f(i)\) 表示 \([1,i]\) 中有多少个数与 \(w_{i+1}\) 配对,那么 \(S_R=f(R)\)。设 \(g(x)\) 表示前 \(i\) 个数有多少个数与 \(x\) 配对。现在 \(S_R=f(R)=g(w_{R+1})\),直接暴力维护 \(g\) 即可。设 \(y\) 表示所有满足题意的结果,设当前新增 \(w_i\) 这个数,就要求哪些 \(x\) 满足 \(w_i \oplus x=y_j\),即求有多少个 \(x\) 满足 \(x=w_i \oplus y_j\)。直接遍历所有 \(y_j\),并将 \(g(y_j \oplus w_i)\) 加一就可以。

  • 解决 \(S_{L-1}\):我们知道,\(\forall x\in[R+1,r]\) 都需要求一下 \([1,L-1]\) 中有多少个数与 \(x\) 配对,需要很长时间。但是注意到 \([1,L-1]\) 这个区间是不变的,所以可以将“某个区间中每个数与某个固定前缀中有多少对数配对”这种问题拎出来,再离线去做,就可以类比上面的方法。

参考代码:

#include<bits/stdc++.h>
#define LL long long
#define mems(a,b) memset(a,b,sizeof a)
using namespace std;
const int N=100010;
int n,m,k;
int len;
int w[N];
LL ans[N];
struct query{
	int id,l,r;
	LL res;//每个询问的结果需要单独存
}q[N];
struct range{//第二次离线
	int id,l,r,t;
};
vector<range> ran[N];
int f[N],g[N];
int getcnt(int x){//求x的二进制表示中有几个1
	int res=0;
	while(x){
		res+=(x&1);
		x>>=1;
	}
	return res;
}
int get(int x){
	return x/len;
}
bool cmp(query a,query b){
	int i=get(a.l),j=get(b.l);
	if(i!=j) return i<j;
	return a.r<b.r;
}
int main(){
	cin>>n>>m>>k;
	for(int i=1;i<=n;i++) scanf("%d",&w[i]);
	vector<int> nums;//配对出来的结果
	for(int i=0;i<1<<14;i++)
		if(getcnt(i)==k) nums.push_back(i);
	for(int i=1;i<=n;i++){//预处理
		for(auto y:nums) g[w[i]^y]++;
		f[i]=g[w[i+1]];
	}
	for(int i=0;i<m;i++){
		int l,r;
		scanf("%d%d",&l,&r);
		q[i]={i,l,r};
	}
	len=sqrt(n);
	sort(q,q+m,cmp);
	for(int i=0,L=1,R=0;i<m;i++){
		int l=q[i].l,r=q[i].r;
		if(R<r) ran[L-1].push_back({i,R+1,r,-1});//存下需要离线的询问
		while(R<r) q[i].res+=f[R++];//先处理不用离线的询问
		if(R>r) ran[L-1].push_back({i,r+1,R,1});
		while(R>r) q[i].res-=f[--R];
		if(L<l) ran[R].push_back({i,L,l-1,-1});
		while(L<l){
			q[i].res+=f[L-1]+!k;//因为L-1相较于L少了L,所以要判断自己是否和自己配对,又因为自己异或自己等于0,所以当且仅当k=0时自己和自己配对
			L++;
		}
		if(L>l) ran[R].push_back({i,l,L-1,1});
		while(L>l){
			q[i].res-=f[L-2]+!k;
			L--;
		}
	}
	mems(g,0);
	for(int i=1;i<=n;i++){//处理range中的询问
		for(auto y:nums) g[w[i]^y]++;
		for(auto &rg:ran[i]){
			int id=rg.id,l=rg.l,r=rg.r,t=rg.t;
			for(int x=l;x<=r;x++) q[id].res+=g[w[x]]*t;
		}
	}
	for(int i=1;i<m;i++) q[i].res+=q[i-1].res;//注意res存的是新增值
	for(int i=0;i<m;i++) ans[q[i].id]=q[i].res;
	for(int i=0;i<m;i++) printf("%lld\n",ans[i]);
	return 0;
}
posted @ 2025-07-25 20:53  liushuangning  阅读(23)  评论(0)    收藏  举报