1 定義
LCA(Least Common Ancestors):最近公用祖先。對於有根樹T的兩個結點u、v,最近公用祖先LCA(T,u,v)表示一個結點x,滿足x是u、v的祖先且x的深度儘可能大(設樹根的深度最小)。另一種理解方式是把T理解為一個無向無環圖,而LCA(T,u,v)即u到v的最短路上深度最小的點。這裡給出一個LCA的例子:對於T=<V,E>,V={1,2,3,4,5},E={(1,2),(1,3),(3,4),(3,5)},則有:LCA(T,5,2)=1,LCA(T,3,4)=3,LCA(T,4,5)=3。
2 離線演算法——Tarjan
演算法設計策略都是基於在執行演算法前輸入資料已知的基本假設,也就是說,演算法在求解問題時已具有與該問題相關的完全資訊,通常將這類具有問題完全資訊前提下設計出的演算法成為離線演算法( off line algorithms)(來自http://baike.baidu.com/view/2734232.htm?fr=ala0_1)
Tarjan演算法是一種用來解決LCA問題的離線演算法,它要求在求解前一次讀入所有LCA詢問(求一個LCA(T,u,v)稱為一個LCA詢問)。利用並查集優越的時空複雜度,我們可以實現LCA問題的O(n+Q)演算法,這裡Q表示詢問的次數。Tarjan演算法基於深度優先搜尋的架構,對於新搜尋到的一個結點,首先建立由這個結點構成的集合,再對當前結點的每一個子樹進行搜尋,每搜尋完一棵子樹,則可確定子樹內的LCA詢問(即求LCA(T,u,v))都已解決。其他的LCA詢問的結果必然在這個子樹之外,這時把子樹所形成的集合與當前結點的集合合并,並將當前結點設為這個集合的祖先。之後繼續搜尋下一棵子樹,直到當前結點的所有子樹搜尋完。這時把當前結點也設為已被檢查過的,同時可以處理有關當前結點的LCA詢問,如果有一個從當前結點到結點v的詢問,且v已被檢查過,則由於進行的是深度優先搜尋,當前結點與v的最近公用祖先一定還沒有被檢查,而這個最近公用祖先的包涵v的子樹一定已經搜尋過了,那麼這個最近公用祖先一定是v所在集合的祖先。下面給出這個演算法的虛擬碼:
LCA(u) {
Make-Set(u)
ancestor[Find-Set(u)]=u/*設定u所在集合的祖先*/
對於u的每一個孩子v {
LCA(v)
Union(v,u)/*把v產生的子集併入u中*/
ancestor[Find-Set(u)]=u/*防止採用樹形啟發學習法合并使u的集合根(代表)變化*/
}
checked[u]=true
對於每個(u,v)屬於P {
if checked[v]=true
then 回答u和v的最近公用祖先為 ancestor[Find-Set(v)]
}
}
上述虛擬碼中關於並查集的函數Make-Set、Find-Set、Union可以參考本空間中關於並查集的文章:http://hi.baidu.com/ly01kongjian/blog/item/e09057310ff40df31b4cffcb.html
由於是基於深度優先搜尋的演算法,只要調用LCA(root[T])就可以回答所有的LCA詢問了,這裡root[T]表示樹T的根,假設所有詢問(u,v)構成集合P。