倍增 || ST-RMQ 求LCA

本文深入解析了倍增与ST-RMQ两种求最近公共祖先(LCA)的算法实现。通过实例讲解了如何利用倍增技巧快速找到二叉树中任意两点的最近公共祖先,并介绍了ST-RMQ算法在LCA问题中的应用。代码示例清晰,适合初学者理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

视频讲解戳这里(bj聚聚讲的非常好,一听就懂)

由luogu一道模板题讲起 传送门 (都9102年,这题还卡输入输出,要吸氧才能过)

LCA:求两个点的最近公共祖先

倍增求LCA:

有很多博客图文并茂讲的非常清楚,倍增LCA的主要思想就是,先将u,v(保证u的深度更深)两点调整到同一水平线

情况一:u==v,那就说明最近公共祖先是u,这种情况说明u,v在同一边,祖先自然是深度较浅的那一个    

情况二:u,v每次往上跳2的j次方(j从大到小),如果二者所跳到的点相同,那就不跳,反之则跳,最后就落在了最近公共祖先的下一个位置

代码结合视频看起来更香噢:

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<string>
#include<cstring>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<cmath>
using namespace std;
typedef long long ll;
const ll inf=0x3f3f3f3f;
const int maxn=5e5+5;
const int maxbit=20;
int n,m,s;
int dep[maxn],fa[maxn][maxbit],lg[maxn];
//记录每个点的深度;fa[i][j],i点往上跳2^J的父亲节点;预处理log2,向下取整
vector<int> G[maxn];
void dfs(int np,int fat){ //当前节点,父亲节点
	dep[np]=dep[fat]+1;
	fa[np][0]=fat;
	for(int j=1;j<=lg[dep[np]]+1;++j){
		fa[np][j]=fa[fa[np][j-1]][j-1]; 
//np往上跳2^j的节点相当于先往上跳到2^(j-1)节点处再往上跳2^(j-1)
	}
	for(int i=0;i<(int)G[np].size();++i){
		if(G[np][i]!=fat){
			dfs(G[np][i],np);
		}
	}
}
int lca(int u,int v){
	if(dep[u]<dep[v])	swap(u,v);
	while(dep[u]!=dep[v]){ //调整平衡
		u=fa[u][lg[dep[u]-dep[v]]];
	}
	if(u==v)	return u; //情况一
	for(int i=lg[dep[u]];i>=0;--i){ //情况二
		if(fa[u][i]!=fa[v][i]){
			u=fa[u][i];
			v=fa[v][i];
		}
	}
	return fa[u][0];
}
int main(){
	scanf("%d%d%d",&n,&m,&s);
	int x,y;
	lg[0]=-1;
	for(int i=1;i<maxn;++i){
		lg[i]=lg[i>>1]+1;
	}
	for(int i=1;i<=n-1;++i){
		scanf("%d%d",&x,&y);
		G[x].push_back(y);
		G[y].push_back(x); 
	}
	dfs(s,0);
	for(int i=1;i<=m;++i){
		scanf("%d%d",&x,&y);
		printf("%d\n",lca(x,y));
	}
	return 0;
}

纯净版:

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<string>
#include<cstring>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<cmath>
using namespace std;
typedef long long ll;
const ll inf=0x3f3f3f3f;
const int maxn=5e5+5;
const int maxbit=20;
int n,m,s;
int dep[maxn],fa[maxn][maxbit],lg[maxn];
vector<int> G[maxn];
void dfs(int np,int fat){
	dep[np]=dep[fat]+1;
	fa[np][0]=fat;
	for(int j=1;j<=lg[dep[np]]+1;++j){
		fa[np][j]=fa[fa[np][j-1]][j-1];
	}
	for(int i=0;i<(int)G[np].size();++i){
		if(G[np][i]!=fat){
			dfs(G[np][i],np);
		}
	}
}
int lca(int u,int v){
	if(dep[u]<dep[v])	swap(u,v);
	while(dep[u]!=dep[v]){
		u=fa[u][lg[dep[u]-dep[v]]];
	}
	if(u==v)	return u;
	for(int i=lg[dep[u]];i>=0;--i){
		if(fa[u][i]!=fa[v][i]){
			u=fa[u][i];
			v=fa[v][i];
		}
	}
	return fa[u][0];
}
int main(){
	scanf("%d%d%d",&n,&m,&s);
	int x,y;
	lg[0]=-1;
	for(int i=1;i<maxn;++i){
		lg[i]=lg[i>>1]+1;
	}
	for(int i=1;i<=n-1;++i){
		scanf("%d%d",&x,&y);
		G[x].push_back(y);
		G[y].push_back(x); 
	}
	dfs(s,0);
	for(int i=1;i<=m;++i){
		scanf("%d%d",&x,&y);
		printf("%d\n",lca(x,y));
	}
	return 0;
}

 ST-RMQ 求LCA:

先求一个dfs序列,那求u,v的lca就是u,v在dfs序中最早出现的位置之间深度最小的那一个,st[i][j]代表i~i+2^j区间中深度最小在dfs序中的下表。

代码结合视频看起来更香噢:

#include<bits/stdc++.h>
using namespace std;
const int maxn=5e5+5;
const int maxbit=20;
vector<int> G[maxn];
int order[maxn<<2],depth[maxn<<2];
//深搜序列    //深度 
int lg[maxn<<2],st[maxn<<2][maxbit];
//预处理log   //st[i][j]代表i~i+2^j区间中深度最小编号 
int first_place[maxn];//dfs序中i最早出现的下标 
int n,m,s,cnt=0; 
inline int read(){
	char ch=getchar();
	int x=0,f=1;
	while((ch>'9'||ch<'0')&&ch!='-'){
		ch=getchar();
	}
	if(ch=='-'){
		f=-1;
		ch=getchar();
	}
	while('0'<=ch&&ch<='9'){
		x=x*10+ch-'0';
		ch=getchar();
	}
	return x*f;
}
void dfs(int np,int dep){
	++cnt;
	first_place[np]=cnt;
	order[cnt]=np;
	depth[cnt]=dep+1;
	for(int i=0;i<(int)G[np].size();++i){
		int to=G[np][i];
		if(first_place[to]==0){
			dfs(to,dep+1);
			++cnt;
			order[cnt]=np;
			depth[cnt]=dep+1;
		}
	}
}
void STinit(){
	for(int i=1;i<=cnt;++i){
		st[i][0]=i;
	}
	int a,b;
	for(int j=1;j<=lg[cnt];++j){
		for(int i=1;i+(1<<j)-1<=cnt;++i){
			a=st[i][j-1];
			b=st[i+(1<<(j-1))][j-1];
			if(depth[a]<depth[b])
				st[i][j]=a;
			else	st[i][j]=b;
		}
	}
}
int main(){
	n=read(),m=read(),s=read();
	lg[0]=-1;
	for(int i=1;i<maxn*2;++i)	lg[i]=lg[i>>1]+1;
	int x,y;
	for(int i=1;i<n;++i){
		x=read(),y=read();
		G[x].push_back(y);
		G[y].push_back(x);
	}
	dfs(s,0);
	STinit();
	for(int i=1;i<=m;++i){
		x=read(),y=read();
		x=first_place[x];
		y=first_place[y];
		if(x>y)	swap(x,y);
		int k=lg[y-x];
		int a=st[x][k],b=st[y-(1<<k)+1][k];
		if(depth[a]<depth[b])
			printf("%d\n",order[a]);
		else	printf("%d\n",order[b]);
		
	}
	return 0;
}

### 求解树的最近公共祖先(LCA)的方法 #### 方法概述 求解 LCA 的方法有多种,包括暴力搜索法、树上倍增法、在线 RMQ 算法、离线 Tarjan 算法以及树链剖分[^1]。 #### 暴力搜索法 暴力搜索法是一种简单直观的方式。该方法通过自顶向下遍历整棵树来寻找两个节点 u 和 v 的路径交点作为它们的最近公共祖先。这种方法的时间复杂度较高,在最坏情况下可能达到 O(n),其中 n 是树中节点的数量[^2]。 #### 树上倍增法 为了提高效率,可以采用树上倍增法。此算法利用动态规划的思想预处理每个结点向上跳 2^k 步所能到达的父亲节点 f[i][j] 表示从 i 出发经过 j 层父辈关系能走到的位置;当查询 lca(x,y) 时先让较深的一个上升至同一深度再一起往上走直到相遇即为所lca。其时间复杂度大约为O(logn)。 ```cpp // C++ code snippet for Binary Lifting method to find LCA using Depth First Search (DFS). const int MAXLOG = 20; // Maximum value of log(N) vector<int> adj[N]; // Adjacency list representation of the tree. int level[N], parent[MAXLOG][N]; void dfs(int node, int par){ parent[0][node]=par; for(auto child : adj[node]){ if(child != par){ level[child]=level[node]+1; dfs(child,node); } } } void preprocess(){ memset(parent,-1,sizeof(parent)); dfs(1,0); // Assuming root is at vertex '1' for(int k=1;k<MAXLOG;++k) for(int i=1;i<=n;++i) if(parent[k-1][i]!=-1) parent[k][i]=parent[k-1][parent[k-1][i]]; } ``` #### 在线 RMQ 算法 另一种有效解决策略是在线 RMQ(Range Minimum Query) 技术转换成 ST(Sparse Table) 或者 Segment Tree 来实现快速区间极值查询从而间接计算出任意两点间的最低共同祖先位置。这类方案同样具备较好的性能表现且易于理解和编码实施。 #### 离线 Tarjan 算法 针对批量询问场景下还可以考虑应用离线 Tarjan 算法。它基于并查集数据结构完成对多个给定目标配对间的关系判定工作,并能在单次扫描过程中同步处理多组请而无需重复访问整个图谱资源消耗相对较小。具体来说就是在执行一次完整的 DFS 遍历时记录下所有待解决问题的信息等到遇到符合条件的情况立即更新答案直至结束为止[^5]。 ```python # Python implementation of Offline Tarjan's algorithm for finding LCAs. from collections import defaultdict class UnionFind(object): def __init__(self): self.parent = {} def make_set(self,x): self.parent[x]=-1 def union_sets(self,a,b): a=self.find(a) b=self.find(b) if a!=b: self.parent[a]=b def find(self,x): while True: try: y=self.parent[x] except KeyError: break if y==-1 or y==x: return x else: x=y def tarjan_offline_lca(tree,nodes_with_queries,default_root='A'): uf=UnionFind() ancestors=defaultdict(list) answers={} def visit(node,parent=None): nonlocal default_root uf.make_set(node) if not parent: parent=node elif node in nodes_with_queries and \ all([q in visited for q in nodes_with_queries[node]]): ancestor=min((uf.find(q)for q in nodes_with_queries[node]),key=lambda z:z) for query_node in nodes_with_queries[node]: answers[(query_node,node)]=(ancestor,'found') visited.add(node) children=[child for child in tree.get(node,[])if child!=parent] for c in children: visit(c,node) for queried_pair in [(u,v)for u,v in queries.items()if v==node]: other=query_to_other_member(*queried_pair) if other in visited: common_anc=uf.find(other) answers[tuple(sorted((*queried_pair)))]=(common_anc,'found') uf.union_sets(default_root,node) visited=set();queries={(a,b):None for a,b in edges};tree=dict() tarjan_offline_lca(tree,queries) print("\n".join(f"LCA({pair}) -> {result}"for pair,result in sorted(answers.values()))) ``` #### 并查集解法 除了上述提到的技术外,并查集也可以用来解答关于树型结构内的 LCA 查询问题。这种方式主要依赖于预先保存所有的提问信息随后借助深度优先搜索过程逐步探索各个顶点最终确定每一对指定对象之间确切存在的最高层连接点。在整个回溯阶段会持续不断地将新发现的关键元素纳入集合之中以便随时获取最新的根部指示器状态变化情况进而得出结论。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值