树链剖分学习笔记
前言
前些日子在洛谷上(应该没有玩OI的不知道洛谷吧,那也安利一波,洛谷OJ服务器稳定,界面优美,功能全面,使用体验很好,值得尝试,点击传送)写LCA的时候用倍增算法被卡常数了1,一气之下就去查了其他的LCA算法,看到tarjan和树剖都能解决这个问题,于是就选择了学习树剖这条(不归之)路.
以上全是废话
真正的前言
我看了很多大犇的博客才终于大概搞明白了树链剖分是个什么逻辑和思路.虽然大犇们写的都很详细了,但对于本蒟蒻来说还是有些不那么容易理解,因此在这里尝试自己重新解释一下树链剖分的思想和具体实现,同时添加一些自己的理解,来帮助理解这个实际上并不困难的算法.
返回目录
正文
树链剖分的基本思想
树链剖分或许更应该被称作一种思想,就是将一棵树拆分成几个部分,使每个部分内的元素之间都具有一定的联系,从而允许我们对进行过剖分的树利用一些其他的高级数据结构进行一些难以暴力解决的操作,如:
- 将节点x到节点y的最短路径上的所有节点/边的权值加某个值
- 将以节点x为根的子树中的所有节点/边的权值加某个值
- 查询节点x到节点y的最短路径上的所有节点/边的权值之和
- 查询以节点x为根的子树中的所有节点/边的权值之和
- (还可以顺便求最近公共祖先LCA)
也就是说,树链剖分就如同它的名字一样,就是将树解剖成若干个部分(称为树链),方便后续的操作.
返回目录
前置知识需求
在学习树链剖分之前,需要先行掌握如下的知识:
- 树的基本概念
- 图的基本概念
- 深度优先搜索(DFS)
- 线段树(或者树状数组)
基本概念
为了完成分解,需要计算几个辅助数组的值.首先定义一些概念(在概念名后面的括号内的是数组名):
- 和普通树相同的概念(不详细解释了)
- 父节点(f)
- 子节点(head)
- 大小2(size)
- 深度(dep)
- 额外添加的概念
- 重子节点(hson):节点的子节点中具有最大的size的一个(这类节点又称为重节点.特别的,根节点是重节点)
- 轻子节点:除重子节点以外的所有节点(这类节点又称为轻节点)
- 重边:连接父节点和重子节点的边
- 轻边:连接父节点和轻子节点的边(除重边的所有边)
- 重链:若干重边连接成的链(下文中也会称为树链)
- 轻链:若干轻边连接成的链
- 链顶(top):该节点所在的重链的顶端节点的序号3
为了确认自己是不是已经理解了这些概念,这里附上一张图,并给出某些数组在这种情况下的值.其中,重节点将在图中以红色标识,重边以粗线标识,轻边以细线标识.
数组名\序号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
f | 0 | 1 | 1 | 1 | 2 | 2 | 3 | 4 | 4 | 4 | 6 | 6 | 9 | 13 |
size | 14 | 5 | 2 | 6 | 1 | 3 | 1 | 1 | 3 | 1 | 1 | 1 | 2 | 1 |
dep | 1 | 2 | 2 | 2 | 3 | 3 | 3 | 3 | 3 | 3 | 4 | 4 | 4 | 5 |
hson | 4 | 6 | 7 | 9 | 0 | 11 | 0 | 0 | 13 | 0 | 0 | 0 | 14 | 0 |
top | 1 | 2 | 3 | 1 | 5 | 2 | 3 | 8 | 1 | 10 | 2 | 12 | 1 | 1 |
值得注意的是,我们所有下标均从1开始,并以0表示不存在.
如果对于任意节点,我们在访问完后优先访问它的重子节点,并将所有节点按照访问顺序编号,将得到这样的一组编号:
仔细观察这两张图以及表格,能够发现如下性质:
- 两个节点x,y在同一条重链上,当且仅当top[x]==top[y]
- 任何一条重链上的点编号连续
- 以任何一个点为根的子树中的所有点编号连续
事实上,这就是树剖得以解决问题的根本.因此,我们引入了一个新值编号(id):从根节点开始,对于任意节点,先访问其重子节点,然后再访问其其他节点所得到的某节点的访问次序编号.正是id具有的连续性,使得我们能够用线段树完成一些操作.
如何使用树剖的结果
在研究树剖的实现之前,先来看看在树剖完成后如何应用其结果.我们由简到繁的讨论如下几个应用:
将以节点x为根的子树中的所有节点/边的权值加某个值
我们假设已经实现了一棵线段树,其名称为t(一下应用中均包含此假设).
关于线段树的具体实现请看完整代码或是(暂时还没有)我关于线段树的学习笔记.
显然,由于以任何一个点为根的子树中的所有点编号连续,这时只需要对以x的编号为起始点,x子树中编号最大的节点的编号为终止点的区间进行区间修改,就可以完成这个操作了.而由于连续,最大的点的编号就是x编号加上x的大小.因此,修改的区间就是 [id[x],id[x]+size[x]) .
查询以节点x为根的子树中的所有节点/边的权值之和
同理,只需要对区间进行区间求和就可以完成了,同样是线段树的基本操作.操作区间同样是 [id[x],id[x]+size[x]) .
求两个节点x和y的最近公共祖先(LCA)
显然,两个节点之间的最短路径会经过两个节点的最近公共祖先.因此,在研究对两点间最短路径上的操作之前,我们先来看看如何用树链剖分得到的数据求LCA.
显然,如果两个节点在同一条树链上,它们的LCA就是两点中深度较小的节点.这时可以直接得到结果:LCA=dep[x]>dep[y]?y:x
.
而当两个节点不在同一树链上时,我们就可以利用top迅速的向上跳跃,来到另一条树链上,尽快的使两个节点到达同一条树链上.这也就解释了为什么我们的原则是”对于任意节点,先访问其重子节点,然后再访问其其他节点”:size的大小可以在一定程度上反映这棵子树的高度,将高度最大的一条作为重链可以使这上面的点迅速跳到顶端,减少跳跃次数,提高算法的效率.
需要注意的是,我们应该让跳跃后到达的链顶深度更大的点跳跃而另一个点保持不动.这样才能保证两个点不会擦肩而过.
比如这个例子中,我们若要求点11和12的LCA,我们若首先让11向上跳跃将到达2,而事实上11和12的LCA是6,这时已经不可能再得到正确的结果了.
代码也不难写出:
int lca(int x,int y)
{
while(top[x]!=top[y])
{
if(dep[top[x]]<dep[top[y]]) swap(x,y);
x=f[top[x]];//注意这里,top[x]和x仍在同一树链上,要到达另外一条树链需要取top[x]的父节点
}
return dep[x]>dep[y]?y:x;
}
代码很简单,但是已经足够正确的求解LCA问题了.
将节点x到节点y的最短路径上的所有节点/边的权值加某个值
我们采用和上面求LCA一样的思路:不断使两个节点向上移动从而逼近,直到两个节点到达同一树链,这时即可一步完成最后一段的操作.
不难想到,为了实现这个操作,我们只需要在每一步移动x点的时候,修改移动经过的点的权值,就可以轻松完成任务了.由于”任何一条重链上的点编号连续”,我们的区间也很好确定:
两点在同一条树链上时,操作区间为