【序言】
說到最近公共祖先,應該是樹論中一個比較重要的話題吧。一般來說,在遇到求最近公共祖先的時候,會有三種常見的做法:對於簡單的模擬題——直接模擬就好了;對於大題目中的求最近公共祖先的小橋段——用tarjan來求,因爲好打不容易錯;對於特意考察最近公共祖先,並且數據範圍比較大的時候——用倍增算法,省空間還是硬道理。至於還有的通過變形將最近公共祖先問題化爲區域最小值問題來做,性價比並不高,如果你硬是想知道,可以百度一下:“LCA問題轉RMQ問題的ST算法”。
【什麼是最近公共祖先?】
最近公共祖先簡稱LCA,以下用LCA代替。
不要期望我解釋什麼是LCA嗯,我知道你知道!(哼,這只是走個過程而已!)好吧,如果你真不知道,我也無法解釋,請看:
LCA(3 4)=2 LCA(3 2)=2 LCA(6 10)=1 LCA(5 6)=4
我想你已經知道了,LCA就是兩個節點前往根節點的兩條路徑第一次交匯的那個節點,也就是距離它們最近的祖先,而且是公共的祖先,哈哈!
【模擬的做法】
還記得剛纔的那句話麼!“LCA就是兩個節點前往根節點的兩條路徑第一次交匯的那個節點”!那麼模擬法豈不是太顯而易見了嗎?直接從要求的一個點開始,不停地往父親走,把它經過的點都標記爲已訪問,直到不能再走爲止,再從另一個點開始,不停往父親走,並檢查它經過的點是不是曾今被訪問過,如果是,那麼這個點就是它們的最近公共祖先。如果你要問我爲什麼,我真的會很難過的,真的。
注意:模擬法在馬虎的時候也是容易出錯誤的,記住一個完整的小流程是“先標記再往上走”而不是“先往上走再標記”,這並不一樣,如上圖,若是找2 與 3的LCA,先模擬2的路徑,如果“先標記再往上走”那麼走完以後被標記的有1與2,如果“先往上走再標記”,那麼被標記的就只有1,顯然這是不可取的,因爲最後求出來的LCA就變成1號節點了!這是常見的一個小錯誤,當然,對於另外一個節點,也應該“先檢查再往上走”,因爲它自己本身這個節點就有可能是它們的LCA。切記啊切記,這樣的錯誤不能出現了啊!!
【tarjan的做法】
剛纔我們一直在做的都是解決兩個節點的LCA是哪個節點,tarjan固然也是解決這樣的問題的,只不過它可以更加快速,在線性的時間階內求出所有的詢問。tarjan到底是怎麼做的?請往下看。
首先,我們來想想這樣一個問題:在如圖的這棵樹中,LCA爲1號節點的有哪些節點對?也許你覺得這個問題實在是太簡單了,一眼就可以看出,只要在1號節點的左子樹隨便找一個節點,再與從1號節點的右子樹中隨便找出的一個節點組成節點對,那麼它們的LCA一定是1號節點。爲什麼?顯然可得,不需要任何理由,感覺就是硬道理。
那麼我們可不可以抽象一樣:若兩節點分別分佈於某節點的左右子樹,那麼該節點爲其LCA。憑感覺得出的定理還是有一定的問題,因爲並沒有考慮到一個節點自己就是LCA的情況,所以我們對定理進行補充:若某節點是兩節點的祖先之一,且這兩節點並不分佈於該節點的一棵子樹中,那麼該節點即爲兩節點的LCA。這就是Tarjan算法賴以生存的基礎。
先不說Tarjan算法,就說剛纔我們得到的那個顯而易見的定理,你有沒有什麼思路呢?你有沒有想到,可以先預處理出所有詢問的LCA,然後再一起回答呢?
對於很多組的詢問,我先確定一個LCA,就假設它是根節點1好了,然後再去檢查所有詢問,看是否滿足剛纔的定理,不滿足就忽視,滿足就賦值,全部弄完,再去假設2號節點是LCA,再去訪問一遍……有沒有發現這個方法無比的通俗與直觀?但是!你要怎麼知道一個節點是在左子樹、右子樹還是都不在呢?我想你只能遍歷一棵樹,那麼,好像這個方法也並沒有比直接模擬法好多少,但是,不要放棄,因爲Tarjan就沒有放棄。
我們覺得剛纔的算法不妥,是因爲多次遍歷的代價實在是太大了,但是細心一點,我們便可以發現,若一個點的父親會被某個點遍歷到,那麼該點也會被那個點遍歷到,也就是說一個點只需要被遍歷一遍即可,因爲遍歷信息是可以傳遞的!
tarjan算法流程:
procedure dfs(i);
begin
設置i號節點的祖先爲i
若i的左子樹不爲空,dfs(i-左子樹);
若i的右子樹不爲空,dfs(i-右子樹);
訪問每一條與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
若詢問爲 6與11的LCA:
step1、比較深度大小
step2、深度不一致,跳至同一深度
step3、6跳8步與9跳8步不滿足要求
6跳4步與9跳4步步滿足要求
6跳2步與9跳2步滿足要求
step4、2跳1步與7跳1步不滿足要求
step5、2和7共同的父親1爲其LCA,輸出結果
觀察倍增算法在樹上的實現,我們發現其實跟兔子跳格子是一樣的,從每個節點跳幾步會到哪個節點是需要我們預處理出來的,方法就跟聰明小白兔晚上打小抄的方法一致,在真正跳的時候,也跟聰明小白兔的方式一致。
也許,現在你更加明白倍增算法最後的那段話了,從一個節點,若想往上跳2步,在沒有預處理的情況下你只能1步1步的跳,因爲你只能知道當前節點的父節點是誰,而無法知道爺爺節點是誰。
【尾聲】
LCA其實是個特別好玩的東西,很多在樹結構中難以想到的東西都或多或少可以用到LCA的思想來工作,更多神祕的東西就等待你去發現啦。
完