1. 程式人生 > 其它 >最近公共祖先(LCA) 倍增優化

最近公共祖先(LCA) 倍增優化

技術標籤:acm競賽樹結構樹形DPc++資料結構

最近公共祖先(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))
    LCA(AB)=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)2dep(LCA(u,v))

倍增演算法實現

思路:
首先約定我們有n個節點,m次查詢。

1. 我們首先預處理求出每個節點的第2的冪次方個祖先,並存放在 f [ i ] [ j ] f[i][j] f[i][j]中,意為編號為i的節點的第 2 j 2^j 2j個祖先節點的編號。一般求到 2 19 2^{19} 219個祖先,也就是524288,即可滿足大部分題目的資料規模。這一步的複雜度是 O ( n l o g n ) O(nlog \space n) O(nlogn)的。
2. 預處理完成後,即可接受查詢。不妨設接受的是 u , v ( d e p ( u ) ≥ d e p ( v ) ) u,v(dep(u) \geq dep(v)) u,v(dep(u)dep(v)
)
的最近公共祖先的查詢。現在我們需要用 f f f陣列來計算出答案。具體步驟如下:

  • 首先將求 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;
    }
}