洛谷 P3384:[模板] 重链剖分 / 树链剖分 ← 线段树+链式前向星

【题目来源】
https://www.luogu.com.cn/problem/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

【数据范围】
对于 30% 的数据: 1≤N≤10,1≤M≤10;
对于 70% 的数据: 1≤N≤10^3,1≤M≤10^3;
对于 100% 的数据: 1≤N≤10^5,1≤M≤10^5,1≤R≤N,1≤P≤2^30。所有输入的数均在 int 范围内。

【算法分析】
● 树链剖分(Tree Chain Decomposition,常指重链剖分 Heavy-Light Decomposition)是一种将树结构转化为线性序列的算法技巧,常用于解决树上路径查询与修改‌以及‌子树查询与修改‌这两大类问题。
其主要包括两个分解步骤:
(1)重链剖分‌:将树的边分为"重边"和"轻边"。对于每个非叶子节点,选择其所有子节点中‌子树大小最大‌的那一个,连接这两点的边即为重边,连接到其他子节点的边则为轻边。由重边相连形成的路径称为重链。
(2)DFS 序重排‌:通过一次深度优先搜索,优先遍历重儿子,从而保证‌每条重链上的节点在新的 DFS 序中是连续存储的‌。

● 树链剖分的核心思想是通过两次 DFS 对树进行剖分,将树分解为若干条"重链",并重新安排节点的访问顺序(DFS 序),使得每条重链上的节点在序列中连续存储,同时每个子树内的节点也连续存储。这样,复杂的树形操作就被转化为了对线性序列的区间操作,可以借助线段树、树状数组等数据结构高效地完成。

● 树链剖分的几个重要概念
(1)重儿子 (Heavy Son)‌:对于树中的一个非叶子节点,其所有子节点中,‌子树大小(含子树的根的节点数)最大‌的那个子节点,称为该节点的重儿子。如果存在多个子节点子树大小相同,可任意选取其中一个作为重儿子。
(2)轻儿子 (Light Son)‌:非叶子节点的所有子节点中,除了重儿子以外的其他子节点,都称为该节点的轻儿子。
(3)重边 (Heavy Edge)‌:连接一个节点与其重儿子的边,称为重边。
(4)轻边 (Light Edge)‌:连接一个节点与其轻儿子的边,称为轻边。
(5)重链 (Heavy Path)‌:由一系列连续的重边首尾相连形成的路径,称为一条重链。整棵树可以被分解为若干条互不相交的重链。
(6)链头 (Head of Chain / Top)‌:每条重链的起始节点,即这条链上‌深度最浅‌的那个节点,称为该重链的链头。树根通常单独构成一条重链,其本身即为链头。

● 树链剖分的两个 dfs 函数
(1)dfs1 函数。求解数组 dep\[\]、pre\[\]、son\[\]、siz\[\]
int depN; //depx represents depth of node x

int preN; //prex represents parent node of node x

int sonN; //sonx represents heavy child of non-leaf node x

int sizN; //sizx represents number of nodes in the subtree rooted at node x

(2)dfs2 函数。求解数组 top\[\]
int topN; //topx represents head node of the heavy chain where node x is located

● 链式前向星:https://blog.csdn.net/hnjzsyjyj/article/details/139369904
大佬 yxc 指出"链式前向星"就是"多单链表",每条单链表基于"头插法"并用 e\[\]、ne\[\]、h\[\] 、val\[\] 等数组进行模拟创建。其中:
eidx:存储序号为 idx 的边的终点值
neidx:存储序号为 idx 的边指向的边的序号(模拟链表指针)‌
ha:存储头结点 a 指向的边的序号
validx:存储序号为 idx 的边的权值(可选)

● 线段树

https://blog.csdn.net/hnjzsyjyj/article/details/152356965

https://blog.csdn.net/hnjzsyjyj/article/details/152364284

【算法代码】

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
const int N=1e5+5;
const int M=N<<1;
int h[N],e[M],ne[M],idx;
int w[N];
int n,m,root,MOD;

//Tree chain's decomposition array
int pre[N],dep[N],siz[N],son[N]; //dfs1
int top[N],pos[N],rk[N],tot; //dfs2

struct Node {
    int le,ri;
    LL sum,add;
} tr[N*4];

void add(int a,int b) {
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}

//dfs1:Solve for pre,dep,siz,son
void dfs1(int u,int fa) {
    pre[u]=fa;
    dep[u]=dep[fa]+1;
    siz[u]=1;
    for(int i=h[u]; i!=-1; i=ne[i]) {
        int j=e[i];
        if(j==fa) continue;
        dfs1(j,u);
        siz[u]+=siz[j];
        if(siz[j]>siz[son[u]]) son[u]=j;
    }
}

//dfs2:Solve for top[]
void dfs2(int u,int tpx) {
    top[u]=tpx;
    pos[u]=++tot;
    rk[tot]=u;
    if(son[u]) dfs2(son[u],tpx);
    for(int i=h[u]; i!=-1; i=ne[i]) {
        int j=e[i];
        if(j==pre[u] || j==son[u]) continue;
        dfs2(j,j); //thin son starts a new chain
    }
}

//Segment tree
void pushup(int u) {
    tr[u].sum=(tr[u<<1].sum+tr[u<<1|1].sum)%MOD;
}

void pushdown(int u) {
    if(tr[u].add) {
        int ls=u<<1,rs=u<<1|1;
        int lazy=tr[u].add;
        LL lenL=tr[ls].ri-tr[ls].le+1;
        LL lenR=tr[rs].ri-tr[rs].le+1;

        tr[ls].sum=(tr[ls].sum+lazy*lenL)%MOD;
        tr[rs].sum=(tr[rs].sum+lazy*lenR)%MOD;
        tr[ls].add=(tr[ls].add+tr[u].add)%MOD;
        tr[rs].add=(tr[rs].add+tr[u].add)%MOD;
        tr[u].add=0;
    }
}

void build(int u,int le,int ri) {
    tr[u]= {le,ri,0,0};
    if(le==ri) {
        tr[u].sum=w[rk[le]]%MOD;
        return;
    }
    int mid=(le+ri)>>1;
    build(u<<1,le,mid);
    build(u<<1|1,mid+1,ri);
    pushup(u);
}

void modify(int u,int le,int ri,int val) {
    if(tr[u].le>=le && tr[u].ri<=ri) {
        LL v=val;
        int len=tr[u].ri-tr[u].le+1;
        tr[u].sum=(tr[u].sum+v*len)%MOD;
        tr[u].add=(tr[u].add+val)%MOD;
        return;
    }
    pushdown(u);
    int mid=(tr[u].le+tr[u].ri)>>1;
    if(le<=mid) modify(u<<1,le,ri,val);
    if(ri>mid) modify(u<<1|1,le,ri,val);
    pushup(u);
}

LL query(int u,int le,int ri) {
    if(tr[u].le>=le && tr[u].ri<=ri) return tr[u].sum;
    pushdown(u);
    int mid=(tr[u].le+tr[u].ri)>>1;
    LL res=0;
    if(le<=mid) res=(res+query(u<<1,le,ri))%MOD;
    if(ri>mid) res=(res+query(u<<1|1,le,ri))%MOD;
    return res;
}

//Operation 1
void addPath(int x,int y,int z) {
    while(top[x]!=top[y]) {
        if(dep[top[x]]<dep[top[y]]) swap(x,y);
        modify(1,pos[top[x]],pos[x],z);
        x=pre[top[x]];
    }
    if(dep[x]>dep[y]) swap(x,y);
    modify(1,pos[x],pos[y],z);
}

//Operation 2
LL sumPath(int x,int y) {
    LL res=0;
    while(top[x]!=top[y]) {
        if(dep[top[x]]<dep[top[y]]) swap(x,y);
        res=(res+query(1,pos[top[x]],pos[x]))%MOD;
        x=pre[top[x]];
    }
    if(dep[x]>dep[y]) swap(x,y);
    res=(res+query(1,pos[x],pos[y]))%MOD;
    return res;
}

//Operation 3
void addTree(int x,int y) {
    modify(1,pos[x],pos[x]+siz[x]-1,y);
}

//Operation 4
LL sumTree(int x) {
    return query(1,pos[x],pos[x]+siz[x]-1);
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    memset(h,-1,sizeof h);

    cin>>n>>m>>root>>MOD;
    for(int i=1; i<=n; i++) cin>>w[i];
    for(int i=1; i<n; i++) {
        int a,b;
        cin>>a>>b;
        add(a,b),add(b,a);
    }

    dfs1(root,0);
    dfs2(root,root);
    build(1,1,n);

    while(m--) {
        int op,x,y,z;
        cin>>op;
        if(op==1) {
            cin>>x>>y>>z;
            addPath(x,y,z%MOD);
        } else if(op==2) {
            cin>>x>>y;
            cout<<sumPath(x,y)<<"\n";
        } else if(op==3) {
            cin>>x>>y;
            addTree(x,y%MOD);
        } else {
            cin>>x;
            cout<<sumTree(x)<<"\n";
        }
    }
    return 0;
}

/*
in:
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

out:
2
21
*/

【参考文献】

https://blog.csdn.net/hnjzsyjyj/article/details/156956145

https://blog.csdn.net/hnjzsyjyj/article/details/156988831

https://blog.csdn.net/hnjzsyjyj/article/details/152356965

https://blog.csdn.net/hnjzsyjyj/article/details/152364284