【白話系列】最近公共祖先

【序言】

        說到最近公共祖先,應該是樹論中一個比較重要的話題吧。一般來說,在遇到求最近公共祖先的時候,會有三種常見的做法:對於簡單的模擬題——直接模擬就好了;對於大題目中的求最近公共祖先的小橋段——用tarjan來求,因爲好打不容易錯;對於特意考察最近公共祖先,並且數據範圍比較大的時候——用倍增算法,省空間還是硬道理。至於還有的通過變形將最近公共祖先問題化爲區域最小值問題來做,性價比並不高,如果你硬是想知道,可以百度一下:“LCA問題轉RMQ問題的ST算法”。


【什麼是最近公共祖先?】


        最近公共祖先簡稱LCA,以下用LCA代替。

        不要期望我解釋什麼是LCA嗯,我知道你知道!(哼,這只是走個過程而已!)好吧,如果你真不知道,我也無法解釋,請看:

        LCA3 4=2   LCA3 2=2    LCA6 10=1   LCA5 6=4

        我想你已經知道了,LCA就是兩個節點前往根節點的兩條路徑第一次交匯的那個節點,也就是距離它們最近的祖先,而且是公共的祖先,哈哈!


【模擬的做法】

        還記得剛纔的那句話麼!“LCA就是兩個節點前往根節點的兩條路徑第一次交匯的那個節點”!那麼模擬法豈不是太顯而易見了嗎?直接從要求的一個點開始,不停地往父親走,把它經過的點都標記爲已訪問,直到不能再走爲止,再從另一個點開始,不停往父親走,並檢查它經過的點是不是曾今被訪問過,如果是,那麼這個點就是它們的最近公共祖先。如果你要問我爲什麼,我真的會很難過的,真的。

        注意:模擬法在馬虎的時候也是容易出錯誤的,記住一個完整的小流程是“先標記再往上走”而不是“先往上走再標記”,這並不一樣,如上圖,若是找與 3LCA,先模擬2的路徑,如果“先標記再往上走”那麼走完以後被標記的有12,如果“先往上走再標記”,那麼被標記的就只有1,顯然這是不可取的,因爲最後求出來的LCA就變成1號節點了!這是常見的一個小錯誤,當然,對於另外一個節點,也應該“先檢查再往上走”,因爲它自己本身這個節點就有可能是它們的LCA。切記啊切記,這樣的錯誤不能出現了啊!!


【tarjan的做法】

        剛纔我們一直在做的都是解決兩個節點的LCA是哪個節點,tarjan固然也是解決這樣的問題的,只不過它可以更加快速,在線性的時間階內求出所有的詢問。tarjan到底是怎麼做的?請往下看。


        首先,我們來想想這樣一個問題:在如圖的這棵樹中,LCA1號節點的有哪些節點對?也許你覺得這個問題實在是太簡單了,一眼就可以看出,只要在1號節點的左子樹隨便找一個節點,再與從1號節點的右子樹中隨便找出的一個節點組成節點對,那麼它們的LCA一定是1號節點。爲什麼?顯然可得,不需要任何理由,感覺就是硬道理。

        那麼我們可不可以抽象一樣:若兩節點分別分佈於某節點的左右子樹,那麼該節點爲其LCA。憑感覺得出的定理還是有一定的問題,因爲並沒有考慮到一個節點自己就是LCA的情況,所以我們對定理進行補充:若某節點是兩節點的祖先之一,且這兩節點並不分佈於該節點的一棵子樹中,那麼該節點即爲兩節點的LCA。這就是Tarjan算法賴以生存的基礎。

        先不說Tarjan算法,就說剛纔我們得到的那個顯而易見的定理,你有沒有什麼思路呢?你有沒有想到,可以先預處理出所有詢問的LCA,然後再一起回答呢?

        對於很多組的詢問,我先確定一個LCA,就假設它是根節點1好了,然後再去檢查所有詢問,看是否滿足剛纔的定理,不滿足就忽視,滿足就賦值,全部弄完,再去假設2號節點是LCA,再去訪問一遍……有沒有發現這個方法無比的通俗與直觀?但是!你要怎麼知道一個節點是在左子樹、右子樹還是都不在呢?我想你只能遍歷一棵樹,那麼,好像這個方法也並沒有比直接模擬法好多少,但是,不要放棄,因爲Tarjan就沒有放棄。

        我們覺得剛纔的算法不妥,是因爲多次遍歷的代價實在是太大了,但是細心一點,我們便可以發現,若一個點的父親會被某個點遍歷到,那麼該點也會被那個點遍歷到,也就是說一個點只需要被遍歷一遍即可,因爲遍歷信息是可以傳遞的!

tarjan算法流程:

        procedure dfsi);

        begin

            設置i號節點的祖先爲i

            若i的左子樹不爲空,dfsi-左子樹);

            若i的右子樹不爲空,dfsi-右子樹);

            訪問每一條與i相關的詢問

                    若另一個節點已經被訪問過,則輸出另一個節點當前的祖先

            標記i爲已經訪問,將所有i的孩子包括i本身的祖先改爲i的父親

        end

STEP 1

節點

1

2

3

4

5

6

7

8

祖先

1

2

3



STEP 2

節點

1

2

3

4

5

6

7

8

祖先

1

2

2


STEP 3

節點

1

2

3

4

5

6

7

8

祖先

1

2

2

4

5



STEP 4

節點

1

2

3

4

5

6

7

8

祖先

1

2

2

4

4

STEP 5

節點

1

2

3

4

5

6

7

8

祖先

1

2

2

4

4

6


STEP 6

節點

1

2

3

4

5

6

7

8

祖先

1

2

2

4

4

4


STEP 7

節點

1

2

3

4

5

6

7

8

祖先

1

2

2

2

2

2


STEP 8

節點

1

2

3

4

5

6

7

8

祖先

1

1

1

1

1

1


STEP 9

節點

1

2

3

4

5

6

7

8

祖先

1

1

1

1

1

1

7


STEP 10

節點

1

2

3

4

5

6

7

8

祖先

1

1

1

1

1

1

7

8


STEP 11

節點

1

2

3

4

5

6

7

8

祖先

1

1

1

1

1

1

7

7


STEP 12

節點

1

2

3

4

5

6

7

8

祖先

1

1

1

1

1

1

1

1


STEP 13

節點

1

2

3

4

5

6

7

8

祖先

1

1

1

1

1

1

1

1


        大致流程如上所示,我們可以驚喜的發現,當我們在檢查一個節點的詢問情況的時候,若與詢問相關的另一個節點已經被訪問,那麼以另一個節點當前的祖先爲祖先,這兩個節點一定是滿足我們憑感覺得到的那個定理的,也就是說,這個祖先一定是最近公共祖先。

        爲什麼?因爲這個神奇的邏輯順序,就是這麼這麼巧,沒有任何問題。

        如果你還是有點懵懂,按照Tarjan的算法流程再將這十來幅手動模擬的圖片看上幾遍,你一定就會懂的。


【倍增的做法】

        倍增來做LCA應該是比Tarjan更容易理解的,因爲它更加直觀,更加符合人模擬的思維。

還記得前面說的模擬的方法來做LCA嗎?其實倍增可以算作是模擬算法在往上走的過程中的一個優化,讓我們不是每次走一步,而是儘可能一次走很多步。

        ps、倍增是什麼?詳情請看http://blog.csdn.net/jarjingx/article/details/8180560 

        既然已經知道了倍增,那麼就不贅述了,直接上算法流程。

        1、預處理出每個節點的深度

        2、讀取一組詢問,對於兩個節點,先跳到同一深度

        3、判斷當前兩節點所在的節點是否爲同一節點,是則其爲LCA,否則繼續下一步

        4、從大往小進行檢查,……8步、4步、2步、1步……,若跳後節點不一致,則可以跳,若節點一致,則不跳

        5、兩節點所在的點的父親節點即爲LCA

若詢問爲 611LCA

        step1、比較深度大小


        step2、深度不一致,跳至同一深度


        step368步與98步不滿足要求

                       64步與94步步滿足要求

                       62步與92步滿足要求


        step421步與71步不滿足要求

        step527共同的父親1爲其LCA,輸出結果

        觀察倍增算法在樹上的實現,我們發現其實跟兔子跳格子是一樣的,從每個節點跳幾步會到哪個節點是需要我們預處理出來的,方法就跟聰明小白兔晚上打小抄的方法一致,在真正跳的時候,也跟聰明小白兔的方式一致。

        也許,現在你更加明白倍增算法最後的那段話了,從一個節點,若想往上跳2步,在沒有預處理的情況下你只能11步的跳,因爲你只能知道當前節點的父節點是誰,而無法知道爺爺節點是誰。


【尾聲】

        LCA其實是個特別好玩的東西,很多在樹結構中難以想到的東西都或多或少可以用到LCA的思想來工作,更多神祕的東西就等待你去發現啦。

                                                                                                                                               

發佈了28 篇原創文章 · 獲贊 167 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章