今天学了个叫 “LCT” 的东西,很多人估计越听越懵。
Link-Cut Tree 确实是个很不好理解的东西,就连我也是。
就算是老师讲也感觉很模糊。
首先,这个 LCT 也可以理解为“用 Splay 来维护树剖”,这里的树剖指的是实链剖分(专门为 LCT 服务的剖分方式)。
一棵树中,处于同一条实链的节点用同一个 Splay 维护。由于 Splay 灵活多变,完全可以代替线段树。
然后,每一个 Splay 内维护的节点键值是他们在原来树中的深度。即对于一个 Splay 节点键值 xxx,左子树的所有节点在树中的深度都小于 xxx,右子树的所有节点在树中的深度都大于 xxx。
于是,代码的前半部分完全用来写 Splay。
注意,有一个重点就是,树中的实链(即一棵 Splay)中的节点连的是双向边,而非实边连的是单向边(儿子认父,父不认儿子),这样有助于判断一个节点是否是 Splay 的根。
但是,之后呢?我们定义一个函数 access(x)access(x)access(x) 表示,现在把 root,...,xroot,...,xroot,...,x 这一条链搞成一条实链,并且 xxx 下面不接任何实边(注意 root,...,xroot,...,xroot,...,x 这些节点原本接的实边要删除)
如上图,红色的为实边(有些节点也可以不接实边,比如 777)。
接下来执行 access(5)access(5)access(5)。
执行 access(8)access(8)access(8)。
可以发现,每做一次 access(x)access(x)access(x),其实就是把 root,...,xroot,...,xroot,...,x 重构成一条实链,并单独用一个 Splay。
accessaccessaccess 函数是 LCT 中最重要的部分,也是 LCT 中唯一连接实线的方式。
void access(int x) //x 不为空,直到根为止
{
int y=0;
while(x)
{
splay(x);
a[x].c[1]=y; //节点 x 原来接的实线清空,重新接到下面的节点,原来接的节点必然深度更深,即在平衡树右边
a[y].fa=x;
pushup(x);
y=x;
x=a[x].fa; //下一次更新实线用到
}
}
函数 evert(x)evert(x)evert(x) :把 xxx 作为 xxx 所在树的与根。
考虑用 access(x)access(x)access(x) 把 xxx 和 rootrootroot 连接,然后伸展到平衡树的根,最后利用翻转来保证所在平衡树的深度。
比如执行 evert(8)evert(8)evert(8),连接了 1,7,81,7,81,7,8,以深度为键值建出平衡树:
把 888 节点伸展到根:
你会发现,如果把 888 作为根,有且只有这棵平衡树的深度需要重构,只需要对整棵树进行翻转即可。
翻转必然要打标记,必然要下放标记。因此在每次 Splay 伸展时,必须先从根传标记。
void evert(int x)
{
access(x);
splay(x);
a[x].rev^=1; //翻转标记
}
函数 findrt(x)findrt(x)findrt(x) 查找 xxx 所在的树的根。
显然,先用 accessaccessaccess 连接 rootrootroot 和 xxx,然后在平衡树中找深度最小的,即一直往左走。
int findrt(int x)
{
access(x); //连接 root,x
splay(x); //把 x 伸展到平衡树的根,也可以理解为找平衡树的根的一种方式
while(a[x].c[0]) //一直往左走
{
x=a[x].c[0];
}
return x;
}
函数 link(x,y)link(x,y)link(x,y),连接原来树上 (x,y)(x,y)(x,y) 两个节点。
类似于并查集的合并,我们可以先把 xxx 作为树根 evert(x)evert(x)evert(x)。
然后把根的父亲接到 yyy 后面。注意此时 x,yx,yx,y 不在同一个 Splay。
void link(int x,int y)
{
if(findrt(x)==
findrt(y))return; //判断根是否相同
evert(x);
a[x].fa=y;
}
函数 cut(x,y)cut(x,y)cut(x,y) 表示切断原来书上 (x,y)(x,y)