莫队算法主要用于可以离线的区间询问回答。
引子
考虑一个这样的问题:假设没有事先求前缀和,你知道了数组第 \(5\) 个数到第 \(100\) 个数的和,现在询问问你第 \(4\) 个数到第 \(102\) 个数的和。怎么快速的计算?
显然直接暴力的去把第 \(4\) 个数加进去,然后把第 \(101\) 个数和 \(102\) 个数加进去。也就是说,当我们连续询问了两个问题,且这两个问题的交集比较大的时候,我们直接暴力的去处理不相交的部分,其实时间复杂度是很优秀的。
莫队就是这样一种处理询问的方式,通过排序,使得两两询问之间的交集尽可能的大。
如何排序
字母 含义 \(L\) 某一询问的左端点 \(R\) 某一询问的右端点 \(block\) 块长 \(N\) 数字个数 \(M\) 询问规模
如何排序使得区间交集尽可能大?
根据分块的思想,莫队按照 \(\dfrac{L}{block}\)(设块长为 \(block\))进行分组,然后每一组中的区间再按照 \(R\) 从小到大排序。
因为 \(L\) 被分了组,所以 \(L\) 端点移动复杂度不大于 \(block\)。
对于相同组, \(R\) 从小到大移动不超过 \(N\)。因为总共有 \(\dfrac{N}{block}\) 组, 所以总复杂度不超过 \(\dfrac{N^2}{block}\) ,均摊到每一次询问上就是 \(\dfrac{N^2 \times block}{M}\)。
当 \(N\) 和 \(M\) 是一个数量级时,每次询问均摊的复杂度就是 \(block + \dfrac{N}{block}\)。当 \(block\) 取 \(\sqrt{L}\) 时,复杂度达到最小值,为 \(O(n\sqrt{n})\)。
code
一次排序 + 四遍 \(while\) 循环即可
注意 \(l\), \(r\) 移动区间时,++l
,--l
,++r
,--r
的顺序排列仅有 \(3\) 种排列是正确的,如下图(图源 OI-Wiki)
口诀:先加后减,先左后右,先小后大
int n, block
struct query {
int l, r, id;
bool operator<(query x) {
return l / block == x.l / block ? (r == x.r ? 0 : ((l / block) & 1) ^ (r < x.r)) : l < x.l;
}
} q[N];
// update nowans
void move() {
//...
}
void mo() {
block = int(ceil(pow(n, 0.5)));
sort(q + 1, q + n + 1);
for (int i = 1; i <= n; ++i) {
query Q = q[i];
while (l < q.l) move(l++, 1);
while (l > q.l) move(--l, 1);
while (r < q.r) move(++r, 1);
while (r > q.r) move(--r, 1);
}
}
应用
区间 \(mex\)
首先莫队把询问离线排序后,我们可以很简单的维护出一个桶。
每次暴力维护移动的时候,就令 \(num[\) \([\) \(l\) \(]\) \(]\) 加加或者减减。问题在于如何快速求 \(mex\)。在四个循环跑完以后,我们已经知道了当前区间形成的桶,怎样快速求出 \(mex\) 呢?
考虑对于桶进行分块。每 \(block\) 个桶合并出一个大的桶。这个桶存一下当前这个块内有多少元素被覆盖。
维护好这个大的桶后,我们就可以在 \(block + \dfrac{N}{block}\) 时间复杂度内求得区间 \(mex\)。
标签:dfrac,询问,个数,笔记,普通,莫队,复杂度,block From: https://www.cnblogs.com/ckb2008/p/mo-note.html