0%

莫队

莫队算法是一种根号复杂度离线算法,也与分块、定期重构并称优雅的暴力。

普通莫队的时间复杂度是 $O(m\sqrt n)$,此外还有许多变种。

普通莫队

当我们遇见一类没有修改的问题时如何处理?

假设我们已经求出了在 $[l,r]$ 上的答案,我们想求出 $[l^\prime,r^\prime]$ 的答案,怎么办?

如果我们能够快速地将答案从 $[l,r]$ 转移到 $[l-1,r]$,$[l+1,r]$,$[l,r+1]$,$[l,r-1]$ 这四个区间上,我们就可以一步步地将 $[l,r]$ 转移到 $[l^\prime,r^\prime]$ 上。可惜这样的时间复杂度不够优秀,达到了 $O(n)$ 一次转移。怎么优化呢?

普通莫队算法将这些询问离线下来,以一种方式排序后重新计算答案,方式是:

  • 对原序列分块,块长为 $\sqrt n$。记 $\operatorname{block}(p)$ 为 $p$ 所属的块。
  • 对于两个询问 $[l_1,r_1]$,$[l_2,r_2]$,如果 $\operatorname{block}(l_1)\neq\operatorname{block}(l_2)$,那么块编号小的在前面。
  • 否则 $r$ 小的在前面。

以这样的顺序转移,时间复杂度只需要 $O(m\sqrt n)$。

复杂度证明:

设块长为 $B$。

首先右端点在左端点块编号保持不动时一共只会移动 $O(n)$ 次,因为右端点编号不降。

其次考虑左端点的移动。在块编号不变时每次移动会移动 $O(B)$ 次。在块发生变化时也会移动 $O(B)$ 次,而一共会发生 $O(n/B)$ 次移动,并且一次移动右端点要回来,也会开销 $O(n)$ 的时间。

所以总复杂度开销是 $O(n/B\cdot (n+B)+nB)=O\left(\dfrac{n^2}{B}+nB\right)$,显然在 $B=\sqrt n$ 是最小,为 $O(n\sqrt n)$。以上默认 $m,n$ 同阶。

这样莫队算法就用 $O(n\sqrt n)$ 的时间解决了问题。

普通莫队奇偶排序优化

就是说,奇数块的询问右端点从小到大排序,偶数块的询问右端点从大到小排序。

感性理解一下这就是不要右端点再回来了,路上直接完成操作。据说能快 30%?

例题 小 Z 的袜子

有一个长度为 $n$ 的数列 $a_1,a_2,\dots,a_n$,有 $m$ 次询问,每次询问给定 $l_i,r_i$,你需要在区间 $[l_i,r_i]$ 中选取两个不同的整数 $i,j$,求 $a_i=a_j$ 的概率。

$n,m\le5\times10^4$。

一打眼看上去,$n,m$ 较小,可以采用时间复杂度带根号的算法。并且可以离线,使用莫队求解。

显然是好做的。我们将 $a$ 离散化后开值域数组,当加入删除一个数的时候统计并计算答案,时间 $O(1)$。

所以时间复杂度 $O(n\sqrt n)$。

带修莫队

普通的莫队是不能支持修改的。

我们可以让操作从 $(l,r)$ 强行变成 $(l,r,\mathrm{time})$,其中 $\mathrm{time}$ 是时间维,代表此次操作前面有 $\mathrm{time}$ 次修改。

只需要我们能够从 $(l,r,\mathrm{time})$ 快速转移到

  • $(l-1,r,\mathrm{time})$,$(l+1,r,\mathrm{time})$;
  • $(l,r-1,\mathrm{time})$,$(l,r+1,\mathrm{time})$;
  • $(l,r,\mathrm{time}-1)$,$(l,r,\mathrm{time}+1)$。

这六种状态,我们就可以使用扩展的莫队求解。排序方式是左端点块编号第一关键字,右端点块编号第二关键字,$\mathrm{time}$ 第三关键字,块长 $O(n^{\frac23})$,总时间复杂度为 $O(n^{\frac53})$,如果 $n,m,t$ 同阶。

复杂度证明:

设 $n,m,t$ 表示大小、询问次数、修改次数。$B$ 为块长。

首先我们不能不将右端点分块,因为有序的右端点会带来乱序的询问,与不对右端点排序的时间开销都是 $O(n)$。

我们不妨设 $q_{i,j}$ 表示左端点在块 $i$、右端点在块 $j$ 的询问次数。

对于 $q_{i,j}$ 内的询问,时间维变化次数是 $O(t)$,左右端点移动次数是 $O(B)$。

那么总时间复杂度是

考虑求此式最小值,利用求导法,设 $\mathsf{time}(B)=mB+\left(\dfrac{n}{B}\right)^2t$,那么 $\mathsf{time}^\prime(B)=m-\dfrac{2n^2t}{B^3}$。

令 $\mathsf{time}^\prime(B)=0$,即 $B=\sqrt[3]{\dfrac{2n^2t}{m}}$。当 $n,m,t$ 同阶时 $B=O(n^{\frac23})$ 最优。总时间复杂度为 $O(n^{\frac53})$。

回滚莫队

回到没有修改的情况。

然后你发现出题人没有这么良心,不妨假装你发现 $(l,r)$ 无法扩展到 $(l+1,r)$ 和 $(l,r-1)$,即你无法支持删除操作。

这时候你还想保住你的莫队!于是你要找一个能只增不删的方法,这就是回滚莫队。

我们在更换块的时候令 $R$ 为当前新块的右端点,并设置 $r=R,l=r+1$。这是一个空区间。

我们在处理问题的时候,如果 $\operatorname{block}(l)=\operatorname{block}(r)$ 就直接暴力扫描处理,否则扩展左端点和右端点。右端点单调递增不用管,左端点每次更新完后都滚回到 $R+1$ 重新扩展。由于 $l$ 一次滚回只会有 $O(\sqrt n)$ 的时间开销,因此不会造成复杂度的退化。

回滚莫队通用模板,其中 $\mathrm{add},\mathrm{del}$ 函数是增加或删除的函数。(删除就是直接撤销增加的影响但是不管答案,答案已经在滚之前就记录下来了,直接恢复。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ll r=bl(q[1].l)*sqr,l=r+1;
for(int i=1;i<=2*n;i++) {
if(bl(q[i].l)>=bl(l)) {
tmp=0;
for(int j=r;j>=l;j--) del(j);
r=bl(q[i].l)*sqr,l=r+1;
}
if(bl(q[i].l)==bl(q[i].r)) {
for(int j=q[i].l;j<=q[i].r;j++) add(j);
ans[q[i].id]=tmp;
for(int j=q[i].l;j<=q[i].r;j++) del(j);
tmp=0;
continue;
}
while(r<q[i].r) add(++r);
ll mv=tmp;
for(int j=q[i].l;j<l;j++) add(j);
ans[q[i].id]=tmp;
for(int j=q[i].l;j<l;j++) del(j);
tmp=mv;
}