[学习笔记]省选算法·莫队

一、开头

神犇MX:你是哪里来的弱菜?你是不是上次NOIP爆0的那个大弱鸡xyz32768?
xyz32768:我就是那个NOIP2017写炸两题的蒟蒻,的确是大弱鸡。
神犇MX:我还听说你连莫队都不会……你恐怕明年NOIP连400都没有了。
xyz32768:莫……莫队?
神犇MX:就是这样:一个 n n 个数的序列,每次询问区间……
xyz32768:反正我也不懂,算了,我已经做好了NOIP2018爆0的准备了。

二、引入

一个问题:一个n个数的序列,每次询问区间 [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 r + 1 个数在 [l,r] [ l , r ] 区间内没有出现,则当前答案 ++ + + 。同样,对于 [l,r1],[l1,r][l+1,r] [ l , r − 1 ] , [ l − 1 , r ] [ l + 1 , r ] 以及其他任何的区间都可以在上一个询问的基础上,通过 l l r移动指针来求得下一个询问的答案。如果满足这样的条件,就是说不同区间的结果可以互相计算得出。

四、莫队算法的流程

可以看出,一次移动指针是 O(1) O ( 1 ) 的。于是想到可以回答第 1 1 个询问之后,不断地移动指针,一个一个移动到后面将要回答的所有区间。
莫队算法的主要思想就是这样。同时,莫队算法利用了可以离线的条件,将询问按照合理的顺序进行求解,实现了O(nn)的复杂度。
首先,将序列分块,即分成 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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值