樹上兩點的最近公共祖先問題(Least Common Ancestors)


概念:

  對於有根樹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 }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM