算法思想
莫队算法是一种解决大部分区间离线的离线算法,无法处理强制在线(比如上次询问的答案作为下一次询问的内容),主要思想为分块,因此时间复杂度与分块相同 O ( n n ) O(n\sqrt{n}) O(nn)
以求解区间和为例,基本思路如下:
如图,如果已知
[
L
,
R
]
[L,R]
[L,R]区间的总贡献,也就是已知
∑
i
=
L
R
a
i
\sum_{i=L}^{R}a_i
∑i=LRai,对之后询问的情况需要讨论,给出的代码只是根据题意的写的,便于理解,真正解题时还需要完善
- 需要加上区间左一格的贡献
int Add(int& res,int& L) { return res+a[--L]; }
- 需要加上区间右一格的贡献
int Add(int& res,int& L) { return res+a[++R]; }
- 减去当前区间最左一格的贡献
int Sub(int& res,int& L) { return res-a[L++]; }
- 减去当前区间最右一格的贡献
int SUb(int& res,int& L) { return res-a[R--]; }
观察上面给出的思路,可以发现每一次的询问都为移动至少1位,当询问的区间是乱序时,显然算法的时间复杂度会退化到 O ( n 2 ) O(n^2) O(n2),因此算法的时间复杂度很大部分取决于询问的顺序,所以需要构造合理的询问区间序列,为了解决这个问题,需要用到分块
分块的思路与实现可以参考:分块笔记&训练
首先按照一般的思路分块,大小为 n \sqrt{n} n,然后对所有询问排序,排序准则如下:对于给出的询问 [ l , r ] [l,r] [l,r],以 l l l所在的块升序排序,如果块相同,以 r r r升序排序,排序好后,可以发现询问之间的区间差别较小,因此每次可以以类似 O ( C ) ( C 为 常 数 ) O(C)(C为常数) O(C)(C为常数)的复杂度进行操作
代码
#include <bits/stdc++.h>
#define ll long long
#define INF 0x3f3f3f3f
using namespace std;
const int maxn=1e5+10;
int part,a[maxn],pos[maxn],n,m,k,res,ans[maxn];
//块数,数据,每个数据对应的块,n,m,当前结果,每次询问的结果
int cnt[maxn];
void Add(int x) {//增加
}
void Sub(int x) {//减少
}
struct node {
int l,r,k;
} q[maxn];
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >>n>>m>>k;
part=sqrt(n);
for(int i=1; i<=n; i++) {//分块,划分块的位置
cin >>a[i];
pos[i]=i/part;
}
for(int i=0; i<m; i++) {//录入范围
cin >>q[i].l>>q[i].r;
q[i].k=i;
}
sort(q,q+m,[](node x,node y) {//升序排序
return pos[x.l]==pos[y.l]?x.r<y.r:pos[x.l]<pos[y.l];
});
int l=1,r=0;//初始化下标
for(int i=0; i<m; i++) {//处理请求
while(q[i].l<l)Add(--l);
while(q[i].r>r)Add(++r);
while(q[i].l>l)Sub(l++);
while(q[i].r<r)Sub(r--);
ans[q[i].k]=res;
}
for(int i=0; i<m; i++)cout <<ans[i]<<endl;
return 0;
}
需要注意的是,分块在算法中扮演的是方便排序与减少复杂度的角色,不需要过多的更新以及合并的操作,是一个辅助的地位
训练
LuoguP2709
题目大意:略
思路:基本上是莫队的模板题
代码
#include <bits/stdc++.h>
#define ll long long
#define INF 0x3f3f3f3f
using namespace std;
const int maxn=1e5+10;
int part,a[maxn],pos[maxn],n,m,k,res,ans[maxn];
//块数,数据,每个数据对应的块,n,m,当前结果,每次询问的结果
int cnt[maxn];
void Add(int x) {
res+=2*cnt[a[x]]+1;
cnt[a[x]]++;
}
void Sub(int x) {
res-=2*cnt[a[x]]-1;
cnt[a[x]]--;
}
struct node {
int l,r,k;
} q[maxn];
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >>n>>m>>k;
part=sqrt(n);
for(int i=1; i<=n; i++) {//分块,划分块的位置
cin >>a[i];
pos[i]=i/part;
}
for(int i=0; i<m; i++) {//录入范围
cin >>q[i].l>>q[i].r;
q[i].k=i;
}
sort(q,q+m,[](node x,node y) {//升序排序
return pos[x.l]==pos[y.l]?x.r<y.r:pos[x.l]<pos[y.l];
});
int l=1,r=0;
for(int i=0; i<m; i++) {//处理请求
while(q[i].l<l)Add(--l);
while(q[i].r>r)Add(++r);
while(q[i].l>l)Sub(l++);
while(q[i].r<r)Sub(r--);
ans[q[i].k]=res;
}
for(int i=0; i<m; i++)cout <<ans[i]<<endl;
return 0;
}
CodeForce 86D
题目大意:和上一题类似,这一次计算出现次数的平方乘数字的和
思路:使用莫队直接计算,注意数据范围
代码
#include <bits/stdc++.h>
#define int long long
#define INF 0x3f3f3f3f
using namespace std;
const int maxn=2e5+10;
int n,m,ans[maxn],cnt[maxn*10],a[maxn],l=1,r=0,pos[maxn],res;
struct node {
int l,r,k;
} q[maxn];
void Add(int x) {
res+=a[x]*(2*cnt[a[x]]+1);
cnt[a[x]]++;
}
void Sub(int x) {
res-=a[x]*(2*cnt[a[x]]-1);
cnt[a[x]]--;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >>n>>m;
for(int i=1; i<=n; i++) {
cin >>a[i];
pos[i]=i/(int)sqrt(n);
}
for(int i=0; i<m; i++) {
cin >>q[i].l>>q[i].r;
q[i].k=i;
}
sort(q,q+m,[](node x,node y) {
return pos[x.l]==pos[y.l]?x.r<y.r:pos[x.l]<pos[y.l];
});
for(int i=0; i<m; i++) {
while(q[i].l<l)Add(--l);
while(q[i].r>r)Add(++r);
while(q[i].l>l)Sub(l++);
while(q[i].r<r)Sub(r--);
ans[q[i].k]=res;
}
for(int i=0; i<m; i++)cout <<ans[i]<<endl;
return 0;
}
SPOJ DQUERY - D-query
题目大意:给出一个整数序列和q次询问区间,每次询问返回区间内有几种数
思路:基本上是莫队的模板,但是需要注意本题题目时间卡的很死,需要快读快写,注意排序的写法
代码
#include <bits/stdc++.h>
#define ll long long
#define INF 0x3f3f3f3f
using namespace std;
const int maxn=2e5+10;
int ans[maxn],cnt[maxn*10],n,m,a[maxn],pos[maxn],l=1,r=0,res;
struct node {
int l,r,k;
} q[maxn];
int read() {//快读
int s = 0, f = 1;
char ch = getchar();
while(!isdigit(ch)) {
if(ch == '-') f = -1;
ch = getchar();
}
while(isdigit(ch)) {
s = s * 10 + ch - '0';
ch = getchar();
}
return s * f;
}
void write(int x) {//快写
if(x < 0) {
putchar('-');
x = -x;
}
if(x > 9)
write(x/10);
putchar(x % 10 + '0');
return;
}
inline void Add(int x) {//内联加快速度
if(cnt[a[x]]==0)res++;
cnt[a[x]]++;
}
inline void Sub(int x) {
if(cnt[a[x]]==1)res--;
cnt[a[x]]--;
}
int main() {
n=read();
for(int i=1; i<=n; i++) {
a[i]=read();
pos[i]=i/(int)sqrt(n);
}
scanf("%d",&m);
for(int i=0; i<m; i++) {
q[i].l=read();
q[i].r=read();
q[i].k=i;
}
sort(q,q+m,[](node x,node y) {//排序写对很重要
return pos[x.l]==pos[y.l]?x.r<y.r:pos[x.l]<pos[y.l];
});
for(int i=0; i<m; i++) {
while(q[i].l<l)Add(--l);
while(q[i].r>r)Add(++r);
while(q[i].l>l)Sub(l++);
while(q[i].r<r)Sub(r--);
ans[q[i].k]=res;
}
for(int i=0; i<m; i++)write(ans[i]),putchar('\n');
return 0;
}
CodeForce 351D
题目大意:给出一个长度为n的正整数序列 a a a,现在定义一次操作如下:
- 选取三个数字 v , t , k v,t,k v,t,k,使得这三个数字满足条件: a v = a v + t = a v + 2 t ⋯ = a v + t k a_v=a_{v+t}=a_{v+2t}\dots =a_{v+tk} av=av+t=av+2t⋯=av+tk
- 将 a v , a v + t , a v + 2 t … , a v + t k a_v,a_{v+t},a_{v+2t}\dots ,a_{v+tk} av,av+t,av+2t…,av+tk这些数字从数组中剔除,并且重新加上下标得到 a 1 , a 2 , … a n − k − 1 a_1,a_2,\dots a_{n-k-1} a1,a2,…an−k−1
- 将得到的序列 a a a以任意顺序重新组合
现在给出多个询问,每次询问都是一个区间,输出以该区间构成的序列将序列内全部元素删除需要多少次操作
思路:首先贪心的想到,如果第一次就将一个数全部去除,那么将剩下的数重新排列,使相同的数下标连续,此时需要的次数为数的种类,如果第一次不能全部去除,那么需要的次数就为数的种类+1,也就是说,判断的关键在于第一次是否能找到一个区间内所有出现位置下标为等差数列的数
因为等差的是下标,而且等差序列可以从前构造也可以从后构造,所以对于前缀和后缀都要统一考虑
统计区间种类数用前面的莫队模板即可,对于维护等差序列,思路如下:
-
增加一个数
如果增加的数没有出现在查询/当前区间,就产生了一个只有单元素的等差数列(下标),计数器增加
反之,如果增加的数破坏了原有的等差序列,计数器减少举个例子,如果左增一个数破坏了原有的序列,设 x x x为当前下标, f r [ x ] fr[x] fr[x]为当前下标右边第一个相等但下标不等差的位置, a f t [ x ] aft[x] aft[x]为x右边第一个相等的位置,判断破坏条件为
fr[x]<=r&&fr[aft[x]]>r
以12121为例,对应的fr为60600,aft为34566,只有121的时候,显然121是可以抽出一个等差数列的,当x=5时,可以看到fr[aft[x]]==r,也就是说,只有fr[x]<=r这个条件是不够的,因为可能出现12121这种情况,相等但不等差的位置相同,而且整体上能够组成一个等差序列,也就是可能x,aft[x],fr[aft[x]]构成等差序列
-
减去一个数
如减去的数在区间内只出现过一次,显然等差序列个数减少了一个
反之,如果删去的数产生了等差序列,等差序列增加,分析同上,具体见代码
代码
#include <bits/stdc++.h>
#define ll long long
#define INF 0x3f3f3f3f
const int maxn=1e5+10;
using namespace std;
int fl[maxn],fr[maxn],pre[maxn],aft[maxn],n,m,a[maxn],last[maxn];
//左/右第一个相等但不等差,前/后一个相等位置
int pos[maxn],res,l=1,r,arith,ans[maxn],cnt[maxn];
//块坐标,数种数,lr边界,等差数列数,答案保存,桶
struct node {
int l,r,k;
} q[maxn];
inline void Addl(int x) {
if(cnt[a[x]]==0)res++,arith++;//如果增加的数未出现过,会增加一个等差数列(只有一个元素)
else if(fr[x]<=r&&fr[aft[x]]>r)arith--;//之前是等差,现在不是
cnt[a[x]]++;
}
inline void Addr(int x) {
if(cnt[a[x]]==0)res++,arith++;//同上
else if(fl[x]>=l&&fl[pre[x]]<l)arith--;//同上
cnt[a[x]]++;
}
inline void Subl(int x) {
if(cnt[a[x]]==1)res--,arith--;//如果删除后不剩下,会减少一个等差数列(只有一个元素)
else if(fr[x]<=r&&fr[aft[x]]>r)arith++;
cnt[a[x]]--;
}
inline void Subr(int x) {
if(cnt[a[x]]==1)res--,arith--;//同上
else if(fl[x]>=l&&fl[pre[x]]<l)arith++;
cnt[a[x]]--;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >>n;
for(int i=1; i<=n; i++) {
cin >>a[i];
pre[i]=last[a[i]];//先前位置为a[i]最后一次出现的位置
last[a[i]]=i;//更新最后一次出现的位置
if(pre[pre[i]]-pre[i]==pre[i]-i||!pre[i])fl[i]=fl[pre[i]];
//判断下标是不是等差,如果是等差,更新对于当前位置i相等但不能构成等差的位置
else fl[i]=pre[pre[i]];//否则直接继承
}
for(int i=1; i<=n; i++)last[i]=n+1;//初始化,重复利用
fr[n+1]=n+1;
for(int i=n; i; i--) {//和前缀一样,求后缀
aft[i]=last[a[i]];
last[a[i]]=i;
if(aft[aft[i]]-aft[i]==aft[i]-i||!aft[i])fr[i]=fr[aft[i]];
else fr[i]=aft[aft[i]];
}
cin >>m;
for(int i=0; i<m; i++) {//录入询问
cin >>q[i].l>>q[i].r;
q[i].k=i;
}
for(int i=1; i<=n; i++)pos[i]=i/(int)sqrt(n);
sort(q,q+m,[](node x,node y) {//分块排序
return pos[x.l]==pos[y.l]?x.r<y.r:pos[x.l]<pos[y.l];
});
for(int i=0; i<m; i++) {
while(q[i].l<l)Addl(--l);
while(q[i].r>r)Addr(++r);
while(q[i].l>l)Subl(l++);
while(q[i].r<r)Subr(r--);
ans[q[i].k]=res;
if(arith==0)ans[q[i].k]++;//如果不存在等差数列,需要多操作一次
}
for(int i=0; i<m; i++)
cout <<ans[i]<<endl;
return 0;
}
总结
莫队算法还是一个较为简单的算法,模板也简短,算法思想也简单容易实现,但是结合性非常强,多使用于区间相关的问题,在题目不太复杂且数据不苛刻的情况下,选用莫队算法比线段树这样的大部头更加划算,而且有些题莫队算法能够解决的非常巧妙,本篇只是介绍了莫队的基本思想与模板,还有更多可以拓展的部分