题解:洛谷 P3384 重链剖分

【题目来源】

洛谷:P3384 【模板】重链剖分 / 树链剖分

【题目描述】

如题,已知一棵包含 \(N\) 个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:

  • 1 x y z,表示将树从 \(x\)\(y\) 结点最短路径上所有节点的值都加上 \(z\)

  • 2 x y,表示求树从 \(x\)\(y\) 结点最短路径上所有节点的值之和。

  • 3 x z,表示将以 \(x\) 为根节点的子树内所有节点值都加上 \(z\)

  • 4 x,表示求以 \(x\) 为根节点的子树内所有节点值之和。

【输入】

第一行包含 \(4\) 个正整数 \(N,M,R,P\),分别表示树的结点个数、操作个数、根节点序号和取模数(即所有的输出结果均对此取模)。

接下来一行包含 \(N\) 个非负整数,分别依次表示各个节点上初始的数值。

接下来 \(N-1\) 行每行包含两个整数 \(x,y\),表示点 \(x\) 和点 \(y\) 之间连有一条边(保证无环且连通)。

接下来 \(M\) 行每行包含若干个正整数,每行表示一个操作。

【输出】

输出包含若干行,分别依次表示每个操作 \(2\) 或操作 \(4\) 所得的结果(\(P\) 取模)。

【输入样例】

5 5 2 24
7 3 7 8 0
1 2
1 5
3 1
4 1
3 4 2
3 2 2
4 5
1 5 1 3
2 1 3

【输出样例】

2
21

【核心思想】

  1. 问题分析:给定一棵包含 \(N\) 个节点的有根树,每个节点有权值,需要支持四种操作:路径加、路径求和、子树加、子树求和,所有结果对 \(P\) 取模。由于树的结构复杂,直接处理路径和子树操作非常低效,需要一种数据结构能将树上的路径和子树转化为连续的区间,从而利用线段树等区间数据结构高效处理。这就是树链剖分(Heavy-Light Decomposition, HLD) 的经典应用场景。

  2. 算法选择

    • 树链剖分:通过轻重链剖分,将树分解成若干条链,使得每个节点被分配一个连续的 DFS 序(dfn),且满足:
      • 任意一条路径可以被分解为 \(O(\log N)\) 条重链上的连续区间。
      • 任意一个子树对应一个连续的区间 [dfn[x], dfn[x] + sz[x] - 1]
    • 线段树:维护剖分后的序列(dfn 序),支持区间加和区间求和,配合懒标记实现 \(O(\log N)\) 的区间操作。
    • 操作映射
      • 路径加/求和:将路径 \((u, v)\) 分解为若干条链,对每条链的连续区间进行线段树操作。
      • 子树加/求和:直接对 [dfn[x], dfn[x] + sz[x] - 1] 进行线段树操作。
  3. 关键步骤

    • 第一次 DFS(dfs1:从根节点 \(R\) 开始,计算每个节点的父节点 fa[u]、深度 dep[u]、子树大小 sz[u],并找出重儿子 son[u](子树最大的儿子)。
    • 第二次 DFS(dfs2:按重儿子优先的顺序分配 DFS 序 id[u](即剖分后的新编号),记录每个节点所在链的顶端 top[u],并将节点权值按新编号存入数组 nw
    • 建线段树:用 nw 数组构建线段树,维护区间和与懒标记。
    • 路径操作(update_path / query_path
      • top[u] != top[v] 时,选择链顶深度较大的节点,将 [id[top[u]], id[u]] 这一段(链顶到 u)在线段树上操作,然后令 u = fa[top[u]] 跳到上一条链。
      • top[u] == top[v] 时,处理剩余同一链上的区间(深度较小的在前)。
    • 子树操作(update_tree / query_tree:直接使用区间 [id[x], id[x] + sz[x] - 1]
    • 所有结果对 \(P\) 取模输出。
  4. 时间/空间复杂度

    • 时间复杂度:预处理 \(O(N)\),每次路径操作 \(O(\log^2 N)\)(实际上为 \(O(\log N)\) 条链,每条链线段树操作 \(O(\log N)\)),子树操作 \(O(\log N)\)。总复杂度 \(O((N+M)\log^2 N)\)\(O((N+M)\log N)\)(取决于实现,通常可接受)。
    • 空间复杂度:\(O(N)\)
  5. 树链剖分的核心思想

    • 将树上问题转化为序列问题:利用轻重链剖分,使重链上的节点编号连续,从而将路径拆分为若干连续区间,用线段树等数据结构高效维护。
    • 轻重链的性质:每个节点到根的路径上最多经过 \(O(\log N)\) 条轻边,因此任何路径最多分解为 \(O(\log N)\) 条重链,保证了操作的高效性。
    • 线段树的辅助:区间加和区间求和是线段树的基本操作,配合懒标记实现 \(O(\log N)\) 的修改和查询。
    • 适用场景:适用于需要频繁处理树上路径和子树的修改/查询问题,是解决这类问题的标准模板。

【算法标签】

提高+ #树链剖分

【代码详解】

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

#define int long long                     // 使用 long long 防止溢出

const int N = 100005;

#define lc u << 1                         // 线段树左孩子
#define rc u << 1 | 1                     // 线段树右孩子

int n, m, r, p;                           // n: 节点数, m: 操作数, r: 根, p: 取模数
int w[N];                                 // 每个节点的初始权值
vector<int> g[N];                         // 邻接表存储树

int fa[N], dep[N], sz[N], son[N];         // 父节点, 深度, 子树大小, 重儿子
int top[N], id[N], nw[N], cnt;            // 链顶, dfs序编号, 新编号对应权值, 时间戳

// 线段树节点结构
struct tree
{
    int l, r;                             // 区间左右端点
    int add, sum;                         // 懒标记, 区间和
} tr[N * 4];

// 第一次 DFS: 预处理父节点、深度、子树大小、重儿子
void dfs1(int u, int father)
{
    fa[u] = father;
    dep[u] = dep[father] + 1;
    sz[u] = 1;
    for (int v : g[u])
    {
        if (v == father) continue;
        dfs1(v, u);
        sz[u] += sz[v];
        if (sz[son[u]] < sz[v])           // 更新重儿子(子树最大的儿子)
            son[u] = v;
    }
}

// 第二次 DFS: 树链剖分,分配 dfs 序,确定每条链的顶端
void dfs2(int u, int t)
{
    top[u] = t;                           // 当前节点所在链的顶端
    id[u] = ++cnt;                        // 分配 dfs 序(剖分后的新编号)
    nw[cnt] = w[u];                       // 新编号对应的权值
    if (!son[u]) return;                  // 叶子节点无重儿子
    dfs2(son[u], t);                      // 优先处理重儿子,延续当前链
    for (int v : g[u])
    {
        if (v == fa[u] || v == son[u]) continue;
        dfs2(v, v);                       // 轻儿子单独开一条链
    }
}

// 线段树: 向上更新父节点区间和
void pushup(int u)
{
    tr[u].sum = tr[lc].sum + tr[rc].sum;
}

// 线段树: 下传懒标记
void pushdown(int u)
{
    if (tr[u].add)
    {
        tr[lc].sum += tr[u].add * (tr[lc].r - tr[lc].l + 1);
        tr[rc].sum += tr[u].add * (tr[rc].r - tr[rc].l + 1);
        tr[lc].add += tr[u].add;
        tr[rc].add += tr[u].add;
        tr[u].add = 0;
    }
}

// 线段树: 建树
void build(int u, int l, int r)
{
    tr[u] = {l, r, 0, nw[r]};             // 初始化,叶子节点 sum = nw[l](此处 nw[r] 与 nw[l] 等价)
    if (l == r) return;
    int mid = (l + r) >> 1;
    build(lc, l, mid);
    build(rc, mid + 1, r);
    pushup(u);
}

// 线段树: 区间加
void update(int u, int l, int r, int k)
{
    if (l <= tr[u].l && tr[u].r <= r)     // 完全覆盖
    {
        tr[u].add += k;
        tr[u].sum += k * (tr[u].r - tr[u].l + 1);
        return;
    }
    pushdown(u);
    int mid = (tr[u].l + tr[u].r) >> 1;
    if (l <= mid) update(lc, l, r, k);
    if (r > mid)  update(rc, l, r, k);
    pushup(u);
}

// 操作1: 将路径 x -> y 上所有节点值加 z
void update_path(int u, int v, int k)
{
    while (top[u] != top[v])              // 不在同一条链上
    {
        if (dep[top[u]] < dep[top[v]]) swap(u, v);
        update(1, id[top[u]], id[u], k);  // 更新当前链的顶端到 u 这一段
        u = fa[top[u]];                   // 跳到链顶的父亲
    }
    // 此时在同一条链上
    if (dep[u] < dep[v]) swap(u, v);
    update(1, id[v], id[u], k);
}

// 操作3: 将以 x 为根的子树内所有节点值加 z
void update_tree(int u, int k)
{
    update(1, id[u], id[u] + sz[u] - 1, k);
}

// 线段树: 区间查询和
int query(int u, int l, int r)
{
    if (l <= tr[u].l && tr[u].r <= r) return tr[u].sum;
    pushdown(u);
    int mid = (tr[u].l + tr[u].r) >> 1;
    int res = 0;
    if (l <= mid) res += query(lc, l, r);
    if (r > mid)  res += query(rc, l, r);
    return res;
}

// 操作2: 查询路径 x -> y 上所有节点值之和
int query_path(int u, int v)
{
    int res = 0;
    while (top[u] != top[v])
    {
        if (dep[top[u]] < dep[top[v]]) swap(u, v);
        res += query(1, id[top[u]], id[u]);
        u = fa[top[u]];
    }
    if (dep[u] < dep[v]) swap(u, v);
    res += query(1, id[v], id[u]);
    return res;
}

// 操作4: 查询以 x 为根的子树内所有节点值之和
int query_tree(int u)
{
    return query(1, id[u], id[u] + sz[u] - 1);
}

signed main()
{
    cin >> n >> m >> r >> p;              // 输入 N, M, R, P
    for (int i = 1; i <= n; i++)
        cin >> w[i];                      // 输入每个节点的初始权值

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

    dfs1(r, 0);                           // 预处理父节点、深度、重儿子
    dfs2(r, r);                           // 树链剖分,分配 dfs 序
    build(1, 1, n);                       // 建线段树

    while (m--)
    {
        int op, x, y, z;
        cin >> op;
        if (op == 1)                      // 1 x y z:路径加
        {
            cin >> x >> y >> z;
            update_path(x, y, z);
        }
        else if (op == 2)                 // 2 x y:路径求和
        {
            cin >> x >> y;
            cout << query_path(x, y) % p << endl;
        }
        else if (op == 3)                 // 3 x z:子树加
        {
            cin >> x >> z;
            update_tree(x, z);
        }
        else                              // 4 x:子树求和
        {
            cin >> x;
            cout << query_tree(x) % p << endl;
        }
    }

    return 0;
}

【运行结果】

5 5 2 24
7 3 7 8 0
1 2
1 5
3 1
4 1
3 4 2
3 2 2
4 5
1 5 1 3
2 1 3
2
21
posted @ 2026-09-02 10:53  团爸讲算法  阅读(20)  评论(0)    收藏  举报