浅谈 LCA

· · 算法·理论

一些定义:fa_ii 的父亲,dep_ii 的深度,dfn_ii 的深度,euler_ii 的欧拉序,\min^{f} \{S\}\forall x \in S, f_x 最小的元素 x\max^f 同理。

0. 定义

节点 u 与节点 v 的 LCA(最近公共祖先) 的定义是深度最深的节点 w 满足 w 同时是 u 的祖先与 v 的祖先,记为 \operatorname{lca}(u,v)=w

1. 求法

LCA 的求法还是挺多的,这里简要介绍几种比较有实用价值的方法。

看起来很多资料都谈到了 tarjan 求 LCA 的方法,但个人认为 tarjan 求 LCA 并不比倍增/DFS 序/欧拉序法复杂度显著优,且需要数据可以离线,而对于很多题目,即使可以离线 LCA 询问,也往往会增加代码难度,作者也没有找到有意义的推广,也并不易于理解。因此没有提及。

此处用 O(A) \sim O(B) 代表该做法需要用 O(A) 的复杂度预处理,之后用 O(B) 的复杂度处理每次询问。

1.1 朴素法

这个方法其实实用价值不那么大,但是是很多方法的基础。朴素法求 LCA 共分为两步,复杂度为 O(N) \sim O(N)

  1. 不断让深度较深的节点跳父亲,直到两个节点的深度相同。
  2. 让两个节点同时跳父亲,直到两个节点相同,即为 LCA。

另一种实现是我们将从 x 到根的路径染色,之后找到从 y 到根的路径上深度最深的有颜色的节点即可。

1.2 倍增法

倍增法较为好写,复杂度在大部分情况下也是足够的,复杂度为 O(N \log N) \sim O(\log N)。下面默认 x 为深度较深的节点,y 为深度较浅的节点。预处理 f_{i,j} 代表 i 的第 2^j 个父亲。

对于朴素法的第一步,\forall 1 \leq i \leq \log_2 N,若 dep_{f_{x,i}} \leq dep_y,则令 x=f_{x,i} 即可,倒序循环,易证对于每个 i 至多只能这样跳一次。(否则会在 i+1 的时候跳,当 i=\log_2N2^i \gt N,所以也不可能出现)

对于第二步 \forall if_{x,i} \neq f_{y,i},则令 x=f_{x,i},y=f_{y,i} 即可,最终的 LCA 即为 f_{x,0},需要特判若第一步后 x=y 则 LCA 为 x

应对一些卡空间的毒瘤题目时,可以将 f_{i,j} 改为 i 的第 w^j 个父亲,其中 w 是一个较小的正整数,注意可以跳多次父亲,时间复杂度为 O(N w \log_w N) \sim O(w \log_w N),空间复杂度为 O(N\log_wN)

1.3 DFS 序法

DFS 序法的优势是复杂度较为优秀,常规实现的复杂度为 O(N \log N) \sim O(1),部分优秀实现可以到达 O(N) \sim O(1)。下面令 x 为 DFS 序较小的节点,y 为 DFS 序较大的节点,w\operatorname{lca}(x,y)。分情况讨论:

情况①与情况②中两个不同的式子看起来很不优雅,考虑统一两个式子,容易发现在 x 不为 y 的祖先时,即使我们令 x 为 DFS 序恰好为 dfn_x+1 的节点,我们仍然会经过至少一个 w 的子节点,且仍然只会在 w 的不含其自身的子树里行动,所以对于情况①可以直接用情况②的式子。

但这样我们需要同时维护 depdfn,看起来还是比较麻烦,但因为父亲的 dfn 最小的节点的父亲一定还是 wdfn_x \gt dfn_y, \forall y \in \operatorname{subtree}(x), y \neq x)。我们直接用一个 ST 表,底层维护父亲编号,高层维护 dfn 最小的节点的父亲即可(st_{i,j} 代表 \min^{dfn}(fa_x, \forall i \lt dfn_x \leq i+2^j))。就达到了预期的复杂度。用四毛子可以达到更优的复杂度,但非常麻烦。

1.4 欧拉序法

欧拉序法的复杂度与 DFS 序法一样优,但在部分题目(e.g. 区间 LCA)中需要同时记录欧拉序与 DFS 序,较为麻烦。令 p_i 为编号为 i 的节点在欧拉序中第一次出现的位置,x=\min^{p}(x,y),y=\max^p(x,y)

结论:\operatorname{lca}(x,y) = \min^{p} \{i, i \in euler_{p_x \cdots p_y}\}。证明为 euler_{p_x \cdots p_y} 一定包含回溯时产生的 \operatorname{lca}(x,y),且一定不包含 fa_{\operatorname{lca}(x,y)}(因为只在 \operatorname{lca}(x,y) 的子树中)。

直接用 ST 表或四毛子维护即可,复杂度为 O(N \log N) \sim O(1)O(N) \sim O(1)

1.5 树链剖分

在树上不断跳重链,直到两个节点处于同一个重链即可。复杂度为 O(N) \sim O(\log N) 且常数较小。

2. 性质

定义 \operatorname{lca}(S) 为集合 S 中所有元素的最近公共祖先。

  1. 若树上 x,y 的简单路径与 a,b 的简单路径相交,则 \operatorname{lca}(x,y)a,b 的简单路径上(或反过来)。

3. 应用

3.1 关于 LCA 本身

3.1.1 [LNOI2014] LCA

3.1.1.1 题面

有一个以 0 为根的有 n 个节点的有根树,m 次询问,每次给定 l,r,x\sum_{i=l}^r dep_{\operatorname{lca}(i,x)}

3.1.1.2 题解

考虑运用朴素法求 LCA 中的思路 ②,我们将从根到 x 的路径上的所有节点染色,容易发现 dep_{\operatorname{lca}(x,y)}=y 到根的路径上被染色的节点数量,依此可以做到 O(nm),注意到我们都是路径修改与查询,考虑树链剖分与线段树。为了避免烦人的零下标,可以将所有节点编号 +1 后处理。

但这样复杂度还是不够,考虑差分,将问题转化为把 y 到根染色,之后查询 x 到根的染色节点数量。将 [l,r] 拆成两个询问 [1,r][1,l-1],分别赋 1-1 的权值,按右端点排序,每当我们遇到一个区间就在当前的线段树上查询一次,否则给当前点到根的路径 +1 即可。

[GXOI/GZOI2019] 旧词 也和这题差不多,我们可以发现,我们加的一实际上是 val_i-val_{fa_i}(而本题中 val_i=dep_i,旧词中 val_i=dep_i^k),我们预处理出所有的 w_i=\sum_{j \leq i} val_j-val_{fa_j} 的值,之后对于下传标记和线段树更新时,我们不再把乘上 r-l+1,转而乘上 w_r-w_{l-1} 即可,且旧词不需要差分。

3.1.2 CF1062E Company

3.1.2.1 题面

给定一棵树,若干次询问,每次询问给定 l,r 求编号为 l \sim r 中的节点任意删去一个后剩余节点 LCA 的深度的最大值。

3.1.2.2 题解

基于 LCA 的性质 ①,可以发现区间 LCA 只和 DFS 序最小与最大的节点有关,只需要分别尝试删去 DFS 序最小的最大的节点,设删去 p,依据结论②我们可以分别求 [l,p-1] 的 LCA 与 [p+1, r] 的 LCA,之后求这两个 LCA 的 LCA 即可,注意本题中根节点的深度为 0

3.1.3 [NOIP2024] 树上查询

3.1.3.1 题面

给定一颗以 1 为根的树,q 组询问,每次询问给定 l,r,x,之后求 [l,r] 中任意长度大于等于 x 的连续子区间的 LCA 深度的最大值。

3.2 LCA 实现树上路径问题

LCA 的此类应用大多可以被树链剖分取代,此类问题常常需要用到性质③。

树链剖分大多具有更低的思维难度与更广泛的适用性,但代价是更高的代码难度。

3.2.1 P3398 仓鼠找 sugar

3.2.1.1 题面

给定一个树,多组询问,对于每个询问给定 a,b,c,dab 的简单路径与 cd 的简单路径是否有交集。

3.2.1.2 题解

基于 LCA 的性质⑤可以将问题转化为判断 ab 的路径是否经过 \operatorname{lca}(c,d)cd 同理),之后利用性质③倍增判断即可,具体的,分别将 ab 跳祖先跳到深度与 \operatorname{lca}(c,d) 相同的节点 a',b'a'=\operatorname{lca}(c,d)b'=\operatorname{lca}(c,d),就说明 ab 的路径经过 \operatorname{lca}(c,d)

作为对照,本题的树剖解法是将树上 ab 的路径 +1,查询树上 cd 的路径和是否为 0,之后将 ab 的路径减一。

3.2.2 [USACO15DEC] Max Flow P

3.2.2.1 题面

给定一颗树,多组询问,每次给定 u,v,将树上 u,v 的简单路径经过的节点权值 +1,求最终权值。

3.2.2.2 题解

基于性质③,我们维护一个树上差分,将操作拆成 u,v\operatorname{lca}(u,v) 链加,对照序列差分定义树上差分,令 dif_i=v_i-\sum v_{j}[j \in \operatorname{children}(i)](其中 v_j 代表实际的权值),对于每个操作给 dif_u \gets dif_u+1,dif_v \gets dif_v+1,dif_{\operatorname{lca}(x,y)} \gets dif_{\operatorname{lca}(x,y)}-2,之后树上前缀和即可。

作为对照,本题的树剖解法是直接将 uv 的路径 +1,最后统计一次。