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;
}
百行代码爽!!!!!