LCA 补充

LCA

之前学废了,回来补。

倍增版

首先是最常见的倍增版子,思路好理解,按倍增记录 \(father\),然后同时往上跳。

注意最后跳到的是那个 \(x \ne y\) 的,也就是 \(lca\) 的儿子,所以最后要返回父亲。

#include<bits/stdc++.h>
using namespace std;
const int N = 5e5+5;
int n,m,rt;
int head[N],tot;
struct E{int u,v;} e[N<<1];
void add(int u,int v) {e[++tot]={head[u],v}; head[u]=tot;}
int dep[N],fa[30][N];
void dfs(int u,int f)
{
	dep[u]=dep[f]+1; fa[0][u]=f;
	for(int i=1;i<=25;i++)
		fa[i][u]=fa[i-1][fa[i-1][u]];
	for(int i=head[u];i;i=e[i].u)
	{
		int v=e[i].v; if(v==f) continue;
		dfs(v,u);
	}
}
int lca(int x,int y)
{
	if(dep[x]<dep[y]) swap(x,y);
	for(int i=25;i>=0;i--) if(dep[fa[i][x]]>=dep[y]) x=fa[i][x];
	if(x==y) return x;
	for(int i=25;i>=0;i--) if(fa[i][x]!=fa[i][y]) x=fa[i][x],y=fa[i][y];
	return fa[0][x];
}
int main()
{
	scanf("%d%d%d",&n,&m,&rt);
  	for(int i=2;i<=n;i++)
	{
		int x,y; scanf("%d%d",&x,&y);
		add(x,y); add(y,x);
	} 
	dep[rt]=1;//!!!
	dfs(rt,0);
	while(m--)
	{
		int x,y; scanf("%d%d",&x,&y);
		printf("%d\n",lca(x,y));
	}
	return 0;
}

DFS 序版

原博客

我们记录每个点的 \(dfn\) 时间戳和 dfs 序。

应用的性质是 \(lca\) 的 dfs 序不会出现在 \(u\)\(v\) 之间出现。并且是在它们之前出现。

其中 dfs 序体现在 dfs 遍历时维护的 \(st\),也就是记录父亲。

最终 \(st[i][u]\) 表示的是以 \(u\) 的时间戳为起点,在dfs序上向后延长 \(2^i\) 位的深度最小值的节点的父亲,也就是 \(dfn\) 最小节点。

这里很神奇的是查询能直接查到 \(lca\),因为我们查询的是 \([dfn_u,dfn_v]\) 这段 \(dfs\) 序上的父亲时间戳最小值显然这个点一定就是 \(lca\)

(挂张图)

#include<bits/stdc++.h>
using namespace std;
#define mi(x,y) (dfn[x]<dfn[y]?(x):(y))
const int N = 5e5+5;
int n,m,rt;
int head[N],tot;
struct E {int u,v;} e[N<<1];
inline void add(int u,int v) {e[++tot]={head[u],v}; head[u]=tot;}

int dfn[N],cnt,st[N][25];
void dfs(int u,int f)
{
	dfn[u]=++cnt; st[cnt][0]=f;
	for(int i=head[u];i;i=e[i].u)
	{
		int v=e[i].v; if(v==f) continue;
		dfs(v,u);
	}
}
int lca(int x,int y)
{
	if(x==y) return x;
	if((x=dfn[x])>(y=dfn[y])) swap(x,y); x++; 
	int k=__lg(y-x+1); 
	return mi(st[x][k],st[y-(1<<k)+1][k]);
}
int main()
{
	// freopen("in.in","r",stdin);
	// freopen("out.out","w",stdout);
	scanf("%d%d%d",&n,&m,&rt);
	for(int i=1;i<n;i++)
	{
		int x,y; scanf("%d%d",&x,&y);
		add(x,y); add(y,x);
	}
	dfs(rt,0);
	for(int i=1;i<=20;i++)
		for(int j=1;j+(1<<i)-1<=n;j++)
			st[j][i]=mi(st[j][i-1],st[j+(1<<(i-1))][i-1]);
	while(m--)
	{
		int x,y; scanf("%d%d",&x,&y);
		printf("%d\n",lca(x,y));
	}
	return 0;
}

树链剖分版

划分轻重链后往上跳,直到跳到同一条链上,其实就是借助了树剖的 dfs,亲民。

#include<bits/stdc++.h>
using namespace std;
const int N = 5e5+5;
int n,m,rt;
int head[N],tot;
struct E{int u,v;} e[N<<1];
void add(int u,int v) {e[++tot]={head[u],v}; head[u]=tot;}

int sz[N],dep[N],fa[N],son[N],top[N];

void dfs1(int u,int f)
{
	fa[u]=f; dep[u]=dep[f]+1; sz[u]=1; son[u]=-1;
	for(int i=head[u];i;i=e[i].u)
	{
		int v=e[i].v;
		if(v==f) continue;
		dfs1(v,u);
		sz[u]+=sz[v];
		if(son[u]==-1||sz[son[u]]<sz[v]) son[u]=v;
	}
}
void dfs2(int u,int t)
{
	top[u]=t;
	if(son[u]==-1) return ;
	dfs2(son[u],t);
	for(int i=head[u];i;i=e[i].u)
	{
		int v=e[i].v;
		if(v!=fa[u]&&v!=son[u]) dfs2(v,v);
	}
}
int lca(int x,int y)
{
	while(top[x]!=top[y])
	{
		if(dep[top[x]]>=dep[top[y]]) x=fa[top[x]];
		else y=fa[top[y]];
	}
	return dep[x]<dep[y]?x:y;
}
int main()
{
	scanf("%d%d%d",&n,&m,&rt);
	for(int i=2;i<=n;i++)
	{
		int x,y; scanf("%d%d",&x,&y);
		add(x,y); add(y,x);
	}
	dfs1(rt,0); dfs2(rt,rt);
	while(m--)
	{
		int x,y; scanf("%d%d",&x,&y);
		printf("%d\n",lca(x,y));
	}
	return 0;
}

写在最后

虽然原博列举了很多 dfs 序 LCA 的优点,但是缺点也有一点点吧。

倍增方法可以处理 k 级祖先,这在一些树上跳的题目中很重要。

树剖可以顺便求出 lca,用途也比较广。

dfs 序码量较短,复杂度更优。

posted @ 2024-07-13 07:25  ppllxx_9G  阅读(18)  评论(0编辑  收藏  举报