LCA 最近公共祖先(树链和倍增)这次真有树链了!!!
概念
最近公共祖先简称 LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。
感觉其实看个图就懂了吧
图中例子 \(lca(u,v)=x;\)
这个问题理解概念不难,主要是学会如何计算,下面介绍三种方法。
方法
1.暴力法
朴素
将其中一个点反复向上跳(遍历),并将经过的点打上标记,到达根节点后,另一个点也开始向上跳,但不需再打标记,当遇到第一个打过标记的点时,即找到了这两个点的 LCA。
优化
可以发现其实没必要分开跳,其实还可以一起向上跳,当跳到的同一个节点时,即找到了最近的 LCA。但注意要先把深度较大的点暴力跳到另一点的同一深度,再一起向上跳。
暴力法的单次查询的复杂度为 \(O(n)\)。其实也就优化了个数组
代码
复杂度太差了,不要写这个,我们学更好的好吗。
2. 倍增法
理论
其实就是在暴力的方法上进行优化,可以发现这样一个一个跳太浪费时间了,我们就可以用倍增的方法每次跳 \(1,2,4,8,16\) 个节点来快速跳到任一祖先节点(根据二进制的性质)。
理论存在实践开始!!!
注意:我们定义 \(pre[x][i]\) 为节点 \(x\) 跳 \(2^i\) 可以到达的父节点。
首先 pre[x][0] 一定为父节点,那 pre[x][1] 就为 pre[pre[x][0]][0] (父节点的父节点) 后面也是同样道理,递推即可。
void init2(int x,int f){ pre[x][0]=f;//父节点 deep[x]=deep[f]+1;//深度 for(int i=0;i<v[x].size();i++){ if(v[x][i]==f){ continue; } init2(v[x][i],x); } } void init(){ init2(s,0); for(int i=1;i<=25;i++){//枚举次方 for(int j=1;j<=n;j++){//枚举点 pre[j][i]=pre[pre[j][i-1]][i-1];//递推 } } }
这个代码求出了每个点的深度 \(deep[]\) 和 \(pre[][]\)。
然后以上全部预处理已完成,可以开始跳!!!。
int lca(int x,int y){ if(deep[x]<deep[y]){//我们选定 x 为深度更大点,对他进行向上跳的操作 swap(x,y); } for(int i=25;i>=0;i--){//倍增跳到同一深度 int fa=pre[x][i]; if(deep[fa]>=deep[y]){//不要跳过头 x=fa; } } if(x==y){//如果一个点为另一点的直接祖先 return x; } for(int i=25;i>=0;i--){//同时向上倍增跳点 int xx=pre[x][i]; int yy=pre[y][i]; if(xx!=yy){//注意不要跳到一样的点,因为我们想要求的是最近的公共祖先 x=xx; y=yy; } } return pre[x][0];//因为他们不会跳到同一个点,所以他们会在最近公共祖先的子节点停下 }
好了这样就完成了,预处理的复杂度为 \(O(n\log n)\),单次查询的复杂度为 \(O(n)\)(好棒!!!)。
完整代码
#include <bits/stdc++.h> using namespace std; const int N=5e5+10; int n,m,s; int pre[N][30]; int deep[N]; vector<int> v[N]; void init2(int x,int f){ pre[x][0]=f; deep[x]=deep[f]+1; for(int i=0;i<v[x].size();i++){ if(v[x][i]==f){ continue; } init2(v[x][i],x); } } void init(){ init2(s,0); for(int i=1;i<=25;i++){ for(int j=1;j<=n;j++){ pre[j][i]=pre[pre[j][i-1]][i-1]; } } } int lca(int x,int y){ if(deep[x]<deep[y]){ swap(x,y); } for(int i=25;i>=0;i--){ int fa=pre[x][i]; if(deep[fa]>=deep[y]){ x=fa; } } if(x==y){ return x; } for(int i=25;i>=0;i--){ int xx=pre[x][i]; int yy=pre[y][i]; if(xx!=yy){ x=xx; y=yy; } } return pre[x][0]; } int main() { ios::sync_with_stdio(false); cin>>n>>m>>s; for(int i=1;i<n;i++){ int u,vv; cin>>u>>vv; v[u].push_back(vv); v[vv].push_back(u); } init(); while(m--){ int u,vv; cin>>u>>vv; cout<<lca(u,vv)<<"\n"; } return 0; }
Tarjan
树链部分
例题讲解
为啥我不写tarjan呢 懒,因为这个算法的复杂度虽好,但可惜是离线的,需要存入输入最后才输出,而大部分倍增已足够,可能后续会补充的吧,咕咕,我只好鸽一会了。
1.货车运输
如果带个人情绪的话,我只能说这题就是个史,超级逆天缝合怪,但要是客观地说的话,其实这题很好地考了多个考点,很检验 oier 的实力,而且也是多个模板的结合(黄+黄=蓝)。
题意:\(n\) 个点,\(m\) 条边,边有边权,询问 \(u\) 到 \(v\) 的路径上边权最小值最大是多少。
可能看到这题时就直接开始跑最短路了(虽然有环不不太行,而且这范围直接爆炸),但如果有环的可以先 Kruskal 求最大生成树使边权尽可能大,然后因为是树所以依靠 LCA 求出路径,并在找 LCA 时不断对路径权值求最小值,然后就好了。
但是这题就是恶心,明明就是模板但就是很麻烦,上代码吧。
点击查看代码
#include <bits/stdc++.h> using namespace std; #define ll long long const int N=500010; int n,m,s; int pre[N][30]; int deep[N]; ll val[N][30]; int vis[N]; struct ss{ int to,w; }; vector<ss> v[N]; int fa[N]; struct node{ int x,y,v; }a[N]; int find(int x){ if(x==fa[x]){ return x; } return fa[x]=find(fa[x]); } bool cmp(node g,node h){ return g.v>h.v; } void dfs(int x,int f,int we){ vis[x]=1; pre[x][0]=f; deep[x]=deep[f]+1; val[x][0]=we; for(int i=1;(1<<i)<=deep[x];i++){ pre[x][i]=pre[pre[x][i-1]][i-1]; val[x][i]=min(val[x][i-1],val[pre[x][i-1]][i-1]); } for(int i=0;i<v[x].size();i++){ ss p=v[x][i]; if(p.to==f){ continue; } dfs(p.to,x,p.w); } } int lca(int x,int y){ if(find(x)!=find(y)){ return -1; } ll ans=2e9; if(deep[x]<deep[y]){ swap(x,y); } for(int i=28;i>=0;i--){ int fa=pre[x][i]; if(deep[fa]>=deep[y]){ ans=min(ans,val[x][i]); x=fa; } } if(x==y){ return ans; } for(int i=28;i>=0;i--){ if(pre[x][i]!=pre[y][i]){ ans=min(ans,val[x][i]); ans=min(ans,val[y][i]); x=pre[x][i]; y=pre[y][i]; } } ans=min(ans,min(val[x][0],val[y][0])); return ans; } int main() { ios::sync_with_stdio(false); cin>>n>>m; for(int i=1;i<=m;i++){ cin>>a[i].x>>a[i].y>>a[i].v; } for(int i=1;i<=n;i++){ fa[i]=i; } sort(a+1,a+m+1,cmp); for(int i=1;i<=m;i++){ if(find(a[i].x)!=find(a[i].y)){ fa[find(a[i].x)]=find(a[i].y); v[a[i].x].push_back({a[i].y,a[i].v}); v[a[i].y].push_back({a[i].x,a[i].v}); } } for(int i=1;i<=n;i++){ if(vis[i]==0){ dfs(i,0,0); } } int q; cin>>q; while(q--){ int u,vv; cin>>u>>vv; cout<<lca(u,vv)<<"\n"; } return 0; }
百行代码爽!!!!!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)