详细讲解 LCA
希望能帮助你掌握 LCA
一、问题引入与基本概念
1.1 直观理解LCA
想象一个家族族谱,我们想知道两个人最近的共同祖先是谁。例如,表兄弟两人的最近共同祖先是他们的祖父,而不是曾祖父。在树形结构中,这个概念被形式化为"最近公共祖先"(LCA)。
让我们通过一个具体例子来理解:
A
/ \
B C
/ \ \
D E F
/ \ / \
G H I J
在这棵树中:
- 节点G和H的LCA是D
- 节点G和E的LCA是B
- 节点I和J的LCA是F
- 节点D和F的LCA是A
1.2 形式化定义
给定一棵有根树T和两个节点u和v,LCA(u,v)是满足以下条件的节点x:
- x是u的祖先
- x是v的祖先
- 在T的所有满足上述两个条件的节点中,x的深度最大
二、解决LCA问题的基本方法
2.1 朴素算法
最直观的方法是:
- 从u节点向上遍历到根,记录所有祖先节点
- 从v节点向上遍历到根,记录所有祖先节点
- 找出两个祖先序列中最后一个相同的节点
这种方法的时间复杂度为O(n),在最坏情况下(如退化成链表)需要遍历整棵树。
2.2 代码实现(朴素方法)
#include<bits/stdc++.h>
using namespace std;
struct node{
int val;
node*pa;
node(int x):val(x),pa(nullptr){}
};
node*a[1005],*b[1005];
int cnt1,cnt2;
void ga(node*u){
cnt1=0;
while(u!=nullptr){
a[++cnt1]=u;
u=u->pa;
}
}
void gb(node*v){
cnt2=0;
while(v!=nullptr){
b[++cnt2]=v;
v=v->pa;
}
}
node*fl(node*u,node*v){
ga(u);
gb(v);
node*lca=nullptr;
int i=cnt1,j=cnt2;
while(i>=1&&j>=1&&a[i]==b[j]){
lca=a[i];
i--;
j--;
}
return lca;
}
三、优化算法:二进制提升法
3.1 算法思想
朴素算法的效率不高,我们可以通过预处理来加速查询。二进制提升法(Binary Lifting)是一种使用动态规划预处理每个节点的2^k级祖先的方法,使得每次查询可以在O(logn)时间内完成。
3.2 预处理阶段
我们定义一个二维数组up[u][k],表示节点u的2^k级祖先。这个数组可以通过动态规划来填充:
up[u][0] = parent[u](直接父节点)up[u][k] = up[up[u][k-1]][k-1](递归定义)
3.3 查询阶段
查询LCA(u,v)的步骤:
- 将u和v调整到同一深度
- 从最大的k开始尝试向上跳跃,直到找到LCA
3.4 代码实现
#include<bits/stdc++.h>
using namespace std;
const int maxn=1000;
const int lg=log2(maxn)+1;
int g[maxn][maxn],cnt[maxn];
int up[maxn][lg];
int dep[maxn];
void dfs(int u,int fa){
up[u][0]=fa;
for(int i=1;i<lg;i++)
up[u][i]=up[up[u][i-1]][i-1];
for(int i=1;i<=cnt[u];i++){
int v=g[u][i];
if(v!=fa){
dep[v]=dep[u]+1;
dfs(v,u);
}
}
}
int lca(int u,int v){
if(dep[u]<dep[v])swap(u,v);
for(int i=lg-1;i>=0;i--)
if(dep[u]-(1<<i)>=dep[v])
u=up[u][i];
if(u==v)return u;
for(int i=lg-1;i>=0;i--)
if(up[u][i]!=up[v][i]){
u=up[u][i];
v=up[v][i];
}
return up[u][0];
}
四、可视化示例
让我们通过一个具体的树结构来理解二进制提升法:
预处理后的up数组(部分):
| 节点 | up[][0] | up[][1] | up[][2] |
|---|---|---|---|
| A | - | - | - |
| B | A | - | - |
| C | A | - | - |
| D | B | A | - |
| E | B | A | - |
| F | C | A | - |
| G | D | B | A |
| H | D | B | A |
| I | F | C | A |
| J | F | C | A |
查询LCA(G, I)的过程:
- depth[G]=3, depth[I]=3(已经同深度)
- 比较up[G][1]=B ≠ up[I][1]=C
- 比较up[G][0]=D ≠ up[I][0]=F
- 比较up[D][0]=B ≠ up[F][0]=C
- 比较up[B][0]=A == up[C][0]=A
- 返回A
五、算法分析与比较
5.1 时间复杂度
-
朴素算法:
- 预处理:无
- 查询:O(n)每次查询
-
二进制提升法:
- 预处理:O(nlogn)(DFS遍历每个节点,对每个节点计算logn个祖先)
- 查询:O(logn)每次查询
5.2 空间复杂度
二进制提升法需要O(nlogn)的额外空间存储up数组。
5.3 适用场景
- 朴素算法适用于查询次数少或树结构经常变化的场景
- 二进制提升法适用于需要大量查询且树结构不变的场景
六、其他LCA算法简介
除了二进制提升法,还有几种常见的LCA算法:
- Tarjan离线算法:使用并查集,在一次遍历中处理所有查询
- RMQ转换法:将LCA问题转化为区间最小值查询问题
- 树链剖分法:通过重链轻链分解来加速查询
七、实际应用示例
7.1 计算树上两点间距离
利用LCA可以高效计算树上任意两点间的距离:
int td(int u,int v,TreeNode* root){
TreeNode* ancestor=findLCA(u,v);
return depth[u]+depth[v]-2*depth[ancestor];
}
7.2 检测节点关系
判断一个节点是否是另一个节点的祖先:
bool check(TreeNode* u,TreeNode* v){
// u是否是v的祖先
TreeNode* ancestor=findLCA(u,v);
return ancestor==u;
}
八、模板
模板而已,前面讲很多了,这里就不做过多讲解了
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5,L=20;
int n,m,s,cnt;
int h[N],to[N<<1],ne[N<<1];
int d[N],f[N][L];
void add(int u,int v){
to[++cnt]=v,ne[cnt]=h[u],h[u]=cnt;
}
void dfs(int u,int fa){
f[u][0]=fa;
for(int i=1;i<L;i++)
f[u][i]=f[f[u][i-1]][i-1];
for(int i=h[u];i;i=ne[i]){
int v=to[i];
if(v!=fa){
d[v]=d[u]+1;
dfs(v,u);
}
}
}
int lca(int u,int v){
if(d[u]<d[v])swap(u,v);
for(int i=L-1;i>=0;i--)
if(d[u]-(1<<i)>=d[v])
u=f[u][i];
if(u==v)return u;
for(int i=L-1;i>=0;i--)
if(f[u][i]!=f[v][i])
u=f[u][i],v=f[v][i];
return f[u][0];
}
int main(){
cin>>n>>m>>s;
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
add(u,v),add(v,u);
}
dfs(s,s);
while(m--){
int a,b;
cin>>a>>b;
cout<<lca(a,b)<<'\n';
}
return 0;
}
九、总结
LCA问题是树结构中的一个基本问题,有着广泛的应用。我们从最直观的朴素算法出发,逐步介绍了更高效的二进制提升法,通过预处理将查询时间从 O(n) 优化到 O(logn)。理解LCA算法不仅能帮助我们解决具体问题,也是学习更复杂树算法的基础。
留下个赞,留下个收藏关注再走吧~~


&spm=1001.2101.3001.5002&articleId=150399437&d=1&t=3&u=a7a87fa5a05d491d948e8fc41f44591d)
3050

被折叠的 条评论
为什么被折叠?



