一、最近公共祖先定义
在一棵树上,对于节点 x , y x,y x,y而言,如果说 z z z节点既是 x x x的祖先节点,也是 y y y的祖先节点,那么我们认为 z z z节点是 ( x , y ) (x,y) (x,y)的公共祖先节点。
公共祖先节点有很多,那么深度最大的节点,被称之为最近公共祖先,记为 l c a ( x , y ) lca(x,y) lca(x,y)。
二、倍增法(在线求lca)
1.朴素法
将其中一个节点 x x x向根节点移动,中途经过的点做标记。再将另一个节点 y y y向上移动,移动到第一个标记过的点,即为最近公共祖先节点。
2.倍增法
可以考虑一次性移动不止 1 1 1个距离。我们知道,任何一个整数都可以分解为若干个 2 i 2^i 2i求和,所以我们就采用每次移动 2 i 2^i 2i距离的方法。
我们需要预处理( b f s bfs bfs)两个数组depth[]
和fa[][]
。其中depth数组,记录节点的深度,根节点深度为 1 1 1。 f a [ j ] [ k ] fa[j][k] fa[j][k]记录第 j j j个节点向上移动 2 k 2^k 2k步到达的节点。对于fa数组的初始化,存在一个递推关系,就是先向上跳 2 k − 1 2^{k-1} 2k−1步,然后再在那个点的基础上,再跳 2 k − 1 2^{k-1} 2k−1步。即, f a [ j ] [ k ] = f a [ f a [ j ] [ k − 1 ] ] [ k − 1 ] fa[j][k] = fa[fa[j][k-1]][k-1] fa[j][k]=fa[fa[j][k−1]][k−1]。
算法步骤如下:
- 将深度更大的节点移动到与另一个节点同等深度的地方
- 两个节点一起向上移动,一直移动到最近公共祖先的儿子节点
- 返回当前点的父亲节点,即为两点的lca
代码模板:
场景1
给定一棵包含 n n n个节点的有根无向树,节点编号互不相同,但不一定是 1 ∼ n 1∼n 1∼n。有 m m m个询问,每个询问给出了一对节点的编号 x x x和 y y y,询问 x x x与 y y y的祖孙关系。对于每一个询问,若 x x x是 y y y的祖先则输出 1 1 1,若 y y y是 x x x的祖先则输出 2 2 2,否则输出 0 0 0。
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 40010, M = 2 * N;
int n,m;
int h[N],e[M],ne[M],idx;
int depth[N],fa[N][16];
queue<int> que;
void add(int a,int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
void bfs(int root)
{
memset(depth,0x3f,sizeof(depth));
depth[0] = 0;
depth[root] = 1;
que.push(root);
while(que.size()){
int t = que.front();
que.pop();
for(int i=h[t];~i;i=ne[i]){
int j = e[i];
if(depth[j]>depth[t]+1){
depth[j] = depth[t] + 1;
fa[j][0] = t;
que.push(j);
for(int k=1;k<=15;k++){
fa[j][k] = fa[fa[j][k-1]][k-1];
}
}
}
}
}
int lca(int a,int b)
{
if(depth[a]<depth[b]) swap(a,b);
for(int k=15;k>=0;k--){
if(depth[fa[a][k]]>=depth[b]){
a = fa[a][k];
}
}
if(a==b) return a;
for(int k=15;k>=0;k--){
if(fa[a][k]!=fa[b][k]){
a = fa[a][k];
b = fa[b][k];
}
}
return fa[a][0];
}
int main()
{
cin >> n;
memset(h,-1,sizeof(h));
int root = 0;
for(int i=1;i<=n;i++){
int a,b;
cin >> a >> b;
if(b==-1) root = a; //题目特定输入规则
add(a,b);
add(b,a);
}
cin >> m;
bfs(root);
while(m--){
int a,b;
cin >> a >> b;
int p = lca(a,b);
if(p==a) puts("1");
else if(p==b) puts("2");
else puts("0");
}
return 0;
}
场景2
利用lca可以很容易求出树上任意两点的距离, d i s t ( a , b ) = d i s t ( a , r o o t ) + d i s t ( b , r o o t ) − 2 ∗ d i s t ( l c a ( a , b ) , r o o t ) dist(a,b) = dist(a,root) + dist(b,root) - 2*dist(lca(a,b),root) dist(a,b)=dist(a,root)+dist(b,root)−2∗dist(lca(a,b),root)
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 10010 , M = 2*N;
int n,m;
int h[N] , e[M] , ne[M] , w[M] , idx;
int depth[N],fa[N][16];
int dist[N];
queue<int> que;
void add(int a,int b,int c)
{
e[idx] = b , w[idx] = c , ne[idx] = h[a] , h[a] = idx ++;
}
void bfs(int root)
{
memset(depth,0x3f,sizeof(depth));
memset(dist,0x3f,sizeof(dist));
depth[0] = 0;
depth[root] = 1;
dist[root] = 0;
que.push(root);
while(que.size()){
int t = que.front();
que.pop();
for(int i=h[t];~i;i=ne[i]){
int j = e[i];
if(depth[j]>depth[t]+1){
depth[j] = depth[t] + 1;
dist[j] = dist[t] + w[i];
fa[j][0] = t;
que.push(j);
for(int k=1;k<=15;k++){
fa[j][k] = fa[fa[j][k-1]][k-1];
}
}
}
}
}
int lca(int a,int b)
{
if(depth[a]<depth[b]) swap(a,b);
for(int k=15;k>=0;k--){
if(depth[fa[a][k]]>=depth[b]){
a = fa[a][k];
}
}
if(a==b) return a;
for(int k=15;k>=0;k--){
if(fa[a][k]!=fa[b][k]){
a = fa[a][k];
b = fa[b][k];
}
}
return fa[a][0];
}
int main()
{
cin >> n >> m;
memset(h,-1,sizeof(h));
for(int i=1;i<n;i++){
int a,b,c;
cin >> a >> b >> c;
add(a,b,c);
add(b,a,c);
}
int root = 1;
bfs(root);
while(m--){
int a,b;
cin >> a >> b;
int p = lca(a,b);
cout << dist[a] + dist[b] - 2*dist[p] << endl;
}
return 0;
}
场景3
利用lca也可以判断一个节点是否在另一个节点的子树里,即判断lca(a,b)==a||lca(a,b)==b
场景4
如果三个点需要汇聚到某个点,问最短距离和(树的边长都为 1 1 1)。汇聚到的这个点不一定是这三个点的lca,但是可以这样做:设三点分别为 a , b , c a,b,c a,b,c,令 p 1 = l c a ( a , b ) , d 1 = l c a ( p 1 , c ) p_1 = lca(a,b),d_1 = lca(p_1,c) p1=lca(a,b),d1=lca(p1,c)。同理可以求出 p 2 , d 2 , p 3 , d 3 p_2,d_2,p_3,d_3 p2,d2,p3,d3。最后比较一下到 d 1 , d 2 , d 3 d_1,d_2,d_3 d1,d2,d3的距离即可。