学习动态树,我们首先需要了解到什么是Splay,推荐这一篇大聚聚yyb的博客。
我们在LCT中列写的Splay会以yyb的splay为基础作出改变,也是方便大家的后继学习,这样的排版。
LCT主要解决什么问题呢?
-
查询一个点的父亲
-
查询一个点所在的树的根
-
修改某个节点的权
-
向从某个节点到它所在的树的根的路径上的所有的节点的权增加一个数
-
查询从某个节点到它所在的树的根的路径上的所有的节点的权的最小值
-
把一棵树从某个节点和它的父亲处断开,使其成为两棵树
-
让一棵树的根成为另一棵树的某个节点的儿子,从而合并这两棵树
-
把某棵树的根修改为它的某个节点
-
查询在同一棵树上的两个节点的LCA
-
修改以某个节点为根的子树的所有节点的权
-
查询以某个节点为根的子树的所有节点的权的最小值
那么,既然是在一颗或者多棵树上维护这样的数据结构,我们势必要现有一棵树,但是这棵树又过于灵活了,我们就想到了一种树链剖分的形式)——实链剖分。这也是穿插起整个Splay的最重要的纽带,我们把一棵树拆成几个Splay的形式,这即可满足这样的条件,在一颗splay上的点都是一条重链上的,也就是代表着一条重链上的点都在一颗splay上面。
但是这样的重链同时又是灵活的,我们有时候需要去拆掉它,也有可能得将一条轻链变成一条重链,并且拆掉原来的重链变轻成为轻链。
现在,我们首先要来了解一下LCT的三个性质:
LCT的主要性质如下:
-
每一个Splay维护的是一条从上到下按在原树中深度严格递增的路径,且中序遍历Splay得到的每个点的深度序列严格递增。
是不是有点抽象哈
比如有一棵树,根节点为11(深度1),有两个儿子2,32,3(深度2),那么Splay有33种构成方式:
{1−2},{3}{1−2},{3}
{1−3},{2}{1−3},{2}
{1},{2},{3}{1},{2},{3}(每个集合表示一个Splay)
而不能把1,2,31,2,3同放在一个Splay中(存在深度相同的点) -
每个节点包含且仅包含于一个Splay中
-
边分为实边和虚边,实边包含在Splay中,而虚边总是由一棵Splay指向另一个节点(指向该Splay中中序遍历最靠前的点在原树中的父亲)。
因为性质2,当某点在原树中有多个儿子时,只能向其中一个儿子拉一条实链(只认一个儿子),而其它儿子是不能在这个Splay中的。
那么为了保持树的形状,我们要让到其它儿子的边变为虚边,由对应儿子所属的Splay的根节点的父亲指向该点,而从该点并不能直接访问该儿子(认父不认子)。
接下去,认识几个LCT的独有的函数:
access(x)函数,将x到根节点的边全部划成重链。做法也不是很难,不断的把到根节点上的轻链变重,然后原本的重链变轻即可。
- 转到根;
- 换儿子;
- 更新信息;
- 当前操作点切换为轻边所指的父亲,转1
就是这样的几步,就构成了access(x)操作。
inline void access(int x) { int y = 0; while(x) { splay(x); c[x][1] = y; pushup(x); y = x; x = fa[x]; } }
然后再看,就是把某一个节点变成原树的根这样的一个操作。
inline void makeroot(int x) { access(x); splay(x); pushr(x); }
但是这里有一个pushr(x),这是个什么操作呢?因为原本上x的深度是最深的,所以反转到splay树的顶端的时候,也一定是只有左儿子,所以,假如将它下面的所有的点都翻转一遍之后,就是将x变成了最浅的,也就是真正的树的根了。
inline void pushr(int x) { swap(c[x][0], c[x][1]); r[x] ^= 1; } //翻转操作
接下去的操作,我们可以看到既然可以制造根了,那么也就是意味着可以找根了呀。我们需要找到这样的原树中的深度最浅的节点。findroot(x)==findroot(y)表明x,yx,y在同一棵树中。
int findroot(int x) { access(x); splay(x); while(c[x][0]) { pushdown(x); x = c[x][0]; } splay(x); //保证时间复杂度 return x; }
这里呢,又有一个pushdown(x)这样的一个操作,那么这个操作是干什么的呢?就是向下传递信息的而已。
inline void pushdown(int x) { if(r[x]) { if(c[x][0]) pushr(c[x][0]); if(c[x][1]) pushr(c[x][1]); r[x] = 0; } }
其中,r[x]就是一个延时标记而已。
那么,有时候,我们需要一段x-y这样的链怎么办,我们是不是需要考虑把这段链给拉出来?这时候出现的函数叫做split(x, y)函数。将x-y的路径拉成一条splay。
inline void split(int x, int y) //把x-y的这条边提取出来 { makeroot(x); access(y); splay(y); }
还有就是我们要连接边的时候,就是将两个块连接起来的时候,就是需要加上一个link(x, y)操作,假如x、y原先不在同一棵原树上,那么就是可以把x、y这样的一条边给连接起来了。
inline void link(int x, int y) //连边 { makeroot(x); if(findroot(y) != x) fa[x] = y; }
接下去还要处理一个断开一条(x,y)直接连接的这条边,将x、y断开(不是断开两个块,就是断开这条线)。
先判一下连通性(注意findroot(y)findroot(y)以后xx成了根),再看看x,yx,y是否有父子关系,还要看yy是否有左儿子。
因为access(y)access(y)以后,假如y与x在同一Splay中而没有直接连边,那么这条路径上就一定会有其它点,在中序遍历序列中的位置会介于xx与yy之间。
那么可能yy的父亲就不是xx了。
也可能yy的父亲还是xx,那么其它的点就在yy的左子树中
只有三个条件都满足,才可以断掉。
当然,我们也可以维护一下size,如果存了的话。因为就剩下x、y两个点了我们只需要判断在splay最上面的那个点的size与2的大小比较即可。
inline void cut(int x, int y) { makeroot(x); if(findroot(y) != x || fa[y] != x || c[y][0]) return; fa[y] = c[x][1] = 0; pushup(x); }