最近公共祖先(LCA) 倍增優化
阿新 • • 發佈:2021-02-13
最近公共祖先(LCA) 倍增優化
定義
最近公共祖先(Lowest Common Ancestor):兩個節點的公共祖先節點中離根最遠(即深度最深)的節點。
性質
- u u u是 v v v的祖先,當且僅當 L C A ( u , v ) = u LCA(u,v)=u LCA(u,v)=u
- 如果 u u u不為 v v v的祖先並且 v v v不為 u u u的祖先,那麼 u , v u,v u,v分別處於 L C A ( u , v ) LCA(u,v) LCA(u,v)的兩棵不同子樹中
- 記
A
,
B
A,B
A,B分別為兩個點集,有
L
C
A
(
A
∪
B
)
=
L
C
A
(
L
C
A
(
A
)
,
L
C
A
(
B
)
)
LCA(A \cup B) = LCA(LCA(A),LCA(B))
- L C A ( u , v ) LCA(u,v) LCA(u,v)一定在 u , v u,v u,v的最短路徑上
- 記 d e p dep dep為深度,dis為最短距離,有 d i s ( u , v ) = d e p ( u ) + d e p ( v ) − 2 ∗ d e p ( L C A ( u , v ) ) dis(u,v) = dep(u) + dep(v) - 2*dep(LCA(u,v)) dis(u,v)=dep(u)+dep(v)−2∗dep(LCA(u,v))
倍增演算法實現
思路:
首先約定我們有n個節點,m次查詢。
2. 預處理完成後,即可接受查詢。不妨設接受的是 u , v ( d e p ( u ) ≥ d e p ( v ) ) u,v(dep(u) \geq dep(v)) u,v(dep(u)≥dep(v)
- 首先將求 u , v u,v u,v的LCA轉換成求 u u u與 v v v同深度的祖先 u ′ u^{\prime} u′與 v v v的LCA
- 如果 u ′ = v u^{\prime} = v u′=v,那麼 L C A ( u , v ) = v LCA(u,v) = v LCA(u,v)=v。
- 否則讓 u ′ u^{\prime} u′與 v v v一起向上跳, 沒錯,是跳。跳 2 i 2^i 2i步, i i i遞減, i i i的初值取決於資料規模。如果 u ′ u^{\prime} u′與 v v v的第 2 i 2^{i} 2i祖先相同,那就繼續往下跳。如果不同就往上跳。直到找到一個臨界值,一個 u ′ u^{\prime} u′與 v v v不同但是 u ′ u^{\prime} u′與 v v v上數第一個祖先相同的臨界點。
- 單次查詢是 O ( l o g n ) O(log\space n) O(logn)的,總共m次查詢,也就是 O ( m l o g n ) O(mlog\space n) O(mlogn)
整個演算法的時間複雜度為
O
(
n
l
o
g
n
+
m
l
o
g
n
)
O(nlog\space n + mlog\space n)
O(nlogn+mlogn)
如果沒有完全理解,那麼直接看程式碼
程式碼實現
該程式碼解決的是洛谷P3379,可以前往該頁面看輸入輸出格式。
#include <algorithm>
#include <vector>
#include <iostream>
#include <cmath>
using namespace std;
const int N = 5e5 + 100;
vector<int> G[N];
int n, m, s;
void add(int u, int v)
{
G[u].push_back(v);
G[v].push_back(u);
}
//以上是存圖
bool vis[N];
double limit; //限制i之增長,實際值為以2為底的log n,其中n為節點個數
int f[N][20], hight[N]; //f[i][j]表示編號為i的節點的第2^i個祖先,hight為高度陣列
void dfs(int s, int h = 0)
{
hight[s] = h;
for(int i = 1; i <= limit; i++)
{
if((1<<i) > h) break; //高度小於2^i時,顯然f[s][i]是沒有意義的,直接跳過
f[s][i] = f[f[s][i-1]][i-1]; //這個狀態轉移方程的意義是s的第2^i個祖先 = (s的第2^(i-1)個祖先)的 第2^(i-1)個祖先。
//dp, 倍增優化之基本。
}
for(auto p : G[s])
{
if(vis[p]) continue;
vis[p] = 1;
f[p][0] = s; //dp的邊界條件
dfs(p, h+1);
}
}
int query(int a, int b)
{
if(hight[a] != hight[b]) //先嚐試將a,b變為同一高度,具體做法是將較深的一個提升到相同高度
{
if(hight[a] < hight[b]) swap(a,b); //保證a比b深
int d = hight[a] - hight[b];//d即為a,b高度差
for(int i = 0; i < 20; i++)
if(d & (1<<i)) a = f[a][i]; //這麼做可以讓a變為a的第i個祖先
}
if(a == b) return a;//此時a,b同深度,如果a=b,萬事大吉
for(int i = 19; i >= 0; i--) //否則我們從上往下走,這過程中保持ab同深度,試圖尋找一個臨界點——ab不相同,但ab的上數第1個祖先相同
{
if(hight[a] < (1<<i)) continue; //高度小於2^i,無意義,繼續
if(f[a][i] == f[b][i]) continue;//此時是公共祖先,但不一定是LCA,繼續
a = f[a][i]; b = f[b][i];
//此時表示ab的第2^(i+1)個祖先相同, 但2^i個祖先不相同,也就是說臨界點藏在[ 2^i,2^(i-1) )中。
//我們便將ab上移,此時如果i為0那麼便是我們要找的臨界點,否則不為0的i為繼續為我們縮小範圍。
}
return f[a][0];//臨界點上數第1個祖先結點便是答案,f[b][0]也可以
}
int main()
{
cin >> n >> m >> s; //n為節點數,m為詢問數,s為根節點
limit = log(n);
int u, v;
for(int i = 1; i < n; i++) //n-1條邊
{
cin >> u >> v;
add(u, v);
}
vis[s] = 1;
dfs(s); //預處理
for(int i = 0; i < m; i++) //m次詢問
{
cin >> u >> v;
cout << "ans = " << query(u, v) << endl;
}
}