基於倍增算法求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是進階的基礎內容,是必須啃下的!