最近公共祖先(LCA)
简介 #
最近公共祖先(Least Common Ancestors
,简称 LCA
).
节点 $p,q$ 的最近公共祖先 $s$ 是这棵树中到 $p,q$ 的距离之和最小的节点.
如何求两个节点的 LCA
?
$p,q$ 两个指针分别指向这两个节点,并且 $p$ 的深度比 $q$ 深.
将 $p$ 不断往父节点方向移,直到 $p,q$ 处于同一深度.
$p$ 和 $q$ 同时往父节点移,直到它们相遇于 $s$ 节点.$s$ 节点为 $p$ 和 $q$ 的 LCA
.
暴力算法($\textcolor{red}{×}$) | LCA 算法($\textcolor{green}{√}$) |
|
---|---|---|
预处理 | $\textcolor{green}{0}$ | $\textcolor{green}{O(n)}$ |
单次查询 | $\textcolor{red}{O(n)}$ | $\textcolor{green}{O(\log{n})}$ |
$m$ 次查询 | $\textcolor{red}{O(mn)}$ | $\textcolor{green}{O(m\log{n})}$ |
int d[]; // d[u]: 节点 u 的深度
int f[]; // f[u]: 节点 u 的父节点
int LCA(int p, int q) {
if(d[p] < d[q]) swap(p, q); // 使 p 的深度 ≥ q
while(d[p] > d[q]) p = f[p]; // 步骤 1
while(p != q) p = f[p], q = f[q]; // 步骤 2
return p;
}
原理 #
暴力算法慢在哪里?
$- -$ $p$ 和 $q$ 每次只能向父节点跳一步!这是因为我们只保存了每个节点的父节点.
进一步思考,开一个二维数组 $f$:$f[p,i]$ 表示 $p$ 的第 $2^i$ 级父节点.
一张图中最多有 $2^{24}$ 个节点,否则光是读入这张图都会超时.因此数组只要开到 $f[n,25]$.
第一步 #
如果已经预处理出了 $f$ 数组,如何让 $p$ 向上跳到和 $q$ 同样深的位置?
采用「试跳法」.枚举 $i=24→0$:
-
若 $f[p,i]$ 的深度小于 $q$ 的深度,会跳过头,所以选择不跳;
-
否则就令 $p = f[p,i]$.
为什么要从 $24$ 开始枚举 $i$ 呢?因为 $p$ 不可能有超过 $2^{24}$ 级的父节点.
时间复杂度为 $O(24)$.
for(int i = 24; i >= 0; i --)
if(d[f[p][i]] < d[q]) continue;
else p = f[p][i];
第二步 #
令 $p$ 和 $q$ 同时往父节点跳,直到它们相遇.
仍然使用「试跳法」,枚举 $i=24→0$:
-
若 $f[p,i] = f[q,i]$,此时 $p,q$ 同时向上跳 $2^i$ 步会相遇,但是我们选择不跳.
-
若 $f[p,i] \not= f[q,i]$,令 $p = f[p,i], q = f[q,i]$,此时 $p,q$ 离相遇又近了一步.
枚举结束时,$p$ 和 $q$ 处于相遇和不相遇的临界状态,如下图.$p$ 或 $q$ 的父节点就是 LCA
.
for(int i = 24; i >= 0; i --) {
if(f[p][i] != f[q][i]) {
p = f[p][i];
q = f[q][i];
}
}
合并两个步骤,得到求 LCA
的代码:
const int LogN = 24;
int LCA(int x, int y) {
if(d[x] < d[y]) swap(x, y);
for(int i = LogN; i >= 0; i --) {
if(d[f[x][i]] >= d[y]) x = f[x][i];
if(x == y) return x; // 如果已经相遇,x 和 y 就是 LCA
}
for(int i = LogN; i >= 0; i --) {
if(f[x][i] != f[y][i]) {
x = f[x][i];
y = f[y][i];
}
}
return f[x][0]; // 返回的是父节点
}
预处理 #
出于 LCA()
函数的需要,我们还需要预处理:
-
每个节点的深度 $d[u]$;
-
$f$ 数组.
首先 $d[u]=d[u$ 的父节点$]+1$,我们可以用一次深搜搞定 $d$ 数组.
$u$ 的 $2^{i+1}$ 级父节点,同时也是「$u$ 的 $2^i$ 级父节点」的 $2^i$ 级父节点.
$$ f[u,i + 1] = f[f[u,i],i] $$
计算顺序:$f[u,1→24]$.
如果你还不理解为什么要算到 $f[u,24]$,请再仔细看一遍 原理.
采用 树形 DP 方法进行预处理.时间复杂度为 $O(24n)$.
const int N = 1e6, LogN = 24;
int n, d[N + 1], f[N + 1][LogN + 1];
vector<int> g[N + 1]; // g[u]: 与节点 u 相连的点集合
void pre(int u, int fa) { // u 的父节点是 fa
f[u][0] = fa;
d[u] = d[fa] + 1;
for(int i = 0; i < LogN; i ++)
f[u][i + 1] = f[f[u][i]][i];
for(int i = 0; i < g[u].size(); i ++) {
int v = g[u][i];
if(v == fa) continue;
pre(v, u);
}
}
int main() {
/* ...省略数据的输入... */
pre(root, 0); // root 为根节点,如果题目没指定,可以为任意节点
}
模板 #
const int N = 1e6, LogN = 24;
int n, d[N + 1], f[N + 1][LogN + 1];
vector<int> g[N + 1];
void pre(int u, int fa) {
f[u][0] = fa;
d[u] = d[fa] + 1;
for(int i = 0; i < LogN; i ++)
f[u][i + 1] = f[f[u][i]][i];
for(int i = 0; i < g[u].size(); i ++) {
int v = g[u][i];
if(v == fa) continue;
pre(v, u);
}
}
int LCA(int x, int y) {
if(d[x] < d[y]) swap(x, y);
for(int i = LogN; i >= 0; i --) {
if(d[f[x][i]] >= d[y]) x = f[x][i];
if(x == y) return x;
}
for(int i = LogN; i >= 0; i --) {
if(f[x][i] != f[y][i]) {
x = f[x][i];
y = f[y][i];
}
}
return f[x][0];
}