【图论】最近公共祖先 学习笔记

LCA

基本概念

对于一个有根树,如果点 \(z\) 既是点 \(x\) 的祖先,又是点 \(y\) 的祖先,则说点 \(z\)\(x\)\(y\) 的公共祖先。每对点的所有公共祖先里,深度最大的那个点被称作这两个或多个点的最近公共祖先(lca)。
lca 有很多优秀的性质,例如经过 lca 的路径是两点间的最近路径等。

LCA 的求法

下面简单介绍几种常用的求解 LCA 的方法。

向上标记法

暴力找出所有公共祖先求公共祖先,然后找出深度最大的即可。时间复杂度 O(n)。

倍增法

倍增的一个经典应用就是优化向上标记法求 LCA,求 LCA 的较常见算法是倍增法。
先预处理出 \(fa[u][j]\) 表示节点 \(u\) 的第 \(2^j\) 个祖先,根据倍增的性质有 \(fa[u][j] = fa[fa[u][j-1]][j-1]\) (\(2^{j-1} + 2^{j-1} = 2 \times 2^{j-1} = 2^j\)),初始条件可以通过一次 DFS 求出每个节点的直接父亲 \(fa[u][0]\) 然后以第二维为阶段递推即可。
查询时,先倍增把两个点调到同一高度,然后再倍增向上调,每次倍增时从大到小调 \(2\) 的整数幂,由二进制拆分可以知道这样是正确的。
参考代码如下:

// 洛谷 P3379 【模板】最近公共祖先(LCA)
#include <bits/stdc++.h>
using namespace std;
// #define int long long

const int N = 5e5 + 5;

vector<int> g[N];

void add(int u, int v) {
    g[u].push_back(v);
    g[v].push_back(u);
}

int n, q, s;
int f[N][20], d[N];

void dfs(int u, int fa) { // 一次 DFS 处理出深度和直接父亲
    for (auto& v : g[u]) {
        if (v == fa) continue;
        f[v][0] = u;
        d[v] = d[u] + 1;
        dfs(v, u);
    }
}

void LCA_init() {
    for (int j = 1;j <= 18;j++) {
        for (int i = 1;i <= n;i++) {
            f[i][j] = f[f[i][j - 1]][j - 1]; // 倍增递推出 fa 数组
        }
    }
}

int lca(int x, int y) {
    if (d[x] > d[y]) swap(x, y);
    for (int j = 18;j >= 0;j--) {
        if (d[f[y][j]] >= d[x]) y = f[y][j]; // 倍增调到同一高度
    }
    if (x == y) return x;
    for (int j = 18;j >= 0;j--) {
        if (f[x][j] != f[y][j]) { // 不是公共祖先就向上调
            x = f[x][j], y = f[y][j]; 
        }
    }
    return f[x][0]; // 由二进制拆分可知,倍增调整完后的父亲的一定是 lca
}

signed main() {
    ios::sync_with_stdio(0);
    #ifdef DEBUG
    clock_t t0 = clock();
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);
    #endif

    // Don't stop. Don't hide. Follow the light, and you'll find tomorrow. 

    cin >> n >> q >> s;
    for (int i = 1, u, v; i <= n - 1; i++) {
        cin >> u >> v;
        add(u, v);
    }
    d[s] = 1;
    dfs(s, 0);
    LCA_init();

    while (q--) {
        int u, v;
        cin >> u >> v;
        cout << lca(u, v) << endl;
    }

    #ifdef DEBUG
    cerr << "Time used:" << clock() - t0 << "ms" << endl;
    #endif
    return 0;
}

时间复杂度:预处理 \(O(n \log n)\),每次查询 \(O(\log n)\)

重链剖分法

欧拉序 + ST 表法

一种 \(O(n \log n)\) 预处理,\(O(1)\) 查询的求解 LCA 方法。

LCA 的运用举例

树上前缀和、子树和、差分

类似序列上的前缀和差分,树上前缀和指的是从根节点到某个点的路径上所有点(边)权之和,子树和是某个子树内所有点(边)权之和。灵活运用树上前缀和、子树和、差分可以解决许多问题。

P1600 [NOIP2016 提高组] 天天爱跑步

对于每条路径 \(u \to v\),记 \(l = lca(u,v)\),则可以把路径分为两部分:\(u \to l\), \(l \to v\)
当路径 \(u \to l\) 对节点 \(x\) 上的观察员有贡献时,必须当且仅当下面两个条件满足:

  1. \(x\)\(u \to l\) 上。
  2. 观察员在第 \(w_x\) 秒观察,则 \(d_r - d_x = w_x \implies d_x + w_x = d_r\),其中 \(d_i\) 表示节点 \(i\) 的深度(这题里深度定义为从根节点出发经过的边数,即根节点深度为 \(0\))。
    其中,\(d_x + w_x\) 为节点 \(x\) 的不变属性,d_r 为路径的属性。而由条件 \(1\) 可知,路径 \(u -> r\) 只会对所经过的点产生贡献。所以记 \(c[x][i]\) 为可对节点 \(x\) 上的观察员产生贡献的路径属性(\(d_r\))为 \(i\) 的节点,对于每个观察员 \(c[x][d_x + w_x]\) 即为所有路径 \(u \to l\) 部分对这个观察员的贡献。

考虑如何计算 \(c[x][i]\)
方法一:树上差分+线段树合并。对每个点建立一颗权值线段树。对于每个 \(u \to l\),进行树上差分,在 \(u\)\(fa[l][0]\) 的权值线段树上的 \(d_r\) 节点上分别 \(+1\)\(-1\),最后求子树和即可(线段树的加法为线段树合并,即每个节点对应相加)。
方法二:直接进行树上差分,在每个点上记录每次修改操作,用一个全局数组来求子树和。

\(l \to v\) 的路径部分求解贡献方法类似,条件 \(2\)\(d_l - d_u + d_l - d_x = w_x \implies d_x + w_x = 2d_l - d_u\)

参考代码:

#include <bits/stdc++.h>
using namespace std;
// #define int long long

const int N = 3e5 + 5;
int n, m;
vector<int> g[N];
int w[N], d[N], fa[N][25];
struct player {
    int u, v, l;
} p[N];

void dfs(int u, int in) {
    for (int v : g[u]) {
        if (v == in) continue;
        d[v] = d[u] + 1;
        fa[v][0] = u;
        dfs(v, u);
    }
}
void lca_init() {
    d[1] = 1;
    dfs(1, 0);
    for (int j = 1;j <= 20;j++)
        for (int u = 1;u <= n;u++)
            fa[u][j] = fa[fa[u][j - 1]][j - 1];
}
int lca(int x, int y) {
    if (d[x] > d[y]) swap(x, y);
    for (int j = 20;j >= 0;j--)
        if (d[fa[y][j]] >= d[x])
            y = fa[y][j];
    if (x == y) return x;
    for (int j = 20;j >= 0;j--)
        if (fa[x][j] != fa[y][j])
            x = fa[x][j], y = fa[y][j];
    return fa[x][0];
}

using pii = pair<int, int>;
int ans[N];
vector<pii> delta[N];
map<int, int> c;
void dfs2(int u, int fa, int sign) {
    int cnt = c[w[u] + d[u] * sign];
    for (auto pr : delta[u]) c[pr.first] += pr.second;
    for (int v : g[u]) {
        if (v == fa) continue;
        dfs2(v, u, sign);
    }
    ans[u] += c[w[u] + d[u] * sign] - cnt;
}

signed main() {
    ios::sync_with_stdio(0);
    // Don't stop. Don't hide. Follow the light, and you'll find tomorrow. 
    cin >> n >> m;
    for (int i = 1, u, v;i < n;i++) {
        cin >> u >> v;
        g[u].push_back(v), g[v].push_back(u);
    }
    for (int i = 1;i <= n;i++) cin >> w[i];
    lca_init();
    for (int i = 1;i <= m;i++) {
        cin >> p[i].u >> p[i].v;
        p[i].l = lca(p[i].u, p[i].v);
    }

    for (int i = 1;i <= m;i++) {
        int u = p[i].u, l = p[i].l;
        delta[u].push_back({ d[u], +1 });
        delta[fa[l][0]].push_back({ d[u], -1 });
    }
    dfs2(1, 0, +1);

    for (int i = 1;i <= n;i++) delta[i].clear();
    c.clear();
    for (int i = 1;i <= m;i++) {
        int u = p[i].u, v = p[i].v, l = p[i].l;
        delta[v].push_back({ d[u] - 2 * d[l], +1 });
        delta[l].push_back({ d[u] - 2 * d[l], -1 });
    }
    dfs2(1, 0, -1);

    for (int i = 1;i <= n;i++) cout << ans[i] << " \n"[i == n];

    return 0;
}

(未完待续)

posted @ 2023-10-14 19:06  蒟蒻OIer-zaochen  阅读(91)  评论(1编辑  收藏  举报