algorithm_2024/src/20241118/LCA.md
2024-11-19 18:47:13 +08:00

5.4 KiB
Raw Blame History

当然可以LCALowest 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实现示例

#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值的选择根据树的规模nLOG应满足2^LOG > n。常见的取值是20因为2^20约等于10^6。
  4. 边的输入形式:确保输入的边是无向的,并正确构建邻接表。
  5. 处理根节点的情况在查询LCA时如果一个节点是另一个节点的祖先算法应能正确返回祖先节点。

五、优化与扩展

  • 空间优化:如果树较大,可以考虑压缩up数组或使用更高效的存储方式。
  • 路径查询:在二叉提升的基础上,可以集成其他信息(如路径和、最小值等)以支持更多复杂的查询。
  • 其他LCA算法根据具体问题需求选择合适的LCA算法可能更加高效。

希望以上内容对你理解和实现LCA有所帮助如果有进一步的问题欢迎继续提问。