Link-Cut-Tree
以下内容为博主乱写的,难免出错,请神犇不要深究,有错请指出。
Link-Cut-Tree(LCT)是解决动态树类问题一种数据结构
OI主神Tarjan发明的,%(mei)%(you)%(ta)%(cai)%(shi)%(zui)%(hou)%(de)
首先以下黑体字是普通线段树能解决的问题
维护一个序列,支持下列操作:
区间求和
区间求最值
区间修改
求连续子段和
添加一段区间
删除一段区间
翻转一段区间
但是遗憾的是,它无法解决上面的东西,但是对于LCT来说,这就太简单了
维护一棵树,支持下列操作:
链上求和
链上求最值
链上修改
断开树上的一条边(cut)
连接两个点,保证连接后仍然是一棵树(link)
见bzoj3282, 这是一道很裸的题,它要求我们写一种数据结构,满足连接两棵树或者断开一条链及能够在树上进行查询
如果是树链剖分的话,显然是不能做的吧,如果硬上的话,时间堪比大暴力或者根本写不出来,因此我们需要一种这样的解决动态树问题的数据结构
大概知道了LCT是干什么的,现在讲一讲如何实现LCT
LCT的基础是Splay(也是Tarjan发明的东西,在这里就不讲了)
正文
大多数同学在初学LCT的时候,都会分不明白什么是Auxiliary Tree(辅助树)
这是我们做LCT的题时真正要维护的东西,而非原树(不要想着去维护原树)
先看一些概念性的东西(或许你会认为和树链剖分一样的东西,但这是不一样的):
Preferred Child:重儿子,重儿子与父亲节点同在一棵Splay中,一个节点最多只能有一个重儿子
Preferred Edge:重边,连接父亲节点和重儿子的边
Preferred Path:重链,由重边及重边连接的节点构成的链
像这个东西
丑爆了有木有,但是记住,一条重链在一棵splay中,而且可以看出,preferred edge 和 树链剖分中的重边 不是一个东西
树链剖分是把一棵树分成一条条链,而LCT维护的,可以是一个森林,然后把这个森林中每棵树的Preferred Path用splay维护
在讲access操作之前,我们需要先知道,一棵splay中的
splay的根节点(B,C,I,L)的father指向原来节点的father
然而其他节点的father却不一定指向原来的father
就是
辅助树的根节点≠原树的根节点
辅助树中的father≠原树中的father
ACCESS 操作是Link-Cut Trees 的所有操作的基础. 假设调用了过程ACCESS(v), 那么从点v 到根结点的路径就成为一条新的Preferred Path. (即与根节点在一棵splay中,这意味着我们可以用splay把它翻上去了)如果路径上经过的某个结点u 并不是它的父亲parent(u) 的Preferred Child, 那么由于parent(u) 的Preferred Child 会变为u , 原本包含parent(u) 的Preferred Path 将不再包含结点parent(u) 及其之上的部分.(大量摘抄,说明很重要,请仔细阅读)
下面是对N进行access操作时的情况
(请仔细阅读)
N与根结点就完美的在同一棵splay中了
基本操作
access
void access( int x ){
for( int t = 0; x; t = x, x = fa[x] ){
splay(x); c[x][1] = t; update(x);
}
}
move to root
void move_to_root( int x ){
access(x); splay(x); rev[x] ^= 1;
}
Find Root
在ACCESS(v) 之后, 根结点一定是v 所属的Auxiliary Tree 的最小结点. 我们先把v 旋转到它所属的Auxiliary Tree 的根. 再从v 开始, 沿着Auxiliary Tree 向左走, 直到不能再向左, 这个点就是我们要找的根结点. 由于使用的是Splay Tree 数据结构保存Auxiliary Tree, 我们还需要对根结点进行Splay 操作.
int find_root( int x ){
access(x); splay(x);
while( c[x][0] ) x = c[x][0];
return x;
}
Cut先访问v, 然后把v 旋转到它所属的Auxiliary Tree 的根, 然后再断开v 在它的所属Auxiliary Tree 中
与它的左子树的连接, 并设置.
void cut( int x, int y ){
move_to_root(x); access(y); splay(y); fa[x] = c[y][0] = 0;
}
link先访问v , 然后修改v 所属的Auxiliary Tree 的Path Parent 为w, 然后再次访问v .
void link( int x, int y ){
move_to_root(x); fa[x] = y;
}
splay的操作
void rotate( int x ){
int y = fa[x], z = fa[y], l, r;
l = (c[y][1]==x); r = l^1;
if( !isroot(y) ) c[z][y==c[z][1]] = x;
fa[x] = z; fa[y] = x; fa[c[x][r]] = y;
c[y][l] = c[x][r]; c[x][r] = y;
update(y); update(x);
}
void splay( int x ){
sta[++top] = x;
for( int i = x; !isroot(i); i = fa[i] ) sta[++top] = fa[i];
while( top ) pushdown(sta[top--]);
while( !isroot(x) ){
int y = fa[x], z = fa[y];
if( !isroot(y) ){
if( (c[y][0]==x)^(c[z][0]==y) ) rotate(x);
else rotate(y);
}
rotate(x);
}
}
就说到这里,感觉有些烂尾
另:在能用多种方法做的一道题的所有方法中,LCT基本上是最慢的
虽然是O((n + q) log n)但splay自带巨大常数
引用一下论文中的运行时间
解法编号解法概述时间复杂度
解法一 动态树, 使用Link-Cut Trees 数据结构O((n + q) log n)
解法二 轻重边路径剖分, 用线段树或虚二叉树维护每条路径O(n + q log2 n)
解法三 轻重边路径剖分, 用Splay Tree 维护每条路径O((n + q) log n)
解法四 轻重边路径剖分, 用一棵“全局平衡二叉树”维护所有路径O((n + q) log n)
解法编号相应参考程序运行时间
解法一 QTREE dynamic-tree link-cut-trees.pas 4.58 秒
解法二 (线段树) QTREE heavy-light-decomposition segment-tree.pas2.65 秒
解法二 (虚二叉树) QTREE heavy-light-decomposition imaginary-bst.pas2.37 秒
解法三 QTREE heavy-light-decomposition splay-tree.pas4.28 秒
解法四 QTREE heavy-light-decomposition global-balanced-bst.pas2.06 秒
练习题
splay:bzoj3223,bzoj2500
bzoj2002 支持分离,link,cut,find
bzoj2631 基本操作
bzoj3669
bzoj3282
参考文献
Link-Cut-Tree.ppt by PoPoQQQ