浅谈LCA
Posted lpf-666
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈LCA相关的知识,希望对你有一定的参考价值。
浅谈 LCA
(LCA) 即 最近公共祖先,定义如下:
给定一颗有根树,若节点 (z) 既是节点 (x) 的祖先,又是节点 (y) 的祖先,则称之为 (x,y) 的公共祖先。
在节点 (x,y) 所有的公共祖先中深度最大的即为最近公共祖先,记为 (LCA(x,y))。
(LCA) 的主要作用是求树上两点之间的路径,同时也有一些别的 玄学 用处。
实现方式
向上标记法
其实就是暴力方法。
-
从 (x) 向根节点走,并标记所有走过的点。
-
从 (y) 向根节点走,当遇到已标记的第一个点时,这个点就是 (LCA(x,y))。
对于这种方法,最坏时间复杂度为 (O(n))。
树上倍增法
这是一个很重要的算法,除了求 (LCA) 之外,还有很大的用处。
预处理
设 (F[x,k]) 表示 (x) 的 (2^k) 辈祖先,特别的若该点不存在就令 (F[x,k]=0),而 (F[x,0]) 自然就是 (x) 的父亲节点。
除此之外 (F[x,k]=f[f[x,k-1],k-1])(类似于动态规划),这就是这个算法的核心。
以上预处理部分,代码实现时用一遍 (dfs) 搞定,时间复杂度为 (O(n log n))。
你会发现它并没有上面的 (O(n)) 优,因为这并不是它快速的原因,它的快体现在查询上。
查询
当求 (LCA(x,y)) 时主要分为以下几步:
- 设 (d[x]) 表示 (x) 的深度。不妨设 (d[x]geq d[y])(否则交换两数)。
- 用二进制拆分的思想将两数调整至同一高度,具体说就是将 (x) 向上走 (k=2^{log n},...,2^1,2^0),检查是否到达的深度比 (y) 节点深,如果是就令 (x=F[x,k])。
- 若此时 (x==y),说明 (LCA(x,y)=y)。
- 再次用二进制拆分的思想将 (x,y),同时向上调整,并保持深度一致二者却不相会,若 (F[x,k] eq F[y,k]) 就令 (x=F[x,k],y=F[y,k]),直到 (F[x,k]=F[y,k])。
- 此时必定只差一步即可相会,(F[x,0]) 即为 (LCA(x,y))。
多次查询的时间复杂度为 (O((n+m)log n))。相比上面的 (O(nm)) 确实有所优化。
代码实现
核心代码如下:
void dfs(int x,int fa){
d[x]=d[fa]+1;
f[x][0]=fa;
for(int i=1;(1<<i)<=d[x];i++)
f[x][i]=f[f[x][i-1]][i-1];
for(int i=head[x];i;i=edge[i].next){
if(edge[i].to!=fa) dfs(edge[i].to,x);
}
return;
}
//以上是预处理
int LCA(int x,int y){
if(d[x]>d[y]) swap(x,y);
for(int i=20;i>=0;i--)
if(d[f[y][i]]>=d[x]) y=f[y][i];
if(x==y) return x;
for(int i=20;i>=0;i--)
if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
return f[x][0];
}
Tarjan 算法
这部分待更。
例题
既然我们已经学会了最最最基本的 (LCA),就用几道例题来加深理解吧。
例题一
将 最大生成树 与 (LCA) 完美结合的问题。
首先货车司机一定是走最大的道路,那么连接一个点与所有其它点的两线的最大值显然只有一条,
那么这就构成了一颗 最大生成树,做法和 最小生成树 一样就好了,只是 (Kruscal) 排序时从大到小排。
然后两点之间的路径用 (LCA) 维护即可。
#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cmath>
#define N 10010
#define M 50010
#define INF 999999999
using namespace std;
int n,m,fa[N][30],w[N][30],depth[N];
int p[N],lg[N];
int head[N],cnt=0;
bool vis[N];
struct node1{
int x,y,dis;
}a[M];
struct node2{
int next,to,val;
}edge[N*2];
bool cmp(node1 a,node1 b){
return a.dis>b.dis;
}
int find(int x){
if(x==p[x]) return x;
return p[x]=find(p[x]);
}
void addedge(int x,int y,int z){
cnt++;
edge[cnt].next=head[x];
edge[cnt].to=y;
edge[cnt].val=z;
head[x]=cnt;
return;
}
void kruscal(){
sort(a+1,a+m+1,cmp);
for(int i=1;i<=n;i++) p[i]=i;
for(int i=1;i<=m;i++){
int fx=find(a[i].x);
int fy=find(a[i].y);
if(fx==fy) continue;
p[fx]=fy;
addedge(a[i].x,a[i].y,a[i].dis);
addedge(a[i].y,a[i].x,a[i].dis);
}
return;
}
void dfs(int x,int last){
vis[x]=true;
for(int i=head[x];i;i=edge[i].next){
int y=edge[i].to;
if(y==last) continue;
depth[y]=depth[x]+1;
fa[y][0]=x;
w[y][0]=edge[i].val;
dfs(y,x);
}
return;
}
void pre_LCA(){
for(int i=1;i<=n;i++){
if(!vis[i]){
dfs(i,0);
fa[i][0]=i;
w[i][0]=INF;
}
}
for(int i=1;i<=20;i++)
for(int j=1;j<=n;j++){
fa[j][i]=fa[fa[j][i-1]][i-1];
w[j][i]=min(w[j][i-1],w[fa[j][i-1]][i-1]);
}
return;
}
int LCA(int x,int y){
if(find(x)!=find(y)) return -1;
int ans=INF;
if(depth[x]<depth[y]) swap(x,y);
for(int i=20;i>=0;i--){
if(depth[fa[x][i]]>=depth[y]){
ans=min(ans,w[x][i]);
x=fa[x][i];
}
}
if(x==y) return ans;
for(int i=20;i>=0;i--){
if(fa[x][i]!=fa[y][i]){
ans=min(ans,min(w[x][i],w[y][i]));
x=fa[x][i],y=fa[y][i];
}
}
return min(ans,min(w[x][0],w[y][0]));
}
int main(){
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)
lg[i]=lg[i-1]+(1<<lg[i-1]==i);
int u,v,q;
for(int i=1;i<=m;i++){
scanf("%d %d %d",&u,&v,&q);
a[i].x=u;
a[i].y=v;
a[i].dis=q;
}
kruscal();
pre_LCA();
int Q;
scanf("%d",&Q);
while(Q--){
scanf("%d %d",&u,&v);
printf("%d
",LCA(u,v));
}
return 0;
}
例题二
这...也算是紫题?
我们可以发现,树上三个点的三对 (LCA) 一定有两个是相同的,这是一件想想的话比较显然的事情。
必然能够找到某个节点,让三个点中的两个在一侧,一个在另一侧。而这个点就是两个公共的 (LCA)。
思考的再深入些(并且结合瞎蒙),我们会发现这个相同的 (LCA) 肯定是深度最小的一个 (LCA)。
这里,我们首先可以显而易见的发现,这个点必须在三个点互相通达的路径上。
我们再思考一下 (LCA) 与路径和的关系。
假设我们知道 (a) 和 (b) 的 (LCA) 是 (x),而且 (x) 是上述的 (3) 个 (LCA) 中深度最大的那个。
那么可以发现从 (x) 到 (a) 的距离加上从 (x) 到 (b) 的距离一定是最小的。
根据上面的结论,我们知道 (a),(c) 和 (b),(c) 的 (LCA) 点 (y) 一定在一个点上,而且这个 (y) 一定比 (x) 深度小。
那么这个时候,我们会发现此时 (a),(b),(c) 到 (x) 的距离和是最小的。证明的话可以这么想:
如果 (x′) 比 (x) 高,那么虽然 (c) 到 (x) 的距离减小了 (w),但是 (a),(b)到 (x‘) 的距离均增大了 (w),显然距离和增大。
如果 (x′) 比 (x) 低,有一个节点到 (x′) 的距离减小了 (w),剩下两个节点到 (x′) 的距离均增大了 (w),显然距离和也增大。
所以我们就找到了到三个点距离和最小的点:这三个点的三对 (LCA) 中,深度大的那两个 (LCA) 就是答案。
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<cmath>
#define N 500010
using namespace std;
int n,m,head[N],cnt=0;
int d[N],f[N][25];
struct node{
int next,to;
}edge[N<<1];
int read(){
int x=0,f=1;char c=getchar();
while(c<‘0‘ || c>‘9‘) f=(c==‘-‘)?-1:1,c=getchar();
while(c>=‘0‘ && c<=‘9‘) x=x*10+c-48,c=getchar();
return x*f;
}
void addedge(int x,int y){
cnt++;
edge[cnt].next=head[x];
edge[cnt].to=y;
head[x]=cnt;
return;
}
void dfs(int x,int fa){
d[x]=d[fa]+1;
f[x][0]=fa;
for(int i=1;(1<<i)<=d[x];i++)
f[x][i]=f[f[x][i-1]][i-1];
for(int i=head[x];i;i=edge[i].next){
if(edge[i].to!=fa) dfs(edge[i].to,x);
}
return;
}
int LCA(int x,int y){
if(d[x]>d[y]) swap(x,y);
for(int i=20;i>=0;i--)
if(d[f[y][i]]>=d[x]) y=f[y][i];
if(x==y) return x;
for(int i=20;i>=0;i--)
if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
return f[x][0];
}
int main(){
n=read();m=read();
int x,y,z;
for(int i=1;i<n;i++){
x=read();y=read();
addedge(x,y);addedge(y,x);
}
dfs(1,0);
for(int i=1;i<=m;i++){
x=read();y=read();z=read();
int p1=LCA(x,y);
int p2=LCA(y,z);
int p3=LCA(x,z);
int ans;
if(p1==p2) printf("%d ",p3);
else if(p2==p3) printf("%d ",p1);
else if(p1==p3) printf("%d ",p2);
printf("%d
",d[x]+d[y]+d[z]-d[p1]-d[p2]-d[p3]);
}
return 0;
}
结语
这...就结束了。
完结撒花。
貌似忘了什么重要的事情:
- 基础部分讲解来自:《算法竞赛进阶指南》
- 例题二部分讲解来自 这篇博客
再次完结撒花。
以上是关于浅谈LCA的主要内容,如果未能解决你的问题,请参考以下文章