Skip to content

最低共同祖先 (Lowest Common Ancestor, LCA)

最低共同祖先

在有根樹上任意兩點 ,兩點祖先交集中,深度最深的一個點,稱為兩點的最低共同祖先 (Lowest Common Ancestor, LCA)。

給定一顆有根樹 ( 個點) 和 筆詢問,每筆詢問輸出兩點 的最低共同祖先。

直觀的做法是利用 DFS 尋找兩點的 LCA,每次查詢時間複雜度為 ,這種辦法就容易超過時間限制。

這裡介紹利用倍增法找尋任意兩點的最低共同祖先,這裡用轉態轉移式定義出定義

  • 狀態: 代表 的第 層祖先。
  • 初始狀態: , 的父節點。
  • 轉移:

和一般的倍增法相同,將算法分成三個步驟。

第一步是 DFS,找出每個點的父節點,以及紀錄進入和離開的時間戳記。

第二步用兩層迴圈算出 剩餘答案。

最後根據 ,用二分搜找出 兩點的最低共同祖先,由於任意兩點 的共同祖先有單調性, 點的所有祖先,在 (包含)之上的祖先是兩點的共同祖先,剩下的只是 的祖先,因此可以用二分搜枚舉 尋找 ,在二分搜過程,時間戳記用於判斷 是否為 的祖先。

DFS 的時間複雜度為 ,計算 剩餘答案的時間複雜度為 ,每次查詢操作的時間複雜度為 ,整題時間複雜度為

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const int LOG = 20;
int par[N][LOG];
int tin[N], tout[N];
int timer = 0;

void dfs(int v, int p)
{
    tin[v] = ++timer;
    par[v][0] = p;
    for (int it : G[v])
    {
        if (it != p)
            dfs(it, v);
    }
    tout[v] = ++timer;
}

void Doubling()
{
    for (int i = 1; i < N; ++i)
    {
        for (int j = 1; j < LOG; ++j)
        {
            par[i][j] = par[par[i][j - 1]][j - 1];
        }
    }
}

bool anc(int v, int u) { return tin[v] <= tin[u] && tout[u] <= tout[v]; }

int LCA(int v, int u)
{
    if (anc(v, u))
        return v;
    for (int j = LOG - 1; j >= 0; --j)
    {
        if (!anc(par[v][j], u))
            v = par[v][j];
    }
    return par[v][0];
}

找出最低共同祖先的算法,可推廣到找尋 路徑上的資訊,例如:路徑長度、最小(大)權重的邊。

假設 ,把 的路徑 可以拆成

例題練習

Atcoder Beginner Contest 209 D - Collision

給定一棵樹,請問 需要移動偶數步還是奇數步?

練習樹上任兩點之間的距離,令 的距離, 的距離

參考程式碼
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void dfs(int v, int p)
{
    tin[v] = ++timer;
    par[v][0] = p;
    if (v != p)
        dist[v] = dist[p] + 1;
    for (int it : G[v])
    {
        if (it != p)
            dfs(it, v);
    }
    tout[v] = ++timer;
}

int main()
{
    int q;
    cin >> n >> q;
    for (int i = 0, u, v; i < n - 1; ++i)
    {
        cin >> u >> v;
        G[u].emplace_back(v);
        G[v].emplace_back(u);
    }
    dist[1] = 1;
    dfs(1, 1);
    Doubling();
    for (int i = 0, u, v; i < q; ++i)
    {
        cin >> u >> v;
        int w = LCA(v, u);
        int res = (dist[u] + dist[v] - 2 * dist[w]) % 2;
        if (res)
            cout << "Road\n";
        else
            cout << "Town\n";
    }
}
CodeForces 609E - Minimum spanning tree for each edge

給定一張無向圖 ,求包含邊 的生成樹中,權重最小的生成樹為何?

先求出一顆最小生成樹 ,權重和為 ,利用最低共同祖先求出節點 節點 到第 個節點的最大權重路徑,對於每一條邊 ,在 中找到從 的最大權重路徑 。答案 (如果 )。

UVa 11354 - Bond

給定一張無向帶權圖,有多筆詢問,詢問 之間的路徑最大權重邊權值最小為何。 也就是詢問最小瓶頸樹中, 之間的路徑最大邊重權為何。

這題要先利用 Krusal 求出最小瓶頸樹,接著利用 LCA 求出每個點 到它的第 層祖先的路徑中的最大邊重權。

參考程式碼
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#include <bits/stdc++.h>
using namespace std;
const int N = 50010;
const int INF = 1e9;
const int LOG = 25;

struct Edge
{
    int s, t, w;
    Edge(){};
    Edge(int _s, int _t, int _w) : s(_s), t(_t), w(_w) {}
    bool operator<(const Edge &rhs) const { return w < rhs.w; }
};
int n, m, djs[N], depth[N], par[N][LOG], maxcost[N][LOG];
vector<Edge> edges;
vector<int> G[N];

void init()
{
    memset(par, -1, sizeof(par));
    memset(maxcost, -1, sizeof(maxcost));
    edges.clear();
    for (int i = 0; i < N; i++)
    {
        djs[i] = i;
        G[i].clear();
    }
}

int query(int x) { return (x == djs[x] ? x : djs[x] = query(djs[x])); }

void MST()
{
    for (int i = 0; i < m; i++)
    {
        int fa = query(edges[i].s), fb = query(edges[i].t);
        if (fa != fb)
        {
            djs[fa] = fb;
            G[edges[i].s].push_back(i);
            G[edges[i].t].push_back(i);
        }
    }
}

void dfs(int s, int f)
{
    depth[s] = depth[f] + 1;
    par[s][0] = f;
    for (auto i : G[s])
    {
        // 不知道 s 存在這條邊的哪個位置,用 xor 消除同樣的數字,留下來的就是另外一個點
        int t = edges[i].s ^ edges[i].t ^ s; 
        if (t != f)
        {
            maxcost[t][0] = edges[i].w;
            dfs(t, s);
        }
    }
}

void preprocess()
{
    for (int i = 1; i <= LOG; i++)
        for (int j = 1; j <= n; j++)
        {
            if (par[j][i - 1] == -1 || par[par[j][i - 1]][i - 1] == -1)
                continue;
            par[j][i] = par[par[j][i - 1]][i - 1];
            maxcost[j][i] =
                max(maxcost[j][i - 1], maxcost[par[j][i - 1]][i - 1]);
        }
}

int solve(int p, int q)
{
    if (depth[p] < depth[q])
        swap(p, q);
    int hibit, ans = -INF;
    for (hibit = 1; (1 << hibit) <= depth[p]; ++hibit)// p 的深度以二進位表示的最高位
        ;
    // 把 p,q 兩個點的深度調整到一樣
    for (int i = hibit - 1; i >= 0; i--)
        if (depth[p] - (1 << i) >= depth[q])
        {
            ans = max(ans, maxcost[p][i]);
            p = par[p][i];
        }
    if (p == q)
        return ans;
    // 讓 p,q 變成最低共同祖先的兒子
    for (int i = hibit - 1; i >= 0; i--)
    {
        if (par[p][i] == -1 || par[p][i] == par[q][i])
            continue;
        ans = max({ans, maxcost[p][i], maxcost[q][i]});
        p = par[p][i];
        q = par[q][i];
    }
    return max({ans, maxcost[p][0], maxcost[q][0]});
}

int main()
{
    cin.tie(NULL);
    for (int ti = 0; cin >> n >> m; ++ti)
    {
        if (ti) cout << '\n';
        init();
        for (int i = 0; i < m; i++)
        {
            int s, t, w;
            cin >> s >> t >> w;
            edges.emplace_back(s, t, w);
        }
        sort(edges.begin(), edges.end());
        MST();
        dfs(1, -1);
        preprocess();
        int q;
        cin >> q;
        for (int i = 0; i < q; i++)
        {
            int x, y;
            cin >> x >> y;
            cout << solve(x, y) << '\n';
        }
    }
}
CodeForces 1328E - Tree Queries

給定一棵樹, 為樹根,每次詢問給定 個點,有沒有一條路徑和這些點的距離 ?

個點中找出最深的點 ,對於每個點 ,判斷 的父節點 是否為 的祖先。