LCA学习笔记
定义
最近公共祖先简称 LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。
求法
有多种求法,目前就学习了倍增和 dfs 序求 LCA ,等后面学新的了再加上。
前置知识: ST 表,dfs 序。
为方便说明,下面全都是求 \(x\) ,\(y\) 的 LCA,设其为 \(z\) 。
朴素算法
设 \(x\) 的深度大于 \(y\) 的深度。先将 \(x\) 上移到与 \(y\) 同一深度。再将 \(x\) 与 \(y\) 同时上移,当 \(x=y\) 时,即为 \(z\) 。
需要 dfs 预处理出深度。预处理 \(O(n)\) ,单次查询 \(O(n)\) 。
倍增算法
是朴素算法的优化,运用倍增思想,预处理出 \(an[k][x]\) 表示节点 \(x\) 的 \(2^k\) 级祖先,用 \(an[k][x]\) 来向上跳。
\(an[0][x]\) 等于 \(x\) 的父节点,其余令 \(an[k][x]=an[k-1][an[k-1][x]]\) ,即 \(x\) 的第 \(2^k\) 个祖先是 \(x\) 的第 \(2^{k-1}\) 个祖先的第 \(2^{k-1}\) 个祖先。
预处理 \(O(n \log n)\) ,查询 \(O(\log n)\) 。
模板题的代码:
点击查看代码
#include <iostream>
#include <cmath>
#define int long long
using namespace std;
const int Max = 5e5+10;
int n,m,root;
int an[25][Max];
int dep[Max];
struct EDGE{
int Head[Max*2],Next[Max*2],Ver[Max*2],tot;
inline void add(int x,int y){
Ver[++tot]=y,Next[tot]=Head[x],Head[x]=tot;
}
}E;
void build(int x,int fa)
{
dep[x]=dep[fa]+1;
for(int i=E.Head[x];i;i=E.Next[i]){
int y=E.Ver[i];
if(y==fa)continue;
an[0][y]=x;
for(int j = 1;j <= __lg(n);j++){
an[j][y]=an[j-1][an[j-1][y]];
}
build(y,x);
}
}
inline int LCA(int x,int y)
{
if(dep[x]<dep[y])swap(x,y);
while(dep[x]!=dep[y])
x=an[__lg(dep[x]-dep[y])][x];
if(x==y)return x;
for(int i=__lg(n);i>=0;i--){
if(an[i][x]!=an[i][y]) x=an[i][x],y=an[i][y];
}
return an[0][x];
}
inline int read(){
int num=0,fl=1;char c=getchar();
while(c<'0'||c>'9'){
if(c=='-') fl=-1;
c=getchar();
}
while(c >='0'&&c <='9'){
num=(num<<3)+(num<<1)+(c^48);
c=getchar();
}
return num*fl;
}
signed main(){
n=read(),m=read(),root=read();
for(int i = 1;i < n;i++){
int x=read(),y=read();
E.add(x,y);
E.add(y,x);
}
build(root,0);
while(m--){
int x=read(),y=read();
printf("%lld\n",LCA(x,y));
}
return 0;
}
DFS序求LCA
定义
DFS 序:对一棵树进行 DFS 后得到的节点序列。在 DFS 序中,祖先节点一定在后代节点前。
dfn :节点在 DFS 中被遍历到的顺序。
过程
设 \(x\) 的 dfn 小于 \(y\) 的 dfn。
性质:当 \(x\) 不是 \(y\) 的祖先时,\(d\) 一定不在 \(dfn[x]\) 到 \(dfn[y]\) 的序列中。
设 \(u\) 为 \(z\) 的儿子且子树中包含 \(y\) ,则 \(dfn[u]\) 一定在 \(dfn[x]\) 到 \(dfn[y]\) 的序列中(具体可以画个图看看)。这就说明当 \(x\) 不是 \(y\) 的祖先时,只需求 \(dfn[x]\) 到 \(dfn[y]\) 的序列中深度最小的任意一个点,它的父节点就是 \(z\) 。这就转化为了 RMQ 问题,可以用 st 表来求解。
当 \(x\) 是 \(y\) 的祖先时,我们可以求 \(dfn[x+1]\) 到 \(dfn[y]\) 的序列中深度最小的任意一个点,其父亲就是 \(z\),即 \(x\) 。
唯一需要特判的情况是当 \(x=y\) 时。
预处理ST表 \(O(n\log n)\) ,单次查询 \(O(1)\) 。
模板题的代码:
点击查看代码
#include <iostream>
#include <cmath>
#define int long long
using namespace std;
const int Max = 5e5+10;
int n,m,root;
int dfn[Max],tot;
struct EDGE{
int Head[Max*2],Next[Max*2],Ver[Max*2],tot;
inline void add(int x,int y){
Ver[++tot]=y,Next[tot]=Head[x],Head[x]=tot;
}
}E;
namespace st{
int st[25][Max];
inline int Min(int x,int y){return dfn[x]<dfn[y]?x:y;}
inline void build()
{
for(int j = 1;j <= __lg(n);j++)
for(int i = 1;i+(1<<j)-1<=n;i++)
st[j][i]=Min(st[j-1][i],st[j-1][i+(1<<j-1)]);
}
inline int getMin(int l,int r)
{
int k=__lg(r-l+1);
return Min(st[k][l],st[k][r-(1<<k)+1]);
}
};
void dfs(int x,int fa){
dfn[x]=++tot;
st::st[0][dfn[x]]=fa;
for(int i = E.Head[x];i;i=E.Next[i]){
int y=E.Ver[i];
if(y==fa)continue;
dfs(y,x);
}
}
inline int LCA(int x,int y){
if(x==y)return x;
x=dfn[x],y=dfn[y];
if(x>y) swap(x,y);
return st::getMin(x+1,y);
}
inline int read(){
int num=0,fl=1;char c=getchar();
while(c<'0'||c>'9'){
if(c=='-') fl=-1;
c=getchar();
}
while(c >='0'&&c <='9'){
num=(num<<3)+(num<<1)+(c^48);
c=getchar();
}
return num*fl;
}
signed main(){
n=read(),m=read(),root=read();
for(int i = 1;i < n;i++){
int x=read(),y=read();
E.add(x,y);
E.add(y,x);
}
dfs(root,0);
st::build();
while(m--){
int x=read(),y=read();
printf("%lld\n",LCA(x,y));
}
return 0;
}