莫队算法笔记(目前只更新到普通莫队,后续内容后续更新)
前提:最近由于一些题需要莫队,所以就学了学,顺便写了一个笔记。
莫队这个算法是一种以离线的方式处理查询的算法,需要先读入所有查询,然后最后一次性打印所有输出,通过按一定的排列降低指针的移动次数从而提升算法性能,代码比较简单易写,需要结合分块的思想。
我们以下面一道题一个例子来说一下开始这个算法。
eg1:
[国家集训队] 小 Z 的袜子
来源:洛谷 \(P1494\) (链接:https://www.luogu.com.cn/problem/P1494)
题意:
给定 \(N\) 只袜子(编号 \(1 \sim N\))和 \(M\) 次询问,每只袜子有颜色 \(C_i\)。
每次询问给出区间 \([L, R]\),要求计算:从该区间内随机选两只袜子,颜色相同的概率。
-
特殊情况:若 \(L = R\),直接输出 \(\boldsymbol{0/1}\);
-
概率需以最简分数 \(\boldsymbol{A/B}\) 形式输出,概率为 \(0\) 则输出 \(\boldsymbol{0/1}\)。
思路:
我相信这道题各种大佬肯定有各种不一样的解法,这里我只以此为展开来说一下我理解的莫队。
我们从最极致的暴力手段出发,可以扫一遍每一次查询的 \([L_{i},R_{i}]\) ,然后维护所求结果的分子,最后和分母化简即可,但这么做的话时间复杂度就是 \(O(n*m)\) ,只要数据不弱,肯定会超时。
那我们该如何优化呢?其实造成时间复杂度高的一个原因就是暴力手段把每一次的查询当作一个独立的个体,没有让某次查询利用好上一次查询的信息。那么就可以延伸一个思路,从 \([L_{i-1},R_{i-1}]\) 到 \([L_{i},R_{i}]\) ,可以让指针进行移动,然后尽可能的减少指针的移动次数,进而在时间复杂度上得到一个优化。那我们要怎么找这么一个思路呢?莫队这个算法就基于分块提供了这么一个思路。
我们先给出结论,然后再分析其时间复杂度,我们可以把区间 \([1,n]\) 分块,设每一块的大小为 \(size\) ,共有 \(len\) 块,然后我们按照以下策略排序:
- 先按照 \(L\) 所属块的大小进行从小到大的排序,如果都在同一个块按照下一个策略进行排序。
- 然后按照 \(R\) 的大小进行从小到大的排序。
这样我们从时间复杂度的角度进行分析,如何设置块的大小让时间复杂度尽可能的小,可以两个维度进行分析,最后两个维度的时间复杂度相加(因为互相独立)即可。
- 从 \(L\) 的角度分析,在同一个块内,每个查询 \(L\) 方向最多移动 \(size\) ,而一共有 \(m\) 次查询,所以时间复杂度为 \(O(size*m)\)
- 从 \(R\) 的角度分析,对每个块而言,每个块内 \(R\) 方向块最多移动 \(n\) 次,而一共有 \(len\) 个块,所以时间复杂度为\(O(len*n)\)
所以最后的时间复杂度应该为 \(O(size*m+len*n)\) ,由于 \(n\) 和 \(m\) 的量级差距不大,所以可以看成 \(O((size+len)*n)\) ,由于 \(len*size≈n\) ,根据基本不等式,若想让 \(size+len\) 的结果尽可能小,就需要让两个量尽可能接近,也就是 \(len=size=n\) ,因此最后时间复杂度为 \(O(n*\sqrt{n})\)
然后就需要从具体思路上的实现来说明了,最本质的问题,如何利用上一次的查询信息呢?
事实上,我们其实只需要考虑分子的大小,分母的大小就是简单的 \(C_{R-L+1}^{2}\) ,分子的大小我们需要不断的更新维护。我们观察每一次指针移动发现,每一次指针移动都是让某个数 \(x\) 的出现次数增加或者减小,因此我们只需要让分子在每一次在某个数 \(x\) 的出现次数增加或者减小前先除去这个数的影响。假设分子的结果为 \(sum\) ,某个数 \(x\) 的出现次数为 \(cnt_{x}\),那么消除影响就是先让 \(sum=sum-C_{cnt_{x}}^{2}\) ,然后对 \(x\) 进行操作后再进行 \(sum=sum+C_{cnt_{x}}^{2}\) 。
根据以上思路直接实现即可。
AC代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=5e4+100;
int n,m;
ll a[N];
ll cnt[N];
ll sum;
struct node{
int l,r,id;
};
struct node1{
ll x,y;
};
node p[N];
node1 ans[N];
int block;
bool cmp(node a,node b){
if(a.l/block!=b.l/block)
return a.l<b.l;
return a.r<b.r;
}
void add(int x){
sum=sum-cnt[x]*(cnt[x]-1)/2;
cnt[x]++;
sum=sum+cnt[x]*(cnt[x]-1)/2;
}
void del(int x){
sum=sum-cnt[x]*(cnt[x]-1)/2;
cnt[x]--;
sum=sum+cnt[x]*(cnt[x]-1)/2;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i];
block=sqrt(n);
for(int i=1;i<=m;i++){
cin>>p[i].l>>p[i].r;
p[i].id=i;
}
sort(p+1,p+1+m,cmp);
ll l=1,r=0;
for(int i=1;i<=m;i++){
if(p[i].l==p[i].r){
ans[p[i].id]={0,1};
continue;
}
while(l>p[i].l){
add(a[--l]);
}
while(r<p[i].r){
add(a[++r]);
}
while(l<p[i].l){
del(a[l++]);
}
while(r>p[i].r){
del(a[r--]);
}
ll x=sum,y=(r-l+1)*(r-l)/2;
if(x==0)
y=1;
else{
ll k=__gcd(x,y);
x/=k;
y/=k;
}
ans[p[i].id]={x,y};
}
for(int i=1;i<=m;i++)
cout<<ans[i].x<<'/'<<ans[i].y<<'\n';
return 0;
}
根据这道题,还有一道比较直接的题,可以拿来先简单体会一下莫队。
eg2:
【模板】莫队 / 小 B 的询问
来源:洛谷 \(P2709\) (链接:https://www.luogu.com.cn/problem/P2709)
题意:
给定长度为 \(n\)、值域为 \([1,k]\) 的整数序列 \(a\),共 \(m\) 次区间查询。
每次查询给出区间 \([l,r]\),计算该区间内每个数字出现次数的平方和:
思路这里就不多赘述了,感觉根据 \(eg1\) 简单改一改即可。
后续的带修莫队和书上莫队日后再扩充,这里就不多赘述了。

浙公网安备 33010602011771号