『圖論』LCA 最近公共祖先

概述篇

LCA (Least Common Ancestors) ,即最近公共祖先,是指這樣的一個問題:在一棵有根樹中,找出某兩個節點 uv 最近的公共祖先。html

LCA 可分爲在線算法離線算法git

  • 在線算法:指程序能夠以序列化的方式一個一個處理輸入,也就是說在一開始並不須要知道全部的輸入。
  • 離線算法:指一開始就須要知道問題的全部輸入數據,而在解決一個問題後當即輸出結果。

算法篇

對於該問題,很容易想到的作法是從 u、v 分別回溯到根節點,而後這兩條路徑中的第一個交點即爲 u、v 的最近公共祖先,在一棵平衡二叉樹中,該算法的時間複雜度能夠達到 O(logn)O(log⁡n) ,可是對於某些退化爲鏈狀的樹來講,算法的時間複雜度最壞爲 O(n)O(n) ,顯然沒法知足更高頻率的查詢。算法

本節將介紹幾種比較高效的算法來解決這一問題,常見的算法有三種:在線 DFS + ST 算法、倍增算法、離線 Tarjan 算法。數據結構

接下來咱們來一一解釋這三種 /* 看似高深,其實也不簡單 */ 的算法。函數

在線 DFS + ST 算法

首先看到 ST 你會想到什麼呢?(腦補許久都沒有想到它會是哪一個單詞的縮寫).net

看過前文 『數據結構』RMQ 問題 的話你即可以明白 ST算法 的思路啦~3d

So ,關於 LCA 的這種在線算法也是能夠創建在 RMQ 問題的基礎上咯~code

咱們設 LCA(T,u,v) 爲在有根樹 T 中節點 u、v 的最近公共祖先, RMQ(A,i,j) 爲線性序列 A 中區間 [i,j] 上的最小(大)值。htm

以下圖這棵有根樹:blog

img

咱們令節點編號知足父節點編號小於子節點編號(編號條件)

能夠看出 LCA(T,4,5) = 2, LCA(T,2,8) = 1, LCA(T,3,9) = 3

設線性序列 A 爲有根樹 T 的中序遍歷,即 A = [4,2,5,1,8,6,9,3,7]

由中序遍歷的性質咱們能夠知道,任意兩點 u、v 的最近公共祖先總在以該兩點所在位置爲端點的區間內,且編號最小。

舉個栗子:

假設 u = 8, v = 7 ,則該兩點所肯定的一段區間爲 [8,6,9,3,7] ,而區間最小值爲 3 ,也就是說,節點 3u、v 的最近公共祖先。

解決區間最值問題咱們能夠採用 RMQ 問題中的 ST 算法

可是在有些問題中給出的節點並不必定知足咱們所說的父節點編號小於子節點編號,所以咱們能夠利用節點間的關係建圖,而後採用前序遍從來爲每個節點從新編號以生成線性序列 A ,因而問題又被轉化爲了區間最值的查詢,和以前同樣的作法咯~

時間複雜度: n×O(logn)n×O(log⁡n) 預處理 + O(1)O(1) 查詢

想了解 RMQ 問題 的解法能夠戳上面的連接哦~


以上部分介紹了 LCA 如何轉化爲 RMQ 問題,而在實際中這兩種方案之間能夠相互轉化

類比以前的作法,咱們如何將一個線性序列轉化爲知足編號條件的有根樹呢?

  1. 設序列中的最小值爲 AkAk ,創建優先級爲 AkAk 的根節點 TkTk
  2. 將 A[1…k−1]A[1…k−1] 遞歸建樹做爲 TkTk 的左子樹
  3. 將 A[k+1…n]A[k+1…n] 遞歸建樹做爲 TkTk 的右子樹

讀者能夠試着利用此方法將以前的線性序列 A = [4,2,5,1,8,6,9,3,7] 構造出有根樹 T ,結果必定知足以前所說的編號條件,但卻不必定惟一。

離線 Tarjan 算法

Tarjan 算法是一種常見的用於解決 LCA 問題的離線算法,它結合了深度優先搜索與並查集,整個算法爲線性處理時間。

首先來介紹一下 Tarjan 算法的基本思路:

  1. 任選一個節點爲根節點,從根節點開始
  2. 遍歷該點 u 的全部子節點 v ,並標記 v 已經被訪問過
  3. 若 v 還有子節點,返回 2 ,不然下一步
  4. 合併 v 到 u 所在集合
  5. 尋找與當前點 u 有詢問關係的點 e
  6. 若 e 已經被訪問過,則能夠肯定 u、e 的最近公共祖先爲 e 被合併到的父親節點

僞代碼:

Tarjan(u)               // merge 和 find 爲並查集合並函數和查找函數
{
    for each(u,v)       // 遍歷 u 的全部子節點 v
    {
        Tarjan(v);      // 繼續往下遍歷
        merge(u,v);     // 合併 v 到 u 這一集合
        標記 v 已被訪問過;
    }
    for each(u,e)       // 遍歷全部與 u 有查詢關係的 e
    {
        if (e 被訪問過)
            u, e 的最近公共祖先爲 find(e);
    }
}
C++

感受講到這裏已經沒有其它內容了,可是必定會有好多人沒有理解怎麼辦呢?

咱們假設在以下樹中模擬 Tarjan 過程(節點數量少一點能夠畫更少的圖o( ̄▽ ̄)o)

存在查詢: LCA(T,3,4)、LCA(T,4,6)、LCA(T,2,1)

img

注意:每一個節點的顏色表明它當前屬於哪個集合,橙色線條爲搜索路徑,黑色線條爲合併路徑。

img

當前所在位置爲 u = 1 ,未遍歷孩子集合 v = {2,5} ,向下遍歷。

img

當前所在位置爲 u = 2 ,未遍歷孩子集合 v = {3,4} ,向下遍歷。

img

當前所在位置爲 u = 3 ,未遍歷孩子集合 v = {} ,遞歸到達最底層,遍歷全部相關查詢發現存在 LCA(T,3,4) ,可是節點 4 此時標記未訪問,所以什麼也不作,該層遞歸結束。

img

遞歸返回,當前所在位置 u = 2 ,合併節點 3u 所在集合,標記 vis[3] = true ,此時未遍歷孩子集合 v = {4} ,向下遍歷。

img

當前所在位置 u = 4 ,未遍歷孩子集合 v = {} ,遍歷全部相關查詢發現存在 LCA(T,3,4) ,且 vis[3] = true ,此時獲得該查詢的解爲節點 3 所在集合的首領,即 LCA(T,3,4) = 2 ;又發現存在相關查詢 LCA(T,4,6) ,可是節點 6 此時標記未訪問,所以什麼也不作。該層遞歸結束。

img

遞歸返回,當前所在位置 u = 2 ,合併節點 4u 所在集合,標記 vis[4] = true ,未遍歷孩子集合 v = {} ,遍歷相關查詢發現存在 LCA(T,2,1) ,可是節點 1 此時標記未訪問,所以什麼也不作,該層遞歸結束。

img

遞歸返回,當前所在位置 u = 1 ,合併節點 2u 所在集合,標記 vis[2] = true ,未遍歷孩子集合 v = {5} ,繼續向下遍歷。

img

當前所在位置 u = 5 ,未遍歷孩子集合 v = {6} ,繼續向下遍歷。

img

當前所在位置 u = 6 ,未遍歷孩子集合 v = {} ,遍歷相關查詢發現存在 LCA(T,4,6) ,且 vis[4] = true ,所以獲得該查詢的解爲節點 4 所在集合的首領,即 LCA(T,4,6) = 1 ,該層遞歸結束。

img

遞歸返回,當前所在位置 u = 5 ,合併節點 6u 所在集合,並標記 vis[6] = true ,未遍歷孩子集合 v = {} ,無相關查詢所以該層遞歸結束。

img

遞歸返回,當前所在位置 u = 1 ,合併節點 5u 所在集合,並標記 vis[5] = true ,未遍歷孩子集合 v = {} ,遍歷相關查詢發現存在 LCA(T,2,1) ,此時該查詢的解即是節點 2 所在集合的首領,即 LCA(T,2,1) = 1 ,遞歸結束。

至此整個 Tarjan 算法便結束啦~

PS:不要在乎最終根節點的顏色和其餘節點顏色有一點點小小差距,多是在染色的時候沒仔細看,總之就這樣咯~

PPS:所謂的首領就是、就是首領啦~

倍增算法

哇!還有一個倍增算法之後繼續補充吧!

總結篇

對於不一樣的 LCA 問題咱們能夠選擇不一樣的算法。

倘若一棵樹存在動態更新,此時離線算法就顯得有點力不從心了,可是在其餘狀況下,離線算法每每效率更高(雖然不能保證獲得解的順序與輸入一致,不過咱們有 sort 呀)

總之,喜歡哪一種風格的 code 是咱們本身的意願咯~

另外, LCA 和 RMQ 問題是兩個很是基礎的問題,不少複雜問題均可以轉化爲這兩類問題來解決。(固然這兩類問題之間也能夠相互轉化啦~)

參考資料

OI wiki https://oi-wiki.org/graph/lca/

http://www.javashuo.com/article/p-umzjucpi-nm.html

https://wizardforcel.gitbooks.io/the-art-of-programming-by-july/content/03.03.html

相關文章
相關標籤/搜索