莫队算法 初探总结

莫队算法分那么几类:

  • 普通序列
  • 带修改
  • 树上
  • 回滚
  • 支持在线(我不会)
  • 二次离线(我不会)

其实上述的类型还可以组合起来(非常的毒瘤)。

个人理解莫队算法的精髓在于如何利用暴力将答案再合理的时间和空间内跑出来。说白了:

[莫队算法=一种很牛逼的自定义排序+分块处理+暴力 ]

首先要理解自定义排序,这个排序之后整个序列可以最快地处理所有的询问(这里暂时不谈第五类问题(支持在线),这里认为莫队是只能离线处理问题的,必须先把所有的问题都离线下来)。怎么为之快,快要看左端点移动的总距离+右端点移动的总距离最小。那么一般用块的奇偶性来排序就够快了。如下:

bool cmp(const node &a,const node &b)
{
	return (p[a.l]^p[b.l])?(p[a.l]<p[b.l]):((p[a.l]&1)?(a.r<b.r):(a.r>b.r));
}

排完序后大致移动的次数就在可控的范围内了。

其次是分块,因为一般这种序列题的复杂度是(O(frac{n^2}{S}+mS))(n)是序列长度,(m)是询问次数,(S)是块大小)。
那么用均值不等式去分析这个复杂度就好了。一般(n),(m)同阶的时候取(sqrt n)最优,但是假如(n)(m)有别的情况的话具体分析,有时候还要决定用时间换空间来保证空间也过得去。

之后就是暴力,想办法怎么通过指针的移动(进入和离开所求区间([ql,qr]))的时候统计答案。
这个要看基本功了(很多时候就是数学功底)。
一般这个复杂度要尽量的低,最好是(O(1)),否则会影响整体的复杂度。

下面我们具体情况具体分析一下:

普通序列莫队

没什么好说的,直接套板子,有时候维护不了了就想想变化量是什么,其实只用知道变化量是什么其实都不太难了。

带修改序列莫队

好像只用在原有的基础上加上一维表示时间即可。但是块大小应该不能是(n^{0.5})了,这样会将整体复杂度退化成(O(n^2))。块大小可以取(n^{frac{2}{3}})或者是(n^{frac{3}{4}})

带修改的莫队,其实相当于普通的两个指针的莫队加了一个新的指针表示时间轴,一般时间轴的移动放在前两维之后。而且要注意一点,对于时间轴上的修改操作,我们一般只交换序列中的信息和修改数组里面的信息,因为这样可以方便下一次时间倒回来的时候重新修正信息。

再谈奇偶排序,可以用,将第三维奇偶排序也是可以的,但是按照个人经验,实测没有多大的提升,有时候反而更慢,所以建议不用。

回滚莫队

回滚莫队一般用于解决一类看似普通莫队,但是很方便加入信息对答案的维护,但是不方便维护删除信息对答案的维护。又或者是只方便删,但是不方便加的一类序列问题。这样的普通的莫队就无能为力了。
回滚莫队的思想,还是利用排序,令某一边的指针单调地变化,另外一个指针暴力移动。

分块后,具体地:(这里只讨论左右端点不同块的情况,同块的可以直接暴力解决,时间过得去的)

  • 对于只加不减的莫队,先按左端点所在的块排序,假如是同一块,则按右端点升序排序。否则按照左端点所在块升序排序。这样的好处是什么呢?这样我们遍历具有同一个块编号的左端点的询问时,先将莫队操作的右指针设为当前块的右端点,左指针设为右端点(+1),然后因为同一块中的右端点必定升序,所以右指针移动的总量不会超过(O(n))。而左指针我们每一个询问都复位到原来左指针的地方(那些更改自然都要复位)。然后暴力扫到当前询问的左端点处。这样总量不超过(O(n^{1.5})),因为最多(O(n))级别的询问个数,左指针到当前左端点肯定不会超过一个块的大小,也就是(sqrt n),所以算起来不会超过(O(n^{1.5}))

  • 对于只减不加的莫队,同理。不过是按同一块中右端点降序排列。然后处理每个块的询问时,先将右指针设成(n),然后从后面往回减(反正只减不加)。然后左指针设为当前块的左端点,然后每次暴力复位左端点即可。复杂度同上,实现基本同上。

不过要注意几个初学者理解起来常见的问题。

  1. 只加不减和只减不加是什么意思?是真的不能加(减)吗?不是的。对于有一些计数器等等的,还是需要具备求逆运算的能力,只不过不方便维护答案而已。又好比上述的暴力复位操作,都是需要逆运算将贡献减掉的,只不过不用在暴力复位的时候想着还原答案而已(因为你可以先存一下当前的答案,然后再做暴力的那一部分,做完的那个答案是用另外一个东西来存的,这样就不影响了,可以放心复位了)。
  2. 不可以用奇偶排序。因为这样就打破了回滚莫队的本质,为什么?很容易想的,仔细看上述分析就懂了。

树上莫队

一般的树上莫队,是可以先求出欧拉序,然后转化为序列上的问题去解决。还有一些像什么子树统计的莫队,那种我认为用树剖+线段树大力维护可能更方便些,复杂度也更优秀。
假如是树链型的树上莫队,先求出欧拉序,准备好LCA。然后对于询问,假如问的是(u,v),那我们钦定(u)(dfs)序比(v)小,那么假如lca是(u)的话,用(first_u)(first_v)来表示树链,否则用(second_u)(first_v)表示树链,但是要注意第二种情况下,lca是没有被统计到的,这里一定要加上特判。(此处的first和second表示第一次/第二次访问那个节点时的编号)。

谈一下注意事项:

  1. 要开(2n)的空间,因为(dfs)序存下的序列都是倍长了的。
  2. 个人经验,这里的加还是删就不是我们能够个人判断的东西了,因为一个序列中某个点经过两次,就代表这个点的子树其实已经被访问完了,对答案其实没有什么影响,所以不是加两次贡献,而是应该删掉之前的贡献。所以这里是应该维护一个(bool)数组,表示每个树上点的访问状态,每次异或,根据那个访问状态来决定操作到底是加还是删。对于那些带修改的也是同理,那个点还是用数组标记来看加/删,假如发现原来就没有的,那也不必改了,因为答案不会有变化,否则就调整那个变化(但是两种情况下都要swap原始值和修改值,前者只不过是不用add/del而已)。

各种组合

大家看情况拼起来就行了,还是靠大家动手实践。

原文地址:https://www.cnblogs.com/Ronald-MOK1426/p/12576502.html