最近公共祖先
对于有根树T的两个结点u和v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u和v的祖先且x的深度尽可能得大。
最近公共祖先有多种求法,下面介绍常见的几种。
朴素解法
这种解法相当于找相交链表的第一个交点:可以先将深度大的节点往上调使两节点深度相同,然后两个节点一起往上调,直到两个节点相同为止;也可以任选一个节点记录它到根节点的路径经过的所有节点,然后从另一个节点往上走,经过的第一次出现在记录中的节点即为所求。
转化为RMQ求解
转化时又有两种做法:
一种是先根遍历求出一个长度为2n-1的序列seq[]以及对应的深度h[][0],并维护各点第一次出现在序列中的位置first[],预处理出区间深度最小值h[][],那么求u和v的LCA转化为求区间first[u]和first[v]内深度取最小值对应的点。
另一种采用树上倍增思想,记f[i][j]表示节点i的2^j的祖先,那么显然f[i][0]为父节点,f[i][j] = f[f[i][j-1]][j-1],求u和v的LCA时,先将深度大的节点往上调到与另一节点同一高度,然后一起往上调。
int n, m, u, v, f[100005][20], h[100005];
vector<int> g[100005];
void dfs(int c, int d, int p) {
h[c] = d; f[c][0] = p;
for (size_t i = 0; i < g[c].size(); i++)
if (g[c][i] != p)
dfs(g[c][i], d + 1, c);
}
int lca(int x, int y) {
if (h[x] < h[y]) swap(x, y);
int d = h[x] - h[y];
for (int i = 19; i >= 0; i--) {
if ((1 << i) & d)
x = f[x][i];
}
if (x == y) return x;
for (int i = 19; i >= 0; i--) {
if (f[x][i] != f[y][i]) {
x = f[x][i];
y = f[y][i];
}
}
return f[x][0];
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i < n; i++) {
scanf("%d%d", &u, &v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1, 1, 1);
for (int j = 1; j <= 19; j++)
for (int i = 1; i <= n; i++)
f[i][j] = f[f[i][j-1]][j-1];
for (int i = 0; i < m; i++) {
scanf("%d%d", &u, &v);
printf("%d\n", lca(u, v));
}
return 0;
}
两种做法都是在线的,第2种做法的时空间复杂度稍更优一些。
Tarjan算法
树链剖分做法
这是另外一种在线做法,思路是先做树链剖分,然后分别处理各种询问。
例如,询问LCA(a, b):
- 如果top[a]等于top[b],那么a与b处于同一条链中,深度较小的就是LCA;
- 否则,LCA必然不在深度大的链中,因此可将处于深链的点x往上调至fa[top[x]];
const int N = 500005;
int n, m, s, a, b;
int fa[N], dep[N], siz[N], son[N], top[N];
vector<int> g[N];
void dfs1(int x, int u, int h) {
fa[x] = u; dep[x] = h; siz[x] = 1;
for (auto v : g[x]) {
if (v != u) {
dfs1(v, x, h + 1);
siz[x] += siz[v];
if (siz[v] > siz[son[x]])
son[x] = v;
}
}
}
void dfs2(int x, int u, int t) {
top[x] = t;
if (son[x] == 0) return;
dfs2(son[x], x, t);
for (auto v : g[x]) {
if (v != u && v != son[x])
dfs2(v, x, v);
}
}
int lca(int u, int v) {
while (top[u] != top[v]) {
if (dep[top[u]] > dep[top[v]])
u = fa[top[u]];
else
v = fa[top[v]];
}
return dep[u] < dep[v] ? u : v;
}
int main() {
scanf("%d%d%d", &n, &m, &s);
for (int i = 1; i < n; i++) {
scanf("%d%d", &a, &b);
g[a].push_back(b);
g[b].push_back(a);
}
dfs1(s, 0, 1);
dfs2(s, 0, s);
for (int i = 0; i < m; i++) {
scanf("%d%d", &a, &b);
printf("%d\n", lca(a, b));
}
return 0;
}
应用
- 树上任意两点u,v之间的距离:dist(u,v) = dist(root,u) + dist(root,v) - 2 * dist(root,lca(u,v));