最近公共祖先LCA
两个结点x和y,在他们的所有公共祖先节点中深度最大的这就是他们的最近公共祖先,记作LCA(x,y)。
常用性质:
- 单点LCA是其本身:LCA(u)=u;
- u是v的祖先时LCA(u,v)=u;
- 若u和v互不为对方的祖先,则它们分别位于LCA(u,v)的不同子树中;
- 在前序遍历中,LCA(S)出现在集合S所有元素之前;在后序遍历中,LCA(S)出现在集合S所有元素之后;
- 两点的 LCA 必位于它们之间的最短路径上;
- 两点距离公式:dis(u,v)=dep[u]+dep[v]-2dep[LCA(u,v)],其中dis表示树上距离,dep表示节点到根节点的距离。
实现法1:涂色
从x点开始往祖宗节点染色,再从y点开始往祖宗节点跑,如果某个祖宗节点被染色了,深度最大的就是两点的最近公共祖先。
时间复杂度:O(n),没啥必要。
实现法2:倍增
在原先涂色的基础上运用倍增的思想来加速。
众所周知,每个数都可以被划分成若干个二次幂,那么我们可以预处理出每个点的二次幂祖宗。
要求LCA时先把深度大的结点X跳到深度小的结点Y同一层,如果此时已经到同一点了,说明Y点就是最近公共祖先。
否则然后让两个结点再同时往上跳,但不能跳到同一点,否则可能把最近公共祖先跳过了,最后跳不动了表示最近公共祖先就在头顶,直接输出X点的父亲。
这样跳的时候要把跳的几个节点划分成若干个二次幂,由于我们已经预处理出了每个结点的二次幂祖宗,所以跳的时候可以直接查询到他的二次幂祖宗。
时间复杂度:预处理O(nlongn),查询O(logn)
P3379 【模板】最近公共祖先(LCA)
模板无需多言。
cpp
using namespace std;//模板参考代码
const int N=5e5+5;
vector<int> E[N];
int fc[20][N],dep[N];
void dfs(int x,int fa){//预处理
dep[x]=dep[fa]+1;
fc[0][x]=fa;//2^0=1
for(int i=1;i<=21;i++){//2^i次幂祖宗,i需满足(1<<i)<=n
fc[i][x]=fc[i-1][fc[i-1][x]];//2^(i-1)+2^(i-1)=2^(i-1)*2^1=2^i
}
for(int i=0;i<E[x].size();i++){
int v=E[x][i];
if(v==fa)continue;
dfs(v,x);//继续跑
}
}
int LCA(int x,int y){//求两点的LCA
if(dep[x]<dep[y]){//把深度大的固定为X,注意是深度达大的移动,反了开心一百年
swap(x,y);
}
for(int i=21;i>=0;i--){
if(dep[fc[i][x]]>=dep[y]){//如果跳了之后依然在y下面或跟y其平
x=fc[i][x];
}
}
if(x==y)return x;//如果已经相遇了就直接返回,不直接返回下面会跳到上一个点
for(int i=21;i>=0;i--){
if(fc[i][x]!=fc[i][y]){//如果没有重叠
x=fc[i][x];
y=fc[i][y];
}
}
return fc[0][x];//返回父亲
}
int main(){
int n,m,s;
scanf("%d%d%d",&n,&m,&s);
for(int i=1;i<n;i++){
int u,v;
scanf("%d%d",&u,&v);
E[u].push_back(v);
E[v].push_back(u);
}
dep[0]=-1;//把0结点深度设为-1可以稍微加一点速
dfs(s,0);
while(m--){
int x,y;
scanf("%d%d",&x,&y);
printf("%d\n",LCA(x,y));
}
return 0;
}
P4281 [AHOI2008] 紧急集合 / 聚会
这题仅仅只是前一题稍微加强了一点,我们知道两个点的距离为dep[x]+dep[y]-dep[LCA(x,y)]*2,那么三个点的话另一个点跑到这两点的LCA距离一定是最近的,所以枚举两两之间的LCA,取最小值。
cpp
int main(){//前面的代码一致
while(m--){
int x,y,z;
cin>>x>>y>>z;
int xy=LCA(x,y);//找到两两之间的LCA
int xz=LCA(x,z);
int yz=LCA(y,z);
int xy_z=LCA(xy,z);//另一个结点要跑到LCA去,所以要求另一点和LCA的LCA
int xz_y=LCA(xz,y);
int yz_x=LCA(yz,x);
int kxy=dep[x]+dep[y]-dep[xy]*2+dep[xy]+dep[z]-dep[xy_z]*2;
//两个节点之间的距离加上另一点和LCA的距离
int kxz=dep[x]+dep[z]-dep[xz]*2+dep[xz]+dep[y]-dep[xz_y]*2;
int kyz=dep[y]+dep[z]-dep[yz]*2+dep[yz]+dep[x]-dep[yz_x]*2;
if(kxy<=kxz&&kxy<=kyz){//找一个最小的,要加等号,因为可能有结点相同
cout<<xy<<" "<<kxy<<"\n";
}else if(kxz<=kxy&&kxz<=kyz){
cout<<xz<<" "<<kxz<<"\n";
}else{
cout<<yz<<" "<<kyz<<"\n";
}
}
return 0;
}
这种写法当然AC,但是他有6遍LCA,如果n有1e6再加个毒瘤数据可能会卡常,所以要找一个更快的写法。
经打表证明,三个点的LCA必定会有两个重叠,这意味着重叠的那一个结点绝对在最上面,单独的结点在下面,而下面的那个结点离三个节点的距离和一定最近,所以第一问只需找出单独点即可,第二问就是单独点对应的两个点的距离再加上另一点和单独点的距离,这样就不用再单独求3遍LCA,常数大大减小。
cpp
int main(){//前面的还是一样
while(m--){
int x,y,z;
cin>>x>>y>>z;
int xy=LCA(x,y);//找到三点LCA
int xz=LCA(x,z);
int yz=LCA(y,z);
if(xy==xz){//如果yz是单独点
cout<<yz<<" "<<dep[y]+dep[z]-dep[yz]*2+dep[x]+dep[yz]-dep[xy]*2<<"\n";
}else if(xy==yz){//如果xz是单独点
cout<<xz<<" "<<dep[x]+dep[z]-dep[xz]*2+dep[y]+dep[xz]-dep[yz]*2<<"\n";
}else{//如果xy是单独点
cout<<xy<<" "<<dep[x]+dep[y]-dep[xy]*2+dep[z]+dep[xy]-dep[xz]*2<<"\n";
}
}
return 0;
}