【小白学算法】LCA倍增法求公共祖先超详细解析+例题[洛谷]P3379最近公共祖先(LCA)

算法介绍

什么是LCA?举个简单的例子,想象一下你的家族族谱,你和你的表哥想找到你们最近的公共祖先,可能是你的爷爷奶奶,也可能是更早的祖先,而最近公共祖先就是距离你们最近的那个共同祖辈,LCA算法就是在一棵树中,找到两个节点最近的公共祖先节点,对于树中的两个节点u和v,他们的lca是满足以下三个条件的节点w:

 1. w是u的祖先;

 2. w是v的祖先;

 3. 在满足前两个条件的所有节点中,w的深度最大(即最深的公共祖先)

核心算法

基本思路

1. 分别找到两个节点到根节点的路径

2. 比较两条路径,找到最后一个相同的节点

具体步骤

1、图的处理,先用dfs遍历图,存下各个点的深度,以及各个点倍增跳转数组

  ```Objective-C++

void dfs(int u,int fa) // u为当前节点,fa为父节点
{
	f[u][0] = fa;
	d[u] = d[fa]+1;
	st[u]=g++;
	for(int i=1;(1<<i)<=d[u];i++)
		f[u][i] = f[f[u][i-1]][i-1];    //预处理f数组
	//存u的倍增数组

	for(int i=head[u];i!=-1;i=e[i].next){
		int v = e[i].to;
		if(v!=fa)
		    dfs(v,u);//遍历u的孩子,继续进入dfs预处理倍增数组
	}
}

倍增法:能跳远就跳远,能跳近就跳近;

预处理:对每个节点预计算它的\(2^0\)\(2^1\)\(2^2\)……级的祖先,便于查询时利用二进制思想,快速跳到LCA。

2、求u和v的lca,首先将u和v跳转到同一个深度,特判一下后,开始找公共祖先

  ```Objective-C++
int lca(int x,int y){
	if(d[x]>d[y]) //令y为更深节点
		swap(x,y);
	for(int i=20;i>=0;i--){
		if(d[y]-(1<<i)>=d[x])
			y=f[y][i];//改变y 使y与x在同一深度
	}
	if(x==y) return y;//特判,此时若x=y,则说明x恰好是y的祖先——>特殊情况
	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];//上跳1步相同,即最近的公共祖先
}

步骤解释

为什么要先对齐深度呢?

假设两个人站在不同的楼层的楼梯上,要找到他们最近的汇合点,首先要让他们站到同一楼层;

为什么要从大到小遍历i?

就像我们去超市买东西,用不同面额的钱币找零一样,优先使用大面额(大步跳跃),剩余的用小面额来补齐(小步调整)。

为什么最后返回 f[u][0] ?

经过第二步之后,u和v已经非常接近LCA了,那么他们的直接父节点就是我们要找的LCA。

模板例题

P3379 【模板】最近公共祖先(LCA)

直接套我上面所给出的模板即可啦~ 不清楚怎么套模板的可以参考下面AC代码示例,我会为大家写详细注释嘟~

#include<bits/stdc++.h>
using namespace std;

const int MAX_N = 500000+7, MAX_M = 500000+7;

int n,m,s;
int d[MAX_N]; //保存节点深度
int f[MAX_N][21]; //保存节点的不同倍数的父节点
int g=1,st[MAX_N];//  dfs序||时间戳

int head[MAX_N], cnt=-1;//cnt记录边的数目
struct Edge{
  int to , next;
}e[MAX_N*2];

void add(int u,int v){
  e[++cnt].to = v;//++cnt先将cnt的值加1,得到一个新的编号,e[++cnt].to=v把新边的重点设为v
  e[cnt].next = head[u];//head[u]保存的是从顶点u出发的当前第一条边的编号
  //e[cnt].next=head[u]让新添加的边指向原来从顶点u出发的第一条边,意味着新边成为了从顶点u出发的边链表中的新头部。
  head[u] = cnt;//head[u]更新为新边的编号,也就是让新边成为从顶点u出发的第一条边
}//头插法存图

void dfs(int u,int fa) // u为当前节点,fa为父节点
{
  f[u][0] = fa;//u向上跳2^0=1步时就是其父节点
  d[u] = d[fa]+1;//该节点的深度是其父节点的深度加一
  st[u]=g++;
  for(int i=1;(1<<i)<=d[u];i++)
      f[u][i] = f[f[u][i-1]][i-1];
//预处理f数组,即u倍增数组

  for(int i=head[u];i!=-1;i=e[i].next){
      int v = e[i].to;
      if(v!=fa)//遍历图,遍历当前节点的子节点,继续进入dfs,
          dfs(v,u);//直至所有节点的倍增数组全部处理完
  }
}

int lca(int x,int y){
  if(d[x]>d[y]) //令y为更深节点
      swap(x,y);
  for(int i=20;i>=0;i--){
      if(d[y]-(1<<i)>=d[x])
          y=f[y][i];//改变y 使y与x在同一深度
  }
  if(x==y) return y;//特判,此时若x=y,则说明x恰好是y的祖先——>特殊情况
  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()
{
  memset(head,-1,sizeof(head));
  int a,b;
  scanf("%d%d%d",&n,&m,&s);
  for(int i=1;i<n;i++)
  {
      scanf("%d%d",&a,&b);
      add(a,b);
      add(b,a); //无向图,要加两次
  }
  dfs(s,0);//进入dfs进行预处理

  for(int i=1;i<=m;i++)
  {
      scanf("%d%d",&a,&b);
      printf("%d\n",lca(a,b));
  }

  return 0;
}
posted @ 2025-08-15 16:26  芝士青瓜不拿铁  阅读(24)  评论(0)    收藏  举报