普通莫队学习笔记

莫队算法主要用于可以离线的区间询问回答。

引子

考虑一个这样的问题:假设没有事先求前缀和,你知道了数组第 \(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

img

口诀:先加后减,先左后右,先小后大

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\)

posted @ 2022-11-27 20:27  CKB2008  阅读(17)  评论(0编辑  收藏  举报