莫队主要思路
莫队主要是维护区间信息的算法,大部分题都可以用其他高级的数据结构做。
但莫队码量小,好理解(本质上是分块优化暴力),不失为一种好做法
莫队代码看起来是暴力,但通过将询问按一些步骤排序降低了时间按复杂度
基础莫队
即使过不了,依旧以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 ,则有 块 ,有m个询问 ,对于每个块被访问的次数为 q[i]
易得,时间复杂度为
由基本不等式,当 时有最优时间复杂度 ,一般把 n , m 看作同级,就有了一般所说的时间复杂度。
回滚莫队
只加不减
算法引入
在普通莫队算法中,我们通过加入/删除一个数的贡献以移动区间。但是这个方法在求例如区间最值时就失去作用
我们发现,上面 l--,r++ (即添加元素)的操作不受影响 ,而删除元素的操作则会受影响。
所以我们考虑不删除 ,只加不减莫队
例题分析
题目分析
同样使用上述方式对询问排序,但不能使用奇偶优化。
定义 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;
}
时间复杂度基本没有区别,不多赘述
只减不加
算法引入
类似于只加不减回滚莫队,改变排序方式,第二关键字改为右端点从大到小
例题分析
题意分析
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;
}
大型纪录片——莫队传奇持续为您播出
下期预告——树上莫队、带修莫队
多多支持 -<^-^>-