莫队
1. 普通莫队
用来解决区间询问的问题:如果已知 \([l,r]\) 的答案,可以快速求得 \([l-1,r],[l+1,r],[l,r-1],[l,r+1]\) 的答案,就可以用莫队处理
如这道例题
如果我们在线做,就会要 \(O(nm)\) 的时间
那我们考虑离线,将询问按 \(l\) 为第一关键字,\(r\) 为第二关键字排序
这在随机数据中可以跑得飞快,但对于一些构造数据就不行了,例如:\((1,1),(2,n),(3,2),(4,n)...\) 这样会将你卡回 \(O(n^2)\)
这时候有人就想到一个方法:利用分块来排序
我们按 \(l\) 将询问分为 \(sqrt(m)\) 组,按照组号为第一关键字,组内按 \(r\) 为第二关键字排序
这样我们保证复杂度是 \(O(n \sqrt{n})\)
其实还有一个卡常技巧:我们可以用奇偶块排序
,即当组号为偶数时,组内按 \(r\) 从小到大排;为奇数时,按 \(r\) 从大到小排,这样在挪动指针时可以充分利用
练习题:
2. 带修莫队
根据我们上面的分析,可以发现普通莫队是不支持修改的
但题目硬要修改怎么办
我们可以考虑将莫队的移动变成三维的:\((l,r,t)\),\([l,r]\) 与原来的意义一样(计算区间),\(t\) 表示执行了前 \(t\) 个修改
对于 \(t\) 的移动,我们可以分两类讨论:
-
若修改位置 \(x\) 不在 \([l,r]\) 内,则操作忽略
-
否则,相当于删除一个元素,再增加一个元素
我们再来研究该怎么排序(默认比较后是从小到大的):
-
首先,如果左端点所在的块不相同,则比较左端点
-
否则,再比较右端点所在的块:如果不相同,则比较右端点,反正最后比较 \(t\)
可以证明,当块的大小为 \(n/q^{\frac{1}{3}}\) 时是最优的(噢忘了提,普通莫队的最优的块的大小是 \(n/\sqrt{q}\),但由于 \(n,q\) 一般是同阶的,所以也会默认为 \(\sqrt{n}\)),此时复杂度为 \(O(nq^{\frac{2}{3}})\)
练习题:
3. 回滚莫队
这是用来处理一类加入容易删除难的问题,即 \([l,r]\) 能快速转移到 \([l-1,r],[l,r+1]\),但 \([l+1,r],[l,r-1]\) 较难
这些问题通常是答案要求取最大值或最小值,如果要删除,就要记录一堆信息
既然删除难,那我们就别删除嘛
于是就有人想出了回滚这操作,他的实现是这样的:
-
排序时,按照左端点所在的块为第一关键字从小到大排序,以右端点为第二关键字从小到大排序
-
我们考虑每次处理一个块内的询问:莫队区间的左端点 \(nl\) 为 \(R[T]+1\),右端点 \(nr\) 为 \(R[T]\)(其中 \(T\) 表示块,\(R[T]\) 表示块的右边界)
-
然后我们分类讨论:对于左右点在同一个块的询问,我们直接暴力求出答案,\(nr\) 不做移动
-
对于左右点不在同一个块的询问,我们先移动 \(nr\) 到询问的右点,然后再将 \(nl\) 移到询问的左点,求出答案后,注意,我们要将 \(nl\) 滚回 \(R[T]+1\) 处,但 \(nr\) 不用撤回(因为块内的所以询问的右点是递增的,不会有删除的操作)
-
回滚的时候将桶、临时答案都恢复 \(nl\) 移动前即可
可以证明,回滚莫队的时间复杂度依然是 \(O(n\sqrt{m})\) 的,但常数略大;注意每次处理完一个块后要将桶和临时答案都清空
回滚莫队也可以处理删除容易加入难的题目,只需要以下地方稍作改动:
-
排序时,依旧按照左端点所在的块为第一关键字从小到大排序,但以右端点为第二关键字从大到小排序
-
莫队区间初始化时,左端点 \(nl\) 为 \(L[T]\) (块的左边界),右端点 \(nr\) 为 \(n\)
-
回滚时 \(nl\) 是滚回 \(L[T]\) 的
分组最好从 \(1\) 开始,能省去不少麻烦RE
练习题:
4. 树上莫队
题意:求树链的颜色个数
由于莫队通常是处理区间问题,因此我们就要考虑将树拍成一个序列
我们考虑记录一个树的欧拉序,记 \(dfn[u]\) 为进入结点 \(u\) 时的序列位置, \(out[u]\) 为退出结点 \(u\) 时的序列位置
当询问一条树链 \((u,v)\) 时(假设 \(dfn[u]<dfn[v]\)),对应到序列上就是:区间 \([out[u],dfn[v]]\) 中只出现一次的点,加上 \(LCA(u,v)\)
真的是太妙了
而我们在做莫队的时候,我们就当第二次加入是删除,第二次删除是加入即可
练习题:
P4074 [WC2013] 糖果公园(树上带修莫队)
(如果出现 \(LCA(x,y)=x\) 的情况,询问的区间会出现 \(l>r\) 的情况,这其实并不影响答案的正确性;但如果实在要统一为 \(l\le r\),那么我们可以询问 \([dfn[x], dfn[y]]\),然后答案不要再计入 \(LCA\))
5. 二次离线莫队
当加入或删除中有一方较困难实现时,我们可以利用回滚莫队做到
\(O(n\sqrt n)\);但某些毒瘤的出题人 (比如lxl) 将每次修
改操作的复杂度升到 \(O(\log n)\) 甚至以上时,莫队似乎就不太
可行了
但道高一尺魔高一丈,于是就有人发明了一种新的科技:二次离线
莫队。它可以将原本 \(O(n\sqrt n\ k)\) 的时间降为 \(O(n\sqrt
n+nk)\)(\(k\) 为一次修改的复杂度)
我们先假设 \(x\) 对区间 \([l,r]\) 的贡献为 \(f(x,l,r)\)
二次离线莫队需要一个比较重要的条件:即 \(f(x,l,r)=f(x,1,r)- f(x,1,l-1)\),即可差分性
显然,每次左指针移动时会产生的修改为
\(f(nl,nl+1,nr)=f(nl,1,nr)-f(nl,1,nl)\),每次右指针移动时会
产生的修改为 \(f(nr,nl,nr-1)=f(nr,1,nr-1)-f(nr,1,nl-1)\)
显然\(f(x,1,x), f(x,1,x-1)\)我们可以用 \(O(n)\) 的时间预处理
出来,而且通常情况下自己对自己不会有贡献,因此有
\(f(x,1,x)=f(x,1,x-1)\)
现在考虑 \(f(nl,1,nr), f(nr,1,nl-1)\) 该如何求出
我们已经将问题转化为 \([1,pos]\) 这样的区间问题,那么我们就考虑用扫描线的方法
考虑将莫队的每次移动都记录下来,如 \(x,1,y\),我们就将 \(x\) 记录到 \(y\) 的桶中,当扫描线扫到 \(y\) 时,用 \(O(1)\) 的时间得出答案
但如果要记录 \(n\sqrt n\) 个询问的话空间复杂度有些吃不消,而我们观察到对于每个询问,由于它的移动是连续的,而且一个指针移动时,另一个指针是不动的(也就是存入的桶相同),那我们就可以存入指针移动的左右端点
而这里也有个卡常小技巧,就是预处理 \(f(x,1,x)\) 时我们可以表示成前缀和的形式,在计入答案时就直接算 \(pre[R]-pre[L-1]\)(\([L,R]\) 就是指针移动的区间)
最后答案需要继承排序后的上一个询问的答案,因为我们每次记录的是莫队指针移动后贡献发生的变化
对于本题,我们先预处理出二进制有 \(k\) 个 \(1\) 的所有数,利用 \(a\oplus b=c \Leftrightarrow a\oplus c=b\),处理出答案
这里注意一个点:莫队的指针要及时更新,否则会影响另一个指针存放的桶的位置! (我不会告诉你我因为这里调试了一个下午的)
练习题:
P5047 [Ynoi2019 模拟赛] Yuno loves sqrt technology II
(配合值域分块 \(O(\sqrt n)\) 插入,\(O(1)\) 查询)
6. bitset
优化莫队
这是一个经典的套路,难以概括,选择一道例题进行介绍
答案显然为 \((r_1-l_1+1)+(r_2-l_2+1)+(r_3-l_3+1)-3\times len\),其中 \(len\) 为三个区间的公共颜色数
如果用 bitset
记录颜色,我们只能求出三个区间公共颜色的种数
实际上,我们可以将序列离散化,使一个数的新值为:比它小的数的个数+1
这样,在莫队加入一个数 \(a\) 时,我们就将 bitset
中 \(id[a]+cnt[a]-1\) 的位置标记为 \(1\),\(id\) 就是离散化后的值, \(cnt\) 是指数值出现的次数
这样,我们在取三个区间的交集时,我们就可以直接通过数出 bitset
中 \(1\) 的个数,求得公共颜色的个数
注意到这道题:开一个大小为 \(10^5\times 10^5\) 的 bitset
显然还是比较吃力的,我们就考虑将询问分成三个部分,分开进行莫队
练习题:
精通了莫队,你就学会了卡常和水分