LCA問題:如何求樹(不限於二叉樹)中兩個節點(不限於葉子節點)的最近公共祖先節點。
LCA算法分為在線算法與離線算法。
在線算法:可以以序列化的方式一個個的處理輸入,也就是說在開始時並不需要已經知道所有的輸入。
離線算法:在開始時就需要知道問題的所有輸入數據,而且在解決一個問題后就要立即輸出結果。
在線算法與離線算法都基於DFS。在線算法與RMQ算法(區間最值查詢)相關,離線算法與Tarjan算法相關。
在線算法 LCA RMQ的相互轉換
各自意義:
LCA:基於有根樹最近公共祖先問題。 LCA(T, u, v):在有根樹T中,詢問一個距離根最遠的結點x,使得x同時為結點u、v的祖先。
RMQ:區間最小值詢問問題。 RMQ(A, i, j):對於線性序列A中,詢問區間[i, j]上的最小(最大)值。
RMQ對於LAC的轉換:
設一個長度為N的序列A,按照如下方法將其遞歸建立為一棵樹:
1)設序列中最小值為Ak,建立優先級為Ak的根節點Tk;
2)將A(1…k-1)遞歸建樹作為Tk的左子樹;
3)將A(k+1…N)遞歸建樹作為Tk的右子樹;
如序列A=(7, 5, 8, 1, 10)建樹的結果為:
對於RMQ(A, i, j):
1)設序列中的最小值為Ak,若i<=k<=j,那么答案為k;
2)若k>j,那么答案為RMQ(A1…k-1, i, j);
3)若k< i,那么答案為RMQ(Ak+1…N, i, j);
而RMQ問題可以采用ST算法解決,則將LCA轉換為RMQ再根據ST算法解決,時間復雜度為O(nlogn)的預處理+O(1)的查詢。
LCA的離線Tarjan算法
基於Tarjan算法與並查集。
Tarjan算法
算法用集合表示一類節點,這些節點跟集合外的點的LCA都一樣,並把這個LCA設為這個集合的祖先。當搜索到節點x時,創建一個由x本身組成的集合,這個集合的祖先為x自己。然后遞歸搜索x的所有兒子節點。當一個子節點搜索完畢時,把子節點的集合與x節點的集合合並,並把合並后的集合的祖先設為x。因為這棵子樹內的查詢已經處理完,x的其他子樹節點跟這棵子樹節點的LCA都是一樣的,都為當前根節點x。所有子樹處理完畢之后,處理當前根節點x相關的查詢。遍歷x的所有查詢,如果查詢的另一個節點v已經訪問過了,那么x和v的LCA即為v所在集合的祖先。
對於每個節點u,關於它的詢問(u,v)只有兩種。
1、v在u的子樹內。
此時LCA(u,v) = u.
2、v不在u的子樹內。
⑴假設v在u的父親的另一棵子樹內。
此時LCA(u,v) = father[u].
⑵如果不滿足條件⑴,則v可能在u的父親的父親的另一棵子樹內。
而此時LCA(u,v) = father[ father[u] ].
⑶……
不論是哪種情況,LCA(u,v)都與u和father[ ]有某種關系。我們能不能抓住這種關系呢?
我們繼續觀察,一直向上取father[ ],貌似和並查集的FIND操作很像呢。
我們用並查集的角度依次考慮上面的情況試試看。
1、v在u的子樹內。
此時dfs(u)還在棧中,沒有執行完,此時沒有向上取father[ ],說明此時u是根。
2、v不在u的子樹內。
⑴假設v在u的父親的另一棵子樹內。
此時的dfs(u)已經執行完並出棧。此時向上取了一次father[ ],說明此時u的父親是根。
⑵如果不滿足條件⑴,則v可能在u的父親的父親的另一棵子樹內。
同理,此時dfs(u的父親)也已經執行完並出棧。此時向上取了兩次father[ ],說明此時u的父親的父親是根。
⑶……
綜上,我們只要保證當dfs(u)在棧中的時候,u是根;當dfs(u)不在棧中的時候,father[u]是根就行了。