最近公共祖先
百科名片
- 最近公共祖先
- Lowest Common Ancestors
- LCA
簡單引入
對於有根樹T的兩個結點u、v,最近公共祖先LCA(T,u,v)表示一個結點x,滿足x是u、v的祖先且x的深度儘可能大。
紅色的都是是A和B的公共祖先,但只有最近的C纔是最近公共祖先。
LCA問題是樹上的一個經典問題,在很多方面有着廣泛的應用,比如求LCP(最長公共前綴),接下來我們就來介紹他的幾種算法。
LCA的算法
暴力枚舉法
如果我們要求a和b的最近公共祖先,就沿着父親的方向把a的所有祖先都標記
一下(類似並查集找父親,但是沒有路徑壓縮),然後在從b開始往上找祖先,
碰到第一個被標記的點,就是a和b的最近公共祖先。
C是最近公共祖先。
求一個對點的LCA時間複雜度高達O(N)。
求m個點對的LCA時間複雜度高達O(mN)。
當m和n都高達10萬的時候,超時了!!!
寶寶難以承受!!!!!
求m個點對的最近公共祖先是可以優化的,一般有兩種:
1、離線算法(Tarjan離線算法):所謂的離線算法指的是把所有問題收集起來以後一起去算,最後一起回答。
2、在線算法(倍增算法):所謂的在線算法就是來一個點對,處理一個點對。
Tarjan離線算法
Robert Tarjan設計了求解的應用領域的許多問題的廣泛有效的算法和數據結構。 他已發表了超過228篇理論文章(包括雜誌,一些書中的一些章節文章等)。Robert Tarjan以在數據結構和圖論上的開創性工作而聞名。 他的一些著名的算法包括 Tarjan最近共同祖先離線算法 ,Tarjan的強連通分量算法等。其中Hopcroft-Tarjan平面嵌入算法是第一個線性時間平面算法。Tarjan也開創了重要的數據結構如:斐波納契堆和splay樹(splay發明者還有Daniel Sleator)。另一項重大貢獻是分析了並查集。他是第一個證明了計算反阿克曼函數的樂觀時間複雜度的科學家。
簡單的介紹一下tarjan算法:
tarjan算法是離線算法,它必須先將所有的要查詢的點對存起來,然後在搜的時候輸出結果。
tarjan算法很經典,因爲算法的思想很巧妙,利用了並查集思想,在dfs下,將查詢一步一步的搜出來。
基本思路:
1 任選一個節點爲根節點,從根節點開始
2 遍歷該點 u 的所有子節點 v ,並標記 v 已經被訪問過
3 若 v 還有子節點,返回 2 ,否則下一步
4 合併 v 到 u 所在集合
5 尋找與當前點 u 有詢問關係的點 e
6 若 e 已經被訪問過,則可以確定 u、e 的最近公共祖先爲 e 被合併到的父親節點
- 對於我們已經保存好的查詢,假設爲(u,v),u爲此時已經搜完的子樹的根節
點,v的位置就只有兩種可能,一種是在u的子樹內,另一種就是在其之外。 - 對於在u的子樹內的話,最近公共祖先肯定是可以直接得出爲u。
- 對於在u的子樹之外的v,我們已經將v搜過了,且已經知道了v的祖先,那麼我們可以根據dfs的思想,v肯定是和u在一顆子樹下的,而且這顆子樹是使得他們能在一顆子樹下面深度最深的。而這個子樹的根節點剛好就是v的並查集所保存的祖先。所以對於這種情況的(u,v),它們的最近公共祖先就是v的並查集祖先。
下面給出僞代碼:
void Tarjan(u) { // merge 和 find 爲並查集合並函數和查找函數
for each(u,v) { // 遍歷 u 的所有子節點 v
Tarjan(v); // 繼續往下遍歷
merge(u,v); // 合併 v 到 u 這一集合
標記 v 已被訪問過;
}
for each(u,e) { // 遍歷所有與 u 有查詢關係的 e
if(e 被訪問過)
u, e 的最近公共祖先爲 find(e);
}
}
下面給出真代碼:
int f[N],n,m,ans[N],check[N];
vector<int> a[N],b[N],id[N];
int find(int x) { return x==f[x] ? x : f[x]=find(f[x]); }
void tarjan(int x) {
f[x]=x;
check[x]=1;
for(int i=0; i<a[x].size(); i++) {
int v=a[x][i];
if(!check[v]) {
tarjan(v);
f[v]=x;
}
}
for(int i=0; i<b[x].size(); i++) {
int v=b[x][i];
if(!check[v]) continue;
ans[id[x][i]]=find(v);
}
}
我們在深度優先遍歷的時候,先遍歷x節點的
左子樹,當遍歷到u的時候,發現v沒有被遍歷過,那麼就不去管lca(u,v)這個問題,然後我們把已經遍歷的x子樹的所有節點都合併到他的父親(即father指向父親),然後當我們遍歷到v的時候,發現u已經遍歷過了,那麼此時u在並查集裏的father就是u和v的最近公共祖先.
時間複雜度:由於每個點只遍歷一次,每個問題只枚舉2次,所以時間複雜度是O(N+2Mα(N))。α(N)爲並查集查詢一次根所需要的時間。
倍增算法
首先一個小問題,給你兩個點a和b,你如何快速的回答這兩個點在樹裏面是否具有祖先和後代的關係。
暴力算法又是o(N),明顯太浪費時間!
引入時間戳的概念:所謂的時間戳就是在給一棵樹進行深度優先遍歷的同時,記錄下計入每個點的時候和離開每個點的時間。
如圖所示,每個節點的左邊是進入的時間,右邊是離開的時間。
如果a是b的祖先,只要滿足 (in[a]<=in[b]) and (out[b]<=out[a])
也就是我們只需要一次深搜,接下來對於任何詢問a和b是否有祖先關係的時候,我們只要O(1)的時間就能回答這個問題。
建立倍增數組:
定義f[i][j]爲與節點i距離爲2^j的祖先的編號。
明顯的f[i][0]就是每個點直接的父親。
另有遞推關係:f[i][j]=f[f[i][j-1],j-1]。
於是我們只需要在nlogn的時間內就可以求出f數組的值。
如果f[i][j]不存在,我們就令f[i][j]=根,方便我們計算
接下來如何求a和b的最近公共祖先呢?
1、如果a是b的祖先,那麼輸出a
2、如果b是a的祖先,那麼輸出b
3、for i:=20 downto 0 do
if f[a][i]不是b的祖先,那麼令 a=f[a][i];
循環結束的時候,f[a][0]就是最近公共祖先。
int lca(int x,int y) {
if(ancestor(x,y)) return x;
if(ancestor(y,x)) return y;
for(int i=20; i>=0; i--)
if(!ancestor(f[x][i],y))
x=f[x][i];
return f[x][0];
}
例題:
CodeVS1036 商務旅行(可惜CodeVS崩潰了)
題目描述
某首都城市的商人要經常到各城鎮去做生意,他們按自己的路線去做,目的是爲了更好的節約時間。
假設有N個城鎮,首都編號爲1,商人從首都出發,其他各城鎮之間都有道路連接,任意兩個城鎮之間如果有直連道路,在他們之間行駛需要花費單位時間。該國公路網絡發達,從首都出發能到達任意一個城鎮,並且公路網絡不會存在環。
你的任務是幫助該商人計算一下他的最短旅行時間。
輸入描述
輸入文件中的第一行有一個整數N,1<=n<=30 000,爲城鎮的數目。下面N-1行,每行由兩個整數a 和b (1<=a, b<=n; a<>b)組成,表示城鎮a和城鎮b有公路連接。在第N+1行爲一個整數M,下面的M行,每行有該商人需要順次經過的各城鎮編號。M<=30000
輸出描述
在輸出文件中輸出該商人旅行的最短時間。
樣例輸入
5
1 2
1 5
3 5
4 5
4
1
3
2
5
樣例輸出
7
代碼
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cstring>
#include <algorithm>
#define RI register int
#define re(i,a,b) for(RI i=a; i<=b; i++)
#define ms(i,a) memset(a,i,sizeof(a))
using namespace std;
typedef long long ll;
int const N=30005;
struct edge{
int to,nt;
} e[N<<1];
int n,m,cnt,sum,ans;
int h[N],vis[N],tin[N],tout[N],dep[N];
int f[N][15];
int ast(int x,int y) {
return tin[x]<=tin[y] && tout[y]<=tout[x];
}
int lca(int x,int y) {
if(ast(x,y)) return x;
if(ast(y,x)) return y;
for(int i=14; i>=0; i--)
if(!ast(f[x][i],y)) x=f[x][i];
return f[x][0];
}
void add(int a,int b) {
e[++cnt].to=b;
e[cnt].nt=h[a];
h[a]=cnt;
}
void dfs(int x,int fa,int d){
tin[x]=++sum;
f[x][0]=fa;
dep[x]=d;
for(int i=h[x]; i; i=e[i].nt) {
int v=e[i].to;
if(v==fa) continue;
dfs(v,x,d+1);
}
tout[x]=++sum;
}
int main() {
scanf("%d",&n);
re(i,1,n-1) {
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
dfs(1,1,0);
re(j,1,14) re(i,1,n)
f[i][j]=f[f[i][j-1]][j-1];
scanf("%d",&m);
int k=1;
re(i,1,m) {
int x;
scanf("%d",&x);
int t=lca(k,x);
ans+=dep[k]+dep[x]-2*dep[t];
k=x;
}
printf("%d\n",ans);
return 0;
}