前言
妈的我拿着五篇博客对照着看才看懂。- 什么是 ?
- 上至 ,都是一种解决动态树的利器!到底有什么用?
若是有这样一个问题
维护一棵树,支持下列操作:
- 链上求和
- 链上求最值
- 链上修改
- 断开树上的一条边
- 连接两个点,保证连接后仍然是一棵树
咋一看,毫无头绪???但是 大爷早已搞出 (Orz)
- 动态树因为会动所以很讨厌,不能使用静态的线段树来维护。于是,把树链剖分的线段树换成 试试看?
- 于是一种算法就出来。 树链剖分 。
原理
- 树链剖分是如何运作的?
- 将树划分成重链和轻链,然后每条链用线段树维护。
- 同样, 也选择重儿子然后连接重链,然而, 的重链更偏向于随缘。
- 我们不必理会重链如何产生,因为树的动态的,所以重链必须不断变换。
- 对于每条重链,我们用 来维护答案,根据所有重链的 ,得出相应答案。
概念
- 引进如下定义:
- 偏爱儿子(重儿子):一个节点最多只有一个偏爱儿子,且偏爱儿子与其父亲同位于一棵 上。与链剖大相近庭的是,这里的偏爱儿子是任意选择的。
- 偏爱边(重边):顾名思义,就是偏爱儿子与其父亲相连的边。
- 偏爱路径:由偏爱边连接而成的链。
- 辅助树:由一条偏爱路径上的所有节点所构成的 称作这条链的辅助树。
- 可以得出,偏爱路径没有公共点。
- 排序的关键字是节点的深度。因为每个点只能发散出一条偏爱边,故一个深度在 上对应一个节点!!!
- 每棵 的根节点指向它的父亲(灰色边)。但它的父亲并不指向它。
即爹不认儿子- 如下图,橙色边表示 ,灰色边表示连接 之间的边。
操作
- 这里的每个操作都十分重要!!
基本函数
- 为了方便写程序,定义如下函数
- 判断一个点是否为当前所属 的根节点。
- 可以考虑判断它是否为它父亲的儿子之一。
- 首先 根节点与它的父亲是虚边。其次,因为“爹不认儿子”,所以它的父亲的儿子中必定没有它。
bool isRoot(int u){
return son[fa[u]][0]!=u&&son[fa[u]][1]!=u;
}
- 判断一个点是其父亲的左儿子还是右儿子。
int Check(int u){
return son[fa[u]][1]==u;
}
- 操作是动态树问题的最重要操作,它的目的是为了能够在一棵 上进行查询,修改,删除,且保证这棵树上只有需要查询的一条链上的点。
- 表示将根节点到 这个点的路径变为偏爱链,将 与其儿子的所有边都变为非偏爱边。
具体如何操作?
我们首先需要把节点 往下连出的边变为非偏爱边,即删除 一边。
- 鉴于 中深度是唯一的,所以我们在 所在的 中,把 旋转为根,其右儿子必定为点 ,删除即可(将右儿子标记为 )。
- 往上走,我们需要把 这条边变为轻边,把 变成重边。
- 前者与上面的思路一样,后者直接将 的右儿子置为 即可。
- 以此类推,可以完美解决这个操作。
- 就不难得到代码。
别人的代码打得很巧妙我就Copy了
void access(int u){
for(int v=0;u;v=u,u=fa[u])
splay(u),ch[u][1]=v,update(u);//旋转为根,删除/连接右节点,更新当前点
}
- 也是动态树的最重要的操作。
- 表示将 变为 的根。
- 考虑这个操作的意义。对于许多操作来说,都是询问一条经过根节点的路径,然而因为 的构造使得这样的问题做起来十分棘手。但是对于只查询根节点一侧的边时,这样的问题便变得十分简单。于是考虑换根。
- 这里有一个很巧妙的思路。
- 首先 ,使得根节点到 的路径在同一棵 上。
- 这时候的 是没有右儿子的。因为没有比它更深的点。
- 此时我们 ,将 变为根。我们希望 同样不含右儿子。
- 但是实际上, 将会变得没有左儿子。
- 怎么办?
- 于是有一个骚操作:直接将 的左右子树翻转!
- 这样以后, 变没有右儿子了。
- 虽然很奇怪,但是确是对的。
void makeroot(int x) {//把x改为原树的根节点
access(x); splay(x);
lazy[x]^= 1;//更新翻转标记
}