莫队初探
莫队说:
众所周知,"莫队算法"是用于一类离线区间询问问题的常用算法,以适用性广、代码量短、实际运行速度快、适合骗分 等优点著称,广泛在OI/ACM中被使用和研究。
莫队作为基于暴力的离线算法,降低时间复杂度的主要方法在于,对各询问作出合适的处理。简单说,通过对各讯问进行合理的排序,使得后面的询问可以充分利用之前询问所得到的信息,就可以神奇地将时间复杂度从 \(O(NM)\) 降至 \(O(N \sqrt{M})\) 。
这样看来的确适合骗分
普通莫队
来道例题:AcWing 2492. HH的项链
最简单的方法当然是来一个询问就对区间[L, R]进行扫描,期望时间复杂度 \(O(NM)\),不能再快了
怎么优化呢?我们可以很容易地发现,对于区间\([L, R]\),我们可以将它的状态转移至\([L + 1, R]\), \([L - 1, R]\), \([L, R + 1]\) 和 \([L, R - 1]\)。因此,我们可以通过双指针的方法,将区间\([L_i, R_i]\)转移至\([L_{i + 1}, R_{i + 1}]\)。我们也可以很容易地发现,时间复杂度还是\(O(NM)\)。并没什么用,但又有点用,因为这是莫队的基础。
再往后,我们可以发现,这种优化的瓶颈在于两个端点的移动次数。只要将右端点升序排序,就可以使右端点最多移动 N 次。这种方式给我们了启发,如果将相邻的左端点也排在一起,复杂度不就降下来了吗?
莫队维护左端点的方法是分块。令每个块的长度为 S,那么总共可以分为\(\frac{N}{S}\) 个块。我们将所有区间左端点 L 按所属的块排序,L 相同时再按右端点 R 递增排序。在这样的顺序下,再用上文说过的双指针的暴力做法。此时我们再分析复杂度可以发现:
- 对于左端点指针,在每个块内移动的次数为 S ,移动 M 次;块间移动的次数为 S ,一共移动 \(\frac{N}{S}\) 次。复杂度为 \(O(SM + N)\).
- 对于右端点指针,当左端点在同一块内时,移动 N 次,一共有 \(\frac{N}{S}\) 个块。复杂度为 \(O(\frac{N ^ 2}{S})\).
- 如此一来,整个算法的时间复杂度为 \(O(SM + N + \frac{N^2}{S})\) 。其中 N 移动小于 \(\frac{N ^ 2}{S}\) ,予以忽略。又由基本不等式 \(a + b \geq 2\sqrt{ab}\) 可知,\(SM + \frac{N ^ 2}{S} \geq 2\sqrt{MN ^ 2} = 2N\sqrt{M}\) ,当且仅当 \(SM = \frac{N ^ 2}{S}\) 时,即 \(S = \frac{N\sqrt{M}}{M}\) 时,有最小复杂度 \(O(N\sqrt{M})\) 。
这就是莫队的实现,没错,就像之前说的,莫队是基于暴力的离线算法。
Code
点击查看代码
#include<algorithm>
#include<cstdio>
#include<cmath>
using namespace std;
const int N = 50000 + 10;
const int M = 200000 + 10;
const int S = 1000000 + 10;
struct Query {
int id, l, r;
};
Query q[M];
int w[N], ans[M], cnt[S];
int n, m, len;
inline int read() {
int s = 0, f = 1;
char c = getchar();
while (c < '0' || c > '9') {
if (c == '-') f = -1;
c = getchar();
}
while (c >= '0' && c <= '9') {
s = (s << 3) + (s << 1) + (c ^ 48);
c = getchar();
}
return s * f;
}
inline int Get(int x) {
return x / len;
}
inline bool cmp(Query a, Query b) {
int i, j;
i = Get(a.l), j= Get(b.l); //取左端点所在区间
return i != j ? i < j : a.r < b.r; //先按块排,再右端点升序
}
inline void Add(int x, int &res) {
if (!cnt[x]) ++res;
++cnt[x];
}
inline void Del(int x, int &res) {
--cnt[x];
if (!cnt[x]) --res;
}
int main() {
n = read();
for (int i = 1; i <= n; ++i) w[i] = read();
m = read();
len = max(1, (int) sqrt((double) n * n / m));
for (int i = 1; i <= m; ++i) {
int l, r;
l = read(), r = read();
q[i] = (Query) {i, l, r};
}
sort(q + 1, q + m + 1, cmp); //排序
for (int k = 1, i = 0, j = 1, res = 0; k <= m; ++k) {
while (i < q[k].r) Add(w[++i], res);
while (i > q[k].r) Del(w[i--], res);
while (j < q[k].l) Del(w[j++], res);
while (j > q[k].l) Add(w[--j], res); //这四句及其精华,同时也比较容易写错,建议在纸上推一边理解
ans[q[k].id] = res;
}
for (int i = 1; i <= m; ++i) printf ("%d\n", ans[i]);
return 0;
}

浙公网安备 33010602011771号