题目描述
给你一棵有根树,要求你计算出指定两个结点的最近公共祖先。
输入格式
第一行为结点个数
n
n
n,结点编号为
1
1
1 到
n
n
n。
接下来
n
−
1
n − 1
n−1 行,每行两个整数,表示第一个结点是第二个结点的父亲。
接下来的一行为两个整数
a
a
a 和
b
b
b,要求计算出结点
a
a
a 和
b
b
b 的最近公共祖先。
输出格式
一行一个整数为最近公共祖先的编号。
样例
样例输入1:
5
2 3
3 4
3 1
1 5
3 5
样例输出1:
3
数据范围
2 ≤ n ≤ 5 × 1 0 5 2 \le n \le 5 \times 10^5 2≤n≤5×105
题解
这是一道经典的模板题。
如果我们直接去暴力枚举每个点进行判断,复杂度很高。
改进算法,从两个点开始,直接向上跳,如果跳到相等的数,就碰到祖先了。
先用 dfs
将两个点的深度求出来,跳到同一高度(如果两个点不在同一深度,不可能相等)。
如果在这个深度两个点已经相等,此时该点为最近公共祖先。
接下来向上跳,跳到碰到相等的数停下,此时该点就为最近公共祖先。
单次询问复杂度为
Θ
(
d
e
p
)
\Theta(dep)
Θ(dep)(
d
e
p
dep
dep 为深度)。
int t1 = dep[x], t2 = dep[y];//x, y 的深度
if(t1 < t2){//保持 t1 深度大
swap(t1, t2);
swap(x, y);
}
//跳到同一高度
while(t1 > t2){
x = fa[x];
-- t1;
}
if(x == y){
printf("%d", x);
continue;
}
while(t1){
x = fa[x];
y = fa[y];
if(x == y){//相等,找到了最近公共祖先
printf("%d\n", x);
break;
}
-- t1;
}
考虑使用倍增进行优化。
预处理:
设
f
i
,
j
f_{i, j}
fi,j 表示第
i
i
i 个点,向上跳
2
j
2^j
2j 步,来到第
f
i
,
j
f_{i, j}
fi,j 个点。
显然,
f
i
,
0
f_{i, 0}
fi,0 就是第
i
i
i 个点的父亲。
接下来进行合并,
f
i
,
j
=
f
[
f
i
,
j
−
1
,
j
]
f_{i, j} = f[f_{i, j - 1}, j]
fi,j=f[fi,j−1,j](从第
i
i
i 个点,先跳
2
j
−
1
2^{j - 1}
2j−1 步,再跳
2
j
−
1
2^{j - 1}
2j−1 步就相当于跳
2
j
2^j
2j 步)。
for(int i = 1; i <= 25; ++ i){//跳 2^i 步
for(int j = 1; j <= n; ++ j){//第 j 个点
f[i][j] = f[f[i][j - 1][j - 1];
}
}
查询:
- 跳到同一深度。
先保持 d e p x > d e p y dep_x > dep_y depx>depy。
如果 d e p x − d e p y dep_x - dep_y depx−depy 的第 i i i 位为 1 1 1(二进制),则 x x x 向上跳 2 i 2^i 2i 步。
int tt = t1 - t2;//深度差
for(int i = 0; i <= 25; ++ i){
if(tt & 1){//第 i 位为 1
x = f[x][i];
}
tt /= 2;
}
- 两点同时向上跳
由于直接向上不知道跳多长,考虑换一种思路,找最近公共祖先的下面最近的点。
如果当前点 x x x 和 y y y 向上跳 2 i 2^i 2i 步已经相等,说明跳上去的点已经是祖先了,进入下一次循环。
否则, x x x 和 y y y 都向上跳 2 i 2^i 2i 步。
答案就为最终的 x x x 或 y y y 的父亲。
for(int i = 25; i >= 0; -- i){//从大到小
if(f[x][i] != f[y][i]){//不同,向上跳
x = f[x][i];
y = f[y][i];
}
}
printf("%d", f[x][0]);
结合上文,我们就可以写出代码了。
//求每个点的深度
void dfs(int x, int y, int fa){
dep[x] = y;
f[x][0] = fa;//第 x 个点向上 1 步就是父亲
for(auto i : v[x]){
if(i == fa){
continue;
}
dfs(i, y + 1, x);
}
}
bool fl[200010];
int main(){
输入 n
for(int i = 1; i < n; ++ i){
int x, y;
scanf("%d %d", &x, &y);
fl[y] = 1;//标记 y 有父亲
//x 和 y 连边
v[x].push_back(y);
v[y].push_back(x);
}
//找 rt
int rt;
for(int i = 1; i <= n; ++ i){
if(!fl[i]){//没有父亲
rt = i;
break;
}
}
dfs(rt, 1, 0);
预处理 f 数组,进行查询
}