本文属于「算法学习」系列文章之一。之前的「数据结构和算法设计」系列着重于基础的数据结构和算法设计课程的学习,与之不同的是,这一系列主要用来记录大学课程范围之外的高级算法学习、优化与应用的全过程,同时也将归纳总结出简洁明了的算法模板,以便记忆和运用。在本系列学习文章中,为了透彻理解算法和代码,本人参考了诸多博客、教程、文档、书籍等资料,由于精力有限,恕不能一一列出,这里只列示重要资料的不完全参考列表:
- 算法竞赛进阶指南,李煜东著,河南电子音像出版社,GitHub Tedukuri社区以及个人题解文章汇总目录
- 算法 第四版 Algorithm Fourth Edition,[美]
Robert Sedgewick, Kevin Wayne著,谢路云译,配套网站
为了方便在PC上运行调试、分享代码,我还建立了相关的仓库。在这一仓库中,你可以看到算法文章、模板代码、应用题目等等。由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏算法学习系列文章目录一文以作备忘。
文章目录
1. 模板问题
先把树链剖分的模板题给出来——已知一棵树,每个结点上包含一个数值,需要设法实现以下操作:
- 操作1:格式
1 x y z,表示将树从x到y结点的最短路径之上,所有结点的值都加上z; - 操作2:格式
2 x y,表示求树中x到y结点的最短路径之上,所有结点的值之和; - 操作3:格式
3 x z,表示将以x为根结点的子树内,所有结点值都加上z; - 操作4:格式
4 x,表示求以x为根结点的子树内,所有结点值之和
这一模板题的具体要求见P3384 【模板】轻重链剖分/树链剖分,本人的题解与代码见这篇文章,完整实现了线段树和树链剖分。
2. DFS序和时间戳
顾名思义,DFS序就是(此处是先根遍历)DFS的顺序。先来个例子,DFS遍历下图的子树,得到的DFS序是 A B D G H I C E J F:

不要把欧拉序和DFS搞混了,欧拉序是:A B D G D H D I D B A C E J E C F C A ,即访问到该结点算一次、返回到该结点算一次。
时间戳就是(此处是先根遍历)DFS第一次访问到每个结点时的“时间”,这一时间是一个从 1 1 1 开始递增的整数。仍以上图为例,分别标出结点对应的时间戳:

DFS序+时间戳的用处在于,它使树具有连续性,使树转变为一个“连续的序列”——我们把树看作数组,时间戳是下标,结点的值分别存储在其时间戳对应的位置。在上图中,“数组”是 arr ,于是 arr[1] = 'A', arr[2] = 'B', arr[3] = 'D', arr[4] = 'G', arr[5] = 'H', arr[6] = 'I', arr[7] = 'C', arr[8] = 'E', arr[9] = 'J', arr[10] = 'F' 。
从示例中,我们可以发现两个重要的性质:
- 一个结点的子树上的结点的时间戳,一定大于结点,且按照DFS序连续。例如,结点
C的子树E, J, F的时间戳为8, 9, 10。 - 某些链(或者说路)上的时间戳也是连续的。例如,
A, B, D, G这条链上的时间戳是连续的,C, E, J这条链上的时间戳也是连续的。
这样一来,我们可以套一个线段树,用线段树的区间修改和区间查询实现操作3和操作4。只是操作1和操作2呢?并不是所有的链都有连续的时间戳——像 A -> B -> D -> G 这条链也可用线段树的区间修改和查询来操作,但像 A -> C -> E -> J 就完全不可行了。这时,我们就需要树链剖分大法了!
3. 树链剖分
顾名思义,树链剖分就是把树拆成若干条不相交的链。
3.1 树链剖分的分类
一般可分为三类:
- 重链剖分——常用,时间复杂度为 O ( log n ) O(\log n) O(logn) ;
- 长链剖分——不常用,时间复杂度为 O ( n ) O(\sqrt{n}) O(n) ;
- 实链剖分——用在Link-Cut Tree中
这里先主要介绍重链剖分。
3.2 重链剖分
相关术语如下:
- 重儿子结点:一个结点的所有儿子结点中,大小最大的那个结点。由于是最重的,所以只有一个。如果有多个儿子大小相等,则随机取一个。
- 轻儿子结点:一个结点除了重儿子结点之外的所有儿子结点,都是轻儿子。特别注意,根结点也是轻儿子。
- 重链:从一个轻儿子结点开始,一路只往重儿子方向走,连出的链即重链。
- 轻链:除了重链之外,全是轻链。
树链剖分之后,再DFS一遍并标出DFS序和时间戳,即可实现题目的四种操作!注意,此处DFS标记时间戳时,我们要优先往重儿子走,轻儿子无所谓。

给出上图为示例,我们实际动手剖一剖:
- 先把重儿子标出来。
A的重儿子是G,G的重儿子是E,E的重儿子随便标个J,C的重儿子是F,F的重儿子是D,D的重儿子是X:

- 再把重链标出来。重链从轻儿子开始,一直往重儿子方向走。根结点
A是轻儿子,于是一直往G, E, J方向走,得到重链A -> G -> E -> J;结点C是轻儿子,于是一直往F, D, X方向走,得到重链C -> F -> D -> X。

- 剖分好之后,我们按照重儿子优先的原则进行DFS,并标出结点的时间戳,得到下图(虽然现在还看不出有什么用):

树链剖分+DFS后,我们不难发现,一条重链上的结点的时间戳都是连续的——这是因为DFS序的性质2:某些链上的时间戳是连续的。我们让重链成为“某些链”,就有了这样的效果。
此外,根据重链剖分的定义,我们可以证明这一引理:除根结点外,任何一个结点的父亲结点都一定在一条重链上。证明很简单,因为父亲结点存在儿子,所以一定存在重儿子,所以一定在一条重链上。
3.3 利用线段树解决问题
现在,我们利用线段树,开始代码的实现,并实际解决问题。
3.3.1 事前准备和大概流程(两遍DFS)
剖分时,我们需要一个时间戳计数器 tim ,以及一个维护树上结点权值的线段树。接着先进行一遍DFS,标记以下内容:
- 结点的父亲是谁,用
fa[maxn]记录; - 结点的大小,用
siz[maxn]记录; - 结点的重儿子是谁,用
son[maxn]记录; - 结点的深度,用
dep[maxn]记录;
此时没有标记时间戳,原因很简单,我们事先不知道重儿子是谁,自然无法重儿子优先进行DFS、标记时间戳。现在有了重儿子的信息,我们再跑一遍DFS,标记以下内容:
- 结点权值的DFS序、时间戳,分别用
w[maxn]和dfn[maxn]记录,所有结点的权值则被存放在v[maxn]中; - 当前结点所在重链的头部结点是谁(最上面的结点),用
top[maxn]记录。注意,重链头部结点的头部是自身
以上是我们事先要做的准备工作。
3.3.2 代码实现关键片段
1. 图存储(链式前向星,初始化为-1版本)
const

本文深入探讨了树链剖分的概念和实现,包括重链剖分的原理、时间戳的性质以及如何利用线段树解决树结构中的常见操作。文章通过实例详细解析了树链剖分的过程,并提供了模板问题的解决方案,同时讨论了树链剖分在求解最短路径和最近公共祖先等问题中的应用。
最低0.47元/天 解锁文章
552





