莫队

概述

  • 莫队算法是一种离线算法,主要用于求序列上的区间式问题。

  • 简单来说,如果题目给出一个序列 \(a\)\(Q\) 个离线询问,询问的范围是一个区间 \([l,r]\),且由 \([l,r]\) 的答案可以在可接受的时间内推出 \([l-1,r],[l,r+1],[l+1,r],[l,r-1]\) 的答案,那么可以使用莫队算法。

  • 莫队的本质是优雅的暴力,莫队的缺陷是离线。

朴素莫队

  • 将整个序列分成 \(\sqrt{n}\) 块,对询问做 \((bel_l,r)\) 的双关键字排序,其中 \(bel_l\)\(l\) 所在的块。通常为第一关键字升序,第二关键字奇偶排序(即按 \(bel_l\) 的奇/偶来升/降序排序)。

  • 把每个询问挂在 \(bel_l\) 上。设置初始区间为 \(l=1,r=0\)(为了加上 \(1\) 且不减去 \(0\))。

  • 开始从左到右扫每个块,回答询问(认为 \(n,Q\) 同阶,不同阶请自行做复杂度平衡,后略):

    • 块内询问:右端点移动 \(O(n)\) 次,左端点每个询问移动 \(O(\sqrt{n})\) 次,总移动次数 \(O(n\sqrt{n}+Q\sqrt{n})\)

    • 块间转移:两个端点合计移动不超过 \(O(n)\),总移动次数 \(O(n\sqrt{n})\)

  • 故时间复杂度为 \(O(n\sqrt{n}k)\),其中 \(k\) 为邻项递推的复杂度。

SP3267 DQUERY

  • 题意:求区间不同的数的个数。

  • 数据范围:\(n\leqslant 3\times 10^4,Q\leqslant 2\times 10^5\)

  • 邻项递推方式为开桶(预先离散化或 u_map)计数。其余按上面来就好。

P4462 [CQOI2018] 异或序列

  • 题意:求区间异或和为 \(k\) 的子区间数。

  • 转化为前缀异或和,利用异或的逆运算性,问题变成 \([l-1,r]\) 中的前缀异或和之异或和为 \(k\) 的二元组数。

  • 利用异或的逆运算性,邻项递推方式为加/减 \(cnt_{xorsum_x\oplus k}\)。注意,加是后加,减是先减,否则会挂(\(k=0\) 教你调代码)。

  • 注意这里区间会有 \(l=0\) 的,此时可以把初始区间设为 \(l=0,r=-1\),让它一开始把 \(0\) 加进来(但不要把 \(-1\) 减出去)。

P1494 [国家集训队] 小 Z 的袜子

  • 题意略。

  • 转为统计方案数,邻项递推方式基本同上。

带修莫队

  • 朴素莫队是无法应对修改的。但我们可以魔改他:

  • 把当前的状态抽象为 \(l,r,t\),其中 \(t\) 为时间。

  • 分成块长为 \(B\)\(num\) 个块。尝试...

  • 我们发现不可能同时让 \(r,t\) 具有原来的 \(r\) 的优秀性质:莫队终究是依赖着“二元”的,多元肯定没法做。

  • 于是将询问的归属抽象为 \((bel_l,bel_r)\) 的二元组。出于卡常考虑,其内部跑双关键字的奇偶排序,\(t\) 基于二元组编号来奇偶排序。

  • 开始按顺序扫每个询问归属的二元组:

    • 二元组内询问:\(t\) 移动 \(O(Q)\) 次,\(l,r\) 每个询问移动 \(O(B)\) 次,总移动次数 \(O(Q\times num^2+Q\times B)\)

    • 二元组间转移:\(l,r,t\) 各移动不超过 \(O(n)\) 次。总移动次数 \(O(n\times num^2)\)

    • 于是总复杂度可以认为是 \(O(n\times num^2+nB)\),希望 \(num^2\approx B\),打表(或者靠直觉)得 \(num=n^\frac{1}{3},B=n^\frac{2}{3}\),总复杂度 \(O(n^\frac{5}{3})\)

  • 特别地,如果很需要卡常,在各个关键字的奇偶顺序完全正确的情况下可以将块长设为 \(\sqrt[3]{nt}\),其中 \(t\) 为修改次数,可以获得 \(O(\sqrt[3]{n^4t})\) 的优秀复杂度,在修改很少的时候表现特别优秀。

P1903 [国家集训队] 数颜色 / 维护队列

  • 题意:求区间不同的数的个数,带单点修改。

  • 数据范围:\(n\leqslant 1.3\times 10^5\)

  • 即 DQUERY 的带修版本,按上面来就好。

回滚莫队

  • 朴素莫队要求我们能够四向递推,然而有的问题中这是不可行的(复杂度无法接受)。

  • 回滚莫队对只能高效增大/减小区间长度的问题给出了优雅的解法。

  • 不妨以区间只能增大为例。仿照朴素莫队,将询问挂在块上,然后回答询问:

    • 块内询问:

      • 先将区间初始化为 \([b_r+1,b_r]\),然后开始回答询问。

      • 注意到左端点可能是乱序的,所以在每次移动左端点后,将左端点,状态和答案回滚到 \(b_r+1\) 处。

      • \(O(n\sqrt{n}+Q\sqrt{n})\)

    • 块间转移:不进行转移,直接 \(O(n)\) 暴力初始化区间。\(O(n\sqrt{n})\)

  • 我们看到,回滚莫队的复杂度仍然是 \(O(n\sqrt{n})\),但常数稍大。注意不要奇偶排序。

  • 大部分情况下回滚莫队无法和带修莫队结合,因为带修莫队的修改本质上是先删再加,需要扩张和收缩都支持。

  • 不过如果带修的修改很怪,譬如插入一个元素,则可以考虑用块状链表实现,\(O(\sqrt{n})\) 地判断当前区间是否含插入的元素,总复杂度仍然为 \(O(n\sqrt{n})\)

  • 注意到这里恐怕没法用 rope,但大概可以用 vector,然后开线段树维护坐标偏移?哈,这也太重工业了,不过也许比块链轻。

  • 如果被卡了就对 vector 套用块链的 split 思想,merge 思想看情况。不过这时就真的只是一个好写的 \(O(\sqrt{n})\) 的块链了(作为对比,纯 vector 在随机数据下是平衡树级别的)。

[JOISC2014] 历史研究

  • 洛谷上是 AT_joisc2014_c 但样例之类的都没有,建议去 LOJ #2874。

  • 题意:给出序列 \(a_n\)\(Q\) 个询问 \([l,r]\),对每个询问,输出 \(\max_i i\times \sum\limits_{j=l}^r[a_j=i]\)

  • 数据范围:\(n,Q\leqslant 10^5,a\leqslant 10^9\)

  • 观察到在区间减小使最大值减小时,不知道新的最大值是什么。

  • 一个思路是用堆来维护,增加邻项转移的复杂度,可以做 \(O(n\sqrt{n}\log n)\)

  • 但显然我们可以做只能扩张的回滚莫队。按题意用桶处理就好。

P4137 Rmq Problem / mex

  • 题意:求区间 \(mex\)

  • 发现还是开桶,如果删去某个数我们会,但如果原来的 \(mex\) 出现了则没办法。

  • 故可以做只能收缩的回滚莫队。

P5906 [模板] 回滚莫队 & 不删除莫队

  • 题意略。

  • 格外恶心的一道回滚莫队,主要是要记录希望回滚到的状态(它的修改完全无法撤销,作为对比,一般的回滚莫队可以撤销,只是撤销时无法计算贡献)。

P3722 [AH2017/HNOI2017] 影魔

  • 题意略。基于势能分析的一道回滚莫队,参看双指针与扫描线-扫描线。

树上莫队

  • 朴素莫队只能处理序列上的问题。自然,我们会考虑这能否扩展到树上路径。

  • 大体思路有两种:基于欧拉环游序的破树成链和基于树分块的真·树上莫队。

破树成链做法

  • 首先把整棵树跑成一个欧拉环游序的链,注意链上有些点(出点)的加/删操作是反的。

  • 然后对带拐弯的询问处理一下 lca,将其拆成两条祖孙链+lca 处特判,绑成一个...

  • 假了,都假了。先放这。

莫队二次离线

  • 暂时就当我没学过这个。我的理解很浅而且很歪...

  • 有时莫队的转移复杂度就是不可接受,回滚也没用。

  • 不妨记贡献函数 \(f(x,y)\)\((x,y)\) 对的贡献,推广地,\(f((x_1,x_2,\dots,x_{k_1}),(y_1,y_2.\dots,y_{k_2}))\) 表示 \(\sum\limits_{i=1}^{k_1} \sum\limits_{j=1}^{k_2} f(x_i,y_j)\)

  • 如果 \(f(x,y)\) 满足差分性,则我们可以尝试利用它加速莫队的转移。

  • 具体来讲,我们将莫队移动过程中的 \(O(n\sqrt{n})\) 个贡献离线,并使用扫描线之类方式求出,然后前缀和就得到了每个询问的答案。

  • 下面是一点关于“到底怎么扫的”的疑问...虽然我确实通过了莫二离的板题,但现在回望时我还是不理解我当初是怎么扫的。先扔在这里吧。
    //* 然而问题是,怎么扫描线?
    // * \(calc([l,r],r+1)\) 可差分,就代表着每对元素的贡献是相互独立的,于是 \(f\) 存在。

// * 那么,如果直接对 \(i\) 扫,譬如令 \(sum_i\) 表示 \(\sum\limits_{j=1}^L f(a_i,a_j)=f(a_i,(a_1,a_2,\dots,a_L))\),扫描线维护之,这里 \(L\) 为扫描线当前指向的位置。

// * 会发现当 \(L+1\)\(i=1\sim n\) 的变化量分别是 \(f(a_i,a_{L+1})\),是极不同的,根本没法扫描线(此部分存疑,等我做过题再说)。

  • 但这不够优,空间为 \(O(n\sqrt{n})\)。设法把它优化掉。

  • 设当前的区间为 \([l,r]\),目标区间为 \([l',r']\),规定操作顺序为 \(-l,+r,+l,-r\)

  • 不妨以 \(-l\) 为例,该次变换发出的所有需求应该形如 \(f(a_k,(a_{k+1},a_{k+2},\dots,a_r))(k\in[l',l))\),对应的差分为 \(f(a_k,(a_1,a_2,\dots a_r))-f(a_k,(a_1,a_2,\dots a_k))\),于是只要求 \(\sum\limits_{i=l'}^{l-1} f(a_i,(a_1,a_2,\dots a_r)))-f(a_i,(a_1,a_2,\dots a_i))\)

  • 考虑对两项分别求和。具体实现的话,目前看来,是随题目变化而变化的,大体分两种,在下面的前两道例题中谈及。

  • 综上,莫队二次离线(求贡献操作相对于莫队过程的离线)能够以 \(O(nk+n\sqrt{n})\) 的时间复杂度,其中 \(k\) 为端点移动代价,\(O(n)\) 的空间复杂度解决序列上的区间式问题,前提是其贡献可差分,且存在扫描线之类的方式能够快速求前缀问题的解。

P4887 【模板】莫队二次离线(第十四分块(前体))

  • 题意:给定序列 \(a_{1\sim n}\)\(Q\) 组询问,求 \(\sum\limits_{i=l}^r \sum\limits_{j=i+1}^r [ppc(a_i\oplus a_j)=K]\)

  • 数据范围:\(n,Q\leqslant 10^{5}\),$a<2^{14} $,空间限制 40MB。

  • 容易想到的一个做法是暴力求贡献,但这样一来 \(k\to n\),这里 \(k\) 为移动指针的代价。注意到 \(a<2^{14}\) 的限制,猜测其可能与复杂度有关,故考虑对值域维护答案。

  • 我们利用一下异或的运算律:若 \(x\oplus y=z \And ppc(z)=K\),则只要求合法的 \(y\) 的数量,于是只要求 \(\sum\limits_{ppc(z)=K} cnt_{x\oplus z}\)。换言之,\(f(x,(\dots,y))=f(x,(\dots))+([\text{y 对 x 合法}]=[x\in \{y\oplus z\mid ppc(z)=K\}])\) 。说人话就是,加入一个新的数 \(y\),只需要将 \(\forall ppc(z)=K,res_{y\oplus z}+1\) 即可。

  • 然而这样之后的指针移动代价还是 \(\binom{14}{K}\to 3432\),显然根本无法接受。注意到贡献是二元的,与其他项无关,故满足差分性,考虑莫二离。显然 \(O(n\sqrt{n}+n\binom{14}{K})\) 的复杂度是勉强能接受的(说实话第二项还是稍微有点大)。

  • 考虑如何实现,我们这里直接谈 \(O(n)\) 空间的实现。

  • 首先对四类指针移动暴力分类讨论,考察其需求,总结如下(记 \(L,R\) 为当前莫队区间的左右端点,\(l_q,r_q\) 为询问区间的左右端点):

    • \(L>l_q\)\(+\sum\limits_{i=l'}^{L-1} f(a_i,(a_1,a_2,\dots ,a_R))-\sum\limits_{i=l'}^{L-1} f(a_i,(a_1,a_2,\dots,a_i))\)

    • \(R<r_q\)\(+\sum\limits_{i=R+1}^{r_q} f(a_i,(a_1,a_2,\dots,a_{i-1}))-\sum\limits_{i=R+1}^{r_q} f(a_i,(a_1,a_2,\dots,a_{L-1}))\)

    • \(L<l_q\)\(-\sum\limits_{i=L}^{q_l-1} f(a_i,(a_1,a_2,\dots,a_R))+\sum\limits_{i=L}^{q_l-1} f(a_i,(a_1,a_2,\dots,a_i))\)

    • \(R>r_q\)\(-\sum\limits_{i=q_r+1}^R f(a_i,(a_1,a_2,\dots,a_{i-1}))+\sum\limits_{i=q_r+1}^R f(a_i,(a_1,a_2,\dots,a_{L-1}))\)

  • 啊,美妙的对称性和恶心的篇幅...观察发现,它们的需求主要分两种(记 \(L\) 为扫描线当前扫到哪里,对应操作的影响为 \(delta\)\(sum_x\)\(f(x,(a_1,a_2,\dots,a_L))\)(其实就是前文的 \(res\)),\(l,r\) 为需求的 \(i\) 的区间,\(wr\) 为需求的 \((a_1,a_2,\dots,a_r)\)\(r\) 的上界,\(k\) 为加减系数即 \(\pm 1\)):

    • \(L=wr\) 时,\(delta=delta+k\sum\limits_{i=l}^r sum_{a_i}\)

    • \(L=l\sim r\) 时,\(delta=delta+ksum_{a_L}\) 或在 \(L=l-1\sim r-1\) 时,\(delta=delta+ksum_{a_{L+1}}\)

  • 应当指出的是,如果不考虑 \(sum\) 可能的定义差异(我们在下一题就会看到,这取决于怎么扫方便),上面的推导对于莫二离是泛用的。

  • 之所以二类里面有一个或,是因为上面的第二和四种情况的前项是 \(f(a_i,(a_1,a_2,\dots,a_{i-1}))\)。两者的后项并不需要特别处理,因为只要将 \(wr\) 微调即可。

  • 到这里我们已经可以直接暴力构造八种,分三类,来构成一个操作序列。对于一类操作,将其扔到 \(wr\) 处的一个 vector 上,扫到时直接处理;对于二类操作,将其扔到 \(l\) 处的一个 vector 上,扫到时将其入队。

  • 然后每次更新 \(L\) 前遍历队列中所有操作,计算 \(sum_{a_L}\)\(sum_{a_{L+1}}\);若该操作尚未过期,即 \(r>L\)\(r-1>L\),则将其放入另一个滚动队列中,否则不放,显然也可以使用链表维护。

  • 显然其空间复杂度确实为 \(O(n)\)。我们再稍微尝试着细节优化一下:

    • 先看看我们的操作结构体里面有什么:\(ct,l,r,wr,k,delta\)。其中 \(ct\in \{0,1,2\},k\in \{-1,1\}\)\(wr\)\(ct\neq 0\) 的操作没用。

    • 唔,首先我们注意到相邻的两个操作的 \(l,r\) 是完全相同的,但这个优化不了,提出来开一个数组的话,结构体内就要存是那个数组的哪个,还是四个 int。

    • \(ct=0,ct=1\)\(ct=2\) 三者的区别利用 \(wr>=0,wr=-1\)\(wr=-2\) 来区分。还剩一个可以开 bool 的 \(k\),然而我们知道 bool 的空间浪费非常严重(八倍),而且结构体有空间自动对齐,大概率对齐成 \(5\) 个 int,所以把 \(k\) 提出来开 bitset。

    • \(delta\) 最后要求和,显然可以把 \(delta\) 提出来但是不优,还要加编号,不如直接开静态的操作数组然后把编号扔到 \(wr\) 或者 \(l\)\(l-1\) 上,并使用链式前向星代替 vector。

    • 最后显然这里我们队列维护编号的空间是链表的 \(\frac{2}{3}\) 而且更好写,但可能时间常数略大。另外注意这样构造出来的操作序列长度上界为 \(4Q\)

    • 如果还是被卡,那还有杀招:把 \(wr\) 提出来。事实上在把操作扔上去之后,\(>0\)\(wr\) 就没有意义了,于是可以考虑把 \(wr\) 提出来,此时它退化成 \(ct\),用两个 bitset 复合表示即可,但太魔怔了。

  • 总之,总算是做完了。时间 \(O(n\sqrt{n})\),空间 \(O(n)\)

P3246 [HNOI2016] 序列

  • 题意:给定序列 \(a_{1\sim n}\)\(Q\) 组询问,形如求子串 \(a_{l\sim r}\) 的所有子串的最小值之和。

  • 数据范围:\(n,Q\leqslant 10^5\)\(2s\)

  • 啊...这里显然 \(calc\) 是可差分的,有机会莫二离。考虑怎么扫描线,显然这才是关键,像上一道题一样的那种,离谱的值域上的扫描线,显然是不行的。

  • 嘛首先这里我们的 \(f\) 肯定是要改定义的,上一道题的 \(f\) 本质上是传入两个数,而我们这里的 \(f\) 应该是传一个子序列的,也无所谓值域这种东西。正确的定义应该是 \(f(l,r)=\min_{i=l}^r a_i\)\(f(L,(l\sim r))=\sum\limits_{R=l}^r f(L,R)\)

  • 定义 \(sum_i=f((1\sim L),i)\),即 \(l\in [1,L],r=i\) 的所有子串的最小值的和。考虑怎么扫这个 \(sum\)显然可以区间最值操作+区间历史版本和。太屎了!我们是前缀的,所以显然可以求出 \(pre_i\) 表示 \(i\) 前面比 \(i\) 小的第一个数的位置,于是区间最值操作转化为区间赋值操作。历史版本和是没有办法啦,还是得维护...除非不考虑空间使用暴力,然而这样复杂度变成了 \(O(n\sqrt{n}\log)\),并不美妙(当然也许能卡过去)。

  • 除了扫描线不同之外,和上一道题没有什么区别。时间 \(O(n\sqrt{n}+n\log n)\),空间 \(O(n)\)

  • p.s.这道题的正解在扫描线那里的影魔处提到了。并不需要这么恶心。反演。用脑子,而非暴力。

莫队配合 bitset

  • 现在不是很想写概述。

  • 简单来说就是有些问题我们需要对应区间的 bitset,此时莫队的唯一作用就是加入新点时将 bitset 上某点的值赋为 \(1\)(显然,我们需要只能扩张的回滚莫队)。

  • 为什么不线段树?因为 bitset 合并的复杂度很糟糕,\(O(\frac{n}{\omega}\log)\) 显然还不如 \(O(\sqrt{n})\),但这不是最关键的,最关键的是空间开不下。

  • 复杂度一般为 \(O(n(\sqrt{n}+\frac{n}{\omega}))\)。其实一般后者严格大于前者。

P3674 小清新人渣的本愿

  • 题意略。

  • 维护两个对称的 bitset,即第一个中 \(0\) 处表示是否有 \(0\),第二个中 \(0\) 处表示是否有 \(n\)

  • 显然直接对位与得到的是和为 \(n\)。那么我们把第二个右移 \(n-x\) 得到的就是和为 \(x\)

  • 减法的话,将第一个左移 \(x\) 再与自己就好了。

  • 乘法暴力之即可,当然可以分解因数做到单次 \(O(\frac{\sqrt{n}}{\ln \sqrt{n}}+d(n))\approx n^\frac{1}{3}\),但复杂度瓶颈不在这里。

  • 总复杂度 \(O(n(\sqrt{n}+\frac{n}{\omega}))\)

P5355 [Ynoi2017] 由乃的玉米田

  • 在上一题的基础上加了除法操作。鉴于这一操作并不是用 bitset 维护的,我把这道题丢到根号分治去了。
posted @ 2023-01-07 08:46  未欣  阅读(184)  评论(0编辑  收藏  举报