【C++】最近公共祖先 LCA

最近公共祖先

百科名片

  • 最近公共祖先
  • 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;
}
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章