树链剖分学习笔记

让我们先来看如下一个问题:
给定一棵n个节点的树,有如下两种操作:
1.修改树上一条边权值为v
2.查询树上两个节点间路径的距离
对于这个问题我们要怎么做?
暴力?妥妥的超时。
这样我们就引入了树链剖分的算法。
在上述题目中,虽然树的边权发生了改变,但是树的形态是没有发生任何变化的。因此我们可以将树上的链取下来存入数据结构中(线段树平衡树均可)利用这些数据结构的优越性能来实现权值的修改和路径距离的查询。
树链剖分有多种方式,使用最广泛的是轻重链剖分
我们将树的节点、边和链分为轻重两种
记录Size(U)为节点U的大小(即子树节点个数),假定U有子节点V,V为U的所有子节点中大小最大的,那么V为重子节点,边(U,V)为重边。U的其他所有的子节点均为轻子节点,其他边相应的称为轻边。
完全由重边构成的一条路径叫做重链
很显然,树链剖分有这样的性质(引自IOI2009国家集训队论文漆子超《分治算法在树的路径问题中的应用》):

1.如果(U,V)为轻边,则Size(V) Size(U)/2;
2.从根节点到任意其他某一节点的路径上,轻边的数量不会大于 O(log n)
3.从根节点到任意其他某一节点的路径上,重链的数量不会大于 O(log n)

下面我们来证明一下这三个性质
对于性质1:已知V为轻子节点,很显然Size(V) Size(U)/2,否则就是重子节点了。
对于性质2:轻边数量最多时,意味着这条路径是一条轻链,即路上所有点都是轻子节点,那么每个点的大小最多为其父节点大小的一半,又有该节点大小一定大于等于1,故轻边数量最多为 O(log n)
对于性质3:贴个图就看出来了

通常情况下,我们会将树中的所有链剖分出来,存在线段树里进行操作,那么这样整个程序的复杂度大致在 O(n log2n) ,这个复杂度虽然看起来不如link-cut-tree的 O(n logn) 可观,但是值得一提的是因为LCT通常是借助Splay实现的,因此常数比较大,很多时候链剖因为常数比较小是优于LCT的。

通过上述对链剖的学习,我们可以发现链剖其实和树的分治存在一定的关系,如果说我们平时说的树的分治是指点分治和边分治的话,链剖可以看做是“链分治”了,这也是为什么漆子超当年在自己《分治算法在树的路径问题上的应用》中为什么花了大半的篇幅来介绍链剖的原因。
再次借用漆子超的一句话来阐述链剖这个算法的一条特性:

“所以路径剖分算法
可以看做是基于链的分治,而且这种分治还有一个特点,每次被删除
结点的儿子必将作为下一次删除的链的头结点。这样,就给我们的维
护带来了方便。”

—————————————我是萌萌哒分割线—————————————

接下来就是链剖到底是怎么实现的了。代码实现可以放到后面其他文章里再提,我们先来看一下链剖过程的文字叙述吧。

  1. 第一遍DFS。在这一遍DFS过程中,我们需要记录出每个节点的Size,Deep,及其祖先Fa(注意,祖先要用类似倍增法求LCA的方式来表示)
  2. 第二遍DFS。这一遍DFS中,我们从根节点开始向下构建重链。具体方法是:我们对每个节点都选择大小最大的子节点来继承之前的重链,其他子节点重新拓展新的重链。同时我们给每个节点打上编号,使得我们可以使用区间来表示整棵树,从而可以套用线段树来实现路径修改查询。完成之后,我们将所有重链首尾相连放在线段树中维护。
  3. 进行权值的修改。权值的修改有两种,修改点权和边权。点权很好修改,直接在线段树中进行改动就可以了(因为已经打过了编号)。至于边权,我们需要分两种情况进行讨论。假设我们要修改的边为(U,V),①若U和V在同一条链上,这当然最好,直接在线段树内进行区间修改就可以了。②若U和V不在同一条链上,这时候就需要用到我们之前记录的Fa数组了。我们把U和V进行移动直到他们在同一条链上,然后进行①的操作。
  4. 进行权值的查询。与修改相似,仍然是基于线段树的单点查询、区间查询等基础操作。

    值得一提的是,在我们进行链剖的预处理过程中其实已经顺便做完了LCA的操作,可以用来求点的公共祖先了。
    在维护区间时,虽然通常我们是使用线段树的,但是根据题目的不同有时我们也会选择Splay。(大概也只有这两种了,因为只有他们能做区间)

具体的题目等以后慢慢做=。=再发新的文章吧

你可能感兴趣的:(数据结构,树链剖分)