模板 最近公共祖先 LCA 的幾種求法

2022-05-24 14:15:10 字數 3367 閱讀 2942

顧名思義就是兩節點最近的公共祖先

lca常用求法:

dfs + st表

倍增tarjan

樹鏈剖分

首先預處理一下每個節點的第 \(2^i\) 個祖先

我們就可以跳著走避免一次走一步的龜速

引理:對於任意乙個非零整數,我們都可以將他用2的次冪表示出來。

這個引理是始終成立的 很顯然

寫程式的時候需要注意乙個地方就是我們要將相對深度關係保持一致,這樣就避免使用同乙個函式出現魔法錯誤的情況。

預處理**

void dfs(int now, int father) //  y 是 x 的祖先的情況

for(int i = 20; i >= 0; i--) // y 不是 x 的祖先的情況,我們需要讓它們盡量接近

return fa[x][0];

}

個人認為倍增查詢的過程是乙個不斷折半搜尋的過程,每一次都會折半地減小範圍,直至搜尋結束。

時間複雜度:\(o((n+q) \log n)\) ( \(n\) 個節點,\(q\) 次詢問)

空間複雜度:\(o(n \log n)\)

tarjan 法是使用並查集對向上標記法的乙個優化:

向上標記法:見李書p378

已經回溯的點標記 2,第一次訪問的點標記 1,未訪問的點無標記

這樣當搜到第二個點時,從第乙個點向上走找到的第乙個標記是 1 的點即為 lca。

但是向上乙個乙個爬父親節點速度顯然太慢,不是我們想要的。

於是採用並查集優化 :

對於每乙個回溯到的點,賦予其標記 \(2\) 的同時將其合併到它的父節點所在集合上。

其中需要注意最初每個節點是獨立的,也就是說,此時合併的父節點不僅滿足標記為 \(1\) 而且父親節點的 fa 是其自身。

此時對於所有關於現在標記為 \(1\) 的 \(x\) 點的詢問,若另乙個 \(y\) 點標記為 \(2\) ,lca 為find(y)

對於詢問的處理:

​ 將詢問看作是一張無向圖,當遍歷到乙個點時同時遍歷關於該點的所有詢問,若詢問中另乙個點已經被標記為 \(2\)

那麼 lca 為被標記 \(2\) 的節點的並查集父親。

為什麼不用考慮一點是另一點祖先的情況?

​ 假設 \(x\) 是 \(y\) 的祖先,那麼當我們搜尋到 \(y\) 的時候肯定是不能出解的,但當我們從 \(y\) 回溯到達 \(x\) 時,\(y\) 已被標記 \(2\),而 \(x\) 被標記為 \(1\),就可以搜出來了。

用到的變數:

tag[maxn]:標記記錄

cntquery:新增的詢問邊

ans[maxn]:按順序記錄答案

qpre[maxn<<1]:鄰接表存圖

fa[maxn]:並查集

詢問結構部分:

struct queryq[maxn<<1];

void addquery(int x, int y, int identity)

並查集處理部分:

int find(int x)
tarjan 部分:

void tarjan(int now) // 遍歷部分

for(int i = qpre[now]; i; i = q[i].next)

if(tag[q[i].v] == 2 && !ans[q[i].id]) // 當前這個詢問被回溯完時

ans[q[i].id] = find(q[i].v);

tag[now] = 2;

return;

// 注意最開始建的是雙向邊,所以我們每次記錄答案的時候判斷一下當前 ans 是否為 0,非 0 不更新

} // now 代表當前子樹的根節點

時間複雜度:\(o(m+n+q)\)

樹鏈剖分,計算機術語,指一種對樹進行劃分的演算法,它先通過輕重邊剖分將樹分為多條鏈,保證每個點屬於且只屬於一條鏈,然後再通過資料結構(樹狀陣列、bst、splay、線段樹等)來維護每一條鏈。

樹剖其實就是一種對暴力求解的優化,讓我們原本的暴力沒那麼「暴力」

對於一般的樹剖,我們需要維護以下幾個量:

維護在節點上的量:

`fa` :當前節點父親

`hson`:當前節點的重兒子

`dep`:當前節點的深度

`siz`:以當前節點為根節點的子樹大小

`dfn`:當前節點的時間戳(dfs序)

`top`:當前節點所在鏈的頂端

維護在區間上的量:

`id`:區間上第 $cnt$ 個值對應的節點標號

實際上,在樹剖處理 \(lca\) 時我們並不需要維護dfnid

和常規的樹剖一樣,我們需要使用兩次 \(dfs\),具體如下:

第一次 \(dfs\)(處理fadepsizhson):

void predfs(int now, int fa)

return;

}

第二次 \(dfs\)(處理top):

void segdfs(int now, int top)
注:通常的樹剖還需要在第二次dfs中處理dfn和id

查詢語句:

int ask(int x, int y) // 始終保持 x 的深度更大

return n[x].dep < n[y].dep ? x : y; // 深度較淺的點即為 lca

}

複雜度:\(o(\log_2)\)

模板:洛谷 p3379 最近公共祖先

結合生成樹且需要判斷聯通性(某oi2013真題):洛谷 p1967 貨車運輸

求LCA(最近公共祖先)

演算法1 樹上倍增 hdu 2586 include include include define maxn 40000 5 define inf 999999999 using namespace std int n,m,t,q,head maxn x,y,z,vis maxn fa maxn c...

LCA 求最近公共祖先

洛谷3379 基本思路 要求兩個點的最近公共祖先,首先我們用乙個陣列fa x i 來表示節點x的向上2 i層的祖先編號,先dfs求出每個點的深度和預處理fa陣列。再求兩個節點的最近公共祖先時,先讓深度大的跳到相同的一層,再嘗試以log2 depth x 的跨度跳至祖先節點,相同時不跳 因為可能不是最...

LCA求最近公共祖先

parent i j 表示節點u的第2 j個祖先 dis i dfs時間戳,即節點到根節點距離,即深度 const int pow 18 const int maxn 5e5 10 int parent maxn pow 10 parent i j 表示節點u的第2 j個祖先 vectoredge ...