1. 程式人生 > >倍增法求Lca(最近公共祖先)

倍增法求Lca(最近公共祖先)

一. 明確問題

看標題便知道了, 這篇部落格力求解決的問題是求出一棵樹的兩個結點的最近公共祖先(LCA), 方法是倍增法.

那麼什麼是Lca呢?

它是一棵樹上兩個結點向上移動, 最後交匯的第一個結點, 也就是說這兩個結點祖先裡離樹根最遠也是離他們最近的結點.

什麼是倍增法呢?

此問題說的是用倍增法求解lca問題, 那麼我們可以推測這種方法還可以解決其他的一些問題(不在當下討論範圍). 在學習的過程中, 我是這麼理解的: 它是一種類似於二分的方法, 不過這裡不是二分, 而是倍增, 以2倍, 4倍, 等等倍數增長

一下沒有理解倍增法沒關係, 看後面的做法, 再結合前面, 前後貫通大概可以理解的七七八八.

二. 思路引導

下面的思路試圖把過程模組化, 如果你不知道一個地方如何實現, 還請不要糾結(比如不要糾結於樹的深度怎麼求, 假設我們求好了樹的深度)

我們找的是任意兩個結點的最近公共祖先, 那麼我們可以考慮這麼兩種種情況:

  1. 兩結點的深度相同.
  2. 兩結點深度不同.

演算法實現來說, 第一種情況是第二種情況的特殊情況, 第二種情況是要轉化成第一種情況的

先不考慮其他, 我們思考這麼一個問題: 對於兩個深度不同的結點, 把深度更深的那個向其父節點迭代, 直到這個迭代結點和另一個結點深度相同, 那麼這兩個深度相同的結點的Lca也就是原兩個結點的Lca. 因此第二種情況轉化成第一種情況來求解Lca是可行的.

現在還不知道如何把兩個結點迭代到相同深度, 別急, 這裡用到的是上面提到的倍增法.

那麼剩下的問題事就解決第一種情況了, 兩個結點深度相同了. 怎麼求他們的Lca呢?

這裡也是用了倍增法. 思路和上一步的倍增法是一樣的, 不同之處有兩點
1. 這次是兩個結點一起迭代向前的.
2. 這次迭代停止的條件和上次不一樣.

OK, 現在無法理解上面的幾個過程沒關係,只要知道我們用遞增法解決了上述的兩個問題. 具體細節看下面分析.

三. 整體框架.

那麼過了一遍上面的思路引導之後, 我們可以大概想一想怎麼實現整個的問題了.

其實用求Lca這個問題可以分為兩塊: 預處理 + 查詢, 其中預處理是O(VlogV), 而一次查詢是O(logV), V代表結點數量. 所以總時間複雜度為O(VlogV +QlogV)

. Q為查詢次數

其步驟是這樣的:
1. 儲存一棵樹(鄰接表法)
2. 獲取樹各結點的上的深度(dfs或bfs)
3. 獲取2次冪祖先的結點, 用parents[maxn][20]陣列儲存, 倍增法關鍵
4. 用倍增法查詢Lca

步驟一

用鄰接表儲存一棵樹, 並用from[]陣列記錄各結點的父節點, 其中沒有父節點的就是root.

parents[u][]陣列儲存的是u結點的祖先結點.
如parents[u][0], 是u結點的2⁰祖先結點, 即1祖先, 也即父節點. 在輸入過程中可以直接得到.
parents[u][1], 是u結點的2¹祖先結點,即2祖先, 也即父親的父親
parents[u][2], 是u結點的2²祖先結點, 即4祖先, 也即(父親的父親)的(父親的父親), 也就是爺爺的爺爺.

理解這個關係很重要, 這也是通過父親結點獲取整個祖先結點的關鍵. 現在可以先跳過.

void getData()
{
    cin >> n;
    int u, v;
    for (int i = 1; i < n; ++i) {
        cin >> u >> v;
        G[u].push_back(v);
        parents[v][0] = u;
        from[v] = 1;
    }
    for (int i = 1; i <= n; ++i) {
        if (from[i] == -1) root = i;
    }
}

步驟二

獲取各結點的深度, 可以用DFS或這BFS方法

void getDepth_dfs(int u) // DFS求深度
{
    int len = G[u].size();
    for (int i = 0; i < len; ++i) {
        int v = G[u][i];
        depth[v] = depth[u] + 1;
        getDepth_dfs(v);
    }
}

void getDepth_bfs(int u) // BFS求深度
{
    queue<int> Q;
    Q.push(u);
    while (!Q.empty()) {
        int v = Q.front();
        Q.pop();
        for (int i = 0; i < G[v].size(); ++i) {
            depth[G[v][i]] = depth[v] + 1;
            Q.push(G[v][i]);
        }
    }
}

步驟三

求祖先

在步驟一里面我們討論了parents陣列的意義, 它存的是結點u的2次冪祖先, 從父親結點開始. 為什麼要存2次冪? 這就是倍增法的思想了, 我們進行範圍縮小不是一步一步的, 那樣太暴力了, 所以我們需要某個跨度, 讓我們能夠先跨越大步, 接近的時候在小步小步跨越, 這樣可以大大節省時間.

讀者可能會疑惑, 先大步, 後小步, 可是我怎麼知道什麼時候該大步, 什麼時候該小步呢? 難道不會不小心跨過頭嗎?

其實不會的, 在程式碼實現上, 這樣的跨越有條件約束, 是非常講究的. 讀者不必為此糾結, 不過要講解也是十分費力不討好的事情, 所以請讀者認證推敲後面Lca函式的程式碼, 認真琢磨為什麼是那樣跨越, 其中真味自會品出. 最好是自己寫幾個例子, 模擬跨越的過程, 在結合現實意義去理解

那麼我們回到當前問題. 請看下面這個公式:

parents[i][j] = parents[parents[i][j-1]][j-1]

這是構造此陣列的公式. 不難理解, 父親的父親就是爺爺, 爺爺的爺爺就是4倍祖先. 請讀者結合現實意義去理解.

void getParents()
{
    for (int up = 1; (1 << up) <= n; ++up) {
        for (int i = 1; i <= n; ++i) {
            parents[i][up] = parents[parents[i][up - 1]][up - 1];
        }
    }
}

步驟四

做完了前面O(VlogV)的預處理操作, 剩下的就是查詢了, 一次查詢O(logV)

因此, 我們可以敏銳的想到: Lca演算法適合查詢次數比較多的情況, 不然, 光是預處理就花了那麼多時間了. 所以說, 查詢是我們享受成果的時候了.

int Lca(int u, int v)
{
    if (depth[u] < depth[v]) swap(u, v); // 使滿足u深度更大, 便於後面操作 
    int i = -1, j;
    // i求的是最大二分跨度 
    while ((1 << (i + 1)) <= depth[u]) ++i;

    // 下面這個迴圈是為了讓u和v到同一深度 
    for (j = i; j >= 0; --j) {
        if (depth[u] - (1 << j) >= depth[v]) { // 是>=, 因為如果<,代表跳過頭了,跳到了上面. 
            u = parents[u][j];
        }
    }

    if (u == v) return u; // 剛好是祖宗 

    // u和v一起二分找祖宗
    for (j = i; j >= 0; --j) {
        if (parents[u][j] != parents[v][j]) {
            u = parents[u][j];
            v = parents[v][j];
        }
    }
    return parents[u][0]; // 說明上個迴圈迭代到了Lca的子結點 
}
  • 首先把u調整到深度更大(或相同)的結點, 便於後面操作.

  • 然後獲取最大跨度i, 所有的跨越都是從i開始的.

  • 再然後把u上升到和v一樣的深度. 也就是我們前面討論過的情況二轉情況一.

  • 最後, 兩個結點同時迭代, 直到找到Lca

至此, 我們的問題就解決了.

完整程式碼

#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
#include <vector>
using namespace std;

const int maxn = 10005;
int parents[maxn][20], depth[maxn];
int n, from[maxn], root = -1;
vector<int> G[maxn];

void init()
{
    memset(parents, -1, sizeof(parents));
    memset(from, -1, sizeof(from));
    memset(depth, -1, sizeof(depth));
}

void getData()
{
    cin >> n;
    int u, v;
    for (int i = 1; i < n; ++i) {
        cin >> u >> v;
        G[u].push_back(v);
        parents[v][0] = u;
        from[v] = 1;
    }
    for (int i = 1; i <= n; ++i) {
        if (from[i] == -1) root = i;
    }
}

void getDepth_dfs(int u)
{
    int len = G[u].size();
    for (int i = 0; i < len; ++i) {
        int v = G[u][i];
        depth[v] = depth[u] + 1;
        getDepth_dfs(v);
    }
}

void getDepth_bfs(int u)
{
    queue<int> Q;
    Q.push(u);
    while (!Q.empty()) {
        int v = Q.front();
        Q.pop();
        for (int i = 0; i < G[v].size(); ++i) {
            depth[G[v][i]] = depth[v] + 1;
            Q.push(G[v][i]);
        }
    }
}

void getParents()
{
    for (int up = 1; (1 << up) <= n; ++up) {
        for (int i = 1; i <= n; ++i) {
            parents[i][up] = parents[parents[i][up - 1]][up - 1];
        }
    }
}

int Lca(int u, int v)
{
    if (depth[u] < depth[v]) swap(u, v);
    int i = -1, j;
    while ((1 << (i + 1)) <= depth[u]) ++i;
    for (j = i; j >= 0; --j) {
        if (depth[u] - (1 << j) >= depth[v]) {
            u = parents[u][j];
        }
    }
    if (u == v) return u;
    for (j = i; j >= 0; --j) {
        if (parents[u][j] != parents[v][j]) {
            u = parents[u][j];
            v = parents[v][j];
        }
    }
    return parents[u][0];
}

void questions()
{
    int q, u, v;
    cin >> q;
    for (int i = 0; i < q; ++i) {
        cin >> u >> v;
        int ans = Lca(u, v);
        cout << ans << endl;
        //cout << u << " 和 " << v << " 的最近公共祖先(LCA)是: " << ans << endl; 
    }
}

int main()
{
    init();
    getData();
    depth[root] = 1;
    getDepth_dfs(root);
    //getDepth_bfs(root);
    getParents();
    questions();
}
/*
9
1 2
1 3
1 4
2 5
2 6
3 7
6 8
7 9
5
1 3
5 6
8 9
8 4
5 8
*/