最近公共祖先简称 LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。
这种算法应用很广泛,可以很容易解决树上最短路等问题。
为了方便,我们记某点集 的最近公共祖先为 或 。
部分内容参考 OI Wiki,文章中所有算法均使用C++实现。
简单来说,树的邻接表存储就是对于每个结点,存储其能通过一条有向或无向边,直接到达的所有结点。
传统的存储方式是使用链表(或模拟链表),这样实现比较麻烦,也容易写错。
此处为了更好的可读性我们使用STL
中的可变长度顺序表vector
。
#include <vector> // 需要使用STL中的vector
#define maxn 100005 // 最大结点个数
std::vector<int> G[maxn];
此时,若要添加一条无向边,可使用:
G[u].push_back(v);
G[v].push_back(u);
若要添加的有向边:
G[u].push_back(v);
遍历能直接到达的所有结点:
for(int u: G[v])
cout << u << endl;
对于两种算法,都需要预处理出每个结点的深度。
一个结点的深度定义为这个结点到树根的距离。
要预处理出所有结点的深度,很简单:
运用树形dp的方法,令 表示结点 的深度,逐层向下推进:
#include <cstdio>
#include <vector>
#define maxn 100005
using namespace std;
vector<int> G[maxn]; // 邻接表存储
int depth[maxn]; // 每个结点的深度
void dfs(int v, int par) // dfs(当前结点,父亲结点)
{
int d = depth[v] + 1; // 子结点的深度=当前结点的深度+1
for(int u: G[v])
if(u != par) // 不加这条判断会无限递归
{
depth[u] = d; // dp更新子结点深度
dfs(u, v); // 往下dfs
}
}
int main()
{
// 构建一张图
// ...
// 假定图已存入邻接表G:
int root = 0; // 默认树根为0号结点,根据实际情况设置
dfs(root, -1); // 对于根结点,父亲结点为-1即为无父亲结点
return 0;
}
令 表示两个待求 LCA 的结点。需提前预处理出每个结点的父亲(记结点 的父亲为 )。
算法步骤:
时间复杂度分析:
参考代码:
#include <cstdio>
#include <vector>
#include <algorithm>
#define maxn 500005
using namespace std;
vector<int> G[maxn];
int depth[maxn], par[maxn];
void dfs(int v)
{
int d = depth[v] + 1;
for(int u: G[v])
if(u != par[v])
{
par[u] = v, depth[u] = d;
dfs(u);
}
}
int lca(int u, int v)
{
if(depth[u] < depth[v])
swap(u, v);
while(depth[u] > depth[v])
u = par[u];
while(u != v)
u = par[u], v = par[v];
return u;
}
int main()
{
int n, q, root;
scanf("%d%d%d", &n, &q, &root);
for(int i=1; i<n; i++)
{
int u, v;
scanf("%d%d", &u, &v);
G[u].push_back(v);
G[v].push_back(u);
}
par[root] = -1, depth[root] = 0;
dfs(root);
while(q--)
{
int u, v;
scanf("%d%d", &u, &v);
printf("%d\n", lca(u, v));
}
return 0;
}
可以发现,程序在最后四个测试点上TLE
了:
这是因为,这四个点是专门针对朴素算法设计的(正好是一个 Subtask),使算法的时间复杂度达到了最坏情况 ,而 ,所以无法通过测试点。当然,朴素算法在随机树上回答 次询问的时间复杂度还是 ,被极端数据卡掉也没办法。
倍增算法是朴素算法的改进算法,也是最经典的 LCA 求法。
预处理:
1
的个数」次游标跳转(详见代码)。时间复杂度分析:
另外倍增算法可以通过交换 fa
数组的两维使较小维放在前面。这样可以减少 cache miss 次数,提高程序效率。
参考代码:
#include <cstdio>
#include <vector>
#include <cmath>
#define maxn 500005
using namespace std;
vector<int> G[maxn];
int fa[maxn][19]; // 2^19=524288
int depth[maxn];
void dfs(int v, int par)
{
fa[v][0] = par;
int d = depth[v] + 1;
for(int i=1; (1<<i)<d; i++)
fa[v][i] = fa[fa[v][i - 1]][i - 1];
for(int u: G[v])
if(u != par)
depth[u] = d, dfs(u, v);
}
inline int lca(int u, int v)
{
if(depth[u] < depth[v])
u ^= v ^= u ^= v;
int m = depth[u] - depth[v];
for(int i=0; m; i++, m>>=1)
if(m & 1)
u = fa[u][i];
if(u == v) return u; // 这句不能丢
for(int i=log2(depth[u]); i>=0; i--)
if(fa[u][i] != fa[v][i])
u = fa[u][i], v = fa[v][i];
return fa[u][0];
}
int main()
{
int n, q, root;
scanf("%d%d%d", &n, &q, &root);
for(int i=1; i<n; i++)
{
int x, y;
scanf("%d%d", &x, &y);
G[--x].push_back(--y);
G[y].push_back(x);
}
depth[--root] = 0;
dfs(root, -1);
while(q--)
{
int u, v;
scanf("%d%d", &u, &v);
printf("%d\n", lca(--u, --v) + 1);
}
return 0;
}
本文详细讲解了 LCA 问题以及求解 LCA 的两种算法。对比如下:
算法 | 预处理时间复杂度 | 单次查询时间复杂度[1] | 空间复杂度 | 能否通过例题[2]? |
---|---|---|---|---|
朴素算法 | ❌ | |||
倍增算法 | ✔️ |
创作不易,希望大家能给个三连,感谢支持!
此时间复杂度按照最坏情况计算。 ↩︎