1. 程式人生 > >最近公共祖先(LCA):離線&線上演算法

最近公共祖先(LCA):離線&線上演算法

問題:求兩個結點的最近公共祖先(即在樹中的公共祖先中高度最低的祖先),下面介紹兩種適用於不同場景的演算法。

Hiho15:離線Tarjan演算法

基本思想

Tarjan演算法適用於離線批量處理多個查詢請求。基本思想是以深度優先搜尋的順序訪問這顆樹,給這棵樹的結點染色,一開始所有結點都是白色的,而當第一次經過某個結點的時候,將它染成灰色,而當第二次經過這個結點的時候——也就是離開這棵子樹的時候,將它染成黑色。


這樣做的意義,舉例說明,當我們深度優先搜尋到A結點時,我們發現A結點和B結點是我們需要處理的一組詢問。此時樹結點的染色情況如下圖:


發現B結點的顏色為灰色,那麼最近公共祖先必然就是B結點(灰色代表第一次進入該結點的子樹,A結點在B的子樹裡);

對於此時A和P的一組詢問,發現P結點顏色為白色,則留待訪問到P結點時處理;

還有A和C的一組詢問,發現C為黑色,則LCA為C結點向上的第一個灰色結點。

  • 總結:我先計算每個結點涉及到的詢問,然後在深度優先搜尋的過程中對結點染色,如果發現當前訪問的結點是涉及到某個詢問,那麼我就看這個詢問中另一個結點的顏色,如果是白色,則留待之後處理,如果是灰色,那麼最近公共祖先必然就是這個灰色結點,如果是黑色,那麼最近公共祖先就是這個黑色結點向上的第一個灰色結點。

利用並查集查詢黑色結點的最近灰色結點

還有一個問題就是如何快速找到黑色結點向上的第一個灰色結點:使用並查集維護。

想一下,當我們到達一個結點C時,該節點為灰色,進入該結點的子樹回到該節點時,子樹結點已經全部變黑,而這些黑色結點向上的第一個灰色結點就是當前結點C.

 

而此節點離開時變黑,他自己向上的第一個灰色結點就是它的父節點D,那麼這一過程,其實就是將C結點代表的集合合併到了D結點代表的集合中去了,如下圖:

 

  • 總結:每個結點最開始都是一個獨立的集合,每當一個結點由灰轉黑的時候,就將它所在的集合合併到其父親結點所在的集合中去。這樣無論什麼時候,任意一個黑色結點所在集合的代表元素就是這個結點向上的第一個灰色結點!
  • 說明:用req陣列表示每個結點的代表元素,對於灰色結點o的特點就是req[o]==o

所以查詢的過程可以寫成如下:

int Tree::find(int no) {
   if (no == req[no])
      return no;
   else{
      req[no] = find(req[no]);
      return req[no];
   }
}


實現該演算法來解決hiho15題目時,為了快速找到樹中某一結點做了對映,由string對映到樹結點在陣列中的索引值,這樣就可以方便使用並查集。

基本思想

就是每次詢問都要直接給出結果,這裡有一種線上演算法,基本思想是從樹的根結點深度優先搜尋,每次經過某一個點——無論是從它的父親節點進入這個點,還是每次從它的兒子節點返回這個點,都按順序記錄下來。這樣就把一棵樹轉換成了一個數組,而找到樹上兩個節點的最近公共祖先,無非就是找到這兩個節點最後一次出現在陣列中的位置所囊括的一段區間中深度最小的那個點,轉成陣列後使用範圍最小值演算法(RMQ)即可搞定。

將樹轉為陣列舉例(圖來自百度圖片):


陣列:5 2 8 6 2 7 2 1 3 1 4 1

注:這裡我稍作調整,即非葉子結點的等每次訪問完一個子節點後再記錄,而葉子節點記錄一次即可。

程式碼附錄在我的github賬號的leetcode倉庫,如有需要請前往檢視。