1. 理解LCA问题与倍增算法
最近公共祖先(Lowest Common Ancestor,简称LCA)是树结构中的一个经典问题。给定一棵有根树和两个节点u、v,找到它们深度最大的公共祖先节点。这个问题在编译器优化、网络路由、生物信息学等领域都有广泛应用。
我第一次接触LCA问题时是在开发一个家谱关系查询系统。需要快速判断两个人的最近共同祖先,传统DFS方法在频繁查询时性能堪忧。后来发现了树上倍增这种优雅的解决方案——预处理O(nlogn),查询O(logn),特别适合需要大量查询的场景。
倍增算法的核心思想是"以空间换时间"。通过预处理每个节点向上2^k层的祖先信息,将查询时的线性跳跃转化为二进制分解的跳跃。这类似于跳表(Skip List)的设计理念,通过建立多级索引来加速查找过程。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 算法实现详解
2.1 数据结构设计
实现LCA倍增算法需要以下核心数据结构:
cpp复制const int MAXN = 1e5 + 5;
const int LOG = 20; // 足够覆盖树高:2^20 ≈ 1e6
vector<int> tree[MAXN]; // 树的邻接表表示
int depth[MAXN]; // 节点深度
int up[MAXN][LOG]; // up[u][k]表示u的2^k级祖先
选择邻接表存储树结构因其空间效率高(O(n)),且便于遍历。LOG值根据最大树高确定,通常20足够应对1e6级别的节点数。
2.2 预处理阶段实现
预处理分为两个DFS遍历:
cpp复制void dfs(int u, int parent) {
up[u][0] = parent; // 直接父节点
for(int k = 1; k < LOG; ++k) {
up[u][k] = up[up[u][k-1]][k-1]; // 动态规划填充祖先表
}
for(int v : tree[u]) {
if(v != parent) {
depth[v] = depth[u] + 1;
dfs(v, u);
}
}
}
这里的关键是动态规划填充up数组。up[u][k] = up[up[u][k-1]][k-1]表示:u的2^k祖先等于u的2^(k-1)祖先的2^(k-1)祖先。这种递推关系使得我们能用O(nlogn)的空间存储所有必要信息。
2.3 查询阶段实现
查询LCA的核心步骤如下:
cpp复制int lca(int u, int v) {
// 确保u是较深的节点
if(depth[u] < depth[v]) swap(u, v);
// 将u提升到与v同一深度
for(int k = LOG-1; k >= 0; --k) {
if(depth[u] - (1 << k) >= depth[v]) {
u = up[u][k];
}
}
// 如果此时u==v,说明v就是LCA
if(u == v) return u;
// 同时向上跳跃寻找LCA
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]; // 返回最终的父节点
}
这个实现有几个关键点:
- 总是从最大的步长开始尝试(LOG-1),这保证了跳跃效率
- 第二个循环中,当up[u][k] == up[v][k]时不跳,避免"跳过"LCA
- 最终返回的是up[u][0],因为循环结束时u和v处于LCA的直接子节
