树上最值及LCA

倍增法求LCA及树上路径极值

前言

最近在整理图论模板,把LCA和树上路径最大/最小值问题一并总结一下。这两个问题本质上都是利用倍增思想,在 O(log n) 时间内完成查询。


一、LCA模板(P3379)

核心思想

预处理每个节点的 2^k 级祖先,查询时先将较深节点上跳至同一深度,再一起上跳找到LCA。

代码

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
int n,m,root;
vector<int>g[N];
int dep[N];
int pr[N][30]; 

void dfs(int u,int fa){
	dep[u]=dep[fa]+1;
	if(u!=root){
		pr[u][0]=fa;
	}
	for(int v:g[u]){
		if(v==fa) continue;
		dfs(v,u);
	}
}

int lca(int u,int v){
	if(dep[u]<dep[v]) swap(u,v);
	for(int i=20;i>=0;i--){
		if(dep[pr[u][i]]>=dep[v]){
			u=pr[u][i];
		}
	}
	if(u==v) return u;
	for(int i=20;i>=0;i--){
		if(pr[u][i]!=pr[v][i]){
			u=pr[u][i];
			v=pr[v][i];
		}
	}
	return pr[u][0];
}

int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n>>m>>root;
	for(int i=2;i<=n;i++){
		int u,v;
		cin>>u>>v;
		g[u].push_back(v);
		g[v].push_back(u);
	}
	dfs(root,0);
	for(int i=1;i<=20;i++){
		for(int u=1;u<=n;u++){
			pr[u][i]=pr[pr[u][i-1]][i-1];
		}
	}
	while(m--){
		int a,b;
		cin>>a>>b;
		cout<<lca(a,b)<<"\n";
	}
	return 0;
}

关键点

  • pr[u][i] 表示 u2^i 级祖先
  • 倍增数组递推:pr[u][i] = pr[pr[u][i-1]][i-1]
  • 深度对齐时注意 pr 数组边界(根节点的祖先为 0)

二、路径最大/最小值(T770744)

核心思想

在LCA的基础上,额外维护 mx[u][i]mi[u][i],分别表示从 u 向上跳 2^i 步路径上的最大/最小边权。

代码

#include<bits/stdc++.h>
#define pii pair<int,int>
using namespace std;
const int N=5e5+10;

int n,q;
vector<pii>g[N];
int dep[N];
int pr[N][20];
int mx[N][20];
int mi[N][20];

void dfs(int u,int fa){
	dep[u]=dep[fa]+1;
	pr[u][0]=fa;
	for(auto [v,w]:g[u]){
		if(v==fa) continue;
		mx[v][0]=w;
		mi[v][0]=w;
		dfs(v,u);
	}
}

int maxl(int u,int v){
	int ans=0;
	if(dep[u]<dep[v]) swap(u,v);
	for(int i=19;i>=0;i--){
		if(dep[pr[u][i]]>=dep[v]){
			ans=max(ans,mx[u][i]);
			u=pr[u][i];
		}
	}
	if(u==v) return ans;
	for(int i=19;i>=0;i--){
		if(pr[u][i]!=pr[v][i]){
			ans=max(ans,mx[u][i]);
			ans=max(ans,mx[v][i]);
			u=pr[u][i];
			v=pr[v][i];
		}
	}
	ans=max(ans,mx[u][0]);
	ans=max(ans,mx[v][0]);
	return ans;
}

int minl(int u,int v){
	int ans=0x3f3f3f3f;
	if(dep[u]<dep[v]) swap(u,v);
	for(int i=19;i>=0;i--){
		if(dep[pr[u][i]]>=dep[v]){
			ans=min(ans,mi[u][i]);
			u=pr[u][i];
		}
	}
	if(u==v) return ans;
	for(int i=19;i>=0;i--){
		if(pr[u][i]!=pr[v][i]){
			ans=min(ans,mi[u][i]);
			ans=min(ans,mi[v][i]);
			u=pr[u][i];
			v=pr[v][i];
		}
	}
	ans=min(ans,mi[u][0]);
	ans=min(ans,mi[v][0]);
	return ans;
}

int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n>>q;
	memset(mi,0x3f,sizeof(mi));
	for(int i=2;i<=n;i++){
		int u,v,w;
		cin>>u>>v>>w;
		g[u].push_back({v,w});
		g[v].push_back({u,w});
	}
	dfs(1,0);
	for(int i=1;i<20;i++){
		for(int u=1;u<=n;u++){
			pr[u][i]=pr[pr[u][i-1]][i-1];
			mx[u][i]=max(mx[u][i-1],mx[pr[u][i-1]][i-1]);
			mi[u][i]=min(mi[u][i-1],mi[pr[u][i-1]][i-1]);
		}
	}
	while(q--){
		int a,b;
		cin>>a>>b;
		cout<<maxl(a,b)<<" "<<minl(a,b)<<"\n";
	}
	return 0;
}

关键点

  • mx[v][0] = wmi[v][0] = w 在 DFS 中初始化
  • 倍增递推时同时更新最大最小值
  • 查询时在跳转过程中同步更新答案
  • 最小值数组需要初始化为 0x3f3f3f3f

三、对比总结

内容 LCA模板 路径极值模板
维护数组 pr[u][i] pr[u][i], mx[u][i], mi[u][i]
DFS初始化 只记录父节点 同时记录边权
查询时 只跳转 跳转时同步更新答案
复杂度 O((n+q)log n) O((n+q)log n)

四、注意事项

  1. 数组大小log n2025,视 n 的范围而定,一般 2e5205e525
  2. 根节点的祖先pr[root][i] = 0dep[0] = 0,深度对齐时不会越界。
  3. 最小值初始化:用 0x3f3f3f3f,确保 min 操作正确。
  4. 边权可能为 0:最大值初始化为 0 没问题,但如果边权可能为负数,需要初始化为 -INF

后记

这两个模板在树上路径问题中非常常用,掌握之后可以扩展到路径异或、路径第 k 大等更多场景。核心就是倍增思想 + 维护额外信息

posted @ 2026-07-07 16:29  diaitor  阅读(3)  评论(0)    收藏  举报