183 lines
5.4 KiB
Markdown
183 lines
5.4 KiB
Markdown
当然可以!LCA(Lowest Common Ancestor,最近公共祖先)是树形结构中一个常见的问题,广泛应用于计算机算法竞赛中。以下是关于如何在C++中实现LCA的一些指导,包括常见的方法和示例代码。
|
||
|
||
## 一、LCA问题简介
|
||
|
||
在一棵树中,给定两个节点,找出它们的最近公共祖先(即在这两个节点的所有公共祖先中,深度最大的一个节点)。
|
||
|
||
## 二、常见的LCA算法
|
||
|
||
1. **二叉提升法(Binary Lifting)/倍增法**:
|
||
- 通过预处理每个节点的2^k阶祖先,能在O(n log n)的时间预处理,并在O(log n)的时间内回答每个查询。
|
||
|
||
2. **Tarjan离线LCA算法**:
|
||
- 使用并查集(Union-Find)结构和DFS,适用于离线查询,在O(n + q α(n))时间内解决,其中q是查询次数,α是阿克曼函数的反函数,几乎可以视为常数。
|
||
|
||
3. **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实现示例:
|
||
|
||
```cpp
|
||
#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函数**:
|
||
1. **深度对齐**:
|
||
- 如果节点u比节点v深,交换u和v。
|
||
- 计算深度差diff,将u提升diff步,使u和v在同一深度。
|
||
2. **共同祖先查找**:
|
||
- 从最高位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. **节点编号**:确保节点编号从1开始或从0开始,并在代码中保持一致。
|
||
2. **树的根节点**:选择一个合适的根节点,通常选择1或给定的节点。
|
||
3. **LOG值的选择**:根据树的规模n,LOG应满足2^LOG > n。常见的取值是20,因为2^20约等于10^6。
|
||
4. **边的输入形式**:确保输入的边是无向的,并正确构建邻接表。
|
||
5. **处理根节点的情况**:在查询LCA时,如果一个节点是另一个节点的祖先,算法应能正确返回祖先节点。
|
||
|
||
## 五、优化与扩展
|
||
|
||
- **空间优化**:如果树较大,可以考虑压缩`up`数组或使用更高效的存储方式。
|
||
- **路径查询**:在二叉提升的基础上,可以集成其他信息(如路径和、最小值等)以支持更多复杂的查询。
|
||
- **其他LCA算法**:根据具体问题需求,选择合适的LCA算法可能更加高效。
|
||
|
||
希望以上内容对你理解和实现LCA有所帮助!如果有进一步的问题,欢迎继续提问。 |