莫队详细解析(基础、回滚)

本文介绍了莫队算法在区间查询问题中的应用,通过分块和优化排序降低时间复杂度,讨论了回滚莫队的只加不减和只减不加策略,以及扩展到树上和带修莫队的可能性。
摘要由CSDN通过智能技术生成

莫队主要思路

莫队主要是维护区间信息的算法,大部分题都可以用其他高级的数据结构做。

但莫队码量小,好理解(本质上是分块优化暴力),不失为一种好做法

莫队代码看起来是暴力,但通过将询问按一些步骤排序降低了时间按复杂度

基础莫队

即使过不了,依旧以HH的项链为例题

题目+分析

题意

给定长为 n 的序列 ,m个询问,每次求区间 l~r 中有多少个不同的数字

 考虑暴力做法,用一个桶记录每个数字的出现次数,在序列上左右移动维护,时间复杂度 O(nm) 

引入莫队思想

对于询问 [1,1] [n,n] [1,1] [n,n] [1,1] [n,n] [1,1] ......  ,我们显然浪费了大量时间移动

但如果将询问变为 [1,1] [1,1] [1,1][1,1][n,n] [n,n] [n,n] ,时间按复杂度大量降低

所以 ,将 长度为 n 的序列分成长度为 根号n 的块 ,将询问 按照 左端点所在块为第一关键字,右端点为第二关键字排序。依次求出答案。

代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int m,n,a[N],ans[N],bs,cnt[N],temp,b[N];
struct query{
	int l,r,id;
}q[N];
bool cmp(query x,query y){
	if(b[x.l]!=b[y.l]) return b[x.l]<b[y.l];
	if(b[x.l]&1) return x.r<y.r;
	return x.r>y.r; //奇偶优化
}
void add(int x) {
	cnt[a[x]]++;
	if(cnt[a[x]]==1) temp++; 
} 
void del(int x) {
	cnt[a[x]]--;
	if(cnt[a[x]]==0) temp--;
}

int main(){
	cin>>n;
	bs=sqrt(n);
	for(int i=1;i<=n;i++) {
		cin>>a[i];
	}	
	for(int i=1;i<=n;i++) b[i]=(i-1)/bs+1;//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+m+1,cmp);
	int l=1,r=0;
	for(int i=1;i<=m;i++) {
		while(r<q[i].r)add(++r);
		while(r>q[i].r) del(r--);
		while(l<q[i].l) del(l++);
		while(l>q[i].l) add(--l);
		ans[q[i].id]=temp;
	}
	for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
	
	return 0;
}

时间复杂度分析

为什么要分成 根号n 长的块?

考虑块长为 s ,则有 \frac{n}{s} 块 ,有m个询问 ,对于每个块被访问的次数为 q[i]

易得,时间复杂度为 \sum _{i=1}^{\frac{n}{s}} (q[i]*s +n) =ms+\frac{n^2}{s}

由基本不等式,当 s=\sqrt{\frac{n^2}{m}}  时有最优时间复杂度 O(n\sqrt{m}) ,一般把 n , m 看作同级,就有了一般所说的时间复杂度。

回滚莫队

只加不减

算法引入

在普通莫队算法中,我们通过加入/删除一个数的贡献以移动区间。但是这个方法在求例如区间最值时就失去作用

我们发现,上面 l--,r++ (即添加元素)的操作不受影响 ,而删除元素的操作则会受影响。

所以我们考虑不删除 ,只加不减莫队

例题分析

link

题目分析

同样使用上述方式对询问排序,但不能使用奇偶优化。

定义 l,r 为上次遍历到的区间 , 对于询问 q[i] 

         1、若 l 和 q[i].l 不属于一个块,则初始化

         2、如果 q[i].l 和 q[i].r 属于一个区间 , 直接求,不影响时间复杂度

         3、否则:

                将右端点 r 更新到 q[i].r ; 并令 t = 当前 temp 

                将左端点 l 更新到 q[i].l ; 得到当前询问答案=temp

                将刚刚 l ~ q[i].l 记录的次数减回去 ,更新 temp=t;

其实就是单调右边,把左边的加出来在减回去(因为左边没有排序保证)

代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+10;
int n,nn,m,a[N],b[N],bs,cnt[N];
int temp,ans[N],y[N];
struct dat{
	int l,r,id;
}q[N];
bool cmp(dat x,dat y){
	if(b[x.l]!=b[y.l]) return b[x.l]<b[y.l];
	return x.r<y.r;
}
void add(int x){
	cnt[a[x]]++;
	int val=y[a[x]]*cnt[a[x]];
	temp=max(temp,val);
}
void del(int x) {
	cnt[a[x]]--;
}

signed main(){
	scanf("%lld%lld",&n,&m);
	bs=sqrt(n);
	for(int i=1;i<=n;i++) {
		scanf("%lld",&a[i]);
		y[i]=a[i];
		b[i]=(i-1)/bs+1;
	}	
	sort(y+1,y+n+1);
	nn=unique(y+1,y+n+1)-y-1;
	for(int i=1;i<=n;i++) {
		a[i]=lower_bound(y+1,y+1+nn,a[i])-y;//离散化
	}
	for(int i=1;i<=m;i++) {
		scanf("%lld%lld",&q[i].l,&q[i].r);
		q[i].id=i;
	}
	sort(q+1,q+m+1,cmp);
	int r=b[q[1].l]*bs,l=r+1;
	for(int i=1;i<=m;i++) {
		if(b[q[i].l]>=b[l]) {
			temp=0;
			for(int j=r;j>=l;j--) del(j);
			r=b[q[i].l]*bs;
			l=r+1;
		} 
		if(b[q[i].l]==b[q[i].r]) {
			for(int j=q[i].l;j<=q[i].r;j++) add(j);
			ans[q[i].id]=temp;
			for(int j=q[i].l;j<=q[i].r;j++) del(j);
			temp=0;
			continue;
		}
		while(r<q[i].r) add(++r);
		int mv=temp;
		for(int j=q[i].l;j<l;j++) add(j);
		ans[q[i].id]=temp;
		for(int j=q[i].l;j<l;j++) del(j);
		temp=mv;
	}
	for(int i=1;i<=m;i++) printf("%lld\n",ans[i]);
	return 0;
}

时间复杂度基本没有区别,不多赘述

只减不加

算法引入

类似于只加不减回滚莫队,改变排序方式,第二关键字改为右端点从大到小

例题分析

link

题意分析

mes(S)表示不属于集合 S 的最小非负整数。

发现取出数时很好维护,加入数时不好维护,所以要用只减不加回滚莫队

具体思路见代码

代码
#include<bits/stdc++.h>

using namespace std;
#define int long long
const int N=2e5+10;
int n,m,a[N],cnt[N],bs,b[N],ans[N];
int L[N],R[N];//第i个询问左端点、右端点
int mex1;//q[i].l~n 的mes
int mex2;//q[i].l~q[i].r; 的mes
struct dat{
	int l,r,id;
}q[N]; 
bool cmp(dat x,dat y){
	if(b[x.l]!=b[y.l]) return b[x.l]<b[y.l];
	return x.r>y.r;
}
void add(int x) {
	cnt[a[x]]++;
}
void del(int x) {
	cnt[a[x]]--;
	if(cnt[a[x]]==0&&a[x]<mex2) mex2=a[x];
}
signed main(){
	scanf("%lld%lld",&n,&m);
	bs=sqrt(n);
	for(int i=1;i<=n;i++) 
	{
		scanf("%lld",&a[i]);
		cnt[a[i]]++;
		b[i]=(i-1)/bs+1;
		L[i]=(b[i]-1)*bs+1;
		R[i]=L[i]-1+bs;
	}
	for(int i=0;i<N;i++) if(cnt[i]==0){
		mex1=mex2=i;
		break;
	}
	for(int i=1;i<=m;i++) {
		scanf("%lld%lld",&q[i].l,&q[i].r);
		q[i].id=i;
	}
	sort(q+1,q+m+1,cmp);
	int l=L[1],r=n;
	for(int i=1;i<=m;i++) {
		if(q[i].l>=l+bs) {
			while(r<n) add(++r);//更新完新的块
			mex2=mex1;
			while(l<L[q[i].l]) del(l++);
            mex1=mex2;//此时mex2=mex1为 q[i].l 到 n 的mes 
		}
		while(r>q[i].r) del(r--);
		int temp=mex2;//此时mex2= 当前块左端点到q[i].r的mes
		for(int j=l;j<q[i].l;j++) del(j);
		ans[q[i].id]=mex2;//此时mex2为当前询问的mex
		for(int j=l;j<q[i].l;j++) add(j);
		mex2=temp;//回滚
	}
	for(int i=1;i<=m;i++) printf("%lld\n",ans[i]);
	return 0;
} 

大型纪录片——莫队传奇持续为您播出

下期预告——树上莫队、带修莫队

多多支持  -<^-^>-

  • 12
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值