线性求LCA算法
tarjan算法求LCA
离线算法,复杂度O(n + m)
简介:
\(Tarjan\) 发明的离线查询两点间最近公共祖先的算法,复杂度优秀,但由于其离线的性质导致适用面不广。
算法概述:
将整棵树的所有节点分为三种类型:
- 已经遍历过,并回溯的节点
- 正在搜索的分支
- 还未搜索到的节点
在搜索过程中数组 \(pd\) 记录每个节点的类型。

在图中可以发现,对于当前搜索到的节点 \(i\) (\(pd_i\) = 1) 与 任意 \(j\) (\(pd_j\) = 2) 的最近公共祖先都在正在搜索的这条分支上。
用 \(G\) 表示 \(i\) 和 \(j\) 的 \(LCA\) ,观察 \(G\) 的性质 , 不难发现 ,\(G\) 与 \(j\) 其实并无关系,只与 \(i\) 有关 , 即 \(i\) 的祖先并且 \(pd\) = 1 。
所以,我们可以利用并查集 , 在回溯的时候将节点合并到其父节点的集合中,查询的时候直接调用并查集查找即可。
目前该算法的大体框架已经理清楚了,现在我们只需要将其串联起来。
- 将询问离线,储存在动态数组里
- 从根节点开始 DFS ,在每次回溯的时候合并到父节点的集合中
- 扫一遍关于当前节点 \(u\) 的所有询问 \(v\) ,如果存在 \(pd_v\) = 2 , 直接调用并查集得出答案。
以上便是 \(Tarjan\) 算法求解 LCA 具体思路 , 具体代码如下
code
int n , m , cnt ;
int head[maxn] , pd[maxn] , dis[maxn] , fa[maxn] , ans[maxn] ;
vector<pii > q[maxn] ;
struct node
{
int to , nxt , dis ;
} ed[maxn << 1] ;
void add(int u , int v , int w )
{
ed[++cnt] = { v , head[u] , w } ;
head[u] = cnt ;
}
void dfs(int u , int fa)
{
for(int i = head[u] ; i ; i = ed[i].nxt )
{
int v = ed[i].to , w = ed[i].dis ;
if(v == fa) continue ;
dis[v] = dis[u] + w ;
dfs(v , u) ;
}
}
int find(int x)
{
if(fa[x] == x) return x ;
return fa[x] = find(fa[x]) ;
}
void tarjan(int u )
{
pd[u] = 1 ;
for(int i = head[u] ; i ; i = ed[i].nxt )
{
int v = ed[i].to ;
if(! pd[v] )
{
tarjan(v) ;
fa[v] = u ;
}
}
for(auto item : q[u] )
{
int v = item.first , id = item.second ;
if(pd[v] == 2)
{
int yy = find(v) ;
ans[id] = dis[u] + dis[v] - 2 * dis[yy] ;
}
}
pd[u] = 2 ;
}
signed main()
{
n = read() , m = read() ;
for(int i = 1 ; i < n ; i++ )
{
int u = read() , v = read() , w = read() ;
add(u , v , w ) , add(v , u , w ) ;
}
for(int i = 1 ; i <= m ; i++ )
{
int u = read() , v = read() ;
if(u == v) continue ;
q[u].push_back({v , i}) ;
q[v].push_back({u , i}) ;
}
dfs(1 , 0 ) ;
for(int i = 1 ; i <= n ; i++ ) fa[i] = i ;
tarjan(1 ) ;
for(int i = 1 ; i <= m ; i++ )
cout << ans[i] << '\n' ;
return 0 ;
}
注 :此代码对应 例题[AcWing1171 距离](1171. 距离 - AcWing题库)

浙公网安备 33010602011771号