1. 程式人生 > 其它 >【演算法筆記】最近公共祖先

【演算法筆記】最近公共祖先

「冥界の土地も有限よ、餘計な霊魂は全て斬る!」

最近公共祖先(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\)

\(z\) 就是 \(\texttt{LCA}(x,y)\)

(當然同時搜兩個不太現實,我們一般是先讓 \(x\) 搜到 \(root\) 之後用 \(y\) 來搜,找到的第一個被標記過得節點就是 \(\texttt{LCA}(x,y)\)

但如果 \(T\) 是一個鏈而 \(x,y\) 分別在鏈的端點處,那麼單次詢問的複雜度就會被卡到 \(\text{O}(n)\)

對於多次詢問,。

Tarjan求 LCA

這是候就有人要問了:“這個暴力能優化嗎?”

看起來好像真的沒什麼辦法。

但是 \(\texttt{Tarjan}\) 老爺子帶著他的並查集跳了出來:

我能優化!

這是一個基於並查集和定義2的離線演算法。

大概思路是 \(\texttt{dfs}\) 遍歷樹 \(T\),利用並查集。

當某個節點 \(u\) 及其子樹遍歷完成後,處理所有和 \(u\) 有關的查詢。

以此達到優化“向上標記法” 的目的。

標記的流程:

  1. 假設我們當前訪問到了節點 \(u\) , 那麼我們新建一個關於 \(u\) 的集合 \(S\) (利用並查集)

  2. 遞迴搜尋 \(u\) 的子樹,如果說 某一顆子樹 \(v\) 被搜完了,我們標記 \(vis[v]=true\)

  3. 很明顯這個子樹遞迴的過程當中也會產生一個新的集合 \(S^{'}\),那麼這時我們將 \(S\)\(S^{'}\) 合併並且以 \(u\) 作為整個大集合的 \(root\)

  4. 重複 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;
}

例題: