文章目录
一、算法概述
- 问题定义:最近公共祖先(LCA, Lowest Common Ancestor)指树中两个节点的所有公共祖先中深度最大的那个节点。例如,在二叉树中,节点A和节点B的最近公共祖先即为离它们最近的共同祖先节点。
- 核心思想:利用倍增法(Binary Lifting)预处理每个节点的二进制级祖先,通过跳跃式向上查找,将LCA查询复杂度优化至对数级别。
- 应用场景:
- 树结构中的路径查询(如求两点间距离)
- 网络流中的汇点分析
- 数据库层级结构查询
二、算法思路
- 预处理阶段:
- 构建每个节点的
2^j
级祖先表f[i][j]
,其中f[i][j]
表示节点i
的第2^j
个祖先。 - 同时记录每个节点的深度
dep[i]
,用于后续深度对齐。
- 构建每个节点的
- 查询阶段:
- 先将两个节点调整到同一深度。
- 从最大的跳跃步长开始,同时向上跳跃两个节点,直到找到共同祖先。
- 倍增原理:通过二进制分解跳跃步长(如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
深度相同。 - 共同跳跃:从最大步长开始,若
u
和v
的2^i
级祖先不同,则同时跳跃,避免跳过LCA。 - 终止条件:当
u
和v
的2^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]=1
,f[6][1]=1
,此时f[2][0]=1
和f[6][0]=5
不同;i=0时,f[2][0]=1
,f[6][0]=5
不同,跳跃后u=1,v=5。 - 最终
f[1][0]=0
,f[5][0]=1
,返回f[1][0]=1,即LCA为1。
- 深度对齐:
4. 虚拟根节点处理
- 当节点无父节点(如根节点)时,
f[root][0]
设为dummyroot
,避免空指针异常。
五、复杂度分析
- 时间复杂度:
- 预处理:O(n log n),BFS遍历O(n),每个节点计算O(log n)级祖先。
- 单次查询:O(log n),两次二进制跳跃(深度对齐和共同跳跃)。
- 总复杂度:O(n log n + q log n),其中q为查询次数。
- 空间复杂度:
- 祖先表
f
:O(n log n),每个节点存储log n级祖先。 - 邻接表和深度数组:O(n)。
- 总空间:O(n log n)。
- 祖先表
- 优化点:
- 若树的深度较小,可减小
maxd
值(如设为log2(n)+1),减少空间占用。 - 对于有根树,可直接指定根节点;若无根树,可任选根节点(如1号节点)。
- 若树的深度较小,可减小
本文为作者(英雄哪里出来)在抖音的独家课程《英雄C++入门到精通》、《英雄C语言入门到精通》、《英雄Python入门到精通》三个课程的配套文字讲解,如需了解算法视频课程,请移步 作者本人 的抖音直播间。