樹上問題/簡單演算法 LCA【最近公共祖先】

wyl123ly發表於2024-07-16

概念引入

  • 最近公共祖先簡稱 \(LCA\)(Lowest Common Ancestor)。兩個節點的最近公共祖先,就是這兩個點的公共祖先裡面,離根最遠的那個。

在下面的說明中,我們設兩個節點分別為 \(x\),\(y\),節點 \(x\),\(y\) 的深度分別表示為 \(dep_x\),\(dep_y\),將樹稱為 \(T\)

演算法詳解:

樸素演算法:

先將節點 \(x\) 的深度提升到與 \(y\) 節點深度相同的位置,然後兩個一起一個一個往上跳,直到相遇。

那麼易知時間複雜度為 \(O(n)\),因為過於樸素,這裡就不給出程式碼了。

倍增演算法:

對於樸素演算法中的:

“然後兩個一起一個一個往上跳”

我們可以用倍增的方式完成跳躍.

那麼如何實現呢?

我們知道,任何一個非負整數都可以進行二進位制拆分.例如:

  • \(7 = 2 ^ 0 + 2 ^ 1 + 2 ^ 2\)
  • \(14 = 2 ^ 1 + 2 ^ 2 + 2 ^ 3\)

那麼如果需要向上跳的次數為 \(n\),則樸素演算法的時間複雜度為 \(O(n)\),而倍增演算法的複雜度就可以達到\(O(log_2 n)\).

( 對於將 \(x\),\(y\) 提升到同一高度的過程,同樣可以使用倍增的演算法來實現,這裡不多加說明 )

接下來考慮如何實現

我們設 \(fa_{x,i}\)\(x\) 節點的 \(2 ^ i\) 級的祖先,即 \(x\) 節點向上走 \(2 ^ i\) 步所到達的節點

  1. 預處理 \(fa\) 陣列:

我們知道:\(2 ^ n = 2 ^ {n-1} * 2 ^ {n - 1}\)

那麼我們可以將向上走 \(2 ^ i\) 步看作先走 \(2 ^ {i - 1}\) 再走 \(2 ^ {i - 1}\)

則有: \(fa_{x,i} = fa_{fa_{x,i - 1},i - 1}\) 這可以說是非常關鍵的一步

因為 \(\forall x \in T\) , \(fa_{x,0}\) (即 \(x\) 的父節點)都可以透過一遍 \(dfs\) 解決

for(int i = 1;i <= log2(n);i++){
    for(int j = 1;j <= n;j++){
        fa[j][i] = fa[fa[j][i - 1]][i - 1];
    }
}
  1. \(x\),\(y\) 提升到同一高度:

主要思想是將 \(x\),\(y\) 的深度差值進行二進位制拆分

if(dep[x] < dep[y]) swap(x,y);//保證x的深度大於y,方便後面的計算
int delta = dep[x] - dep[y];
for(int i = 0;i <= log2(n);i++){
    if((1 << i) & delta) x = fa[x][i];//向上跳,本質是對delta進行二進位制拆分
}
  1. \(x\),\(y\) 同時向上跳

這裡有一個細節,即是將 \(i\)\(log_2 n\)\(0\) 列舉,這樣可以避免拆分的重複或漏

for(int i = log2(n);i >= 0;i--){
    if(fa[x][i] != fa[y][i]){
        //Leap!
        x = fa[x][i];
        y = fa[y][i];
    }
}

我們可以將第二步和第三步封裝成一個函式:

int lca(int x,int y){
    if(dep[x] < dep[y]) swap(x,y);
    int delta = dep[x] - dep[y];
    for(int i = 0;i <= log2(n);i++){
        if((1 << i) & delta) x = fa[x][i];
    }
    if(x == y){
        return x;
    }
    for(int i = log2(n);i >= 0;i--){
        if(fa[x][i] != fa[y][i]){//這裡最終跳躍到的是lca(x,y)的子節點
            x = fa[x][i];
            y = fa[y][i];
        }
    }
    return fa[x][0];
}

那麼我們的倍增演算法就好了,可知其時間複雜度為 \(O(log_2 n)\)

這裡附上完整程式碼:

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 5e5 + 7;
const int LOG = 30;
int n,m,r;
int dep[MAXN];
int fa[MAXN][LOG];
bool vis[MAXN];
vector<int> tree[MAXN];
void dfs(int root){
	vis[root] = true;
	if(tree[root].size() == 0){
		return;	
	}
	for(int to : tree[root]){
		if(!vis[to]){
			dep[to] = dep[root] + 1;
			fa[to][0] = root;
			dfs(to);	
		}		
	}
}
int lca(int x,int y){
	//Leap to a same depth:
	int dx = dep[x],dy = dep[y];
	if(dep[x] != dep[y]){
		if(dx < dy){
			swap(dx,dy);
			swap(x,y);
		}
		int delta = dx - dy;
		for(int i = 0;i <= LOG - 2;i++){
			if((1 << i) & delta) x = fa[x][i];
		}
	}
	if(x == y){
		return x;
	}
	//Leap to the child node of lca(x,y)
	for(int i = LOG - 2;i >= 0;i--){
		if(fa[x][i] != fa[y][i]) {
            x = fa[x][i];
            y = fa[y][i];
        }
	}
	return fa[x][0];
}
int main(){
	scanf("%d%d%d", &n, &m, &r);
	for(int i = 1;i < n;i++){
		int x,y;
		scanf("%d%d", &x, &y);
		tree[x].push_back(y);
		tree[y].push_back(x);
	}
	dep[r] = 1;
	fa[r][0] = 0;
	//Pre-Processing
	dfs(r);
	for(int i = 1;i <= LOG - 2;i++){
		for(int j = 1;j <= n;j++){
			fa[j][i] = fa[fa[j][i - 1]][i - 1];
		}
	}
	for(int i = 1;i <= m;i++){
		int x,y;
		scanf("%d%d", &x, &y);
		printf("%d\n", lca(x,y));
	}
	return 0;
}

相關文章