树链剖分就是把一棵树拆成几条链来处理,便于线段树进行区间操作。
引入
我们知道区间操作通常用线段树。但是如果要对一棵树上的路径或子树进行操作呢?这就需要把树拆分成链来处理,然后用线段树来维护这些链。
通常说的树链剖分实际上是指重链剖分,就是以树的重链为基础来对树进行拆分,拆分成几个重链。
实际上就是对树进行标号,使得那些路径上的点标号连续。这样就便于线段树进行区间操作了。
首先需要明白的几个姿势
- 重(zhòng)儿子:一个非叶子节点的儿子中,子节点最多的那个儿子。
- 重边(不是chóng边):一个非叶子节点和它重儿子之间的连边。
- 重链:相邻重(zhòng)边连接起来形成的一条链叫重链。
- 轻儿子、轻边:就是剩下的儿子和边。
需要注意的是,每一条重链的起点其实是轻儿子或根节点。
对于是轻儿子同时也是叶子节点的节点来说,它自身就是一条长度为 1 的重链。
树链剖分
有了上面的前置姿势,下面就要正式介绍树链剖分啦。
对节点进行标号
需要两个 DFS。
第一次 DFS 统计深度和子树大小,第二次 DFS 对树进行正式的标号。
需要拿一些数组存下这些信息,我们这样规定:
siz
记录子树大小
fa
记录每个节点的父亲节点
dep
记录每个节点的深度
以上就是第一遍 DFS 所需记录的信息。
所以第一遍 DFS 很简单,只需要这样写。
重头戏在第二遍 DFS 上。
我们还需要额外的数组来记录信息。
pos
记录经过第二遍 DFS 后节点的标号
top
记录每个节点所在重链上的顶端节点
需要注意的是根节点的 top
值是它本身,记得初始化。
所以第二遍 DFS 这样写
这样进行两遍 DFS 中,我们已经完成了链剖分的全部操作,成功地把树拆分成了几条链。
显然同一条重链上的节点的标号是连续的,这样便于我们进行区间操作。
路径操作
对于同一条链上的两点,它们的标号在线段树上是连续的,我们可以很轻易地进行区间操作来实现路径操作。
但更多的情况是不在同一条链上,所以需要一些神奇的操作。
就那路径上的节点求和为例吧。
我们先取出深度较深的那个点,然后求出这个点到它所在重链的顶端节点的和,然后再把这个点更新为它所在重链顶端节点的父亲节点,这样就又调到了顶端节点上面的那条重链上。再次取出深度较深的那个点,如此往复,直到两点在同一条重链上,这样直接进行区间操作即可。
写成伪(Python)代码就是
区间更新也是类似的操作。把求和替换成线段树更新即可。
子树操作
对于每一个子树来说,它们的标号也是连续的(因为是 DFS)。
直接进行区间操作即可。对于以 u 为根的子树,它在区间上的右端点标号为 pos[u] + siz[u] - 1
求 LCA(最近公共祖先)
树链剖分是可以在线求 LCA 的,而且实测比倍增跑得快。
只要两个点在同一条重链上,那么它们的 LCA 一定是深度小的那个节点。
那么如果它们不在一条重链上呢?参考上面路径操作的方法。往上跳直到跳到同一条重链上为止。
伪(Python)代码
时间复杂度
(由于本人太蒟蒻了不会证,所以以下内容是网上抄的)
性质一
如果边 (u,v) 为轻边,那么 size(v)≤size(u)/2。
性质二
树中任意两个节点之间的路径中轻边的条数不会超过 log2n ,重路径的数目不会超过 log2n。
根据以上两点性质以及线段树查询和修改的复杂度 O(log2n),可以得知总复杂度为 O(log22n)。
例题
模板题
传送门
操作:路径更新与求和,子树更新与求和。
完整代码
[ZJOI2008]树的统计
传送门
操作:单点修改,查询路径最大值与和。
完整代码