根号算法
1. 分块
咕了,以后讲了再写。
2. 莫队
暴力。
2.1 算法介绍
莫队算法用于 离线 处理问题,但这不代表它不支持修改。
莫队的思想是,将一次询问进行延拓,进而得到下一次询问的答案。当然,这不只是延拓,还有向内收缩的操作。为了行文方便,我们在下文中只称之它为延拓。
直接暴力地延拓显然无法保证复杂度,因为每次延拓的最坏时间复杂度是 \(O(n)\) 的,总最坏时间复杂度就是 \(O(nm)\) 的,显然无法承受。
但是,我们可以通过分块来优化这个过程(设块长为 \(B\))。具体地,我们可以以 左端点所在块的编号 为第一关键字,右端点为第二关键字对询问排序。
对于左端点的移动,它每次只在单个块内或者相邻块间移动,那么移动左端点的时间复杂度是 \(O(mB)\)。而对于右端点,考虑左端点在一个块内的所有询问,你会发现它们的右端点是单调递增的。所以对于一个块,移动右端点的总时间复杂度是 \(O(n)\) 的,一共有 \(O(\frac{n}{B})\) 个块,所以移动右端点的时间复杂度为 \(O(\frac{n^2}{B})\)。这样,莫队的总时间复杂度为 \(O(mB + \frac{n^2}{B})\),当 \(B = \frac{n}{\sqrt{m}}\) 时,时间复杂度最优,为 \(O(n \sqrt{m})\)。
莫队采用了上述 根号平衡 思想,平衡了左端点和右端点的移动总距离。从本质上讲,这其实是平衡了块长与块的数量。
2.2 回滚莫队
咕。
2.3 带修莫队
与普通莫队类似,这类莫队加入了一维时间维度,进而解决了修改的问题。时间复杂度 \(O(n ^ {\frac{5}{3}})\)。
2.4 莫队二次离线
如果莫队的扩展操作并不好 \(O(1)\) 处理,那么我们可以让莫队 再次离线,即离线处理莫队的扩展操作。
设 \(f(i, [l, r])\) 表示 \(i\) 对区间 \([l, r]\) 的贡献。考虑右移 \(r\),现在我们需要计算 \(f(r + 1, [l, r])\)。考虑进行差分,那么我们只需要计算 \(f(r + 1, [1, r]) - f(r + 1, [1, l - 1])\)。\(f(r + 1, [1, r])\) 的计算是不难的,扫一遍序列即可。而对于计算 \(f(r + 1, [1, l - 1])\),扫一遍序列,边扫边统计答案,扫到 \(l - 1\) 时 \(O(1)\) 统计 \(r + 1\) 的贡献。由于离线下来的扩展操作数量是 \(O(n \sqrt{m})\) 个,所以时间复杂度是 \(O(n \sqrt{m})\)。
莫队二次离线可以与树状数组、值域分块、根号分治等算法或数据结构结合,需要具体题目具体分析。
2.5 一些技巧
2.5.1 奇偶排序
其实就是标号为奇数的块右端点升序排序,偶数的就降序,这样复杂度会更优,比原来的快 \(30\%\) 左右。
2.5.2 复杂度平衡
莫队的修改次数是 \(O(n \sqrt{m})\) 的,但查询次数是 \(O(m)\) 的。也就是说,我们可以使用 \(O(1)\) 修改,\(O(\sqrt{n})\) 查询的值域分块或 \(O(1)\) 修改,\(O(\frac{n}{\omega})\) 查询的 bitset 来维护。
2.6 例题
为了方便分析,例题中假设 \(n,m\) 与值域同阶。
P5047 [Ynoi2019 模拟赛] Yuno loves sqrt technology II
显然,从 \([l, r]\) 扩展到 \([l, r + 1]\) 时,位置 \(r + 1\) 的贡献是 \([l, r]\) 中大于 \(a_{r + 1}\) 的数的个数。我们可以用树状数组来维护,但这样总复杂度为 \(O(n \sqrt{n} \log{n})\),有待优化。
考虑莫队二次离线。贡献 \(f(r + 1, [1, r])\) 是容易预处理的,树状数组即可。而对于贡献 \(f(r + 1, [1, l - 1])\),我们可以对 值域 进行分块,维护块间后缀和和块内后缀和,这样我们可以做到 \(O(1)\) 统计答案,总时间复杂度降至 \(O(n \sqrt{n})\)
代码。
P5501 [LnOI2019] 来者不拒,去者不追
与上一题一样,我们考察位置 \(r + 1\) 的贡献。显然,所有大于 \(a_{r + 1}\) 的数排名都增加了 \(1\)。也就是说,位置 \(r + 1\) 产生了大于 \(a_{r + 1}\) 的数之和的贡献。当然,还需要加上 \(r + 1\) 本身的贡献,也就是它的排名乘上 \(a_{r + 1}\)。
在线计算显然是复杂的,考虑莫队二次离线。贡献 \(f(r + 1, [1, r])\) 依旧是容易预处理的,树状数组即可。而对于贡献 \(f(r + 1, [1, l - 1])\),我们也可以使用值域分块。但这时就不只是维护后缀和了,还需要维护前缀和。总体来说,维护方式与上一题是几乎一样的。时间复杂度也是 \(O(n \sqrt{n})\)。
代码。
P5398 [Ynoi2018] GOSICK
\(r + 1\) 的贡献在线维护依旧是困难的,于是考虑莫队二次离线。
对于贡献 \(f(r + 1, [1, r])\),预处理时枚举因数再结合桶即可,时间复杂度 \(O(n \sqrt{n})\)。
对于贡献 \(f(r + 1, [1, l - 1])\),如果是统计 \(a_{r + 1}\) 的倍数个数依旧是不难的,与预处理的做法一样,复杂度 \(O(n \sqrt{n})\)。但统计 \(a_{r + 1}\) 的因数比较困难,因为不能像预处理那样直接枚举因数,否则时间复杂度就来到了 \(O(n ^ 2)\)。下面,我们将重点讨论 \(a_{r + 1}\) 因数的统计方法。
实质上是要统计对于 \(i \in [1, l - 1]\),满足 \(a_i\) 是 \(a_{r + 1}\) 的 \(i\) 的个数。考虑根号分治,如果 \(a_i \geq \sqrt{n}\),直接枚举 \(a_i\) 的倍数即可,时间复杂度 \(O(n \sqrt{n})\)。如果 \(a_i < \sqrt{n}\),考虑枚举这样 \(a_i\),将满足 \(a_j\) 是它的倍数的 \(j\) 在一个标记数组中标记为 \(1\),否则为 \(0\)。你会发现,对于每个询问,我们离线下来的都是一个区间。这样,我们只需要对每个这样的区间求出标记数组的区间和即可。由于 \(a_i < \sqrt{n}\),且区间只有 \(O(n)\) 个,区间和可以通过标记数组的前缀和 \(O(1)\) 求出,总时间复杂度 \(O(n \sqrt{n})\)。
需要卡常,挺毒瘤的。
代码。

浙公网安备 33010602011771号