学习笔记:最近公共祖先

最近公共祖先

最近公共祖先,简称 LCA

例题: P3379

倍增做法求 LCA

倍增做法是一种在线做法,时间复杂度为: O((n+m)logn)

n 为点数,m为询问次数

算法详解

定义

首先我们先 dfs 预处理两个数组:

depth[i] :表示 i 号节点的深度

fa[i][j] :表示 i 号节点的 2j 级祖先的编号


状态转移方程

然后我们再来思考怎么 dfs 时求出这两个数组:

由于 dfs 时我们传了两个参数: nowfather

那么 now 节点的深度就等于 father 的深度加一

now20 级祖先就等于 father

那么求 i>0fa[now][i] 我们可以用 ST 表的思想

我们先让 now2i1 步,然后再跳 2i1

那么这时 now 就等于 now2i 级祖先

所以状态转移方程就是:

depth[now]=depth[father]+1fa[now][i]={fatheri=0fa[fa[now][i1]][i1]i>0


查询

这个过程一共分两步:

我们先假设深度目前深度更大的点为 x ,深度更小的节点为 y

那么如果不满足这个条件,就先交换两个节点

  1. 先让 x 跳到与 y 深度相同的祖先上:

    那么这里就可以用多重背包的二进制优化的思想

    假设需要跳 5 步,那么我们就可以先让 x 跳到 22 级祖先上,再跳到目前的 x20 级祖先上

    那么 x 就一共跳了 22+20=5

  2. 再让 xy 同时往上跳,直到跳到最近祖先上:

    但是这样不太好处理,那么我们就可以先让 xy 跳到 LCA 的下面那一层的节点,再跳到 LCA

    那么我们就可以从二进制下位数高的枚举到位数低的,如果跳完后两个节点不同,那么就说明跳完后一定不会是 LCALCA 的祖先,那么就让他们往上跳

    那么最后返回的结果就是 fa[x][0]

但是需要注意:有可能执行完第一步后 xy 就已经相等了,那么这时就需要直接返回答案为x


Code

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+1;

vector<int> g[N];
int n,m,root,depth[N],fa[N][20];

void dfs(int now,int father) //预处理dpeth和fa数组 
{
	depth[now]=depth[father]+1;
	fa[now][0]=father;
	for (int i=1;i<=19;i++)
		fa[now][i]=fa[fa[now][i-1]][i-1];
	
	for (int to:g[now])
		if (to!=father)
			dfs(to,now);
}
int LCA(int x,int y) //倍增求LCA 
{
	if (depth[x]<depth[y]) 
		swap(x,y);
	for (int i=19;i>=0;i--)
		if (depth[fa[x][i]]>=depth[y]) 
			x=fa[x][i];
	if (x==y) 
		return x;
	for (int i=19;i>=0;i--)
		if (fa[x][i]!=fa[y][i])
			x=fa[x][i],y=fa[y][i];
	return fa[x][0];
}
int main()
{
	scanf("%d%d%d",&n,&m,&root);
	for (int i=1,u,v;i<n;i++)
	{
		scanf("%d%d",&u,&v);
		g[u].push_back(v);
		g[v].push_back(u);
	}
	dfs(root,0);
	for (int i=1,x,y;i<=m;i++) {
		scanf("%d%d",&x,&y);
		printf("%d\n",LCA(x,y));
	}
	return 0;
}

Tarjan 算法求 LCA

Tarjan 算法是一种离线算法,时间复杂度为 O(n+m)

算法详解

我们先把每个查询得两个节点的对应的节点和查询编号存下来

然后对树进行 dfs 一遍,在 dfs 时,将所有点分成三大类:

  1. 已经遍历过的点,标记为 2
  2. 正在搜索的点,标记为 1
  3. 还未搜索到的点,标记为 0

对于正在搜索的点,我们可以把他们全部合并到他们的根节点上

然后我们再找出与当前正在搜的点 x 有关的询问

如果对应的 y 已经被遍历过,那么他们的 LCA 就等于合并 y 合并到的节点编号 ( 建议自己画图理解一下 )

Code

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+1;

vector<int> g[N];
int fa[N],ans[N],vis[N];
vector< pair<int,int> > Q[N]; //Q[i].first存查询的另外一个点,Q[i].second存查询编号

int find(int x) {
    if (fa[x]!=x) fa[x]=find(fa[x]);
    return fa[x];
}
void tarjan(int x)
{
    vis[x]=1;
    for (int y:g[x])
   	if (!vis[y]) {
		tarjan(y);
		fa[y]=x;
	}
    vis[x]=2;
    for (auto now:Q[x]) {
        int y=now.first,id=now.second;
        if (vis[y]==2) ans[id]=find(y);
    }
}
int main()
{
    int n,m,root;
    scanf("%d%d%d",&n,&m,&root);
    for (int i=1,u,v,val;i<n;i++)
    {
        scanf("%d%d",&u,&v);
        g[u].push_back(v);
        g[v].push_back(u);
    }
    for (int i=0,x,y;i<m;i++)
    {
        scanf("%d%d",&x,&y);
        Q[x].push_back(make_pair(y,i));
        Q[y].push_back(make_pair(x,i));
    }
    for (int i=1;i<=n;i++) fa[i]=i;
    tarjan(root);
    for (int i=0;i<m;i++) printf("%d\n",ans[i]);
    return 0;
}

重链剖分求 LCA

不会重链剖分的可以看下 this

时间复杂度 O(n+mlogn),实际运行时间一般比倍增快很多

Code

#include<bits/stdc++.h>
using namespace std;
const int N=6e5;
vector<int> g[N];
int depth[N],fa[N];
int size[N],son[N],top[N];

void dfs1(int u,int f)
{
	fa[u]=f,size[u]=1;
	depth[u]=depth[f]+1;
	for (int v:g[u])
		if (v!=f) {
			dfs1(v,u);
			size[u]+=size[v];
			if (size[v]>size[son[u]]) son[u]=v;
		}
}
void dfs2(int u,int topf)
{
	top[u]=topf;
	if (son[u]) dfs2(son[u],topf);
	for (int v:g[u])
		if (!top[v])
			dfs2(v,v);
}
int LCA(int x,int y)
{
	while (top[x]!=top[y]) {
		if (depth[top[x]]<depth[top[y]]) swap(x,y);
		x=fa[top[x]];
	}
	return depth[x]<depth[y]?x:y;
}
int main()
{
	int n,m,s,x,y;
	scanf("%d%d%d",&n,&m,&s);
	for (int i=1;i<n;i++) {
		scanf("%d%d",&x,&y);
		g[x].push_back(y);
		g[y].push_back(x);
	} dfs1(s,0),dfs2(s,s);
	while (m--) {
		scanf("%d%d",&x,&y);
		printf("%d\n",LCA(x,y));
	}
	return 0;
}

欧拉序求 LCA

先对树跑遍 dfs,求出欧拉序,每次 dfs 到一个节点就把这个节点加入欧拉序,再遍历子节点,dfs 遍历完整棵子树后回到父节点再把父节点加入欧拉序

由于每条边最多进一次出一次,所以欧拉序的长度为 2n1(起点一开始也要加)

再对欧拉序跑遍 ST 表,处理区间中深度最小的节点

询问时查询两点在欧拉序中第一次出现的位置间深度最小的节点即可

时间复杂度 O(nlogn+m)

Code

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+1,M=1e6;
int n,m,pos[N],depth[N];
int len,id[M],st[M][20];
vector<int> g[N]; int s;

int read() {
	int x=0; char ch=0; while (!isdigit(ch) ) ch=getchar();
	while (isdigit(ch) ) x=(x<<3)+(x<<1)+(ch&15),ch=getchar();
	return x;
}
void dfs(int u,int fa) {
	depth[u]=depth[fa]+1,id[++len]=u,pos[u]=len,st[len][0]=u;
	for (int v:g[u]) if (v^fa) dfs(v,u),id[++len]=u,st[len][0]=u;
}
void init()
{
	for (int j=1;j<20;j++)
		for (int i=1;i+(1<<j)-1<=len;i++) {
			int x=st[i][j-1],y=st[i+(1<<j-1) ][j-1];
			st[i][j]=depth[x]<depth[y]?x:y;
		}
}
int LCA(int x,int y)
{
	int l=pos[x],r=pos[y];
	if (l>r) swap(l,r); int k=log2(r-l+1);
	x=st[l][k],y=st[r-(1<<k)+1][k];
	return depth[x]<depth[y]?x:y;
}
int main()
{
	n=read(),m=read(),s=read();
	for (int i=1,x,y;i<n;i++)
	{
		x=read(),y=read();
		g[x].push_back(y);
		g[y].push_back(x);
	}
	dfs(s,0),init();
	while (m--) printf("%d\n",LCA(read(),read() ) );
	return 0;
}
posted @   懵逼自动机  阅读(40)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示