### 什么是最近公共祖先 一个**有根**树上,若点u与点v同时属于点f的子树,且点f的深度尽可能深(离根最远),则称f为u与v的最近公共祖先。 ### 朴素算法 1. 将更深的点上移 $1$ 格。如果点一样深,那就一起上移 $1$ 格; 2. 当u和v相遇时,相遇的点即为f。 预处理时间复杂度 $O(n)$,查询时间复杂度 $O(n)$。 ### 优化:倍增上跳 可以发现,朴素算法的时间瓶颈在于上跳。当f和u或v距离比较大时,上跳耗时很大。 类比一下,当你想要从广铁一中去北京天安门,你**可能**会这么做: 1. 走路到杨箕地铁站 2. 坐地铁到广州南站 3. 坐高铁到北京西站 4. 坐地铁到天安门西站 5. 走路到天安门前 而一般人不会从广铁一中直接走到广州南站,也没有办法直接坐高铁到天安门前。 正如步行、地铁、高铁的多级交通,倍增也可以这么类比:上跳的时候,先一次跳很远,再跳近一些,再跳近一些,最后到达目标。 因此,倍增LCA算法分为预处理和查询两个阶段。 #### 预处理 先定义 `fa[u][i]`为从节点u上跳 $2^i$ 格的目的地。`fa[u][0]`就是u的直接祖先。 这个数组是很好创建的。我们可以使用DFS遍历树上每个节点,并计算这个节点的上跳目的地。 可以发现,上跳一次 $2^i$ 格,就是上跳两次 $2^{i-1}$ 格。所以我们可以列出递推式:```fa[u][i]=fa[fa[u][i-1]][i-1]```。 这样,我们就实现了计算 `fa[u][i]` 数组。特别地,当 `fa[u][i]=0` 时,代表u上跳 $2^i$ 格会跳出树根。 时间复杂度 $O(n\log n)$,空间复杂度 $O(n\log n)$,因为对于每一个点都要计算 $\log n$ 个上跳目的地。 #### 查询 控制上跳的幅度是查询的重点。如果上跳跳到了f的上方,就很难回来了,查询的结果也会变成“公共祖先”而不是“最近公共祖先”了。 因此步骤为: ```mermaid flowchart LR classDef node fill:#DDDDEE,stroke:#000088; classDef edgeLabel background-color:#DDDDEE; A([开始]) -->B[设置变量i为MAXF] B-->C{u和v深度相等吗} C-->|相等|D{u和v一致吗} C-->|不相等|N{更深的点上跳2^i格的目的地比更浅的点还要浅吗} N-->|是的|F N-->|不是|E[上跳2^i格] E-->F[i下调1] F-->C D-->|不一致|H[设置变量i为MAXF] H-->G{i>=0} G-->|否|M([返回u的父亲]) G-->|是|J{u和v同时上跳2^i格的目的地一致吗} J-->|一致|K[i下调1] J-->|不一致|L[u和v同时上跳2^i格] L-->K K-->G C-.-T1@{ shape: comment, label: "思考1:为什么u和v的深度最后必然相等?" } J-.-T2@{ shape: comment, label: "思考2:为什么这里不允许上跳2^i格的目的地一致?" } M-.-T3@{ shape: comment, label: "思考3:此时为什么u的父亲一定是所求的**最近**公共祖先?"} ``` 时间复杂度 $O(\log n)$,因为最多上跳 $O(\log n)$ 次。 #### 思考答案 ##### 思考1 假设两个点一开始的深度差为d,d可以转换为一个二进制数。 每次检查更深的点上跳是否会超出更浅的点,便是从高位到低位检查d的某一位是否为 $1$;而执行上跳,便是发现这一位为 $1$ 后,将这个 $1$ 变成 $0$。 最后,二进制数d一定可以变为 $0$,两个点就处在了同一深度。 ##### 思考2 因为若目的地一致,无法判断这个目的地是否在最近公共祖先之上。一旦跳到最近公共祖先之上,很难跳回来。 ##### 思考3 因为先前的上跳已经跳到了“u和v不相遇”的极限,显然,此时u和v再跳 $1$ 格就相遇了,相遇的这个点就是**最近**公共祖先了。 ### 例题 1. [Luogu P3379 【模板】最近公共祖先](https://www.luogu.com.cn/problem/P3379) 最近公共祖先算法只需一道例题即可,因为和树有关的题,有很多都要用到最近公共祖先算法。 ### 更快的算法 虽然本文讲解的是倍增求LCA,但建议同学们以后学一下[DFS序求LCA](https://www.luogu.com.cn/article/pu52m9ue),在搭配ST表的情况下可以把单次查询的时间复杂度从 $O(\log n)$ 降到 $O(1)$,在某些题会大有帮助。