《最近公共祖先》基础概念

一、算法概述

  1. 问题定义:最近公共祖先(LCA, Lowest Common Ancestor)指树中两个节点的所有公共祖先中深度最大的那个节点。例如,在二叉树中,节点A和节点B的最近公共祖先即为离它们最近的共同祖先节点。
  2. 核心思想:利用倍增法(Binary Lifting)预处理每个节点的二进制级祖先,通过跳跃式向上查找,将LCA查询复杂度优化至对数级别。
  3. 应用场景
    • 树结构中的路径查询(如求两点间距离)
    • 网络流中的汇点分析
    • 数据库层级结构查询

二、算法思路

  1. 预处理阶段
    • 构建每个节点的2^j级祖先表f[i][j],其中f[i][j]表示节点i的第2^j个祖先。
    • 同时记录每个节点的深度dep[i],用于后续深度对齐。
  2. 查询阶段
    • 先将两个节点调整到同一深度。
    • 从最大的跳跃步长开始,同时向上跳跃两个节点,直到找到共同祖先。
  3. 倍增原理:通过二进制分解跳跃步长(如1, 2, 4, 8…),将O(n)的线性查找优化为O(log n)的跳跃查找。

三、伪代码实现

1. 数据结构与全局变量

常量 maxn = 100010  # 最大节点数
常量 maxd = 18      # 最大倍增层数(2^18 ≈ 26万,足够覆盖1e5节点的树)
常量 dummyroot = 0  # 虚拟根节点(若树无指定根节点)
二维数组 f[maxn][maxd]  # f[i][j]:i的2^j级祖先
数组 dep[maxn]  # dep[i]:节点i的深度
数组 child[maxn]  # 邻接表存储树结构

2. 核心函数框架

算法:倍增法求最近公共祖先(LCA)
输入:树结构、查询节点u和v
输出:u和v的最近公共祖先节点

函数 LCA_Init(n):
    # 初始化树结构
    for i from 1 to n:
        child[i].clear()  # 清空邻接表
    memset(f, dummyroot, sizeof(f))  # 祖先表初始化为虚拟根

函数 LCA_AddEdge(u, v):
    # 添加无向边(树为无向图)
    child[u].push_back(v)
    child[v].push_back(u)

函数 LCA_PreProcess(root):
    # 预处理祖先表和深度数组(BFS遍历)
    队列 q
    f[root][0] = dummyroot  # 根节点的直接祖先为虚拟根
    dep[root] = 0  # 根节点深度为0
    q.push(root)
    
    while q不为空:
        u = q.front()
        q.pop()
        
        # 计算2^j级祖先(j从1到maxd-1)
        for i from 1 to maxd-1:
            f[u][i] = f[ f[u][i-1] ][i-1]  # 倍增转移
            if f[u][i] == dummyroot:
                break  # 超出树深度,提前终止
        
        # 处理子节点
        for v in child[u]:
            if v == f[u][0]:  # 跳过父节点(避免循环)
                continue
            f[v][0] = u  # 直接父节点为u
            dep[v] = dep[u] + 1  # 深度比父节点大1
            q.push(v)

函数 LCA_Get(u, v):
    # 查询u和v的最近公共祖先
    # 步骤1:确保u的深度不小于v
    if dep[u] < dep[v]:
        return LCA_Get(v, u)  # 交换u和v
    
    # 步骤2:将u向上跳跃,使u和v深度相同
    for i from maxd-1 down to 0:
        if dep[u] - (1<<i) >= dep[v]:  # 可以跳跃2^i步
            u = f[u][i]
    if u == v:
        return u  # 深度对齐后相等,直接返回
    
    # 步骤3:同时向上跳跃,直到找到共同祖先
    for i from maxd-1 down to 0:
        if f[u][i] != f[v][i]:  # 跳跃后不相遇
            u = f[u][i]
            v = f[v][i]
    
    # 步骤4:最终u和v的父节点即为LCA
    return f[u][0]

# 主程序(示例)
输入 n  # 节点数
LCA_Init(n)
输入 n-1 条边,调用LCA_AddEdge(u, v)
输入 root  # 根节点
LCA_PreProcess(root)
输入查询次数 q
for each query:
    输入 u, v
    输出 LCA_Get(u, v)

四、算法解释

1. 预处理过程(LCA_PreProcess)

  • BFS遍历:从根节点出发,逐层访问树节点,确保父节点先于子节点处理。
  • 祖先表构建:利用递推公式f[u][j] = f[f[u][j-1]][j-1],表示节点u的第2^j级祖先等于其第2^(j-1)级祖先的第2^(j-1)级祖先。例如:
    • f[u][1]u的父节点(2^1级祖先)
    • f[u][2]u的祖父节点(2^2级祖先)
  • 深度记录:每个节点的深度等于父节点深度+1,根节点深度为0。

2. 查询过程(LCA_Get)

  • 深度对齐:若u的深度小于v,交换两者;否则通过二进制分解(如16, 8, 4…)让u向上跳跃,直到与v深度相同。
  • 共同跳跃:从最大步长开始,若uv2^i级祖先不同,则同时跳跃,避免跳过LCA。
  • 终止条件:当uv2^i级祖先相同时,它们的父节点即为LCA。

3. 示例演示

  • 树结构:根节点为1,子树结构为1-2-3-4,1-5-6
  • 预处理祖先表
    • f[4][0]=3, f[4][1]=2, f[4][2]=1, f[4][3]=0(虚拟根)
    • f[6][0]=5, f[6][1]=1, f[6][2]=0
  • 查询LCA(4,6)
    • 深度对齐:dep[4]=3, dep[6]=2,将4向上跳1步(2^1)到2(深度2)。
    • 共同跳跃:从i=1开始,f[2][1]=1f[6][1]=1,此时f[2][0]=1f[6][0]=5不同;i=0时,f[2][0]=1f[6][0]=5不同,跳跃后u=1,v=5。
    • 最终f[1][0]=0f[5][0]=1,返回f[1][0]=1,即LCA为1。

4. 虚拟根节点处理

  • 当节点无父节点(如根节点)时,f[root][0]设为dummyroot,避免空指针异常。

五、复杂度分析

  1. 时间复杂度
    • 预处理:O(n log n),BFS遍历O(n),每个节点计算O(log n)级祖先。
    • 单次查询:O(log n),两次二进制跳跃(深度对齐和共同跳跃)。
    • 总复杂度:O(n log n + q log n),其中q为查询次数。
  2. 空间复杂度
    • 祖先表f:O(n log n),每个节点存储log n级祖先。
    • 邻接表和深度数组:O(n)。
    • 总空间:O(n log n)。
  3. 优化点
    • 若树的深度较小,可减小maxd值(如设为log2(n)+1),减少空间占用。
    • 对于有根树,可直接指定根节点;若无根树,可任选根节点(如1号节点)。

  本文为作者(英雄哪里出来)在抖音的独家课程《英雄C++入门到精通》、《英雄C语言入门到精通》、《英雄Python入门到精通》三个课程的配套文字讲解,如需了解算法视频课程,请移步 作者本人 的抖音直播间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

英雄哪里出来

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值