树链剖分学习笔记
本文最后更新于 2025年5月17日 下午
注:本文结合了一些 Drest 和 Targanzqq 的笔记和讲解内容,在此感激不尽.
0 一些前置芝士
- 线段树
1 什么是树链剖分
在大多数情况下,我们所说的"树链剖分"通常都是指的重链剖分.
众所周知,在处理序列问题上,我们有很多种方法,比如线段树,树状数组,平衡树…
那么,如何处理树上问题呢?
一种思路是将一棵树拍成一个序列(如dfs序),然后对序列进行操作.而树链剖分也运用了类似这样的思想.
树链剖分的大体思路就是把一棵树剖成若干条重链,然后根据性质用线段树(也可以用其他数据结构,如树状数组)维护.
2 一些定义
-
重儿子:一个结点的所有儿子中子树结点最多(子树大小最大)的儿子
-
轻儿子:与重儿子相对而言,一个结点所有儿子除了重儿子之外的都是轻儿子
-
重边:一个结点与其重儿子所连的边
-
重链:由若干条重边连接起来的链
同时,单个的轻儿子也可算作一条重链
如图,蓝色的节点就是重儿子.
图中有4条重链,分别是1->2->4->8 , 3->6 ,7和5.

3 一些性质
树链剖分的实现十分依赖重链的性质,因为这牵扯到复杂度分析.
树链剖分有两个最常用的性质,分别是:
-
每条重链的顶端一定是一个轻儿子
-
每个轻儿子一定是一条重链的顶端
-
树上任意两点之间的唯一确定路径一定可以被分为不超过 条重链.
下面分别证明这两个性质:
-
假设存在一条重链,使得其顶端为重儿子,那么这个重儿子必然还有一条连向其父亲节点的重边,因此就不是重链的顶端,与题设矛盾,所以每条重链的顶端一定是一个轻儿子.
-
假设存在一个轻儿子,使得这个轻儿子不是一条重链的顶端,那么其父亲节点一定有一条重链连向这个轻儿子,所以这个节点是重儿子,与题设矛盾,所以一个轻儿子一定是一条重链的顶端.
-
待证
在确定每个点的重儿子之后,我们对整棵树进行遍历,先遍历每个节点的重儿子,再遍历每个节点的轻儿子.
然后,我们根据遍历的顺却重新确定每个节点的编号,如下图.

然后,我们惊奇的发现了这个编号的两个性质:
-
任意一条重链上的所有节点编号是连续的
-
任意一个子树内的所有节点编号是连续的
由于这两个性质,我们可以将这棵树按照这个编号拍成一个序列,然后用数据结构维护这个序列,使得实现我们想要的功能.
4 如何实现
注: 下面的所有内容均以树链剖分模板题 P3384 为例
上面的部分都是理论基础,下面我们来讲讲如何代码实现.
大体的思路是先处理出来这棵树的各种信息,然后转化为序列再用数据结构(这里采用线段树)维护.
4.0 线段树
线段树代码如下:
1 | |
4.1 预处理信息
首先,我们需要通过两次dfs来处理下列信息:
-
:表示i在树中的深度
-
:表示i的子树大小
-
:表示i的父亲节点
-
:表示i的重儿子
-
:表示i所在重链的顶端
-
:表示i节点的新编号
-
:表示重新编号后的i的节点的值,令 表示重新编号前的i的节点的值,则有
第一次dfs处理前4个信息,可以直接通过记录 来处理这4个信息.
1 | |
第二次dfs处理后3个信息,我们可以通过记录当前点和当前点所在重链的顶端来进行dfs.
1 | |
处理完信息后,我们就可以开始考虑如何维护这四种操作:
-
1 x y z,表示将树从 到 结点最短路径上所有节点的值都加上 。 -
2 x y,表示求树从 到 结点最短路径上所有节点的值之和。 -
3 x z,表示将以 为根节点的子树内所有节点值都加上 。 -
4 x表示求以 为根节点的子树内所有节点值之和
也就是维护路径和,路径修改,子树和与子树修改.
4.2 路径操作
以查询操作为例,当 两点不在一条重链上时,我们要将两个点都跳到一条重链上,并且由于每条重链上的点的编号都是连续的,我们可以直接记录每条跳过的重链的值的和.
当两个点到一条重链上时,可以直接记录两点之间的值的和.
修改操作同理,将代码中的记录操作改成修改操作即可.
示例代码如下:
1 | |
4.3 子树操作
子树操作相对而言简单很多.
由于每个子树在序列中都是连续的,长度为 的一段,我们可以直接进行操作.
代码如下:
1 | |
4.4 主函数部分:
主函数就非常简单了.
代码如下:
1 | |
4.5 完整代码
1 | |
4.6 时间复杂度分析
前面的两次dfs,由于是每个点遍历一次,所以时间复杂度
每次对于进行路径的操作,在两个点跳到同一条重链上的过程中,由于重链的性质,一定不会跳超过 次.又因为每次跳的时候又会进行一次线段树的操作,时间复杂度 ,所以每次进行路径操作的时间复杂度是 .
对于每次的子树操作,只需要进行一次线段树的操作,所以时间复杂度
由于需要进行 次操作,所以总时间复杂度为
5 后记
好长。