LCA 最近公共祖先(树链和倍增)这次真有树链了!!!

概念

最近公共祖先简称 LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。

感觉其实看个图就懂了吧

image

图中例子 \(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

树链部分

我是一个链接,你要点我才可以看,我现在真的有树链了

例题讲解

学习笔记1

学习笔记2

为啥我不写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;
}

百行代码爽!!!!!

posted @ 2024-08-30 08:35  sad_lin  阅读(91)  评论(2编辑  收藏  举报