基於倍增算法求LCA

基於倍增算法求LCA


0.總結

Get to the points first. The article comes from LawsonAbs!
  • 二進制拆分
  • LCA的樹上倍增算法實現

1.問題

在談LCA之前,說一下什麼是二進制拆分

1.1 二進制拆分

我們都知道,計算機中的數都是用0/1來表示的,也就是說任何一個正整數都可以用二進制來表示出來,但是小數卻是不行的。用二進制來表示任何一個正整數的思想就叫做二進制拆分。

1.2 LCA問題
  • 給定一棵樹【說明無環】的兩個節點a,b
  • 節點a到根節點的路徑 和 節點b到根節點的路徑上的第一個交點【深度最大的一個點】就是LCA

2.分析

  • 使用二進制拆分思想倍增的求出LCA
  • 對於普通的輸入,先指定根節點
  • 預處理部分:
    (1)用bfs得到每個節點的深度hei[i]
    (2)定義f[i][j]表示節點i往根節點方向走2j步到達的節點編號。如果往上走的步超過了根節點的高度,那麼就設其值爲0。問題關鍵是怎麼得到這個數組的值?
    可以推導節點間滿足公式:f[i][j] = f[[f[i][j-1]][j-1],故在 bfs 的時候,用一個for循環進行相應的操作進行賦值即可。
    (3)使用 lca(int x,int y) 求出LCA
    lca()函數主要是三步驟:
    step1.迭代更新,直到將更深的y變得與x的深度相同
    step2. 深度相同時,情況1:二者已經相同了,此時就是LCA
    step3.深度相同時:情況2:可能只是同一層,但LCA還在高處,走多少步還是不知道的,所以還是需要從大往小走
    需要注意的點:
    1》事先求出樹的最大高度k = log2(n),此時樹是一條鏈。
    2》for循環中i的初值就是這個k。因爲最大就是到2k步。接着一次次的縮小即可。
    3》在for循環中不停地更新二者的值時,防止將值取到0【也就是temp不能爲0】

3.代碼

3.1 代碼
#include<cstdio>
#include<iostream>
#include<cmath>
#include<queue> 
using namespace std;
const int maxN = 500005;//最大結點數 
const int maxM = 1000010; //因爲是無向邊 ,所以要*2 
const int maxD = 25;//最深 

typedef struct {
	int next,to;
}Edge ;

typedef struct {
	int id,de;
}Node;

queue<Node> que;
Edge edge[maxM];//邊長 
int f[maxN][maxD];
//f[i][j] 表示節點i向上走2^j步所能到達的節點的序號 ;
//如果f[i][j]=0,則表示 從i節點向上走2^j步,沒有父節點 
int hei[maxN],vis[maxN],head[maxN];//深搜知道各個節點的深度;該頂點是否訪問過;每個頂點的頭指針 
int eN;//邊數
int n,m,s,k;//k = log2(n) 

void add(int a,int b){
	edge[eN].next = head[a];
	edge[eN].to = b;
	head[a] = eN;
	eN++; 
} 

//預處理f[][]
void bfs(){
	Node temp;
	que.push((Node){s,0});
	int topId,deep;//隊首;隊尾
	int eTo;//相鄰的點 
	while(!que.empty()){//隊列非空時		 
		topId = que.front().id;//取隊頭拿id 
		deep = que.front().de;//取隊頭拿深度 
		vis[topId] = 1;//已訪問 
		hei[topId] = deep;//記錄深度,後面用到 
		que.pop(); 
		//計算f[][]
		if(deep > 1){
			int k = log2(deep); //最多走2^k就到根節點了 
			for(int i = 1;i <=k; i++){ //利用遞推求得關係式 
				f[topId][i] = f[ f[topId][i-1]][i-1];
			} 
		}
					
		for(int i = head[topId]; i!=-1; i=edge[i].next){					
			eTo = edge[i].to;	
			if(vis[eTo]){//如果該頂點已經訪問過了,則不能再放進去了 
				continue;
			} 
			que.push((Node){eTo,deep+1});//放進隊列
			f[eTo][0] = topId;//這是很重要的一步 ,不能疏漏了 
		}
	}	
}


void lca(int x,int y){
	//step1.迭代更新,直到將更深的y變得與x的深度相同 
	for(int i = k;i >= 0 && hei[y] > hei[x];i--)
	{
		int temp = f[y][i];//向上走2^i步到的節點編號爲temp
		//如果走過頭了,temp就爲0了,任何hei[i]都大於hei[0],所以要避免這種情況 
		if(temp&&hei[temp]>=hei[x]){
			y = temp;
		}
	}
	
	//step2. 深度相同時,情況1:二者已經相同了,此時就是LCA 
	if(x == y){//因爲x較淺 
		printf("%d\n",x);
	}
	//step3.深度相同時:情況2:可能只是同一層,但LCA還在高處,走多少步還是不知道的,
	//所以還是需要從大往小走 
	else{		
		for(int i = k;i>=0;i--){//依然是從大往小走 
			if(f[x][i]!= f[y][i]){//保證二者不相會 => 因爲要找出最小的lca 
				x = f[x][i];
				y = f[y][i];
			}
		} 	
		//最後還得走一步 
		printf("%d\n",f[x][0]) ;
	}
} 

int main(){	
	scanf("%d%d%d",&n,&m,&s);
	k = log2(n);
	int a,b;//有邊相連 
	fill(head,head+maxN,-1);//初始化爲-1 
	fill(f[0],f[0]+maxN*maxD,0);//初始爲0 
	for(int i = 0;i<n-1;i++ ){
		scanf("%d%d",&a,&b);		
		add(a,b);
		add(b,a); 
	}
			
	bfs(); 

//驗證輸出f[][]函數是否搞對了
//	for(int i = 1;i<=n;i++){
//		cout <<i<<":  ";
//		for(int j = 0;j<log2(n);j++){
//			cout <<f[i][j] <<" ";
//		}cout <<"\n";
//	}

	for(int i = 0;i<m;i++){
		scanf("%d%d",&a,&b);		
		if(hei[a] > hei[b]) //比較高度, 
			lca(b,a);//a更深 
		else
			lca(a,b);//b更深 
	}
}
3.2 測試用例:
9 5 1
1 2
1 3
2 4
2 5
5 9
3 6
3 7
6 8
1 1
1 2
2 8
6 7
8 7

3 3 1
1 2
2 3
1 2
1 3
2 3

4.相關例題

  • P3379 【模板LCA】
    有很多題【可以說只要涉及到樹上兩點間的距離這種問題】都是基於LCA來做,所以熟練這個 樹上倍增求LCA 就相當重要了。相關的例題可以查看題單——基礎樹上問題。私以爲,LCA是進階的基礎內容,是必須啃下的!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章