码迷,mamicode.com
首页 > 其他好文 > 详细

浅谈LCA

时间:2020-03-31 17:18:22      阅读:60      评论:0      收藏:0      [点我收藏+]

标签:lca   str   拆分   调整   点距   ahoi2008   last   tps   基本   

浅谈 LCA

\(LCA\) 即 最近公共祖先,定义如下:

给定一颗有根树,若节点 \(z\) 既是节点 \(x\) 的祖先,又是节点 \(y\) 的祖先,则称之为 \(x,y\) 的公共祖先。

在节点 \(x,y\) 所有的公共祖先中深度最大的即为最近公共祖先,记为 \(LCA(x,y)\)。

\(LCA\) 的主要作用是求树上两点之间的路径,同时也有一些别的 玄学 用处。

实现方式

向上标记法

其实就是暴力方法。

  1. 从 \(x\) 向根节点走,并标记所有走过的点。

  2. 从 \(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)\) 时主要分为以下几步:

  1. 设 \(d[x]\) 表示 \(x\) 的深度。不妨设 \(d[x]\geq d[y]\)(否则交换两数)。
  2. 用二进制拆分的思想将两数调整至同一高度,具体说就是将 \(x\) 向上走 \(k=2^{log\ n},...,2^1,2^0\),检查是否到达的深度比 \(y\) 节点深,如果是就令 \(x=F[x,k]\)。
  3. 若此时 \(x==y\),说明 \(LCA(x,y)=y\)。
  4. 再次用二进制拆分的思想将 \(x,y\),同时向上调整,并保持深度一致二者却不相会,若 \(F[x,k]\neq F[y,k]\) 就令 \(x=F[x,k],y=F[y,k]\),直到 \(F[x,k]=F[y,k]\)。
  5. 此时必定只差一步即可相会,\(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\n",LCA(u,v));
	}
	return 0;
}

例题二

[AHOI2008]紧急集合 / 聚会

这...也算是紫题?

我们可以发现,树上三个点的三对 \(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\n",d[x]+d[y]+d[z]-d[p1]-d[p2]-d[p3]);
	}
	return 0;
}

结语

这...就结束了。

完结撒花。

貌似忘了什么重要的事情:

  1. 基础部分讲解来自:《算法竞赛进阶指南》
  2. 例题二部分讲解来自 这篇博客

再次完结撒花。

浅谈LCA

标签:lca   str   拆分   调整   点距   ahoi2008   last   tps   基本   

原文地址:https://www.cnblogs.com/lpf-666/p/12606460.html

踩
(0)
赞
(0)
   
举报
评论 一句话评论(0)
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!