树链剖分详解

  问题是这样的:对于一株树(无向无环连通图),为每个结点分配对应的权重。要求能高效计算任意两个结点之间的路径的各类信息,其中包括路径长度(路径上所有结点的权重加总),路径中最大权重,最小权重等等。到这里一切都还是比较简单的,我们可以利用Tarjan的LCA算法在线性时间复杂度内快速求解。但是如果还要求允许动态修改任意结点的权值,那么问题就不简单了。很容易发现这有些类似于线段树的问题,但是线段树是作用在区间上,而我们的问题是发生在树上的,因此线段树并不适用。而树链剖分则是可以用于求解这类问题的高效算法,其更新权值的时间复杂度为O(log2(n)),而统计路径信息的时间复杂度为O((log2(n))^2)。下面对树链剖分进行讲解。

  对于任意一株树,我们记r.size表示以结点r为根的子树中的结点总数,称为r的大小。记r.next表示r的所有子结点中size属性最大的一个结点(如果没有子结点允许留空),称为r的重孩子,而其余子结点称为r的轻孩子,与重孩子相连的边称为重边,而与轻孩子相连的边称为轻边。利用深度优先搜索算法可以在O(n)时间复杂度内计算所有结点的size和next属性。

  很容易发现通过将每个结点与其重孩子通过一条特殊的链条连接,那么我们就在图中建立了多条互不相交的链路,每个结点都处于一个链路中(链路可能只有一个结点),这些链路称为重链。每一条重链中所有结点深度不同,我们称深度最小的为该重链的源点,对应的我们为每一个结点赋予一个属性src,用于记录其所在重链的源点,而且很显然同一链路中相邻结点具有父子关系。这个过程可以以一次O(n)时间复杂度的深度优先搜索完成。

  之后我们将树中的重链全部取出,并首尾相连,这样我们就得到了一个连续区间,处于同一条重链中的结点紧密相连。当然我们不需要真的改变数据结构,我们只需要通过深度优先搜索为每个结点分配唯一的索引index,并且同一条重链上的结点分配的索引紧密相连,之后利用这些索引以及线段树(或者其他数据结构)进行修改和查询权值的操作。

  对于更新权值的操作,我们只需要取对应的结点的index属性,并利用这个属性去修改线段树中对应的值即可,这里的时间复杂度为线段树的时间复杂度,由于每个结点对应一个索引,因此线段树实际维护的是长度为n的区间,修改的时间复杂度为O(log2(n))。

  对于统计两点u,v之间的路径的总长度,我们可以先计算u与v的最低公共祖先lca,之后求s(u,lca)+s(v,lca)-s(lca,lca)。其中s函数用于求某个结点到其某个祖先之间的路径长度。对于s(x,y)的实现我们可以从起点x出发,如果x与y不在同一重链上我们则直接统计x与x.src的距离,这里利用线段树时间复杂度为O(log2(n)),同时让x赋值为x.src.father,之后x必定不为空,理由是y是x 的祖先。之后重复上面过程直到x与y同处一条重链,之后利用线段树高效计算x与y之间的距离,至此流程结束。总的时间复杂度显然取决于从叶结点到根结点中重链数目,下面说明一些树链剖分的命题:

  命题1:对于具有父子关系的一对结点y与x,若x是y的轻孩子,那么x.size<y.size/2。

  证明:这是轻孩子的定义所可以直接得到的,y.size>=1+x.size+y.next.size>x.size*2。

  命题2:从根结点到任意叶结点之间的轻链数目不会超过log2(n),重链数目不会超过log2(n)+1。

  证明:由命题1得知,一条从根到叶结点的路径上轻边数目不会超过log2(n),且由于路径上重链和轻边是交替出现的,因此重链数目上限为轻边数目加一,即log2(n)+1。

  因此结合这两个命题可以保证我们s(x,y)的时间复杂度为O(log2(n)*log2(n))。

  树链的建立只涉及到了深度优先搜索,时间复杂度为O(n),而线段树的在知道初始值时可以以O(n)时间复杂度建立,因此预处理的时间复杂度为O(n)。

posted @ 2018-01-05 20:34  cccwiseee  阅读(1170)  评论(1编辑  收藏  举报