1. 程式人生 > 資訊 >三年之期已到,LPL 第二支 S11 隊伍誕生:EDG 重返《英雄聯盟》夏決舞臺

三年之期已到,LPL 第二支 S11 隊伍誕生:EDG 重返《英雄聯盟》夏決舞臺

總結LCA的一般寫法即延伸應用

方法1:向上標記法

時間複雜度\(O(n)\)
待查詢點為a和b,首先從a點向根節點進行搜尋,將路徑上的點進行標記,
再從b點向根節點進行搜尋,同時檢測路徑上的點是否被標記過,第一次檢測到的點即為a,b兩點的最近公共祖先

方法2:倍增

時間複雜度
預處理:\(O(nlog_n)\)
查詢: \(O(log_n)\)

相關概念及性質

  1. 節點祖先:從根到該節點所經分支上的所有節點,從根節點到此節點的路徑中除了此節點本身(但是在最近公共祖先中的祖先可以是節點本身),其餘節點均為此節點的祖先節點,(兒子->父親->父親的父親->...->根
  2. 深度:對於任意節點n,n的深度為從根到n的唯一路徑長度,根的深度定為0(為了演算法實現的方便,根的深度一般定義為1)

實現思路
為了便於說明演算法流程,以一道實際題目為例

\(depth[i]\):節點\(i\)的深度為\(depth[i]\)
\(f[i][k]\):節點i走\(2^k\)步所能到的點為\(f[i][k]\)

預處理以上兩個陣列,之後對於查詢\(lca(a, b)\)

  1. 先將兩個點跳到同一層(\(depth[a] == depth[b]\))
for (int k = 15; ~k; -- k) // 題中最多40000個點,0~15位二進位制最大表示資料為 2^16-1=65535>40000
    if (depth[f[a][k]] >= depth[b]) // 判斷要跳到的目標位置與b的位置關係
        a = f[a][k];
  1. 如果此時兩個點不相同,讓兩個點同時向上跳,一直跳到它們最近公共祖先的下一層;反之,說明它們本身就為祖孫關係
    這裡利用二進位制拼湊的思想,此時兩個點的層數是相同的,目標位置均為最近公共祖先的下一層,故兩者要跳的距離也是相同的。設此值為dis

計算一個數的二進位制組成時,應當從大數向小數進行嘗試.eg:對於11,應當從16開始判斷,一直到1
16 8 4 2 1
0 1 0 1 1

按照上述方式,兩點初始步幅最大,之後步幅逐漸減小,最終一定可以拼湊出dis步,兩點一定可以走到目標位置

從大到小遍歷所有步幅,如果兩者能夠走到同一個點(\(f[a][k] == f[b][k]\)),不走,不能走到同一個點(\(f[a][k] != f[b][k]\)

)才走

因為\(f[a][k] == f[b][k]\)只能滿足該位置是兩者的公共祖先,而非最近公共祖先,所以把目標位置定為最近公共祖先的下一層,便於程式碼實現

if (a == b) return a;
for (int k = 15; ~k; -- k) // 15的理由同上
    if (f[a][k] != f[b][k])
    {
        a = f[a][k];
        b = f[b][k];
    }
  1. 如果此時兩個點不相同,那麼任意一個點再向上走1步即為兩點的最近公共祖先
return f[a][0];

上述題目程式碼實現

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 4e4 + 10, M = N * 2;

int n, m;
int h[N], e[M], ne[M], idx;
int depth[N], f[N][16];
queue<int> q;

void add(int a, int b)
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx ++;
}
// 更新depth和f
bool bfs(int root)
{
    /** 這裡depth起到2個作用:
     * 1. 儲存某個點的深度
     * 2. 標記某個點是否已經被搜尋過(同樣可以通過引入st陣列)
     * 將depth初始化為無窮大,通過判斷是否為無窮大判斷某個點是否被搜尋過,depth的初始值只需要是一個非正常值,能夠區分已搜尋點和未搜尋點即可
     */

    memset(depth, 0x3f, sizeof depth);
    depth[0] = 0; // f[a][k]當k較大時,從a點出發可能超出了根節點,即f[a][k]為0,定義走不到的點的深度為0,便於區分
    depth[root] = 1;
    q.push(root);

    while (q.size())
    {
        int t = q.front();
        q.pop();
        for (int i = h[t]; ~i; i = ne[i])
        {
            int p = e[i];
            if (depth[p] == 0x3f3f3f3f)
            {
                q.push(p);

                depth[p] = depth[t] + 1;

                f[p][0] = t;
                for (int k = 1; k <= 15; ++ k) // f[p][k] 需要用到f[p][k - 1],故需要從小到大
                    f[p][k] = f[f[p][k - 1]][k - 1];
            }
        }   
    }
}
int lca(int a, int b)
{
    if (depth[a] < depth[b]) swap(a, b); // 保證a下b上
    for (int k = 15; ~k; -- k)
        if (depth[f[a][k]] >= depth[b]) // depth[0] = 0,決定了這裡從a點出發不會跳轉到b點之上
            a = f[a][k];
    if (a == b) return a;
    for (int k = 15; ~k; -- k)
        if (f[a][k] != f[b][k])
        {
            a = f[a][k];
            b = f[b][k];
        }
    return f[a][0];
}
int main()
{
    int root;
    memset(h, -1, sizeof h);
    cin >> n;
    while (n --)
    {
        int a, b;
        cin >> a >> b;
        if (b == -1) root = a;
        else add(a, b), add(b, a);
    }

    bfs(root);

    cin >> m;
    while (m --)
    {
        int a, b;
        cin >> a >> b;
        int p = lca(a, b);
        if (p == a) cout << 1 << endl;
        else if (p == b) cout << 2 << endl;
        else cout << 0 << endl;
    }
    return 0;
}

方法3:Tarjan

時間複雜度\(O(n + m)\)

實現思路
深度優先遍歷,將點分類
[1] 正在搜尋的點
[2] 已經遍歷且回溯過的點
[3] 還未搜尋的點

演算法有些抽象以一道題目為例進行解釋

//      *
//      |1
//      * 
//    2/ \3
//    *   *
/**
 * 兩個葉子節點距離 = (2 + 1) - (3 + 1) - 2 * 1
 * Tarjan離線
 */
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <queue>
#include <vector>

using namespace std;
using PII = pair<int, int>;

const int N = 1e4 + 10, M = N * 2;

int n, m;
int h[N], e[M], ne[M], w[M], idx;
int p[N];
queue<int> q;
int res[M];
int st[N];
int dis[N];
vector<PII> query[M];

int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}
void add(int a, int b, int c)
{
    e[idx] = b;
    ne[idx] = h[a];
    w[idx] = c;
    h[a] = idx ++;
}
// 初始化各點與根節點的距離
void bfs(int root)
{
    memset(dis, 0x3f, sizeof dis);
    dis[root] = 0;
    q.push(root);

    while (q.size())
    {
        int t = q.front();
        q.pop();

        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (dis[j] == 0x3f3f3f3f)
            {
                dis[j] = dis[t] + w[i];
                q.push(j);
            }
        }
    }
}
/**
 * tarjan難在回溯
 * 很多操作之間都存在嚴格的先後關係,由於結合遞歸回溯,難理解很多
 */
void tarjan(int x)
{
    st[x] = 1; // 搜尋開始
    // 遍歷u節點下的所有子節點
    for (int i = h[x]; ~i; i = ne[i])
    {
        int j = e[i];
        if (!st[j]) // st = 0表示不是正在搜尋且不是已經搜尋完成,是還未搜尋
        {
            tarjan(j);
            p[j] = x;
        }
    }

    for (auto question : query[x])
    {
        int y = question.first, id = question.second;
        if (st[y] == 2)
        {
            int ancestor = find(y);
            /**
             * find(y)為什麼是最近公共祖先?
             * 這個和tarjan(j),p[j] = x的先後順序有關,只有在把以x節點為根節點的子樹全部搜尋完成後,
             * x節點的p[x]才會更新會上層節點,所以x節點以下的節點所找到的父節點都是x而非整棵樹的根節點
             * 比較適合理解這個問題的例子
             *         a
             *         |
             *         b
             *        / \
             *       c   d
             * 詢問(c, d)距離
             * tarjan(a) -> tarjan(b) -> tarjan(c), p[c] = b; tarjan(d)
             * p[a] = a     p[b] = b      p[c] = b
             * 因為在d那一層計算(c,d)距離時,p[b] = b並沒有更新,但c點的tarjan已經結束
             * 所以p[c]已經更新為b,所以find(c) = b,是c與d的最近公共祖先,而非根節點a
             */
            res[id] = dis[x] + dis[y] - 2 * dis[ancestor];
        }
    }

    st[x] = 2; // 搜尋完成
}
int main()
{
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for (int i = 0; i < n - 1; ++ i)
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c), add(b, a, c);
    }

    for (int i = 0; i < m; ++ i)
    {
        int a, b;
        cin >> a >> b;
        /**
         * 儲存了2次
         * 答案儲存依據的是問題編號i,所以多儲存並不會影響最終結果
         * 需要儲存2次的原因是距離的計算只有在回溯的時候可以計算出來
         * 我們無法確定點與點之間的位置關係,所以需要儲存2次數
         */
        query[a].push_back({b, i});
        query[b].push_back({a, i});
    }

    for (int i = 1; i <= n; ++ i) p[i] = i;

    bfs(1); // 初始化各節點距離根節點距離
    tarjan(1);

    for (int i = 0; i < m; ++i) cout << res[i] << endl;

    return 0;
}