最近公共祖先LCA

最近公共祖先LCA

倍增法求LCA

vector<int> e[500010];
int dep[500010],fa[500010][20];

void solve(){
	cin>>n>>m>>s;
	for (int i=1;i<=n-1;i++){
		int u,v;
		cin>>u>>v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	dfs(s,0);
	while (m--){
		int a,b;
		cin>>a>>b;
		cout<<lca(a,b)<<endl;
	}
}

定义 \(Fa_{i,j}\) 表示 \(i\) 节点向上跳 \(2^j\) 层的祖先节点,可以由递推式得来

\[Fa_{i,j}=Fa_{Fa_{i,j-1},{j-1}} \]

首先DFS遍历树,每次记录节点的深度以及该节点的父节点,即 \(Fa_{x,0}=p\)

void dfs(int x,int p){
	dep[x]=dep[p]+1;//当前节点深度为父节点深度+1
	fa[x][0]=p;
	for (int i=1;i<=19;i++)//倍增法递推
		fa[x][i]=fa[fa[x][i-1]][i-1];
	for (auto v:e[x]){//DFS叶子节点
		if (v==p) continue;
		dfs(v,x);
	}
}

利用 \(Fa_{i,j}\) 计算 LCA

首先需要将节点 \(x,y\) 跳到同一层,然后两个节点一起向上跳到最近公共祖先的下一层

int lca(int x,int y){
	if (dep[x]<dep[y]) swap(x,y);//保证x的深度较大
	for (int i=19;i>=0;i--){
		if (dep[fa[x][i]]>=dep[y])//将深节点x跳到y节点同一层上
			x=fa[x][i];
	}
	if (x==y) return y;//如果y是x的祖先节点,那么直接返回
	for (int i=19;i>=0;i--){
		if (fa[x][i]!=fa[y][i])
			x=fa[x][i],y=fa[y][i];//x,y一起向上跳
	}
	return fa[x][0];//返回当前节点的父节点,即x,y对应的LCA
}

Tarjan算法求LCA

这是一种离线算法,需要对询问进行处理后再逐步求得所有答案

主要采用并查集的维护方式,自底向上对可以求得的询问进行回答

int n,m,s;
vector<int> e[500010];
vector<pair<int,int>> q[500010];
bool vis[500010];
int fa[500010];
int ans[500010];

int find(int x){
	if (x==fa[x]) return x;
	return fa[x]=find(fa[x]);//路径压缩不影响答案
}

void solve(){
	cin>>n>>m>>s;
	for (int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	for (int i=1;i<=m;i++){
		int a,b;
		cin>>a>>b;
		if (a==b){
			ans[i]=a;
			continue;
		}
		q[a].push_back({b,i});
		q[b].push_back({a,i});
	}
	for (int i=0;i<=n;i++) fa[i]=i;
	tarjan(s);
	for (int i=1;i<=m;i++) cout<<ans[i]<<endl;
}

递归到底层返回时,查找与这个点有关的询问 [a,b] 如果另外一个节点已经被访问过了,那么就可以计算答案

依靠DFS序得到的公共祖先一定是最近的,因为只有在处理完节点有关的询问后才会回溯到上一层

void tarjan(int x){
	vis[x]=1;//标记访问
	for (auto v:e[x]){
		if (vis[v]) continue;
		tarjan(v);//递归
		fa[v]=x;
	}
	for (auto it:q[x]){
		if (vis[it.first])//如果另外一个节点也被访问过
			ans[it.second]=find(it.first);//计算答案
	}
}

树链剖分求LCA

重儿子:父节点所有儿子中子树节点最多的儿子,轻儿子:父节点除开重儿子之外的儿子

重边:连接父节点与他的重儿子的边,轻边:连接父节点与他轻儿子的边

重链:多条连续重边构成的路径

引理:

  • 一棵树能够被剖分为若干条重链
  • 重链的顶点一定是一个轻儿子
  • 树上任意一条连接 \(n\) 个节点路径可以被切分为不超过 \(\log n\) 条重链

HLD

图引自OIWIKI

vector<int> e[500010];
int dep[500010],sz[500010];
int fa[500010],son[500010];
int top[500010];

void solve(){
	cin>>n>>m>>s;
	for (int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	dfs1(s,0);
	dfs2(s,s);
	for (int i=1;i<=m;i++){
		int a,b;
		cin>>a>>b;
		cout<<lca(a,b)<<endl;
	}
}

需要两次DFS处理出不同的信息

void dfs1(int x,int p){
	dep[x]=dep[p]+1;
	fa[x]=p;
	sz[x]=1;
	for (auto v:e[x]){
		if (v==p) continue;
		dfs1(v,x);
		sz[x]+=sz[v];
		if (sz[son[x]]<sz[v]) son[x]=v;//v的子树大小如果比之前记录的重儿子更大,那么更新重儿子
	}
}

首先需要通过第一次 DFS 处理出每个节点的深度,父亲以及他形成的子树的大小

son[i] 数组记录 i 节点的重儿子编号,通过每个儿子的子树大小进行更新

void dfs2(int x,int t){
	top[x]=t;
	if (!son[x]) return;//如果没有重儿子,说明x节点为叶子节点,直接返回
	dfs2(son[x],t);//向下沿着重儿子扩展重链
	for (auto v:e[x]){
		if (v==fa[x]||v==son[x]) continue;
		dfs2(v,v);/沿着轻儿子新开一条重链
	}
}

第二次 DFS 开始树链剖分,top[i] 记录 i 节点所在重链的顶点

枚举 \(i\) 节点所有儿子时,如果该儿子是重儿子就继承父节点所在重链的顶点,如果是轻儿子,那么就从这个轻儿子开始重新建立一条重链

int lca(int x,int y){
	while(top[x]!=top[y]){
		if (dep[top[x]]<dep[top[y]]) swap(x,y);//保证x节点深度最大
		x=fa[top[x]];//沿着重链向上跳
	}
	return dep[x]<dep[y]?x:y;//返回LCA
}

计算 LCA 的原理是让询问的两个节点 \(x,y\) 沿着重链向上跳,当跳到同一条重链上时深度较小的点为 LCA

与倍增法类似,每次都选择深度较大的节点向上跳

posted @ 2025-05-03 13:15  才瓯  阅读(25)  评论(0)    收藏  举报