【树论】树链剖分

一、为什么需要树链剖分?

1.1 问题背景

在树上,我们经常需要处理以下类型的操作:

  • 将节点 uv 的路径上的所有点权 + x
  • 查询节点 uv 的路径上所有点权的最大值
  • 将以 u 为根的子树所有节点权值 + x
  • 查询以 u 为根的子树的所有节点权值和

这些问题如果用 LCA + 暴力遍历路径,单次操作复杂度为 O(n),效率极低。

1.2 解决思路

树链剖分的核心思想是:

把树“拉直”成一条线,然后用线段树维护这条线,从而将树上路径操作转化为区间操作。

但直接“拉直”会丢失结构信息。树链剖分通过一种有规则的 DFS 序,保证了:任意两点间的路径,最多由 O(log n) 条连续的链组成


二、核心概念与定义

2.1 重儿子(Heavy Child)

  • 对于一个非叶子节点 u,其子树大小最大的子节点称为 u重儿子
  • 如果有多个子树大小相同,任选一个即可。

2.2 轻儿子(Light Child)

  • 除了重儿子之外的所有子节点都是轻儿子。

2.3 重边(Heavy Edge)

  • 父节点与其重儿子之间的边。

2.4 轻边(Light Edge)

  • 父节点与其轻儿子之间的边。

2.5 重链(Heavy Path)

  • 由重边连接形成的极大路径。
  • 每条重链的起点是轻儿子或根节点,终点是叶子节点或重儿子的轻儿子

关键性质:从任意节点到根的路径上,最多经过 O(log n) 条轻边,因此最多经过 O(log n) 条重链。


三、树链剖分的两个 DFS

3.1 第一次 DFS:计算子树大小、深度、父节点、重儿子

void dfs1(int u, int parent, int depth) {
    dep[u] = depth;
    fa[u] = parent;
    sz[u] = 1;  // 子树大小
    hson[u] = 0; // 重儿子初始化为 0

    for (int v : tree[u]) {
        if (v == parent) continue;
        dfs1(v, u, depth + 1);
        sz[u] += sz[v];
        if (sz[v] > sz[hson[u]]) {
            hson[u] = v; // 更新重儿子
        }
    }
}

3.2 第二次 DFS:生成 DFS 序、分配链顶、构建线性序列

int dfn = 0; // DFS 序计数器
int top[MAXN]; // top[u] 表示 u 所在重链的顶部节点
int id[MAXN];  // id[u] 表示 u 在线段树中的位置(新编号)

void dfs2(int u, int tp) {
    id[u] = ++dfn;     // 分配新编号
    top[u] = tp;       // 记录链顶

    if (hson[u]) {
        dfs2(hson[u], tp); // 重儿子继承当前链顶
    }

    for (int v : tree[u]) {
        if (v == fa[u] || v == hson[u]) continue;
        dfs2(v, v); // 轻儿子开启新链,链顶是自己
    }
}

🌟 重点:重儿子被优先访问,保证了同一条重链上的节点在线段树中是连续的


四、树链剖分的应用:路径操作

4.1 路径查询/修改的核心思想

uv 的路径拆成若干段重链,每段在 DFS 序上是连续的,可以用线段树区间操作。

4.2 核心函数:跳链过程

// 查询 u 到 v 路径上的最大值
int query_path(int u, int v) {
    int res = -INF;
    while (top[u] != top[v]) {
        if (dep[top[u]] < dep[top[v]]) swap(u, v);
        // u 的链顶更深,先处理 u 所在链
        res = max(res, seg_tree.query(id[top[u]], id[u]));
        u = fa[top[u]]; // 跳到链顶的父节点(轻边)
    }
    // 现在 u 和 v 在同一条链上
    if (id[u] > id[v]) swap(u, v);
    res = max(res, seg_tree.query(id[u], id[v]));
    return res;
}

4.3 子树操作

子树操作更简单!因为子树的 DFS 序是连续的

// 查询 u 的子树和
int query_subtree(int u) {
    return seg_tree.query(id[u], id[u] + sz[u] - 1);
}

五、C++ 实现(支持路径加、路径最大值查询)

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

const int MAXN = 1e5 + 5;
const int INF = 0x3f3f3f3f;

// ======== 树结构 ========
int n, m, r;
vector<int> tree[MAXN];
int w[MAXN]; // 初始点权

// ======== 第一次 DFS 数据 ========
int dep[MAXN], fa[MAXN], sz[MAXN], hson[MAXN];

// ======== 第二次 DFS 数据 ========
int dfn = 0;
int id[MAXN], top[MAXN];

// ======== 线段树 ========
struct Node {
    int l, r;
    int sum, max_val;
    int lazy;
} seg_tree[4 * MAXN];

void push_up(int u) {
    seg_tree[u].sum = seg_tree[u*2].sum + seg_tree[u*2+1].sum;
    seg_tree[u].max_val = max(seg_tree[u*2].max_val, seg_tree[u*2+1].max_val);
}

void push_down(int u) {
    if (seg_tree[u].lazy) {
        int lz = seg_tree[u].lazy;
        seg_tree[u*2].sum += lz * (seg_tree[u*2].r - seg_tree[u*2].l + 1);
        seg_tree[u*2+1].sum += lz * (seg_tree[u*2+1].r - seg_tree[u*2+1].l + 1);
        seg_tree[u*2].max_val += lz;
        seg_tree[u*2+1].max_val += lz;
        seg_tree[u*2].lazy += lz;
        seg_tree[u*2+1].lazy += lz;
        seg_tree[u].lazy = 0;
    }
}

void build(int u, int l, int r) {
    seg_tree[u] = {l, r, 0, -INF, 0};
    if (l == r) {
        seg_tree[u].sum = seg_tree[u].max_val = w[r]; // w 是原点权
        return;
    }
    int mid = (l + r) >> 1;
    build(u*2, l, mid);
    build(u*2+1, mid+1, r);
    push_up(u);
}

void modify(int u, int l, int r, int val) {
    if (seg_tree[u].l >= l && seg_tree[u].r <= r) {
        seg_tree[u].sum += val * (seg_tree[u].r - seg_tree[u].l + 1);
        seg_tree[u].max_val += val;
        seg_tree[u].lazy += val;
        return;
    }
    push_down(u);
    int mid = (seg_tree[u].l + seg_tree[u].r) >> 1;
    if (l <= mid) modify(u*2, l, r, val);
    if (r > mid) modify(u*2+1, l, r, val);
    push_up(u);
}

int query_sum(int u, int l, int r) {
    if (seg_tree[u].l >= l && seg_tree[u].r <= r) {
        return seg_tree[u].sum;
    }
    push_down(u);
    int mid = (seg_tree[u].l + seg_tree[u].r) >> 1;
    int res = 0;
    if (l <= mid) res += query_sum(u*2, l, r);
    if (r > mid) res += query_sum(u*2+1, l, r);
    return res;
}

int query_max(int u, int l, int r) {
    if (seg_tree[u].l >= l && seg_tree[u].r <= r) {
        return seg_tree[u].max_val;
    }
    push_down(u);
    int mid = (seg_tree[u].l + seg_tree[u].r) >> 1;
    int res = -INF;
    if (l <= mid) res = max(res, query_max(u*2, l, r));
    if (r > mid) res = max(res, query_max(u*2+1, l, r));
    return res;
}

// ======== 树链剖分 DFS ========
void dfs1(int u, int parent, int depth) {
    dep[u] = depth;
    fa[u] = parent;
    sz[u] = 1;
    hson[u] = 0;

    for (int v : tree[u]) {
        if (v == parent) continue;
        dfs1(v, u, depth + 1);
        sz[u] += sz[v];
        if (sz[v] > sz[hson[u]]) {
            hson[u] = v;
        }
    }
}

void dfs2(int u, int tp) {
    id[u] = ++dfn;
    top[u] = tp;

    if (hson[u]) {
        dfs2(hson[u], tp); // 重儿子继承链顶
    }
    for (int v : tree[u]) {
        if (v == fa[u] || v == hson[u]) continue;
        dfs2(v, v); // 轻儿子开启新链
    }
}

// ======== 路径操作接口 ========
void modify_path(int u, int v, int val) {
    while (top[u] != top[v]) {
        if (dep[top[u]] < dep[top[v]]) swap(u, v);
        modify(1, id[top[u]], id[u], val);
        u = fa[top[u]];
    }
    if (id[u] > id[v]) swap(u, v);
    modify(1, id[u], id[v], val);
}

int query_path_max(int u, int v) {
    int res = -INF;
    while (top[u] != top[v]) {
        if (dep[top[u]] < dep[top[v]]) swap(u, v);
        res = max(res, query_max(1, id[top[u]], id[u]));
        u = fa[top[u]];
    }
    if (id[u] > id[v]) swap(u, v);
    res = max(res, query_max(1, id[u], id[v]));
    return res;
}

// ======== 子树操作接口 ========
void modify_subtree(int u, int val) {
    modify(1, id[u], id[u] + sz[u] - 1, val);
}

int query_subtree_sum(int u) {
    return query_sum(1, id[u], id[u] + sz[u] - 1);
}

// ======== 主函数 ========
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);

    cin >> n >> m >> r;
    for (int i = 1; i <= n; ++i) {
        cin >> w[i];
    }

    for (int i = 1; i <= n - 1; ++i) {
        int u, v;
        cin >> u >> v;
        tree[u].push_back(v);
        tree[v].push_back(u);
    }

    // 树链剖分预处理
    dfs1(r, 0, 1);
    dfs2(r, r);
    build(1, 1, n);

    // 处理 m 个操作
    while (m--) {
        int op, x, y, z;
        cin >> op;
        if (op == 1) { // 路径加
            cin >> x >> y >> z;
            modify_path(x, y, z);
        } else if (op == 2) { // 路径最大值
            cin >> x >> y;
            cout << query_path_max(x, y) << '\n';
        } else if (op == 3) { // 子树加
            cin >> x >> z;
            modify_subtree(x, z);
        } else if (op == 4) { // 子树和
            cin >> x;
            cout << query_subtree_sum(x) << '\n';
        }
    }

    return 0;
}

六、复杂度分析

操作时间复杂度说明
预处理(两次 DFS)O(n)
路径修改/查询O(log² n)每条链 O(log n),最多 O(log n) 条链
子树修改/查询O(log n)连续区间,一次线段树操作

路径操作 O(log² n) 是树链剖分的标准复杂度。


七、核心性质与优势

  1. 路径拆分高效:任意路径最多被拆分为 O(log n) 段重链。
  2. 支持多种操作:区间加、区间最值、区间和、子树操作等。
  3. 可扩展性强:可结合懒标记、多种线段树形态。
  4. 代码模式固定:两次 DFS + 跳链循环,易于模板化。

八、注意事项

  • hson[u] 初始化为 0,表示无重儿子。
  • top[u]dfs2 中赋值。
  • 线段树要支持区间修改(懒标记)。
  • 输入输出较大时使用 ios::sync_with_stdio(false)

九、总结

树链剖分是将树的路径问题转化为线性区间问题的强大工具。其核心在于:

  1. 重轻儿子划分 → 形成重链
  2. 两次 DFS → 构建 DFS 序与链顶
  3. 跳链循环 → 将路径拆为 O(log n) 段
  4. 线段树维护 → 高效区间操作
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值