【概念】
给定一棵树,若节点 z 既是结点 x 的祖先,也是节点 y 的祖先,并且在 x 和 y 的祖先中深度最大,称为 x , y 的 最近公共祖先(Lowest Common Ancestors)。
【算法】
1.向上标记法
从 x 向上走到根节点,并标记所有经过的点。
从 y 向上走到根节点,第一次遇到的已标记的节点即为 x 与 y 的 最近公共祖先 。
对于每个询问的时间复杂度为 O(n)。
<代码实现>
inline void dfs(int x,int fa){
for(int i=lin[x];i;i=e[i].Next){
if(e[i].Id==fa) continue;
Fa[e[i].Id]=x;
Dep[e[i].Id]=Dep[x]+1;
dfs(e[i].Id,x);
}
}
inline void Add_Tag(int x){
for(;Fa[x];++T[x],x=Fa[x]);
}
inline void Del_Tag(int x){
for(;Fa[x];--T[x],x=Fa[x]);
}
inline int Find_Lca(int x){
for(;Fa[x]&&!T[x];x=Fa[x]);
return x;
}
inline void Work(){
dfs(S,0);
for(int i=1;i<=M;i++){
int x,y; scanf("%d%d",&x,&y);
Add_Tag(x);
printf("%d\n",Find_Lca(y));
Del_Tag(x);
}
}
2.树上倍增法
设 f[i][j] 代表从编号为 i 的节点向上走 2^j 步到达的节点编号为 f[i][j] 。若该节点不存在,则令 f[i][j]=0 (令 f[i][j]=-1 也可以,但是需要注意以 f[i][j] 作下标可能越界)。
若 x 与 y 不在同一深度,则利用二进制拆分思想 将 x 与 y 调整至同一深度 。
若 此时 x = y ,则已经找到 LCA, LCA(x,y)= x 。
否则 用二进制拆分思想,将 x 与 y 同时向上调整,并保持 x!= y 。
设 x , y 向上移动D步所到达的节点为他们最近公的共祖先。
移动 x ,y 我们可以发现:
当已经枚举到第 k 位,通过一系列累加和(0/1*2^max_k+0/1*2^(max_k-1)+.....0/1*2^(k+1) )所得到的和为D'。
若 D'+2^k < D 则 x, y 向上走 D'+2^k 步到达的节点不是他们的公共祖先,即 f[x][k] != f[y][k] (x 与 y 在不断往上更新)。
若D'+2^k >= D 则 x, y 向上走D'+2^k 步到达的节点时他们的公共祖先,即 f[x][k] = f[y][k]
所以,当 f[x][k] != f[y][k] 时 x=f[x][k],y=f[y][k] (相当于D'+2^k) 我们保证了最后求的的 D' 比 D 小 1。
时间复杂度为 O((n+m)log n)。
<代码实现>
inline void dfs(int x,int fa){
for(int i=lin[x];i;i=e[i].Next){
if(e[i].Id==fa) continue;
Fa[e[i].Id]=x;
Dep[e[i].Id]=Dep[x]+1;
dfs(e[i].Id,x);
}
}
inline int Lca(int x,int y){
if(Dep[x]<Dep[y]) x^=y^=x^=y;
for(int i=lg;i>=0;i--)
if(Dep[f[x][i]]>=Dep[y])
x=f[x][i];
// for(int D=d[y]-d[x],i=0;D;D>>=1,i++)
// if(D&1)
// y=fa[y][i];//二进制拆分往上挪
if(x==y) return x;
for(int i=lg;i>=0;i--)
if(f[x][i]!=f[y][i])
x=f[x][i],y=f[y][i];
return f[x][0];
}
inline void Work(){
dfs(S,0);
for(int i=1;i<=N;i++) f[i][0]=Fa[i];
lg=(int)(log(N)/log(2))+1;
for(int i=1;i<=lg;i++)
for(int j=1;j<=N;j++)
f[j][i]=f[f[j][i-1]][i-1];
for(int i=1;i<=M;i++){
int x,y; scanf("%d%d",&x,&y);
printf("%d\n",Lca(x,y));
}
}
3.Tarjan算法(离线)
Tarjan算法基本上使用并查集对“向上标记法”的优化。将 m 个询问一次性读入,统一计算。
时间复杂度为:O(n+m)。
<代码实现>
inline int get(int x){
return x==Fa[x]?x:Fa[x]=get(Fa[x]);
}
inline void Tarjan(int x,int fa){
for(int i=lin[x];i;i=e[i].Next){
if(e[i].Id==fa) continue;
Tarjan(e[i].Id,x);
Fa[e[i].Id]=x;
}
for(int i=Lin[x];i;i=E[i].Next){
if(vis[E[i].Id]){
Ans[E[i].v]=get(E[i].Id);
}
}
++vis[x];
}
int main(){
...
for(int i=1;i<=M;i++){
int x,y; scanf("%d%d",&x,&y);
Insert(x,y,i); Insert(y,x,i);//Lin & E
}
for(int i=1;i<=N;i++) Fa[i]=i;
Tarjan(S,0);
...
}
4.树链剖分
跳到同一重链,答案即为深度较浅的点
inline void dfs(int x,int fa){
Size[x]=1;
for(int i=lin[x];i;i=e[i].Next){
if(e[i].Id==fa) continue;
Dep[e[i].Id]=Dep[x]+1;
Fa[e[i].Id]=x;
dfs(e[i].Id,x);
Size[x]+=Size[e[i].Id];
if(Size[e[i].Id]>Size[Son[x]]) Son[x]=e[i].Id;
}
}
inline void DFS(int x,int TOP){
Top[x]=TOP;
if(Son[x]) DFS(Son[x],TOP);
for(int i=lin[x];i;i=e[i].Next){
if(e[i].Id==Fa[x]||e[i].Id==Son[x]) continue;
DFS(e[i].Id,e[i].Id);
}
}
inline int Lca(int x,int y){
for(;Top[x]^Top[y];x=Fa[Top[x]])
if(Dep[Top[x]]<Dep[Top[y]]) x^=y^=x^=y;
if(Dep[x]<Dep[y]) x^=y^=x^=y;
return y;
}
int main(){
...
dfs(S,0);
DFS(S,S);
for(int i=1;i<=M;i++){
int x,y; scanf("%d%d",&x,&y);
printf("%d\n",Lca(x,y));
}
return 0;
}