树上最值及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]表示u的2^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] = w和mi[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) |
四、注意事项
- 数组大小:
log n取20或25,视n的范围而定,一般2e5用20,5e5用25。 - 根节点的祖先:
pr[root][i] = 0,dep[0] = 0,深度对齐时不会越界。 - 最小值初始化:用
0x3f3f3f3f,确保min操作正确。 - 边权可能为 0:最大值初始化为 0 没问题,但如果边权可能为负数,需要初始化为
-INF。
后记
这两个模板在树上路径问题中非常常用,掌握之后可以扩展到路径异或、路径第 k 大等更多场景。核心就是倍增思想 + 维护额外信息。

浙公网安备 33010602011771号