本文内容部分参考这篇博客,但图片,代码均为原创
树链剖分
定义
树链剖分是一种将树按轻重边 划分成若干条链,使任意两点路径可在 \(O(log\,n)\) 条链内分解,并可用线段树等结构维护路径或子树的算法。 它通过两次 DFS 求出重儿子、链顶、DFS 序等信息,从而把树上操作转化为序列区间操作,实现路径查询、路径修改、子树查询等高效处理。
核心思想
树链剖分(通常指重链剖分)通过以下方式将树结构线性化:
- 重儿子:子树大小最大的儿子。
- 重边:连接父节点和重儿子的边。
- 重链:由连续重边构成的链。
- 轻边/轻链:非重边构成的链。
关键性质:
- 每个节点最多只有一个重儿子。
- 从根到任意节点所经过的轻链 数量不超过 \(log\,n\),详见 oi-wiki。
- 重链在 DFS序 中是连续区间,可以用线段树维护。

实现方法
上面讲了一些废话,现在我们进入正题,如何实现树链剖分。
1.变量声明
为了实现树链剖分,我们需要声明下列变量(这里以树链剖分模板题为例)
cpp
int n,m,r,p,a[N];
//n:节点数,m:问题数,r:根节点,a数组:初始的点权
vector<int>e[N];
//vector模拟邻接表存图
int d[N],f[N],siz[N],son[N];
//d数组:节点的深度,f数组:节点的父亲,siz数组:以某节点为根的子树的大小,son数组:某节点的重儿子
int id[N],w[N],top[N],times;
//id数组:类似dfs序,w数组:dfs序所对应的节点的权值,top数组:某条链的开头,times:时间戳
struct tree{
int l,r;
ll sum,lazy;
}t[N<<2];
//线段树基操
2.函数:dfs1
dfs1要处理 \(4\) 件事情
- 初始化每个点的深度 \(di\)
- 初始化每个点的父亲 \(fi\)
- 处理每个非叶子节点的子树大小
- 处理每个非叶子结点的重儿子编号
cpp
inline void dfs1(int u,int fa){
d[u]=d[fa]+1;
f[u]=fa;
siz[u]=1;
int maxson=-1;
for(auto v:e[u]){
if (v==fa)continue;
dfs1(v,u);
siz[u]+=siz[v];
if (siz[v]>maxson){
son[u]=v;
maxson=siz[v];
}
}
}
3.函数 dfs2
dfs2要处理 \(4\) 件事情
- 标记每个节点的新编号 id
- 初始化每个新编号的权值 w
- 处理每个点所在链的顶端 top
- 处理每一条链
cpp
inline void dfs2(int u,int topf){
id[u]=++times;
w[times]=a[u];
top[u]=topf;
if (!son[u])return;
dfs2(son[u],topf);
for(auto v:e[u]){
if (v==f[u] or v==son[u])continue;
dfs2(v,v);//每一个轻儿子都有一条从他自己开始的链
}
}
//处理顺序,先重再轻
解释 :处理顺序为什么是先重再轻呢?
我们所需的新编号应该是树上的一段连续的区间,处理顺序先重再轻可以确保每一条重链的新编号是连续的,又因为是 dfs 深度优先搜索,所以每一个子树的新编号也是连续的。

图片中蓝色数字即为该节点的新编号。
4.建线段树
最基础的"区间修改,区间查询的线段树",就不详细解释了,不会的参考这里
5.查询与修改
cpp
//两点之间的路径权值修改
inline void range_add1(int x,int y,int val){
val%=p;
while(top[x]!=top[y]){
if (d[top[x]]<d[top[y]])swap(x,y);//让x成为深度更深的节点
update(1,id[top[x]],id[x],val);//让当前位置x,至x所属的链的top这个区间加上val
x=f[top[x]];//跳到这条链的头部的父亲,即跳出这条链
}
//此时x,y在同一条链上,在x至y的区间这个区间加上val
if (d[x]>d[y])swap(x,y);//x深度更小,y深度更大
update(1,id[x],id[y],val);
}
cpp
//以某点为根的子树的权值修改
inline void range_add2(int x,int val){
val%=p;
//id 编号表示树上的一段区间,在x的子树的这段区间进行区间修改
update(1,id[x],id[x]+siz[x]-1,val);
}
cpp
//两点之间路径权值查询
inline ll range_ask1(int x,int y){
ll ans=0;
while(top[x]!=top[y]){
if (d[top[x]]<d[top[y]])swap(x,y);//让x成为深度更深的节点
ans+=query(1,id[top[x]],id[x]);//加上从当前位置x,至x所属的链的top这个区间的权值
ans%=p;
x=f[top[x]];//跳到这条链的头部的父亲,即跳出这条链
}
//此时x,y在同一条链上,加上x至y的区间的权值
if (d[x]>d[y])swap(x,y);
ans+=query(1,id[x],id[y]);
ans%=p;
return ans;
}
cpp
//以某点为根的子树的权值查询
inline ll range_ask2(int x){
ll ans=0;
//id 编号表示树上的一段区间,在x的子树的这段区间进行区间查询
ans+=query(1,id[x],id[x]+siz[x]-1);
ans%=p;
return ans;
}
例题
完整代码
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
#define ls (rt<<1)
#define rs (rt<<1|1)
#define ll long long
int n,m,r,p,a[N];
vector<int>e[N];
int d[N],f[N],siz[N],son[N];
int id[N],w[N],top[N],times;
struct tree{
int l,r;
ll sum,lazy;
}t[N<<2];
inline void dfs1(int u,int fa){
d[u]=d[fa]+1;
f[u]=fa;
siz[u]=1;
int maxson=-1;
for(auto v:e[u]){
if (v==fa)continue;
dfs1(v,u);
siz[u]+=siz[v];
if (siz[v]>maxson){
son[u]=v;
maxson=siz[v];
}
}
}
inline void dfs2(int u,int topf){
id[u]=++times;
w[times]=a[u];
top[u]=topf;
if (!son[u])return;
dfs2(son[u],topf);
for(auto v:e[u]){
if (v==f[u] or v==son[u])continue;
dfs2(v,v);
}
}
inline void pushup(int rt){
t[rt].sum=t[ls].sum+t[rs].sum;
}
inline void pushdown(int rt){
if (t[rt].lazy){
ll lz=t[rt].lazy;
t[rt].lazy=0;
t[ls].lazy+=lz;
t[rs].lazy+=lz;
t[ls].sum+=lz*(t[ls].r-t[ls].l+1);
t[rs].sum+=lz*(t[rs].r-t[rs].l+1);
t[ls].lazy%=p,t[rs].lazy%=p;
t[ls].sum%=p,t[rs].sum%=p;
}
}
inline void build(int rt,int l,int r){
t[rt].l=l,t[rt].r=r;
if (l==r){
t[rt].sum=w[l];
return;
}
int mid=(l+r)>>1;
build(ls,l,mid);
build(rs,mid+1,r);
pushup(rt);
}
inline void update(int rt,int l,int r,int val){
if (l<=t[rt].l and t[rt].r<=r){
t[rt].lazy+=val;
t[rt].sum+=val*(t[rt].r-t[rt].l+1);
t[rt].lazy%=p;
t[rt].sum%=p;
return;
}
pushdown(rt);
int mid=(t[rt].l+t[rt].r)>>1;
if (l<=mid)update(ls,l,r,val);
if (mid<r)update(rs,l,r,val);
pushup(rt);
}
inline ll query(int rt,int l,int r){
if (l<=t[rt].l and t[rt].r<=r){
return t[rt].sum;
}
pushdown(rt);
int mid=(t[rt].l+t[rt].r)>>1;
ll ans=0;
if (l<=mid)ans+=query(ls,l,r);
if (mid<r)ans+=query(rs,l,r);
return ans;
}
inline void range_add1(int x,int y,int val){
val%=p;
while(top[x]!=top[y]){
if (d[top[x]]<d[top[y]])swap(x,y);//让x成为深度更深的节点
update(1,id[top[x]],id[x],val);//让当前位置x,至x所属的链的top这个区间加上val
x=f[top[x]];//跳到这条链的头部的父亲,即跳出这条链
}
//此时x,y在同一条链上,在x至y的区间这个区间加上val
if (d[x]>d[y])swap(x,y);//x深度更小,y深度更大
update(1,id[x],id[y],val);
}
inline void range_add2(int x,int val){
val%=p;
update(1,id[x],id[x]+siz[x]-1,val);//id 编号表示树上的一段区间,在x的子树的这段区间进行区间修改
}
inline ll range_ask1(int x,int y){
ll ans=0;
while(top[x]!=top[y]){
if (d[top[x]]<d[top[y]])swap(x,y);//让x成为深度更深的节点
ans+=query(1,id[top[x]],id[x]);//加上从当前位置x,至x所属的链的top这个区间的权值
ans%=p;
x=f[top[x]];//跳到这条链的头部的父亲,即跳出这条链
}
//此时x,y在同一条链上,加上x至y的区间的权值
if (d[x]>d[y])swap(x,y);
ans+=query(1,id[x],id[y]);
ans%=p;
return ans;
}
inline ll range_ask2(int x){
ll ans=0;
ans+=query(1,id[x],id[x]+siz[x]-1);//id 编号表示树上的一段区间,在x的子树的这段区间进行区间查询
ans%=p;
return ans;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>r>>p;
for(int i=1;i<=n;i++){
cin>>a[i];
a[i]%=p;
}
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs1(r,0);
dfs2(r,r);
build(1,1,n);
while(m--){
int op;
cin>>op;
if (op==1){//x到y的路径上的点加z
int x,y,z;
cin>>x>>y>>z;
range_add1(x,y,z);
}
if (op==2){
int x,y;
cin>>x>>y;
cout<<range_ask1(x,y)<<"\n";
}
if (op==3){
int x,y;
cin>>x>>y;
range_add2(x,y);
}
if (op==4){
int x;
cin>>x;
cout<<range_ask2(x)<<"\n";
}
}
return 0;
}
编辑不易,点个推荐再走吧~~