UOJ Logo return20071007的博客

博客

【理性愉悦】如何优雅地玩转线性-常数时间复杂度

2023-07-30 22:53:04 By return20071007

本文收录部分 $\Theta(n)$ 预处理 $\Theta(1)$($\Theta(n)-\Theta(1)$)操作的强制在线算法(这个说法准确吗……反正大概就这意思啦)。

对于同一个问题可能不会收录所有算法,点名 RMQ。

基本前置:Word-RAM Model($w=\Omega(\log n)$)。


1.RMQ

世界名画。

首先我们很容易导出一个 $\Theta(n\log n)-\Theta(1)$ 的算法,用 ST 表还是二区间合并看心情。我们把原序列按 $\Theta(\log n)$ 分块,块间运行这个算法。

询问如果跨块则可以用一个后缀+块间+一个前缀拼起来,其中前后缀显然也可以 $\Theta(n)$ 预处理出来。当然如果不在意常数的话,前后缀的部分也可以一并用块内的算法运行,无妨(事实上这样比较方便在板子题测正确性)。

那么我们现在仅考虑一个块内的询问。我们考虑 RMQ 的一种 $\Theta(n)-\Theta(\log n)$ 离线算法:把询问 $[l,r]$ 按右端点排序,扫描线维护序列 $[1,r]$ 下标上的单调栈。我们发现单调栈的一个位置 $i$ 实际上代表着 $[i,r]$ 的后缀最小值在 $i$,那么我们在单调栈上二分找到第一个 $\ge l$ 的位置就可以了。我们试图把这个算法套上块。由于序列长度仅为 $O(\log n)$,我们实际上可以把每个位置的前缀单调栈压在一个二进制数里,进而把每个位置的前缀单调栈都存下来。这一步让我们不再需要离线。至于找第一个 $\ge l$ 的位置更是可以用一步位运算直接化归到找一个二进制数最低位 1 的位置,而这个是可以 $\Theta(1)$ 做的。这一步让我们撇去了复杂度里的 $\log$。

综上所述,我们现在的 RMQ 算法成功被优化成了 $\Theta(n)-\Theta(1)$。


2.LCA

常数比较好的写法是 dfs 序求 LCA,序列长度上比欧拉序求 LCA 短一倍。在此介绍一下。

考虑我们现在求 $u$ 和 $v$ 的 LCA,不妨有 $dfn_u\le dfn_v$,这里 $dfn$ 指节点的 dfs 序。

如果 $u=v$ 那么答案就是 $u$,直接判掉;否则,我们考虑 $[dfn_u+1,dfn_v]$ 这一段 dfn 上对应深度最小的节点。如果 $u$ 是 $v$ 的祖先,那么这个点一定是 $u$ 的儿子;否则,考虑从 $u$ 到 $v$ 的 dfs 过程中一定不会再经过 LCA(其在 $u$ 前已被访问),也不会跨越到 LCA 的子树之外。考虑到 $u$ 与 $v$ 在 LCA 的不同子树中,这部分一定会访问到 $v$ 对应子树的根(否则无法跨越到该子树内),那么这个根就是我们求出深度最小的节点。我们发现,这个节点一定是 LCA 的儿子,直接取它的父亲作为答案即可。

求区间最小深度这部分使用 RMQ 的优化,我们把 LCA 也成功优化成了 $\Theta(n)-\Theta(1)$。


3.LA

其实就是给定一棵树求给定点的给定级祖先。根据长链剖分那一套理论我们有一个简单的 $O(n\log n)-\Theta(1)$ 算法。我们简单优化一下这个东西并把它改成 $O(p\log n)-\Theta(1)$ 的,其中 $p$ 是树的叶子数。考虑到我们唯一更改的其实是只需要维护每个叶子的二的幂级祖先,这个我们在 dfs 的过程中维护一个祖先栈并在访问到叶子时直接根据下标找祖先即可。

这里我们介绍一个有趣的东西:ART Decomposition。简单来说,这个算法对树进行分解。考虑每个极高(自身满足且父亲不满足或没有父亲)点满足其子树有 $O(B)$ 个节点,那么很显然所有这些被取出的子树两两无交。把所有这些子树从原树上删去,那么新树的叶子必然满足原来子树内有 $\Omega(B)$ 个点,那么新树有 $O(\frac nB)$ 个叶子。我们取 $B=\Theta(\log n)$,那么新树可以直接跑那个长剖算法,我们把问题规模实际上降低到了 $O(\log n)$。

(注意这里 ART 玩起来很舒适是因为我们的复杂度瓶颈给到了叶子数量上。如果不是这样的话,我们还需要考虑把只有一个儿子的节点和儿子压起来,并用一个链上数据结构处理压起来的链。一般而言为了复杂度考虑这部分还得再分块,而且有时需要不同的维护方法,主打的就是一个阴间。)

我们套娃一层,现在问题的规模变成了 $O(\log\log n)$。那么我们对于每个点存下它的所有祖先,这需要的二进制位数是 $O(\log\log n\log\log\log n)$ ,也即 $O(w)$ 的。接下来根据一些简单的位运算操作我们就可以在常数时间内求出 k 级祖先了。

upd on 2024.10.16:上面那个写的时候脑子不好,实际上括号序的种类数十分有限,即使带上 LA 的两个参数实际情况数也很少,可以直接预处理,不用叠层了。

我们把 LA 也成功优化成了 $\Theta(n)-\Theta(1)$。


4.UFDS(上树版)

我们现在解决这样一个问题:给定一棵树支持两种操作:连接一个点与父亲的边,或者查询两个点是否联通。

老规矩,我们先试图导出一个 $O(n\log n)-\Theta(1)$ 的算法。考虑对每个点维护其联通块编号,合并时启发式合并即可。简便起见,我们的启发式合并可以直接用度数之和,这样在树上就可以直接暴搜求 size 了。

我们考虑把树按 $\Theta(\log n)$ 分块。块间显然可以运行这个算法。至于块内,我们考虑把一个点与父亲的边状态用一个布尔值标记作为这个点的点权。我们考虑用一个联通块内最高的点作为这个联通块的根,那么一个点所在联通块的根就是最低的权为 1 的点。我们考虑把每个点按照拓扑序(具体的爱啥序啥序)排列并顺次对应给一个二进制位,这一位的值表示这个点的权值;然后我们对于每个点维护一个二进制数压缩存储块内的每一个数(拓扑序位置意义下)是否为其祖先。查询的时候把两个部分按位与然后取最高位(拓扑序最大)即可。

我们还涉及到另外一个问题:查 $u$ 和 $v$ 所在块的 LCA,以及其块与两段路径对应的临界点。我们考虑 LCA 那个算法,求出最小值所在位置附带本身 dfs 序作为第二关键字,那么求出最大 dfs 序的最小值就是 $v$ 所在块一边路径与 LCA 的临界点。至于 $u$ 的临界点,可以直接暴力反过来搞一遍,也可以求出最小 dfs 序的最小值再维护一下其左边的兄弟,没有所谓。我们把 UFDS 也成功优化成了 $\Theta(n)-\Theta(1)$。


5.SDFU(?)

标题瞎起的,本质上就是想说把 UFDS 的问题反过来:给定一棵树支持两种操作:断开一个点与父亲的边,或者查询两个点是否联通。别忘了这篇文章的大前提是强制在线。

我们发现块内并没有什么区别,我们接下来只考虑怎么导出一个 $O(n\log n)-\Theta(1)$ 的算法。我们注意到上面那个启发式合并的算法,如果我们每次能精准找到断开两部分中 size 较小的一个并重新赋联通块编号,那么我们的问题就解决了。我们考虑修改一下 dfs 的步骤:维护一个栈,每个元素有三个部分:当前点,当前点的前驱和已经搜到了这个点的哪一条出边;每一次,弹出栈顶元素,把对应出边的点(若合法)入栈,然后修改原栈顶的已搜到出边。如此一来,我们考虑直接对断开边 $(u,v)$ 的 $u$ 和 $v$ 进行一个平行的 dfs,哪边先到底哪边就 size 较小。当我们找到较小的 size 时另一边也至多搜了这个规模次,于是我们的复杂度就正确了。继续树分块,我们把 SDFU(?)也成功优化成了 $\Theta(n)-\Theta(1)$。


6.区间绝对众数

标题终于不是令人迷惑的字母缩写力(喜)。

注意这个做法依赖哈希,所以不同于其他命题,这个东西的复杂度会看起来有点蠢。不过哈希一般是可以防卡的,所以先这样吧。

我们把问题分为两部分:求值和检验。

对于求值,老规矩 $\Theta(\log n)$分块,由于摩投具有结合律所以块间可以直接猫。块内递归一层把问题规模降到 $O(\log\log n)$,注意到这时候离散化后的本质不同序列和查询只有 $O((\log\log n)^{\log\log n}\times(\log\log n)^2)=O(2^{\log\log\log n\times\log\log n}(\log\log n)^2)$ 也即 $O(n)$ 种。直接摩投预处理即可。

对于检验,就是给定 $[l,r,x]$ 判断 $x$ 是否为 $[l,r]$ 的绝对众数。我们有结论,对于给定的 $x$,其作为绝对众数出现的所有区间并集大小和是 $O(c)$ 的,其中 $c$ 为 $x$ 的出现次数。具体证明可以考虑把折线图画出来然后对于每个峰把贡献累进去然后缩掉。我们考虑如何求出这个并集。我们考虑枚举“一段并集中的极长连续段左起第一个 $x$”出现的位置 $k$,可以证明其一定是 $2i-p_i$($p_i$ 表示 $x$ 第 $i$ 次出现的位置)某个前缀最小值所在处(的 $p$)。我们注意到这个连续段的左端点一定存在一个可以实现绝对众数的区间满足其右起第一个 $x$ 出现的位置是 $k$ 开始第一个后缀最大值,而右端点对应区间右起第一个 $x$ 出现的位置是能承受的极远后缀最大值,这两部分都可以双指针维护,最后把所有区间丢一起取并(左端点天然单调)然后用一个游标维护所有并集中的极长连续段(及其左端点前一个位置)的 $x$ 出现次数的前缀和,用哈希表维护。我们意识到,如果前缀和表中查不到 $l-1$ 或者 $r$ 那么一定寄了,否则我们两个前缀和一减就可以判了。

如此一来,我们把区间绝对众数也优化成了 $\Theta(n)-\Theta(1)$,虽然是期望意义下的。


7.RMSQ

人话:区间最大子段和。

我们不妨原序列是 $A$,下标从一开始。我们首先计算其前缀和数组 $C$,规定 $C_0=0$。我们考虑对于 $C$ 的每一个位置 $i$ 求出其之前最后一个 $C$ 值不小于自己的位置 $L_i$,若不存在则记为 0。我们于是有了第一条性质:$\forall i\in[1,n],j\in(L_i,i)$ 有 $C_j\lt C_i$。我们注意到,对于子段 $[j,i]$,如果 $j\le L_i$,那么换成 $[j,L_i]$ 一定不劣,于是我们不需要考虑这样的子段对答案的可能贡献。

我们再对于每个 $i$ 预处理 $[L_i,i)$ 上 $C$ 的最小值位置加一(如果有多个,取最靠右的那一个),记作 $P_i$。我们于是有了第二条性质:$\forall i\in[1,n],j\in[P_i,i)$ 有 $C_j\gt C_{P_i-1}$。综合这两条性质我们可以推出:$\forall i\in[1,n],j\in[P_i,i)$ 有 $C_j\in(C_{P_i-1},C_i)$。

(当然 $P$ 的定义本身也带给我们一个比较显然的性质:$\forall i\in[1,n],j\in[L_i,i)$ 有 $C_j\ge C_{P_i-1}$。这个不用太过在意。)

我们再记 $M_i=C_i-C_{P_i-1}$,换而言之就是 $[P_i,i]$ 的区间和。我们注意到,对于以 $i$ 为结尾的有效子段 $[j,i]$,由于 $j\in(L_i,i]$,故其和 $C_i-C_{j-1}\le C_i-C_{P_i-1}$。也就是说,如果只是要求全局的最大子段和,我们把所有 $M$ 取 $\max$ 已经做完了。

考虑对 $[l,r]$ 区间查询的解决方案。我们考虑暂且无视 $P$ 的限制直接硬做,求出 $M_l$ 到 $M_r$ 的最大值所在位置 $i$。如果 $P_i\ge l$,那我们显然已经做完了,返回即可。否则我们即有 $P_i\lt l$。由于 $P_i\gt L_i$,我们得到 $L_i\lt l$。根据我们前面的性质,我们推得 $\forall j\in[l,i)$ 有 $C_j\lt C_i$,也就是说以 $j$ 为结尾的最大子段和一定不如改成以 $i$ 结尾,直接不考虑。对于以 $i$ 为结尾的最大子段,其左端点减一的 $C$ 值要取到最小,这是一个 RMQ 的形式,直接做掉即可。对于以 $j\in(i,r]$ 的最大子段,我们考虑 $P_j$ 的位置:

若 $P_j\le P_i$:$P_i\in[L_j,j)$,推出 $C_{P_i-1}\ge C_{P_j-1}$;$i\in(L_j,j)$,推出 $C_i\lt C_j$。进而 $M_j=C_j-C_{P_j-1}\gt C_i-C_{P_i-1}=M_i$,与 $M_i$ 的最大性矛盾。

若 $P_i\lt P_j\le i$:$i\in[P_j,j)$,推出 $C_i\gt C_{P_j-1}$;但 $P_j-1\in[P_i,i)$,推出 $C_{P_j-1}\lt C_i$,矛盾。

若 $i\lt P_j\le j$:$P_j\ge l$,这是一个合法的区间。

综上所述,对于 $j\in(i,r]$,一定有 $P_j\ge l$ 合法,直接在 $M$ 上查 RMQ 即可。

于是我们就在三次 RMQ 内解决了区间最大子段和问题。由于 RMQ 已经是 $\Theta(n)-\Theta(1)$ 的了,我们把区间最大子段和也优化成了 $\Theta(n)-\Theta(1)$。


总结一下,序列上的问题化归到最后本质上基本都是 $\log$ 分块然后块间二进制瞎搞(个别的递归一层)。树上的问题除了 LCA 拍上链以外别的基本都是用的树分块或者 ART(实际上 UFDS 和反问题也可以这么搞,就是有点阴间)。

更多的问题能不能用类似的套路搞,比如区间 or?或者进一步区间 nand?限于笔者水平,在此不得而知,若读者有所了解,还请不吝赐教;如若文章存在纰漏,也请劳烦指出。

有个暴论,一个正常的 01 序列静态区间查询全都可以这么搞,因为 $\log$ 分块后块内总状态数有限,如果不行就再递归一层。

至此,且容我暂且搁笔。

评论

wl_zhengbeizhi
good
pp_orange
好文,解决了很多疑惑,主打一个阴间()
pp_orange
LA那一段,菊花图上这样分块是不是会退化啊,还是我没理解,或者说的其他阴间东西缝合()
tl_xujiayi
求加快速幂 $O(\log n)-O(1)$ 技巧/kk
142857cs
"有个暴论,一个正常的 01 序列静态区间查询全都可以这么搞,因为 $\log$ 分块后块内总状态数有限,如果不行就再递归一层。" 感觉难点在于如何快速求出是哪个状态吧
LFCode
写得太好了!!有许多值得借鉴的思想,超级有趣!
EntropyIncreaser
我们可以大致将这几个序列上的做法分离成两个模块化的问题. 第一部分是序列半群本身的分治结构, 我们知道对固定的常数 $k$, 可以在 $O(n \gamma_k(n))$ 时间预处理 - $k$ 时间询问完成. 其中 $\gamma_k$ 是一种类 Ackermann 型增长函数的反函数, 有 $\gamma_2 = O(\log n), \gamma_3 = O(\log\log n), \gamma_4 = O(\log^* n), \gamma_k = O(\gamma_{k-2}^* (n))$ 等等. (其中 $f^*$ 表示 $f$ 要花几次从 $n$ 迭代到 $O(1)$.) 第二部分是将序列分成 $\gamma_k(n)$ 大小的块, 这样垮块的询问都可以 $O(n)$ 预处理 $O(1)$ 询问了. 所以问题转换成对于足够小的块大小 $L$, 让它们有足够少的等价类, 并且能够 $O(L)$ 时间定位到这个等价类. (接下文)
EntropyIncreaser
抽象地来说, 上面的论述其实是说明了一个序列的 RMQ 问题和最大子段和问题的 "决策树深度" 是 $O(n)$ 的. 理想情况下, 我们有一个巨大无比的机器 $\mathcal T$, 它处理询问的方式就是每次给出一组询问来收集序列的某种性质. 然后只做了 $O(n)$ 次询问之后, 它就宣告自己胜利了, 可以 $O(1)$ 回答任何一个询问. 可以看出这是一个比 RAM 更强的机器, 但我们需要支付额外的代价在 RAM 中真正实现它, 这个代价就是打表. 如果 $\gamma_k(n)$ 大小的序列的决策树 $|\mathcal T|$ 的整个状态转移过程可以接受, 那么就可以通过在小块上实现这个决策机来完成整个问题的 $O(n)$ - $O(1)$ 解答. 这个思想在困难的 3SUM 问题和 $(\min, +)$ 矩阵乘法上都有过应用. (接下文)
EntropyIncreaser
对于 $(\min, +)$ 矩阵乘法, 如果我们可以在比较模型下做到 $O(n^{3-\delta})$ 次决策树深度, 就可以把矩阵切成 $(\log n)^{1/(3-\delta)}$ 大小的块, 在 $n^{3} / (\log n)^{\delta/(3-\delta)}$ 时间内计算 $(\min, +)$ 矩阵乘法了. 事实上确实是有 $\tilde O(n^2)$ 决策树深度的. 不过 $(\min, +)$ 矩阵乘法用别的技术可以除以 $\exp \Omega\left(\sqrt{\log n}\right)$, 这又是后话了. 对于 3SUM 问题, 如果我们可以在比较模型下做到 $O(n^{2-\delta})$ 次决策树深度, 就可以把序列 $A, B$ 切成 $s=(\log n)^{1/(2-\delta)-o(1)}$ 大小的块, 然后变成 $O(n^2/s^2)$ 个 $s$ 级别的问题, 在 $n^2 / (\log n)^{\delta / (2-\delta) -o(1)}$ 的时间内解决 3SUM. (接下文)
EntropyIncreaser
但上面这个做法撑死了也只能除一个 $\log$, 通过适当修改划分问题的方式, 是存在进一步优化的前景的, 但这个时候就像 142857cs 所说, "定位一个状态" 的时间会成为新的障碍, Chan 通过一些计算几何角度的技术克服了一些障碍, 得到了除以 $(\log n)^{2-o(1)}$ 的复杂度. 总而言之, "打表" 也是有大学问的! (乐)
hjqhs
/jy
carp_oier
好文!顶!!!

发表评论

可以用@mike来提到mike这个用户,mike会被高亮显示。如果你真的想打“@”这个字符,请用“@@”。