概念:
對於有根樹T的兩個節點u,v,最近公共祖先LCA(T, u, v)表示一個節點 x, 滿足 x 是 u , v 的祖先且 x 的深度盡可能的大.即從 u 到 v 的路徑一定經過點 x.
算法:
解決LCA問題比較經典的是Tarjan - LCA 離線算法,還有另外一種方法,是經過一系列處理將LCA問題轉化為和數據結構有關的RMQ問題加以解決.這里只闡述下Tarjan - LCA 算法.
Tarjan - LCA算法:
此算法基於 DFS 框架,每搜到一個新的節點,就創建由這個節點構成的集合,再對當前節點的每個子樹進行搜索,回溯時把當前點並入到上一個點所在的集合之中,每回溯一個點,關於這個點與已被訪問過的所有點的詢問就會得到解決.如果有一個從當前點到節點 v 的詢問,且 v 已被訪問過,那么這兩點的最近公共祖先一定是 v 所在集合的代表.
偽代碼:
對於每一點 u:
1:建立以 u 為代表的集合;
2:依次遍歷與 u 相連的每個節點 v,如果 v 沒有被訪問過,那么對 v 使用Tarjan - LCA 算法,結束后,將 v 的集合並入 u 的集合.
3:對於 u 有關的詢問(u, v),如果 v 被訪問過,則結果就是v 所在的集合的代表元素.
實現上關於集合的查找和合並用並查集實現,存圖用的鏈式前向星.
用圖來描述下:
初始化圖:
為了方便描述,A ~ H 映射為 1 ~ 8了.圖中灰色的點“代表未被訪問的點”; 綠色的點代表 “正在訪問的點”, 即還在執行LCA算法未結束的點; 紅色的點代表“已經被訪問過的點”,即 徹底完成Tarjan - LCA算法的點.注意這個算法是遞歸的,即同時存在多個正在訪問的點.
第一步:從根節點開始訪問,訪問 A 節點,並構造以 A 節點構成的集合.完成后節點的狀態如圖所示:
且此時pre數組的值為:(pre數組為並查集的數組,pre[i] = j 代表 i 的祖先為 j)
pre 0 1 2 3 4 5 6 7 8
-1 1 -1 -1 -1 -1 -1 -1 -1
(A節點未處理結束........)
第二步:訪問 B 節點,並創建由 B 節點構成的集合,完成此步之后圖的狀態如圖所示:
此時的pre數組為:
pre 0 1 2 3 4 5 6 7 8
-1 1 2 -1 -1 -1 -1 -1 -1
(B節點未處理結束........)
第三步選擇第一個與 B 節點相連的 C 節點,訪問之,並同樣創建以其為代表的集合:
此時的pre數組為:
pre 0 1 2 3 4 5 6 7 8
-1 1 2 3 -1 -1 -1 -1 -1
(C節點未處理結束........)
第四步訪問第一個與 C 節點相連的節點 E,創建由 E 節點構成的集合,此時pre數組和圖的狀態為:
pre 0 1 2 3 4 5 6 7 8
-1 1 2 3 -1 5 -1 -1 -1
此時由於 E 號節點沒有了子節點,於是開始處理關於 E 號節點的查詢,這里為了方便觀察,用二維數組lca[i][j] 來表示 LCA(i, j)了.因為這里的 A, B, C, 三個節點已被訪問過,所以可以計算出關於 E 節點的部分查詢:(關於(E, v)的查詢為 v 節點所在集合的祖先,即pre[v])
lca 1 2 3 4 5 6 7 8
5(E) 1 2 3 -1 -1 -1 -1 -1
此時關於 E 號節點的處理就已經全部完成,由 E 回溯到 C, 並把 E 加入到 C 所在的集合,即執行 pre[5] = find(pre[3]), 之后pre[5] == 3.
第五步:訪問第二個與 C 節點相連的節點 F,做相同的操作,pre數組以及圖此時的狀態:
pre 0 1 2 3 4 5 6 7 8
-1 1 2 3 -1 3 6 -1 -1
同樣由於 F 節點也沒有了子節點,處理關於 F 節點的查詢,這里被訪問過得節點有 A, B, C, E,對其采用算法的 "3 步驟"之后:
lca 1 2 3 4 5 6 7 8
6(F) 1 2 3 -1 3 -1 -1 -1
此時關於 F 節點的有關操作就結束了,同樣回溯到 C 節點,也就是步驟三,下一步將訪問最后一個與 C 節點相鄰的節點 G,回溯到 C之后,把 F 點合並到 C所在的集合, 即 pre[6] = find(pre[3]), pre[6] = 3.
第六步:訪問節點 G,之后的pre數組以及圖為:
pre 0 1 2 3 4 5 6 7 8
-1 1 2 3 -1 3 3 7 -1
可以看到也沒有了子節點,那么同上,做出相同的操作:
lca 1 2 3 4 5 6 7 8
7(G) 1 2 3 -1 3 3 -1 -1
此時關於 G 節點的處理也結束了,回溯到 C, 並且把 G 節點並入到集合 C. pre[7] = find(pre[c]), pre[7] = 3;
(此時由於已經沒有了和 C 相鄰的節點,那么接下來就要會到第三步)
第三步:此時 C 節點的所有子樹都訪問完畢,pre數組和圖為:
pre 0 1 2 3 4 5 6 7 8
-1 1 2 3 -1 3 3 3 -1
訪問的 C 節點此時也沒有了相鄰節點,那么對於 C 節點的所有處理即將也要完成,對節點C 執行算法的第三步得:
lca 1 2 3 4 5 6 7 8
3(C) 1 2 3 -1 3 3 3 -1
執行完畢后,繼續回溯到上一節點 B,並把集合 C 和集合 B合並,pre[3] = find(pre[2]), pre[3] = 2.
(此時第三步運行完畢,將返回第二步.)
第七步:訪問第二個 B 節點的子節點 D,構造由 D 構成的集合.此時pre數組以及圖的狀態為:
pre 0 1 2 3 4 5 6 7 8
-1 1 2 2 4 2 2 2 -1
(由於D節點還有子節點,那么繼續下一步,此時第七步還未完成......)
第八步:訪問與 D 節點的子節點 H,此時pre數組以及圖的狀態為:
pre 0 1 2 3 4 5 6 7 8
-1 1 2 2 4 2 2 2 8
此時由於 H 節點已經沒有了子節點,那么就該處理關於 H 節點的詢問了,處理完成后lca數組如下:
lca 1 2 3 4 5 6 7 8
8(H) 1 2 2 4 2 2 2 -1
之后再回溯到 D 節點,並且把自己並入到 D 節點所在的集合.
(此時第八步運行結束,將返回到第七步)
第七步:此時正處於訪問節點 D 的狀態此時pre數組以及圖的狀態為:
pre 0 1 2 3 4 5 6 7 8
-1 1 2 2 4 2 2 2 4
緊接着,由於 D 節點的所有相鄰節點已經訪問完畢,那么此時就需要處理關於 D 節點的詢問並回溯到 B 節點,並把 D 所在的集合和 B 所在的集合合並,pre[4] = find(pre[2]), pre[4]=2.
lca 1 2 3 4 5 6 7 8
4(D) 1 2 2 4 2 2 2 4
(此時第七步完成,將返回到第二步)
第二步:
同理,B 節點的所有子節點在此時也全部訪問完畢,此時的pre數組以及圖的狀態為:
pre 0 1 2 3 4 5 6 7 8
-1 1 2 2 2 2 2 2 2
此時處理關於 B,點的詢問,處理完成后回溯到 A 節點,回溯之后把 B 所在的集合並入到A所在的集合.
lca 1 2 3 4 5 6 7 8
2(B) 1 2 2 2 2 2 2 2
(此時第二步也全部完畢,即將回溯到第一步)
第一步:此時pre數組以及圖的狀態為:
pre 0 1 2 3 4 5 6 7 8
-1 1 1 1 1 1 1 1 1
處理關於A節點所有查詢,結束整個算法:
lca 1 2 3 4 5 6 7 8
1(A) 1 1 1 1 1 1 1 1
最終的pre數組, lca數組, 圖的狀態:
pre 0 1 2 3 4 5 6 7 8
-1 1 1 1 1 1 1 1 1
lca 1 2 3 4 5 6 7 8
1(A) 1 1 1 1 1 1 1 1
2(B) 1 2 2 2 2 2 2 2
3(C) 1 2 3 -1 3 3 3 -1
4(D) 1 2 2 4 2 2 2 4
5(E) 1 2 3 -1 5 -1 -1 -1
6(F) 1 2 3 -1 3 6 -1 -1
7(G) 1 2 3 -1 3 3 7 -1
8(H) 1 2 2 4 2 2 2 8
由於LCA(u, v) = LCA(v, u),所以上圖再經過調整就可以得出所有詢問了.
至此,Tarjan - LCA算法就用圖描述完畢了.
代碼:
//說明:使用鏈式前向星存圖和所有詢問,head[]和edge[]表示圖, qhead[]和qedge[]表示詢問.由於鏈式前向星只能存儲有向邊,那么對於無向圖的樹來說,每條邊要存儲兩次.對於集合的操作用並查集來完成.
1 #include <bits/stdc++.h> 2 3 const int maxn = 1000; 4 int pre[maxn]; 5 int head[maxn]; 6 int qhead[maxn]; 7 8 struct NODE {int to;int next;int lca;}; 9 NODE edge[maxn]; 10 NODE qedge[maxn]; 11 12 int Find(int x) { return x == pre[x] ? x : pre[x] = Find(pre[x]);}//並查集 13 14 bool visit[maxn];//標志訪問 15 void LCA(int u) { 16 pre[u] = u;//構造包含當前點的集合 17 visit[u] = true;//標記 18 for(int k = head[u]; k != -1; k = edge[k].next) { 19 if(!visit[edge[k].to]) { 20 LCA(edge[k].to);//對未訪問的子節點進行LCA 21 pre[edge[k].to] = u;//將 一顆子樹所在的集合 和 當前集合 合並 22 } 23 } 24 for(int k = qhead[u]; k != -1; k = qedge[k].next) { 25 if(visit[qedge[k].to]) {//處理查詢 26 qedge[k].lca = Find(qedge[k].to); 27 qedge[k ^ 1].lca = qedge[k].lca; 28 } 29 } 30 } 31 32 int main() { 33 //輸入圖 34 //相關初始化 35 LCA(start); 36 return 0; 37 }