【演算法筆記】最近公共祖先
「冥界の土地も有限よ、餘計な霊魂は全て斬る!」
最近公共祖先(Lowest Common Ancestors)
定義1
我們假設有一顆有根樹 \(T\),對於 \(\forall x,y \in T\)。
一定存在至少一個節點 \(z \in T\) ,滿足 \(z\) 是 \(x\) 的祖先 且 \(z\) 是 \(y\) 的祖先。
特別的,一個節點 \(x\) 的祖先也可以是 \(x\) 自己。
那麼所有的 \(z\) 所組成的集合中 ,深度最大的一個 \(z\) 則稱為 \(x,y\) 的最近公共祖先,一般記作 \(\texttt{LCA}(x,y)\)。
倍增Lca演算法就基於這個定義。
定義2
是這樣子的:
若存在一個無向無環圖 \(T\), 那麼\(x \to y\) 的最短路上的深度最小的點就是 \(\texttt{LCA}(x,y)\)
注意,這個“深度最小” 還是從樹的角度來說的。
Tarjan演算法就基於這個定義。
圖例:
如圖所示,藍點 \(x,y\) 的最近公共祖先是黃色點 \(\texttt{LCA}(x,y)\)
向上標記法求 LCA
考慮怎麼來求 \(\texttt{LCA}\)。
首先第一個想法是根據定義1,我們從 \(x\) 向上搜尋 \(x\) 的祖先,同時從 \(y\) 開始向上搜尋 \(y\) 的祖先,搜到一個標記一個。
然後找出深度最大的被同時標記的節點 \(z\)
(當然同時搜兩個不太現實,我們一般是先讓 \(x\) 搜到 \(root\) 之後用 \(y\) 來搜,找到的第一個被標記過得節點就是 \(\texttt{LCA}(x,y)\))
但如果 \(T\) 是一個鏈而 \(x,y\) 分別在鏈的端點處,那麼單次詢問的複雜度就會被卡到 \(\text{O}(n)\) 。
對於多次詢問,。
Tarjan求 LCA
這是候就有人要問了:“這個暴力能優化嗎?”
看起來好像真的沒什麼辦法。
但是 \(\texttt{Tarjan}\) 老爺子帶著他的並查集跳了出來:
我能優化!
這是一個基於並查集和定義2的離線演算法。
大概思路是 \(\texttt{dfs}\) 遍歷樹 \(T\),利用並查集。
當某個節點 \(u\) 及其子樹遍歷完成後,處理所有和 \(u\) 有關的查詢。
以此達到優化“向上標記法” 的目的。
標記的流程:
-
假設我們當前訪問到了節點 \(u\) , 那麼我們新建一個關於 \(u\) 的集合 \(S\) (利用並查集)
-
遞迴搜尋 \(u\) 的子樹,如果說 某一顆子樹 \(v\) 被搜完了,我們標記 \(vis[v]=true\)
-
很明顯這個子樹遞迴的過程當中也會產生一個新的集合 \(S^{'}\),那麼這時我們將 \(S\) 和 \(S^{'}\) 合併並且以 \(u\) 作為整個大集合的 \(root\) 。
-
重複 2,3 ,直到 \(u\) 的所有子樹都被訪問完(即 \(vis\) 都被標記),這時將 \(vis[u]\) 標記為 \(true\)
這時候已經遍歷完成了 \(u\) 以及 \(u\) 的所有子樹,我們就可以處理查詢了。
考慮一個查詢 \((u,v)\) 。
-
如果說 \(vis[v]=true\) ,也就是已經被標記過了。
那麼 \(\texttt{LCA}(u,v)\) 就是 \(\texttt{root}(v)\) (並查集的查詢根)
為什麼呢?
因為 \(vis[v]=true\) 的話,根據上面的標記流程,我們可以知道,
它帶著它的子樹此時一定被併到了它的父親的集合裡去,
而它的父親又有可能被合到它(\(v\))父親的父親的集合裡。
以此類推,搜到大集合的根的時候,這個根一定沒被標記過,否則它也將會被併到它(根)的父親(祖先)節點的集合裡去。
所以 \(\texttt{LCA}(u,v)\) 就是 \(\texttt{root}(v)\) 。(反證法)
-
如果說 \(vis[v]\not= true\)。
那麼跳過這個詢問,當 \(v\) 和 \(v\) 的子樹遍歷完的時候一定會回來更新 \((u,v)\) 的(上一種情況)。
因為每個點只會別標記一次,每個詢問也只處理一次。
所以複雜度為 \(\text{O}(n+q)\) (\(q\) 是查詢次數)。
是所有\(\texttt{LCA}\) 的演算法裡面最快的。
Code:
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
const int si_n=5e5+10;
const int si_m=5e5+10;
struct Tree{
int ver,Next,head;
}e[si_m<<1];
int cnt=0;
void add(int u,int v){
e[++cnt].ver=v,e[cnt].Next=e[u].head;
e[u].head=cnt;
}
int pa[si_n];
int root(int x){
if(pa[x]!=x){
return pa[x]=root(pa[x]);
}
return pa[x];
}
vector<int>que[si_n],pos[si_n];
int lca[si_n];
bool vis[si_n];
int n,q,s;
void tarjan(int u){
vis[u]=true;
for(register int i=e[u].head;i;i=e[i].Next){
int v=e[i].ver;
if(vis[v]==true) continue;
tarjan(v),pa[v]=root(u);
}
for(register int i=0;i<(int)que[u].size();++i){
int v=que[u][i],po=pos[u][i];
if(vis[v]==true) lca[po]=root(v);
}
}
int main(){
scanf("%d%d%d",&n,&q,&s);
for(register int i=1;i<=n;++i){
pa[i]=i,vis[i]=false;
que[i].clear(),pos[i].clear();
}
for(register int i=1;i<n;++i){
int u,v;
scanf("%d%d",&u,&v);
add(u,v),add(v,u);
}
for(register int i=1;i<=q;++i){
int u,v;
scanf("%d%d",&u,&v);
if(u==v) lca[i]=u;
else{
que[u].pb(v),que[v].pb(u);
pos[u].pb(i),pos[v].pb(i);
}
}
tarjan(s);
for(register int i=1;i<=q;++i){
printf("%d\n",lca[i]);
}
return 0;
}
倍增求 LCA
在樹上問題當中有個神奇的演算法:
樹上倍增。
在 \(\texttt{LCA}\) 當中,倍增也是一個極其神(bao)奇(li)的演算法。
你想,我們要維護的無非是一個節點 \(u\) 的所有祖先,然後擴充套件到整棵樹的節點的祖先。
利用倍增的思想(和 \(\texttt{ST}\) 有那麼一丟丟的像)。
我們設 \(f[u,k] , (u \in T,k \in [1,\log(n)])\) 表示 \(u\) 的 \(2^{k}\) 級祖先。
那麼很容易想到 \(f[u,k]=f[f[u,k-1],k-1]\)。
特別的,\(f[u,0]=father(u)\)。
這個東西本質上來說就是 \(\texttt{DP}\) ,所以我們直接將其預處理出來。
複雜度是 \(\text{O}(n\times\log(n))\) 的。
有人就問了,如果是 \(3,5,7 ......\) 級祖先怎麼辦????
哦,二進位制拆分不就完了……
在嘗試跳的時候從大的開始跳,然後一個一個向下試就可以了。
這個做法的思路就是不斷讓 \(u,v\) 向上跳,然後直到 \(u=v\) 或者 \(f[u,0]=f[v,0]\) 即可。
很簡單,所以直接上程式碼:
#include<bits/stdc++.h>
using namespace std;
const int si=5e5+8;
int n,m,root;
struct Tree{
int ver,head,Next;
}e[si<<1];
int cnt=0;
void add(int u,int v){
e[++cnt].ver=v;e[cnt].Next=e[u].head;
e[u].head=cnt;
}
int dep[si];
int f[si][20];
void dfs(int i,int fa){
dep[i]=dep[fa]+1;
f[i][0]=fa;
for(register int j=1;j<18;++j){
f[i][j]=f[f[i][j-1]][j-1];
}
for(register int j=e[i].head;j;j=e[j].Next){
int v=e[j].ver;
if(v==fa) continue;
dfs(v,i);
}
}
int lca(int u,int v){
if(dep[u]<dep[v]) swap(u,v);
for(register int i=19;i>=0;--i){
if(dep[f[u][i]]>=dep[v]) u=f[u][i];
}
if(u==v) return u;
for(register int i=19;i>=0;--i){
if(f[u][i]!=f[v][i]){
u=f[u][i],v=f[v][i];
}
}
return f[u][0];
}
int main(){
scanf("%d%d%d",&n,&m,&root);
for(register int i=1,u,v;i<n;++i){
scanf("%d%d",&u,&v);
add(u,v),add(v,u);
}
dfs(root,0);
while(m--){
int u,v;
scanf("%d%d",&u,&v);
printf("%d\n",lca(u,v));
}
return 0;
}