洛谷P3379 【模板】最近公共祖先(LCA)『用欧拉序列转化为 RMQ 问题』解法

题目链接:https://www.luogu.com.cn/problem/P3379

解题思路:

对一棵树进行 DFS,无论是第一次访问还是回溯,每次到达一个结点时都将编号记录下来,可以得到一个长度为 \(2n-1\) 的序列,这个序列被称作这棵树的欧拉序列。

在下文中,把结点 u 在欧拉序列中第一次出现的位置编号记为 \(pos(u)\)(也称作节点 u 的欧拉序),把欧拉序列本身记作 \(E[1..2n-1]\)

有了欧拉序列,LCA 问题可以在线性时间内转化为 RMQ 问题,即 \(pos(LCA(u, v))=\min\{pos(k)|k\in E[pos(u)..pos(v)]\}\)

这个等式不难理解:从 \(u\) 走到 \(v\) 的过程中一定会经过 \(LCA(u,v)\),但不会经过 \(LCA(u,v)\) 的祖先。因此,从 \(u\) 走到 \(v\) 的过程中经过的欧拉序最小的结点就是 \(LCA(u, v)\)

用 DFS 计算欧拉序列的时间复杂度是 \(O(n)\),且欧拉序列的长度也是 \(O(n)\),所以 LCA 问题可以在 \(O(n)\) 的时间内转化成等规模的 RMQ 问题。

示例程序:

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6 + 5;

int st[maxn][20], n, m, rt, pos[maxn], ord[maxn], idx, id[maxn];
vector<int> g[maxn];

void build_st() {
    for (int j = 0; (1<<j) <= idx; j++) {
        for (int i = 1; i+(1<<j)-1 <= idx; i++) {
            if (!j)
                st[i][j] = pos[ ord[i] ];
            else
                st[i][j] = min(st[i][j-1], st[i+(1<<j-1)][j-1]);
        }
    }
}

int query(int l, int r) {
    int m = log2(r - l + 1);
    return id[min(st[l][m], st[r-(1<<m)+1][m])];
}

void dfs(int u, int p) {
    ord[++idx] = u;
    if (!pos[u]) {
        pos[u] = idx;
        id[idx] = u;
    }
    for (auto v : g[u]) {
        if (v != p) {
            dfs(v, u);
            ord[++idx] = u;
        }
    }
}

int main() {
    scanf("%d%d%d", &n, &m, &rt);
    for (int i = 1; i < n; i++) {
        int u, v;
        scanf("%d%d", &u, &v);
        g[u].push_back(v);
        g[v].push_back(u);
    }
    dfs(rt, -1);
    assert(idx == 2*n-1);
    build_st();
    while (m--) {
        int u, v;
        scanf("%d%d", &u, &v);
        int l = pos[u], r = pos[v];
        if (l > r) swap(l, r);
        printf("%d\n", query(l, r));
    }
    return 0;
}

参考链接:https://oi.wiki/graph/lca/#%E7%94%A8%E6%AC%A7%E6%8B%89%E5%BA%8F%E5%88%97%E8%BD%AC%E5%8C%96%E4%B8%BA-rmq-%E9%97%AE%E9%A2%98

posted @ 2025-04-18 15:32  quanjun  阅读(43)  评论(0)    收藏  举报