前言
最近公共祖先是一种求树上两点最近的共同祖先的算法。
题目大意
如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先。
对于 100 % 100\% 100% 的数据, 1 ≤ N , M ≤ 5 × 1 0 5 1 \leq N,M\leq 5\times 10^5 1≤N,M≤5×105, 1 ≤ x , y , a , b ≤ N 1 \leq x, y,a ,b \leq N 1≤x,y,a,b≤N,不保证 a ≠ b a \neq b a=b。
思路
首先暴力做法很好想,就是先把两点的深度弄成一样的,让较深的点一步一步往上爬,之后再让两个点同时往上爬,时间复杂度 O ( n ) \mathcal{O}(n) O(n),这是不行的。
倍增法优化。设 f a [ u ] [ i ] fa[u][i] fa[u][i] 为 u u u 的第 2 i 2^i 2i 个祖先。之后就考虑直接跳那么多层变成 O ( log n ) \mathcal{O}(\log{n}) O(logn) 的时间复杂度查询一次。
预处理就不说了,很简单。一会儿写注释里。
之后先考虑调到同一深度,两点 u , v u,v u,v,不妨设 v v v 的深度大于 u u u。那么 v v v 需要跳 d e p [ v ] − d e p [ u ] dep[v]-dep[u] dep[v]−dep[u] 层。那么,如果对这个数进行二进制分解,如果从右往左第 i i i 位是 1 1 1,那么就可以往上跳 2 i − 1 2^{i-1} 2i−1 层。这样深度就一样了。
之后枚举每一个 i i i,也就是指数,从大到小。如果祖先不一样了,就说明两个点肯定到那里都不相聚,就往上跳。最后相遇了为止。
代码
#include <iostream>
using namespace std;
typedef long long ll;
const ll MAXN=1e6+5;
struct edge{
ll to,nxt;
}e[MAXN];
ll head[MAXN],tot,n,m,s,fa[MAXN][37],dep[MAXN];
void add(ll u,ll v){
e[++tot].nxt=head[u];
e[tot].to=v;
head[u]=tot;
}
void dfs(ll u,ll f){
fa[u][0]=f;//往上一级是父亲
dep[u]=dep[f]+1;
for (int i = 1; i <=31 ; ++i) {
fa[u][i]=fa[fa[u][i-1]][i-1];//2^i=2^{i-1}+2^{i-1}
}
for (ll i = head[u]; i ; i=e[i].nxt) {
ll v=e[i].to;
if(v!=f){
dfs(v,u);
}
}
}
ll lca(ll u,ll v){
if(dep[u]>dep[v]){
swap(u,v);//v的深度大于u
}
ll y=dep[v]-dep[u];
for (int i = 0; y ; ++i) {
if(y&1){
v=fa[v][i];//二进制分解如果为1就往上跳
}
y>>=1;
}
if(u==v){
return u;//如果都碰上了就直接返回
}
for (int i = 30; i>=0; --i) {
if(fa[u][i]!=fa[v][i]){
u=fa[u][i];//祖先不同就都往上跳
v=fa[v][i];//同上
}
}
return fa[u][0];//跳到最低位,再往上就一样了。
}
int main(){
scanf("%lld%lld%lld",&n,&m,&s);
for (int i = 1; i <n ; ++i) {
ll u,v;
scanf("%lld%lld",&u,&v);
add(u,v);
add(v,u);
}
dfs(s,0);
for (int i = 1; i <=m ; ++i) {
ll u,v;
scanf("%lld%lld",&u,&v);
printf("%lld\n", lca(u,v));
}
return 0;
}
总结
学习了通过倍增的方法来大幅度优化暴力LCA的时间复杂度。