5.4 KiB
5.4 KiB
当然可以!LCA(Lowest Common Ancestor,最近公共祖先)是树形结构中一个常见的问题,广泛应用于计算机算法竞赛中。以下是关于如何在C++中实现LCA的一些指导,包括常见的方法和示例代码。
一、LCA问题简介
在一棵树中,给定两个节点,找出它们的最近公共祖先(即在这两个节点的所有公共祖先中,深度最大的一个节点)。
二、常见的LCA算法
-
二叉提升法(Binary Lifting)/倍增法:
- 通过预处理每个节点的2^k阶祖先,能在O(n log n)的时间预处理,并在O(log n)的时间内回答每个查询。
-
Tarjan离线LCA算法:
- 使用并查集(Union-Find)结构和DFS,适用于离线查询,在O(n + q α(n))时间内解决,其中q是查询次数,α是阿克曼函数的反函数,几乎可以视为常数。
-
Euler Tour + RMQ:
- 通过欧拉序列将LCA问题转化为范围最小查询问题,预处理O(n)或O(n log n),查询O(1)或O(log n)。
其中,二叉提升法由于实现简单且适用于在线查询,故在此详细介绍。
三、二叉提升法实现LCA
1. 算法思路
-
预处理阶段:
- 使用DFS或BFS遍历树,记录每个节点的深度和2^k阶祖先。
- 通常需要计算log₂(n)的上限,以确定需要记录多少个祖先。
-
查询阶段:
- 调整两个节点的深度,使它们位于同一深度。
- 从高到低(即从最大的k到0)同时提升两个节点,直到找到它们的公共祖先。
2. C++ 示例代码
以下是一个基于二叉提升法的LCA实现示例:
#include <bits/stdc++.h>
using namespace std;
const int MAX = 100005;
const int LOG = 20; // 因为2^20 > 10^6
vector<vector<int>> adj(MAX);
int up[MAX][LOG]; // up[u][k] 是节点u的2^k祖先
int depth_node[MAX];
// 深度优先搜索,预处理深度和up数组
void dfs(int u, int parent_node){
up[u][0] = parent_node;
for(int k = 1; k < LOG; ++k){
up[u][k] = up[ up[u][k-1] ][k-1];
}
for(auto &v: adj[u]){
if(v != parent_node){
depth_node[v] = depth_node[u] + 1;
dfs(v, u);
}
}
}
// 寻找两个节点的LCA
int lca(int u, int v){
if(depth_node[u] < depth_node[v]) swap(u, v);
// 将u提升到与v相同的深度
int diff = depth_node[u] - depth_node[v];
for(int k = 0; k < LOG; ++k){
if(diff & (1 << k)){
u = up[u][k];
}
}
if(u == v) return u;
// 同时提升u和v,直到他们的祖先相同
for(int k = LOG-1; k >= 0; --k){
if(up[u][k] != up[v][k]){
u = up[u][k];
v = up[v][k];
}
}
return up[u][0];
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
int n; // 节点数量
cin >> n;
// 假设节点编号从1到n
for(int i = 1; i < n; ++i){
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
// 假设1为根节点
depth_node[1] = 0;
dfs(1, 1);
int q; // 查询次数
cin >> q;
while(q--){
int u, v;
cin >> u >> v;
cout << lca(u, v) << "\n";
}
return 0;
}
3. 代码解释
-
变量说明:
adj
:邻接表,表示树。up[u][k]
:节点u的第2^k个祖先。depth_node[u]
:节点u的深度(根节点深度为0)。
-
DFS函数:
- 对树进行DFS遍历,计算每个节点的深度和其各级祖先。
up[u][0]
是节点u的直接父节点。up[u][k]
通过up[ up[u][k-1] ][k-1]
计算得到,即节点u的2^k祖先是节点up[u][k-1]
的2^(k-1)祖先。
-
LCA函数:
- 深度对齐:
- 如果节点u比节点v深,交换u和v。
- 计算深度差diff,将u提升diff步,使u和v在同一深度。
- 共同祖先查找:
- 从最高位k开始,同时检查u和v的2^k祖先是否不同。如果不同,则同时提升u和v到各自的2^k祖先。
- 最后,u和v的父节点即为LCA。
- 深度对齐:
-
主函数:
- 输入节点数n。
- 输入n-1条边,构建树。
- 假设1为根节点,进行DFS预处理。
- 处理q个LCA查询。
4. 使用示例
假设输入:
5
1 2
1 3
2 4
2 5
3
4 5
4 3
5 3
输出:
2
1
1
表示节点4和5的LCA是2,节点4和3的LCA是1,节点5和3的LCA是1。
四、注意事项
- 节点编号:确保节点编号从1开始或从0开始,并在代码中保持一致。
- 树的根节点:选择一个合适的根节点,通常选择1或给定的节点。
- LOG值的选择:根据树的规模n,LOG应满足2^LOG > n。常见的取值是20,因为2^20约等于10^6。
- 边的输入形式:确保输入的边是无向的,并正确构建邻接表。
- 处理根节点的情况:在查询LCA时,如果一个节点是另一个节点的祖先,算法应能正确返回祖先节点。
五、优化与扩展
- 空间优化:如果树较大,可以考虑压缩
up
数组或使用更高效的存储方式。 - 路径查询:在二叉提升的基础上,可以集成其他信息(如路径和、最小值等)以支持更多复杂的查询。
- 其他LCA算法:根据具体问题需求,选择合适的LCA算法可能更加高效。
希望以上内容对你理解和实现LCA有所帮助!如果有进一步的问题,欢迎继续提问。