易懂科技:将LCA、RMQ、LA优化至理论最优复杂度

约瑟夫用脑玩

2021-02-23 17:03:45

Algo. & Theory

前言

LCA、RMQ、LA(树上 K 级祖先)是三类经典问题,但这三类的的经典/常用解法并非理论最优复杂度。

然鹅有一种简单的思想,即 \texttt{Method of Four Russians} ,可以同时将这三种算法优化至理论最优复杂度。

下文作者将对三种问题进行简单介绍,并详细解释此思想、以及如何用于解决这三类问题,并提供了笔者的代码实现,还附赠对于此算法解决 LCA 的小优化,并增补对 RMQ 的优化和对 LCA 的再优化。

问题介绍

由于都是经典问题及解法,默认以下问题及算法读者有所掌握,不会的话左转模板区看看。

  1. LCA(Lowest Common Ancestor),最近公共祖先,求树上两点的祖先中深度最大的一个,是一类广为人知经典问题。

    在这里用 O(A)-O(B) 分别表示预处理复杂度和询问复杂度。

    常见算法有 O(n\log{n})-O(\log{n}) 的倍增算法、O(n)-O(\log{n}) 的树剖算法、O(n)-O(\sqrt{n}) 的长链剖分算法、O(n\log{n})-O(1)的欧拉环游序+普通 RMQ 算法等。

    由于一棵树为 O(n),理论最优复杂度应为 O(n)-O(1)

  2. RMQ(Range Maximum/Minimum Query),区间最值问题,求序列一个区间中的最大/最小,也是一类广为人知经典问题。

    常见算法有 O(n\log{n})-O(1) 的 ST 算法、O(n)-O(\log{n}) 的线段树/树状数组算法、O(n)-O(\sqrt{n}) 的暴力根号分块算法等。

    由于一个序列为 O(n),理论最优复杂度同样应为 O(n)-O(1)

  3. LA(Level Ancestor),树上 K 级祖先,求树上某点的第 K 级祖先,也算容易实现的经典问题了。

    常见算法有 O(n\log{n})-O(\log{n}) 的倍增算法、O(n)-O(\log{n})的树剖算法(当然询问可以做到比较奇怪的复杂度,如 O(\log{\log{n}}/\sqrt{\log{n}}) 等)、O(n\log{n})-O(1) 的长剖算法等。

    一棵树为 O(n),显然理论最优复杂度仍应为 O(n)-O(1)

    部分常见算法由于与此算法无关,故下文不作介绍。

思想引入

而其核心在于对块间和块内 **分别** 解决问题以达到更优的复杂度。 我们先来玩一下分块:(如果你对分块有足够的了解, **请跳至下一个标题** ) 尝试直接用分块思想优化 RMQ。(你也可以大概看看思想,反正都~~是乱搞~~会优化失败的) 从 RMQ 中的暴力分块算法看起,我们将序列分为 $\sqrt{n}$ 块,预处理每块的最值,询问时遍历边角块与整块取最值。 对于 RMQ 问题,我们发现次算法复杂度相较并不优秀,但我们通过思考可以发现遍历取最值可以预处理优化,即根号分块套暴力或 ST 算法。 记 $T=(\sqrt{n}\log{\sqrt{n}}/(\sqrt{n})^2)$,复杂度为 $O(T+T\sqrt{n})-O(1)$。 具体分析:块间整体一次可以选择暴力处理或套用 ST 算法,复杂度即 $T$,同时有 $\sqrt{n}$ 个小块,每个块内一次也有同样的选择,询问均为 $O(1)$。 我们发现这个花里胡哨的复杂度仍为 $O(T\sqrt{n})=O(n\log{n}/n\sqrt{n})$,但我们显然可以感觉出来是块的大小不平衡导致的,于是我们尝试调整块的大小,然后将块的大小调为 $n/1$ 的块后发现调回了 ST 算法/暴力。。。 ## Method of Four Russians 虽然对于 RMQ 问题这样优化是没有出路的,但我们已经对分块优化算法的这种方法有所了解。 在这里分块失败的原因一个是由于问题本身不能直接用分块最优化,即没有**合适**的算法来套用。还有个原因是因为块间和块内的算法**相同**,最后就会导致分成了一个块。 于是 $\texttt{Method of Four Russians}$ 就出来给我们提供了更完美的分块思路: 将集合大小为 $n$ 问题分为大小为 $B$ 的 $\frac{n}{B}$ 个块,$B$ 通常取 $c\log{n}$,$c$ 为一个常数,且通常不超过 1。 然后对于所有大小为 $B$ 的小块(理性理解一下这个块其实特别小)整体套用一个 $O(B\log{B})$ 的算法,对于小块内部套用 $O(x^{c\log{n}} ⋅ y)$ 的算法。 这里块间的算法通常使用原算法,而块内算法为暴力算法,同时使得所有 $\frac{n}{B}$ 个块都属于已处理的集合 $x^{c\log{n}}$。这里 $x$ 为一个常数,前面 $y$ 为某个算法的复杂度。 ## 对 LCA 的优化 对于 LCA 问题,我们已有较优秀的算法 欧拉环游序+ST 来求 LCA。(算是经典操作了吧,网上随便都有) 我们先求出这棵树的欧拉环游序,考虑在此基础用 $\texttt{Method of Four Russians}$ 进行优化。 一个完整的欧拉环游序有个优(qi)秀(guai)的性质:相邻两点的深度差为 1。事实上用的并不多,有的人为了追求更优的复杂度可以将欧拉环游序缩减为一半,但却破坏了此性质。 我们将这个序列(长度设为 $n$)分为长为 $l=\frac{\log{n}}{2}$ 的块,对于块间直接上原算法 ST,设 $k=\dfrac{n}{\frac{\log{n}}{2}}=\frac{2n}{\log{n}}$,复杂度为 $O(k\log{k})=O(n)$。 对于块内,我们要利用上面的性质,求出每一个小块的**深度的**差分序列,于是有每个差分序列仅由 $\text{1,-1}$ 构成。 又由于长度仅为 $l=\frac{\log{n}}{2}$ ,故本质不同的差分序列只有 $2^{l-1}=O((2^{\log{n}})^{\frac{1}{2}})=O(\sqrt{n})$ 个。 对于每个差分序列,将首项置为 0 还原出一个序列,对其预处理 RMQ,复杂度为 $O(\sqrt{n} * (l^2/l\log{l}))<O(n)$,这里可以直接暴力处理区间,如果你够闲你可以对小块也做 ST。 询问时就按分块的方法询问,对于块间有 ST 表,对于块内可以找到对应的差分序列询问前后缀,此处的前后缀也可以不用差分序列,直接遍历 $O(n)$ 预处理也能做到 $O(1)$ 询问(常数还小)。 但特别地,有两点处于同一小块中的情况,还是找到对应的差分序列询问区内,这也是预处理小块的意义所在。 由于这是第一个讲解的问题,有点抽象,我们来看点栗子。~~听说没有图不让过?~~ ![](https://cdn.luogu.com.cn/upload/image_hosting/vh5zjw9g.png) 欧拉环游序为:$1\ 2\ 1\ 3\ 4\ 3\ 5\ 3\ 1$。 令块的大小 $l=3$,有 $1\ 2\ 1|3\ 4\ 3|5\ 3\ 1$,第一块深度最小为 1 号点,同理第二块为 3 号点, 第三块为 1 号点,区间询问的话取最小就是了。 考虑块内,(深度)差分序列分别为 $\text{1\ -1\ |\ 1\ -1\ |\ -1\ -1}$。 对 $\text{1\ -1}$ 来说,还原后为 $0\ 1\ 0$,最小位置的数组为: $mn[\text{-1,1}][i][j]$ 表示这种序列 $i\sim j$ 区间的最小位置:$\begin{bmatrix}1&1&1\\/&2&3\\/&/&3\end{bmatrix}

\text{-1\ -1} 来说,还原后为 \text{0\ -1\ -2},最小位置的数组为:

mn[\text{-1,-1}][i][j]$ 表示这种序列 $i\sim j$ 区间的最小位置:$\begin{bmatrix}1&2&3\\/&2&3\\/&/&3\end{bmatrix}

若询问 2、5 的LCA,在第一块 \text{1\ -1} 中查询区间 (2,3),找到第三个位置,对应节点 1,第二块直接取 3,第三块查询区间 (1,1),找到第一个位置,对应节点 5,对以上三个取最小,答案为 1 号节点。

若询问 4、4 的LCA,在第二块中查询区间 (2,2),对应节点 4。

代码实现(仅供参考)

对于 RMQ 的优化

你不是说不能优化 RMQ 的吗

咳咳,虽然不能直接优化 RMQ,但我们可以用笛卡尔树(含简单解释)将区间 (l,r) 转化为树上 l,r 对应两点 LCA 的问题,就可以直接套用上面的 LCA 优化了。

由于利用了相邻两点的深度差为 1 的性质,故也有人称其为 \pm 1 RMQ 或是约束 RMQ。(听听就好)

代码实现(仅供参考)

对于 LA 的优化

我们已有一种较优秀的算法 长剖+倍增数组(O(n\log{n})-O(1)) 来求 LA。(长剖经典应用,不会的去请先学长剖)

(不是我不想讲,是真没什么讲的 =\ \ = ,上面的欧拉环游序也是)

考虑一个简单的优化:对于每个点预处理出任意一个子树中的叶子节点以及距离(设为 d),显然可以 O(n) 做到,于是我们将一个点的 K 级祖先转化为一个叶子的 K+d 级祖先,故只用对所有叶子节点做倍增数组 O(L\log{n})

由于叶子数 L 可能为 O(n),故最坏复杂度仍为 O(L\log{n})=O(n\log{n})

考虑在此基础上用 \texttt{Method of Four Russians}

我们将靠近叶子的点删掉,得到一棵新树。更具体地,我们将原树上子树不超过 B=\frac{\log{n}}{4} 的节点删去,于是有新树的叶子节点数不超过 \frac{n}{B},因为他们在原树中的子树均超过了 B

考虑询问点在新树上,套用之前的叶子节点 K+d 级祖先算法,有复杂度 k=\frac{n}{B},O(k\log{n})-O(1)

考虑询问点为被删除的点,可以发现所有被删的节点构成了每棵树大小不超过 B 的森林。

还是预处理到新树的叶子节点的距离 d,若有 K\ge d,那么转化为新树上叶子节点的 K-d 级祖先,O(n) 预处理然后套上面的询问即可。

否则与新树无关,且森林间每棵树独立,以下将被删树的大小看做 B

考虑被删树种类的可能性,如果你对 Catalan 数比较熟悉的话可以直接报出有 \dfrac{C_{2B}^{B}}{B+1} 种(这就是 Catalan 数,如果你知道多叉树可以转化为二叉树的话会更容易看出来)。

我们考虑得到一棵树的括号序列,即进入一个子树时加前括号,退出时加后括号,B 对括号的方案数即为 Catalan 数。Catalan 的简单解释

反之,如果我们有一个括号序列我们可以反推出这棵树,对于每种树(也及每种括号序列)暴力处理 K(有K\le d) 级祖先即可,复杂度 O(\dfrac{C_{2B}^{B}}{B+1} * B^2(/B\log{B}))=O(4^B * B^2)=O(\sqrt{n}\log^2{n})<O(n)。(当然你可以闲到对每种树写长剖)

也就是我们对于每种被删树,取出它的括号序列,通过预处理的直接 O(1) 询问即可。你会发现这个和上面 LCA 的差分序列处理(思路)十分相似,可以参考代码以便理解。

询问的方法在上面已经讲解过就不再赘述。

代码实现(仅供参考)

LCA 的另一种优化

上文的 LCA 优化中,块间的复杂度为 k=\dfrac{n}{\frac{\log{n}}{2}}=\frac{2n}{\log{n}},O(k\log{k})=O(n) ,实则 k\log{k} 大于 n,故此算法预处理的瓶颈在这里。

考虑调整块的大小 l=\log{n},则本质不同的小块数变为 2^{l-1}=O(2^{\log{n}})=O(n) (略小于 n)个。

但注意到其优秀的性质,我们将差分序列中的 1 看作 0,-1 看作 1,即对应为一个 01 序列,其最小值可以由比它去掉最高位(即最大的一个 1)后的 01 序列 O(1) 转移过来,故小块复杂度仍为 O(n)

若询问一个差分序列的区间,用位运算直接取出对应的 01 序列,末尾空位用 0 补齐 l-1 ,就可以直接由预处理过的 01 序列得到,故询问小块依旧为 O(1)

且显然有块间复杂度 k=\dfrac{n}{\log{n}},O(k\log{k})=O(n) (也略小于 n)。

复杂度瓶颈优化为更严格的 O(n),但也仅为理论常数上的优化。

代码实现

RMQ 的另一种优化

之前笔者由于太累(lan)了,没有阐述他人提出的更好的 idea,故在此增补一下。其实其他优化与上文都有异曲同工之妙,不比上文困难,还是值得一看。

LCA 的另优化给了我们启发,我们并不用像 LCA 初优化中的暴力那样,将每一种状态的每一个情况处理出来。我们只关心我们需要的,故我们只快速地预处理了差分序列最小位置,达到了优化的目的。

我们再来看 RMQ,我们发现像之前那样显示地将笛卡尔树建立出来再求 LCA 十分地冗余,感觉绕了一大圈才回到原问题。

考虑在这里建立笛卡尔树的本质,其实是使起点固定的单调栈得以区间询问。

先将块的大小设为 l=\log{n},对于块间还是使用原算法 ST ,而对于块间维护一个单调栈。

显然可以发现,对于区间 (l,r) 的最大值即为最后一个为 r 的单调栈中位置大于 l 的第一个位置的值。

借用一个例子:

对于序列 a:1\ 3\ 5\ 2\ 4 维护单调栈(以下为位置的编号):

1:1 2:2 3:3 4:3\ 4 5:3\ 5

询问区间 (2,4) 中的最大值,就为第 4 个单调栈中比位置 2 大的第一个位置 3,位置 3 的值为 5,即为区间最大。

于是我们只用对于每个末尾用 01 序列存下其单调栈的出现情况,查询时同样运用刚刚 LCA 的用的技巧,用位运算取出对应 01 序列直接由预处理得到答案。

由于没有建笛卡尔树和欧拉环游序,序列长度为 n,在块取 l=\log{n} 时是特别标准的 O(n)-O(1) 复杂度,理论常数远胜 RMQ 初优化。

代码实现(仅供参考)

LCA 再优化

我会套娃!

注意到我们已经有了一个常数优秀的 O(n)-O(1) RMQ,考虑对优化欧拉环游序。

由于欧拉序列长为 2n(-2) 且我们询问端点只有 n 个,考虑对欧拉序缩起来。

简单来说,欧拉序是边从上往下时加入一次,从下往上又加入一次。

而我们显然发现最后一次从下往上加入的点是不必要的,也就是说每个点返回时我们使其不加入欧拉序查询也显然是对的,而这样就只剩下了 n-1 个点。

虽然失去了其优秀的性质,但我们直接套用另优化 RMQ,常数得以更进一步。

代码实现(仅供参考)

后序

这三类问题的最优复杂度流传的并不广,多数人最多也就听过约束 RMQ 能优化 LCA。个人认为,问题在于算法优势不明显,而且卡树剖会被骂,笔者希望这些算法能在常数或是码量上有更好的提升,当然也希望大家都能理解这些算法,使它们能有更广泛的应用。这样就能出毒瘤卡常题了

如果我有什么错别字、废话或是不清晰的地方可以跟我说。

参考文献

2017IOI国家候选队论文《非常规大小分块算法初探》——徐明宽

26535 的博客《推广一种常数较小,码量不大的 O(n)-O(1) RMQ》

skip2004 的博客《最近公共祖先》