浅谈LCA

Posted lpf-666

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈LCA相关的知识,希望对你有一定的参考价值。

浅谈 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] eq 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
",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
",d[x]+d[y]+d[z]-d[p1]-d[p2]-d[p3]);
	}
	return 0;
}

结语

这...就结束了。

完结撒花。

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

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

再次完结撒花。

以上是关于浅谈LCA的主要内容,如果未能解决你的问题,请参考以下文章

数据结构浅谈倍增求LCA

浅谈Mybatis

浅谈RMQ

浅谈AngularJS中的$parse和$eval

浅谈树链剖分

关于强连通分量浅谈