莫队算法简介
莫队算法由队长莫涛发明,是区间修改的神器(其实是暴力)
莫队算法可以解决的经典问题
给出一个长度为n的区间,每次查询
[l,r]
[
l
,
r
]
有多少种颜色之类的问题;
我们随机一个数据
1,2,3,4,5,5,6,6,6
1
,
2
,
3
,
4
,
5
,
5
,
6
,
6
,
6
询问
[1,3],[2,2],[4,5],[4,6],[5,9],[6,8]
[
1
,
3
]
,
[
2
,
2
]
,
[
4
,
5
]
,
[
4
,
6
]
,
[
5
,
9
]
,
[
6
,
8
]
就这样了
考虑暴力算法
每一次直接从枚举区间 [l,r] [ l , r ] 就可以
莫队算法
我们假设已经知道了区间
[l,r]
[
l
,
r
]
的答案,那么我们就可以
O(1)
O
(
1
)
的时间复杂度内求出
[l,r+1][l,r−1][l+1,r][l−1,r]
[
l
,
r
+
1
]
[
l
,
r
−
1
]
[
l
+
1
,
r
]
[
l
−
1
,
r
]
的答案,对于上面那一个问题,我们先把所有的询问都读入进来(这也就说明了莫队算法是一个离线算法,如果有强制在线的莫队算法就无能为力了)。按照一定的规则排序
怎么排序呢? 分块
我们把序列分块,然后按照询问的左端点所在的块作为第一关键字,询问的右端点作为第二关键字进行排序;
先分块一般分
n−−√
n
块,莫队的时间复杂度是玄学,取决于块的大小,有时大点好,有时小点好
分块之后是
[1,2,3][4,5,6][7,8,9]
[
1
,
2
,
3
]
[
4
,
5
,
6
]
[
7
,
8
,
9
]
这个是按照下标排序的
把上面询问排序之后是
[2,2],[1,3],[4,5],[4,6],[6.8],[5.9]
[
2
,
2
]
,
[
1
,
3
]
,
[
4
,
5
]
,
[
4
,
6
]
,
[
6.8
]
,
[
5.9
]
然后用两个指针
nowl,nowr
n
o
w
l
,
n
o
w
r
来记录当前所处理的区间的左右端点(初始化一般为0),按排序从左到右进行处理询问。
- 若当前左指针小于所处理区间的左端点,不断减去当前指针所指的位置对答案的贡献,然后不断把指针移向左端点
- 若当前左指针大于所处理区间的左端点,不断加上当前指针所指的位置对答案的贡献,然后不断把指针移向左端点
- 若当前右指针小于所处理区间的右端点,不断加上当前指针所指的位置对答案的贡献,然后不断把指针移向右端点
- 若当前右指针大于所处理区间的右端点,不断减去当前指针所指的位置对答案的贡献,然后不断把指针移向右端点
实现可以看后面例题的代码
——————莫队核心;
莫队算法时间复杂度分析
莫队算法采取了分块的方法进行排序,排序非瓶颈
- 考虑询问的左端点位于同一块的时候,右指针在同一个块中最多移动 n−−√ n 次 , 一共分为了 n−−√ n 个块,复杂度为 O(nn−−√) O ( n n )
- 考虑询问右端点,由于在所有块中右端点为递增的顺序,所以在每一个块中右端点最多转移 n n 次,一共分了个块,复杂度为 O(nn−−√) O ( n n ) ;
- 考虑如果询问区间跨越块的话,每一个块中左端点最多可以增加
n−−√
n
次,一共
n
n
个块,复杂度.
综上莫队的时间复杂度为 O(nn−−√) O ( n n ) .
例题
[SDOI2009]HH的项链 from洛谷
这个题是莫队的模板题,直接按照上面的做就可以,代码附详解
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<cmath>
const int MAXN=1e6;
using namespace std;
int cur;
struct Node
{
int l,r,pos;
friend bool operator < (Node A,Node B)
{
if(A.l/cur==B.l/cur) return A.r<B.r;//若询问的区间左端点在一个块内,按右端点排序
else return A.l/cur<B.l/cur;//反之按左端点排序
}
}q[MAXN];
int n,m,val[MAXN],ans[MAXN],num[MAXN],answer;
inline void Remove(int x) { if(--num[val[x]]==0) answer--; }//去除,若==0则说明对答案有贡献,答案-1
inline void Add(int x){ if(++num[val[x]]==1) answer++; }//增加,若==1说明没出现过,对答案有贡献,答案+1
inline int read()
{
int x=0;
char ch=getchar();
while(ch<'0'||ch>'9') ch=getchar();
while(ch>='0'&&ch<='9') x=x*10+ch-'0',ch=getchar();
return x;
}
int main()
{
n=read();
for(int i=1;i<=n;i++) val[i]=read();
m=read();
for(int i=1;i<=m;i++)
{
q[i].l=read(),q[i].r=read();
q[i].pos=i;
}
cur=sqrt(n);//块的数量
sort(q+1,q+m+1);//排序
int nowl=0,nowr=0;
for(int i=1;i<=m;i++)
{
while(nowl<q[i].l) Remove(nowl++);
while(nowr>q[i].r) Remove(nowr--);
while(nowl>q[i].l) Add(--nowl);
while(nowr<q[i].r) Add(++nowr);//莫队板子
ans[q[i].pos]=answer;
}
for(int i=1;i<=m;i++)
{
printf("%d\n",ans[i]);
}
return 0;
}
带修改莫队的算法
莫队算法还可以支持修改,我们也是把询问和修改都读入进来,进行离线操作.
对于每一组询问,我们额外维护离这一组询问的最近的修改。
当我们查询到一个区间时,如果当前修改到的大于我们对这一组询问维护的修改,我们就该回去,反之就改过来.
可能有点难理解,看看代码就明白了。
while(nowl<q[i].l) Remove(nowl++);
while(nowl>q[i].l) Add(--nowl);
while(nowr<q[i].r) Add(++nowr);
while(nowr>q[i].r) Remove(nowr--);
while(now<q[i].pre) Change_(++now,i);//当前询问的区间的最近修改还没有改,就改
while(now>q[i].pre) Change_(now--,i);//当前修改的数量已经大于最近询问的修改,改回去