一、开头
神犇MX:你是哪里来的弱菜?你是不是上次NOIP爆0的那个大弱鸡xyz32768?
xyz32768:我就是那个NOIP2017写炸两题的蒟蒻,的确是大弱鸡。
神犇MX:我还听说你连莫队都不会……你恐怕明年NOIP连400都没有了。
xyz32768:莫……莫队?
神犇MX:就是这样:一个
n
n
个数的序列,每次询问区间……
xyz32768:反正我也不懂,算了,我已经做好了NOIP2018爆0的准备了。
二、引入
一个问题:一个个数的序列,每次询问区间
[l,r]
[
l
,
r
]
内出现了多少个不同的数。
一个想法是线段树,但是也很容易看出,这里线段树上维护的内容没有合并性质,因此线段树不可做。而对于区间的内容没有合并性质的问题,可以用下面介绍的莫队算法解决。
三、莫队算法的条件
1、不包含修改操作。
2、题目允许离线,也就是允许在所有询问全部读入完之后回答所有询问。
3、不同区间的结果可以互相计算得出。怎么理解这个条件呢?
就上面的问题而言,如果上一次已经回答了
[l,r]
[
l
,
r
]
区间的答案,并且已经存下了
[l,r]
[
l
,
r
]
区间里的所有数值的出现次数,那么如果下面要询问
[l,r+1]
[
l
,
r
+
1
]
的结果,就只要把右指针
r
r
向右移一个单位,并将序列的第个数的出现次数
++
+
+
,同时维护当前的答案,也就是说,如果第
r+1
r
+
1
个数在
[l,r]
[
l
,
r
]
区间内没有出现,则当前答案
++
+
+
。同样,对于
[l,r−1],[l−1,r][l+1,r]
[
l
,
r
−
1
]
,
[
l
−
1
,
r
]
[
l
+
1
,
r
]
以及其他任何的区间都可以在上一个询问的基础上,通过
l
l
和移动指针来求得下一个询问的答案。如果满足这样的条件,就是说不同区间的结果可以互相计算得出。
四、莫队算法的流程
可以看出,一次移动指针是
O(1)
O
(
1
)
的。于是想到可以回答第
1
1
个询问之后,不断地移动指针,一个一个移动到后面将要回答的所有区间。
莫队算法的主要思想就是这样。同时,莫队算法利用了可以离线的条件,将询问按照合理的顺序进行求解,实现了的复杂度。
首先,将序列分块,即分成
n−−√
n
块,每个块的大小为
n−−√
n
。
然后,就将询问按照左端点所在的块为第一关键字,右端点的位置为第二关键字进行从小到大排序,这样,就能像上面那样不断移动指针,可以达到
O(nn−−√)
O
(
n
n
)
的复杂度。
五、复杂度证明
不妨把左端点在同一个块内的询问分成一组。
先考虑右端点的移动次数。由于在同一组询问内的右端点是递增的,所以在同一组内,右端点移动了
O(n)
O
(
n
)
次。同时在跨越两个组时,右端点的移动次数也是
O(n)
O
(
n
)
,即右端点一共移动了
O(nn−−√)
O
(
n
n
)
次。
再考虑左端点的移动次数。可以看出,在同一组询问内,左端点一次移动的次数为
O(n−−√)
O
(
n
)
次。
再加上在跨越两个组时,左端点的移动次数也是
O(n−−√)
O
(
n
)
,因此左端点一共移动了
O(nn−−√)
O
(
n
n
)
次。复杂度得证。
六、BZOJ 1878代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 5e4 + 5, M = 1e6 + 5, L = 2e5 + 5;
int n, a[N], ans[N], cnt[M];
struct cyx {
int l, r, bl, id;
inline bool operator < (cyx b) const
{return bl < b.bl || (bl == b.bl && r < b.r);}
} ask[L];
int main() {
int i, m, S; scanf("%d", &n); S = sqrt(n);
for (i = 1; i <= n; i++) scanf("%d", &a[i]);
scanf("%d", &m); for (i = 1; i <= m; i++) {
scanf("%d%d", &ask[i].l, &ask[i].r);
ask[i].id = i; ask[i].bl = (ask[i].l - 1) / S + 1;
}
sort(ask + 1, ask + m + 1);
int tot = 0, sl = 0, sr = 0;
for (i = 1; i <= m; i++) {
int l = ask[i].l, r = ask[i].r;
while (sl < l) if (!--cnt[a[sl++]]) tot--;
while (sl > l) if (!cnt[a[--sl]]++) tot++;
while (sr < r) if (!cnt[a[++sr]]++) tot++;
while (sr > r) if (!--cnt[a[sr--]]) tot--;
ans[ask[i].id] = tot;
}
for (i = 1; i <= m; i++) printf("%d\n", ans[i]);
return 0;
}
七、题目
1、[BZOJ1878][SDOI2009]HH的项链:
http://www.lydsy.com/JudgeOnline/problem.php?id=1878
2、[BZOJ2038][2009国家集训队]小z的袜子:
http://www.lydsy.com/JudgeOnline/problem.php?id=2038
3、[BZOJ3236][AHOI2013]作业:
http://www.lydsy.com/JudgeOnline/problem.php?id=3236
4、[BZOJ4540][HNOI2016]序列:
http://www.lydsy.com/JudgeOnline/problem.php?id=4540
5、[BZOJ4542][HNOI2016]大数:
http://www.lydsy.com/JudgeOnline/problem.php?id=4542