最近公共祖先
最近公共祖先(Lowest Common Ancestors, LCA
)指有根树中距离两个节点最近的公共祖先。祖先指从当前节点到树根路径上的所有节点。
u
和 v
的公共祖先指一个节点既是 u
的祖先,又是 v
的祖先。u
和 v
的最近公共祖先指距离 u
和 v
最近的公共祖先。
若 v
是 u
的祖先,则 u
和v的最近公共祖先是 v
可以使用 LCA
求解树上任意两点之间的距离。求 u
和 v
之间的距离时,若 u
和 v
的最近公共祖先为 lca
,则 u
和 v
之间的距离为 u
到树根的距离加上 v
到树根的距离减去2倍的 lca
到树根的距离:
d
i
s
t
[
u
]
+
d
i
s
t
[
v
]
−
2
∗
d
i
s
t
[
c
a
]
dist[u]+dist[v]-2*dist[ca]
dist[u]+dist[v]−2∗dist[ca]。
求解 LCA
的方法有很多,包括暴力搜索法、树上倍增法、在线 RMQ
算法、离线 Tarjan
算法和树链剖分。
- 在线算法:以序列化方式一个一个地处理输入,也就是说,在开始时并不需要知道所有输入,在解决一个问题后立即输出结果。
- 离线算法:在开始时已知问题的所有输入数据,可以一次性回答所有问题。
1、暴力搜索法
暴力搜索法有两种:向上标记法和同步前进法。
1.1 向上标记法
从 u
向上一直到根节点,标记所有经过的节点;若 v
已被标记,则 v
节点为 LCA(u, v)
;否则 v
也向上走,第1次遇到已标记的节点时,该节点为 LCA(u, v)
。
int LCA(int u,int v){
if(u==v){
return u;
}
flag[u]=1;
while(fa[u]!=u){// u向上走到根
u=fa[u];
flag[u]=1;
}
if(flag[v]){
return v;
}
while(fa[v]!=v){
v=fa[v];
if(flag[v]){// v向上
return v;
}
}
}
复杂度 O ( n ) O(n) O(n)
1.2 同步前进法
将 u
、v
中较深的节点向上走到和深度较浅的节点同一深度,然后两个节点一起向上走,直到走到同一个节点,该节点就是 u
、v
的最近公共祖先,记作 LCA(u,v)
。若较深的节点 u
到达 v
的同一深度时,那个节点正好是 v
,则 v
节点为 LCA(u,v)
。
复杂度 O ( n ) O(n) O(n)
2、树上倍增法
树上倍增法不仅可以解决 LCA
问题,还可以解决很多其他问题,掌握树上倍增法是很有必要的。
F
[
i
,
j
]
F[i, j]
F[i,j] 表示
i
i
i 的
2
j
2^j
2j 辈祖先,即
i
i
i 节点向根节点走
2
j
2^j
2j 步到达的节点。
F [ i , j ] = F [ F [ i , j − 1 ] , j − 1 ] F[i,j]=F[F[i,j-1],j-1] F[i,j]=F[F[i,j−1],j−1]
算法设计
- 创建
ST
- 利用
ST
求解LCA
和前面暴力搜索中的同步前进法一样,先让深度大的节点 y y y 向上走到与 x x x 同一深度,然后 x x x 、 y y y 一起向上走。和暴力搜索不同的是,向上走是按照倍增思想走的,不是一步一步向上走的,因此速度较快。
void ST_create() {
for(int j=1; j<=k; j++) {
for(int i=1; i<=n; i++) {
F[i][j]=F[F[i][j-1]][j-1];
}
}
}
int LCA_st_query(int x,int y){//求区间x,y的最近公共祖先
if(d[x]>d[y]){//保证x的深度小于等于y
swap(X,y);
}
for(int i=k;i>=0;i--){//y向上走到与x同一深度
if(d[F[y][i]]>=d[x]){
y=F[y][i];
}
}
if(x==y){
return X;
}
for(int i=k;i>=0;i--){//x,y一起向上走
if(F[x][i]!=F[y][i]){
x=F[x][i],y=F[y][i];
}
}
return F[x][0];// 返回x的父亲
}
预处理 O ( n l o g n ) O(nlogn) O(nlogn)