一、基本概念:
給定一棵有根樹,若節點z既是節點x的祖先,也是節點y的祖先,則稱z是x,y的公共祖先。在x,y的所有公共祖先中,深度最大的乙個稱為x,y的最近公共祖先,記為lca(x,y)。
lca(x,y)是x到根的路徑與y到根的路徑的交匯點。它也是x與y之間的路徑上深度最小的節點。求最近公共祖先的方法通常有五種。
在此介紹三種,另外兩種還沒學。
二、向上標記法:
從x向上走到根節點,並標記所有經過的節點。
從y向上走到根節點,當第一次遇到已標記的節點時,就找到了lca(x,y)。
對於每個詢問,向上標記法的時間複雜度最壞為o(n)。
三、樹上倍增法:
樹上倍增法是乙個很重要的演算法。除了lca之外,它在很多問題中都有廣泛應用。設f(x,k)表示x的2k輩祖先,即從x向根節點走2k步到達的節點。特別的,若該節點不存在,則令f(x,k)=0。f(x,0)就是x的父節點。除此之外,1≤k≤logn ,f(x,k)=f(f(x,k-1),k-1)。
這類似於乙個動態規劃的過程,階段就是節點的深度。因此,我們可以對樹進行廣度優先遍歷,按照層次順序,在節點入隊之前,計算它在f陣列中相應的值。
以上是預處理部分,時間複雜度為o(nlogn),之後可以多次對不同的x,y計算lca,每次詢問的時間複雜度為o(logn)。
基於f陣列計算lca(x,y)分為以下幾步:
(1)----設d(x)表示x的深度。不妨設d(x)≥d(y)。(否則可以交換x,y)。
(2)----用二進位制拆分思想,把x向上調整到於y同一深度。
具體來說,就是依次嘗試從x向上走k=2logn,……21,20步,檢查到達的節點是否比y深。在每次檢查中,若是,則令x=f(x,k)。
(3)----若此時x==y,說明已經找到了lca,lca就是y。
(4)----用二進位制拆分思想,把x,y同時向上調整,並保持深度一致且二者不相匯。
具體來說,就是依次嘗試把x,y同時向上走k=2logn,……21,20步,在每次嘗試中,
若f(x,k)≠f(y,k)(即仍未相會),則令x=f(x,k),y=f(y,k)。
(5)----此時x,y必定只差一步就相會了,他們的父節點f(x,0)就是lca。
以hdoj2586 how far away為例:
時間複雜度為:o((n+m)logn)。
#include
#include
#include
#include
#include
#include
#include
#define ll long long
using
namespace std;
const
int maxn=
50010
;int f[maxn][20
],d[maxn]
,dis[maxn]
;int ver[maxn*2]
,nt[maxn*2]
,edge[maxn*2]
,head[maxn]
;int t,n,m,tot,t;
queue<
int>q;
void
add(
int x,
int y,
int z)
void
bfs(
void)}
}int
lca(
int x,
int y)
if(x==y)
return x;
for(
int i=t;i>=
0;i--
)return f[x][0
];}int
main
(void
)bfs()
;for
(int i=
1;i<=m;i++)}
return0;
}
四、lca的tarjan演算法:
tarjan演算法本質上是使用並查集對「向上標記法」的優化。它是乙個離線演算法,需要把m個詢問一次性讀入,統一計算,最後統一輸出。時間複雜度o(n+m)。
在深度優先遍歷的任意時刻,樹中節點分為三類:
(1)----已經訪問完畢並且回溯的節點。在這些節點上標記乙個整數2。
(2)----已經開始遞迴,但尚未回溯的節點。這些節點就是當前正在訪問的節點x以及x的祖先。在這些節點上標記乙個整數1。
(3)----尚未訪問的節點。這些節點沒有標記。
對於正在訪問的節點x,它到根節點的路徑已經標記為1。若y是已經訪問完畢並且回溯的節點,則lca(x,y)就是從y向上走到根,第乙個遇到的標記為1的節點。
可以利用並查集進行優化,當乙個節點獲得整數2的標記時,把它所在的集合合併到它的父節點所在的集合中(合併時它的父節點標記一定為1,且單獨構成乙個集合)。
這相當於每個完成回溯的節點都有乙個指標指向它的父節點,只需查詢y所在集合的代表元素(並查集的get操作),就等價於從y向上一直走到乙個開始遞迴但尚未回溯的節點(具有標記1),即lca(x,y)。
此時掃瞄與x相關的所有詢問,若詢問當中的另乙個點y的標記為2,就知道了該詢問的回答應該是y在並查集中的代表元素(get(y)函式的結果)。
同樣以how far away為例:
時間複雜度為o(n+m)。
#include
#include
#include
#include
#include
#include
#include
#include
#define ll long long
using
namespace std;
const
int maxn=
50010
;int ver[maxn*2]
,nt[maxn*2]
,edge[maxn*2]
,head[maxn]
;int fa[maxn]
,d[maxn]
,v[maxn]
,lca[maxn]
,ans[maxn]
;vector<
int>query[maxn]
,query_id[maxn]
;int t,n,m,tot,t;
void
add(
int x,
int y,
int z)
void
add_query
(int x,
int y,
int id)
int_get
(int x)
void
tarjan
(int x)
for(
int i=
0;i.size()
;i++)}
v[x]=2
;}intmain
(void
) tot=0;
int x,y,z;
for(
int i=
1;i)for
(int i=
1;i<=m;i++)}
tarjan(1
);for(
int i=
1;i<=m;i++
)printf
("%d\n"
,ans[i]);
}return0;
}
最近公共祖先 LCA 最近公共祖先
直接暴力搜尋參考 普通搜尋每次查詢都需要 樸素演算法是一層一層往上找,倍增的話直接預處理出乙個 具體做法是 維護乙個 的關係來線性求出這個陣列 int anc n 31 int dep n 記錄節點深度 void dfs int u,int parent for int i 0 i g u size...
最近公共祖先 最近公共祖先(LCA)
如題,給定一棵有根多叉樹,請求出指定兩個點直接最近的公共祖先。輸入格式 第一行包含三個正整數n m s,分別表示樹的結點個數 詢問的個數和樹根結點的序號。接下來n 1行每行包含兩個正整數x y,表示x結點和y結點之間有一條直接連線的邊 資料保證可以構成樹 接下來m行每行包含兩個正整數a b,表示詢問...
LCA 最近公共祖先
定義 對於有根樹t的兩個結點u v,最近公共祖先lca t,u,v 表示乙個結點x,滿足x是u v的祖先且x的深度盡可能大。另一種理解方式是把t理解為乙個無向無環圖,而lca t,u,v 即u到v的最短路上深度最小的點。現在給定乙個根為1的樹,求某兩個點的最近公共祖先。思路 預處理出每個點的深度再一...