最近公共祖先 lca

定义

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

例题

最近公共祖先有多种不同求法,根据题目要求选择适合的写法

例题传送门

题意

给你一个 \(n\) 个节点,\(n-1\) 条边的树,数据保证可以构成树,树的根节点为 \(s\),现在有 \(m\) 组询问,每组询问包含两个正整数 \(a,b\),表示询问 \(a\) 节点和 \(b\) 节点的最近公共祖先。

数据范围

对于 \(30\%\) 的数据,\(N\le 10\),\(M\le 10\)。

对于 \(70\%\) 的数据,\(N\le 10000\),\(M\le 10000\)。

对于 \(100\%\) 的数据,\(N\le 500000\),\(M\le 500000\)。

解法一:暴力求解

在树中,每个节点都有其对应的深度,深度是相对于根节点而言的,而要找到 \(x\),\(y\) 的 lca 深度是唯一的,当 \(x\) 与 \(y\) 的深度相同时,每一次上跳父节点可保证深度总是相同的,这是只需要保证父节点重合则说明找到了最近公共祖先。

所以在存储的时候,需要存储每个点的深度,同时要存储每个节点的父节点。

需要预处理整棵树,时间复杂度 \(O(n)\),单次查询的复杂度为 \(O(n)\),算法简单,耗时,期望得分70分。

代码如下

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
int n,m,s,d[N],f[N];
vector<int>e[N];
inline void dfs(int u,int fa){
    d[u]=d[fa]+1;
    f[u]=fa;
    for(auto v:e[u]){
        if (v!=fa){
            dfs(v,u);
        }
    }
}
inline void solve(int x,int y){
    if (d[x]>d[y]){
        swap(x,y);
    }
    while(d[x]<d[y]){
        y=f[y];
    }
    while(y!=x){
        y=f[y],x=f[x];
    }
    if (x==0){
        cout<<s<<"\n";
    }else{
        cout<<x<<"\n";
    }
}
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>m>>s;
    for(int i=1;i<=n-1;i++){
        int u,v;
        cin>>u>>v;
        e[u].push_back(v);
        e[v].push_back(u);
    }
    dfs(s,0);
    while(m--){
        int a,b;
        cin>>a>>b;
        solve(a,b);
    }
    return 0;
}

洛谷的数据有点水,O(n^2)的复杂度都能得100分

解法二:倍增算法

倍增算法是暴力算法的改进版,通过引入二维的 \(f\) 数组,来减少节点上跳的次数,将单次查询的复杂度降到了 \(O(logn)\),倍增思路和,ST表的思路差不多,不会 ST表 的可以参考这里

定义 \(fui\) 表示点 \(u\) 的 \(2^i\) 个祖先,\(fui\) 可以通过 dfs 预处理出来,不难发现 \(fui=ff\[ui-1]i-1\),\(u\) 的第 \(2^i\) 个祖先就是 \(u\) 的第 \(2^{i-1}\) 个祖先的 \(2^{i-1}\) 个祖先。

预处理复杂度 \(O(nlogn)\),单次查询复杂度 \(O(logn)\),总复杂度 \(O(nlogn+mlogn)\) ,轻松通过此题,期望得分100分

代码如下

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
int n,m,s,d[N],f[N][21];
vector<int>e[N];
inline void dfs(int u,int fa){
    d[u]=d[fa]+1;
    f[u][0]=fa;
    for(int i=1;(1<<i)<=d[u];i++){
        f[u][i]=f[f[u][i-1]][i-1];
    }
    for(auto v:e[u]){
        if (v!=fa){
            dfs(v,u);
        }
    }
}
inline void solve(int x,int y){
    if (d[x]>d[y]){
        swap(x,y);
    }
    for(int i=20;i>=0;i--){
        if (d[x]<=d[y]-(1<<i)){
            y=f[y][i];
        }
    }
    if (x==y){
        cout<<x<<"\n";
        return ;
    }
    for(int i=20;i>=0;i--){
        if (f[x][i]==f[y][i]){
            continue;
        }else{
            x=f[x][i],y=f[y][i];
        }
    }
    cout<<f[x][0]<<"\n";
    return;
}
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>m>>s;
    for(int i=1;i<=n-1;i++){
        int u,v;
        cin>>u>>v;
        e[u].push_back(v);
        e[v].push_back(u);
    }
    dfs(s,0);
    while(m--){
        int a,b;
        cin>>a>>b;
        solve(a,b);
    }
    return 0;
}

解法三:dfs序+RMQ算法

前置知识,ST表 怎么又是你,用于解决区间最值问题。

那么 lca 怎么与 RMQ 联系起来呢?

因为 lca 是树上问题,而对树的遍历可以使用一个桥梁,dfs序,让树的遍历成为了一个区间序列。

下面进行具体说明

以上图的树为例

序号 :01 02 03 04 05 06 07 08 09 10 11 12 13

深度 :01 02 03 02 03 04 03 04 03 02 01 02 01

节点 :01 02 04 02 05 06 05 07 05 02 01 03 01

得到这张表后,你就能在表中发现有意思的东西,例如你想求 lca(4,6)时,只需要找到从4号节点到6号节点的随便一个区间,再找出区间中深度最小的节点,该点即为 lca(4,6)。

序号 \(1\sim 13\) 是整个区间,找到的区间可以是 \(3\sim 6\),其中深度最小为 \(2\),所对的节点即为 \(2\) 号节点。证明自己搜去吧

用二维数组 \(st\) 维护 dfs序 中的区间最小值,同时用二维数组\(isdt\)存储区间最小值所对的节点,\(dfn\) 数组记录 dfs序 ,\(iddfn\) 记录某个节点第一次遍历到时的 dfs序。

注意:

  1. \(st\)存储的是dfs序的区间最值,要注意边界和初始化问题

  2. \(dfn\)数组虽然叫dfs序,但是 \(dfni\) 表示时间戳为 \(i\) 时所对的节点编号

  3. 由于 \(st\) 存的是一段 dfs 序的区间信息,所以传入的参数应该也是 dfs 序

  4. 搞明白每个变量所代表的是什么,不然就会像我一样调 \(2\) 小时

时间复杂度为 \(O(nlogn+m)\),但是空间开销大,预处理常数大,期望得分100分

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
int n,m,s;
int st[N*2][21],idst[N*2][21],lg[N*2];
int dfn[N*2],d[N*2],times,iddfn[N];
vector<int>e[N];
inline void dfs(int u,int fa){
	dfn[++times]=u;
	if (iddfn[u]==0)iddfn[u]=times;
	d[u]=d[fa]+1;
	for(auto v:e[u]){
		if (v==fa)continue;
		dfs(v,u);
		dfn[++times]=u;
	}
}
inline void init(){
	lg[2]=1;
	for(int i=3;i<=2*n;i++){
		lg[i]=lg[i/2]+1;
	}
	for(int i=1;i<=times;i++){
		st[i][0]=d[dfn[i]];
		idst[i][0]=dfn[i];
	}
	for(int j=1;j<=20;j++){
		for(int i=1;i+(1<<j)-1<=2*n;i++){
			if (st[i][j-1]<st[i+(1<<(j-1))][j-1]){
				st[i][j]=st[i][j-1];
				idst[i][j]=idst[i][j-1];
			}else{
				st[i][j]=st[i+(1<<(j-1))][j-1];
				idst[i][j]=idst[i+(1<<(j-1))][j-1];
			}
		}
	}
}
inline void solve(int l,int r){
	if (l>r)swap(l,r);
	int p=lg[r-l+1];
	if (st[l][p]<st[r-(1<<p)+1][p]){
		cout<<idst[l][p]<<"\n";
	}else{
		cout<<idst[r-(1<<p)+1][p]<<"\n";
	}
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>m>>s;
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	dfs(s,0);
	init();
	while(m--){
		int l,r;
		cin>>l>>r;
		solve(iddfn[l],iddfn[r]);
	}
	return 0;
}

解法四:tarjan算法

上面几种算法都属于在线算法,而tarjan解决 lca 属于离线算法。

tarjan算法相较于上面的算法要更稳定一些,时间复杂度也比较居住,前置知识,并查集

基本思路:

  1. 读入数据的过程中将每组询问像建图那样存起来,正反建两次
  2. 从根节点开始进行遍历
  3. 遍历节点 \(u\) 的所有子节点 \(v\),并标记这些点以被访问过
  4. 若 \(v\) 还有子节点,则返回 3.,否则进行下一步
  5. 合并 \(v\) 到 \(u\) 上,注意顺序不能颠倒,因为 \(u\) 是 \(v\) 的父亲节点
  6. 寻找所有与 \(u\) 相连的,需要进行查询的节点 \(v'\)。
  7. 若 \(v'\) 以访问过,则可以确定 \(u\) 和 \(v\) 的最近公共祖先即为 \(find(v')\)。

时间复杂度 \(O(n+m)\),期望得分100分

代码如下

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
//#define int long long
const int N=5e5+5;
int n,m,s;
int ans[N];
int fa[N];
bitset<N>vis;
vector<int>e[N];
vector<pair<int,int>>q[N];
inline int find(int x){
	return x==fa[x] ? x : fa[x]=find(fa[x]);
}
inline void tarjan(int u,int father){
	vis[u]=true;
	for(auto v:e[u]){
		if (v==father or vis[v])continue;
		tarjan(v,u);
		fa[v]=u;
	}
	for(auto i:q[u]){
		int v=i.first,id=i.second;
		if (vis[v]){
			ans[id]=find(v);
		}
	}
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>m>>s;
	for(int i=1;i<=n-1;i++){
		int u,v;
		cin>>u>>v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	for(int i=1;i<=n;i++)fa[i]=i;
	for(int i=1;i<=m;i++){
		int x,y;
		cin>>x>>y;
		if (x==y){
			ans[i]=x;
			continue;
		}
		q[x].push_back({y,i});
		q[y].push_back({x,i});
	}
	tarjan(s,0);
	for(int i=1;i<=m;i++){
		cout<<ans[i]<<"\n";
	}
	return 0;
}