莫队算法——从入门到黑题

众所周知,莫队是由莫涛大神提出的,一种玄学毒瘤暴力骗分区间操作算法,它以简短的框架、简单易记的板子和优秀的复杂度闻名于世。然而由于莫队算法应用的毒瘤,很多可做的莫队模板题都有着较高的难度评级,令很多初学者望而却步。然而,如果你真正理解了莫队的算法原理,那么它用起来还是很简单的。当然某些套左套右的毒瘤除外


0、前置芝士:

莫队算法还是比较独立的。不过你还是得了解了解以下的一些知识:

\(1\)、分块的基本思想(开根号等)

\(2\)、STL中sort的用法(手写cmp函数或重载运算符实现结构体的多关键字排序)

\(3\)、基(du)础(liu)的卡常技巧(包含#pragma GCC optimize系列)

\(4*\)、倍增/树剖 求LCA(树上莫队所需)

\(5*\)、数值离散化(用于应付很多题目)

至此全部完毕。撒花~~(雾
诶,别走啊qwq,我可不是在劝退qwq,如果你认为自己不懂这些东西也没关系,往下看吧qwq

1、莫队算法是个啥

来历:

前面已经介绍过了(逃

有兴趣的同学可以看一下莫涛大神的知乎

然而这个算法到底是用来搞什么操作的呢?我们先看个例题:

Luogu P1972 [SDOI2009]HH的项链

题目描述

HH 有一串由各种漂亮的贝壳组成的项链。HH 相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含义。HH 不断地收集新的贝壳,因此,他的项链变得越来越长。有一天,他突然提出了一个问题:某一段贝壳中,包含了多少种不同的贝壳?这个问题很难回答……因为项链实在是太长了。于是,他只好求助睿智的你,来解决这个问题。

输入输出格式
输入格式:

第一行:一个整数N,表示项链的长度。

第二行:N 个整数,表示依次表示项链中贝壳的编号(编号为0 到1000000 之间的整数)。

第三行:一个整数M,表示HH 询问的个数。

接下来M 行:每行两个整数,L 和R(1 ≤ L ≤ R ≤ N),表示询问的区间。

输出格式:

M 行,每行一个整数,依次表示询问对应的答案。

输入输出样例
输入样例#1:
6
1 2 3 4 3 5
3
1 2
3 5
2 6
输出样例#1:
2
2
4
说明
数据范围:

对于100%的数据,N <= 500000,M <= 500000。


题意简明易懂:给你一个长度不大于\(n≤5×10^5\)的序列,其中数值都小于等于\(10^6\),有\(m≤5×10^5\)次询问,每次询问区间\([l,r]\)中数值个数(也就是去重后数字的个数)。

不过这个例题卡了莫队,所以请左转数据弱化版:SP3267 DQUERY - D-query

题目到手,我们开始分析本题的算法。这题最简单做法无非暴力——用一个\(cnt\)数组记录每个数值出现的次数,再暴力枚举\(l\)\(r\)统计次数,最后再扫一遍cnt数组,统计\(cnt\)不为零的数值个数,输出答案即可。设最大数值为\(s\),那么这样做的复杂度为\(O(m(n+s))∽O(n^2)\),对于本题实在跑不起。

我们可以尝试优化一下:

优化1:每次枚举到一个数值\(num\),增加出现次数时判断一下\(cnt_{num}\)是否为0,如果为0,则这个数值之前没有出现过,现在出现了,数值数当然要+1。反之在从区间中删除\(num\)后也判断一下\(cnt_{num}\)是否为0,如果为0数值总数-1。这样我们优化掉了一个\(O(ms)\),但还是跑不起。
优化2:我们弄两个指针 \(l\)\(r\) ,每次询问不直接枚举,而是移动 \(l\)\(r\) 指针到询问的区间,直到\([l,r]\)与询问区间重合。在统计答案时,我们也只在两个指针处加减\(cnt\),然后我们就可以用优化1中的方法快速地统计答案啦\(qwq\)

优化2具体步骤如下:

假设这个序列是这样子的:(其中\(Q1\)\(Q2\)是询问区间)

1539583-20181214111957440-289670786.jpg

我们初始化\(l=1\)\(r=0\)(如果\(l=0\),那么我们还需要删除一个数值\(0\),使其出现次数变成-1,导致一些奇奇怪怪错误),如下图(由于画图软件中\(l\)\(1\)看不出区别,我只好在图中使用\(L\)\(R\)来表示qwq):

1539583-20181214112312924-142905583.jpg

我们发现 \(l\) 已经是第一个查询区间的左端点,无需移动。现在我们将 \(r\) 右移一位,发现新数值1:

1539583-20181214132130120-1554450961.jpg

\(r\) 继续右移,发现新数值2:

1539583-20181214132211435-482551090.jpg

继续右移,发现新数值4:

1539583-20181214132245702-439750869.jpg

\(r\) 再次右移时,发现此时的新位置中的数值2出现过,数值总数不增:

1539583-20181214132410275-1972965271.jpg

接下来是两个7,由于7没出现过,所以总数+1:

1539583-20181214132450055-1521044095.jpg

继续右移发现3:

1539583-20181214132530925-1512284326.jpg

继续右移,但接下来的两个数值都出现过,总数不增。

1539583-20181214132620562-1439006141.jpg

至此,\(Q1\)区间所有数值统计完成,结果为5。

现在我们又看一下\(Q2\)区间的情况:

首先我们发现, \(l\) 指针在\(Q2\)区间左端点的左边,我们需要将它右移,同时删除原位置的统计信息。

\(l\)右移一位到位置2,删除位置1处的数值1。但由于操作后的区间中仍然有数值1存在,所以总数不减。

1539583-20181214134652168-9341337.jpg

接下来的两位也是如此,直接删掉即可,总数不减。

1539583-20181214134950911-245687742.jpg

\(l\) 指针继续右移时,发现一个问题:原位置上的数值是2,但是删除这个2后,此时的区间\([l,r]\)中再也没有2了(回顾之前的内容,这种情况就是删除后\(cnt_2 = 0\)),那么总数就要-1,因为有一个数值已经不在该区间内出现了,而本题需要统计的就是区间内的数值个数。此步骤如下图:

1539583-20181214141945700-1377126698.jpg

再右移一位,发现无需减总数,而且\(l\)已经移到了\(Q2\)区间的左端点,无需继续移下去(如下图)。当然 \(r\) 还是要移动的,只不过没图了,我相信大家应该知道做法的\(qwq\)

1539583-20181214142409184-977743178.jpg

\(r\)的最后位置:

1539583-20181214142435118-121917103.jpg

至于删除操作,也是一样的做法,只不过要先删除当前位置的数值,才能移动指针。

有了以上的内容,这段代码就可以很容易写出啦qwq:

int aa[maxn], cnt[maxn], l = 1, r = 0, now = 0; //每个位置的数值、每个数值的计数器、左指针、右指针、当前统计结果(总数)
void add(int pos) {//添加一个数
    if(!cnt[aa[pos]]) ++now;//在区间中新出现,总数要+1
    ++cnt[aa[pos]];
}
void del(int pos) {//删除一个数
    --cnt[aa[pos]];
    if(!cnt[aa[pos]]) --now;//在区间中不再出现,总数要-1
}
void work() {//优化2主过程
    for(int i = 1; i &
  • 10
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值