LCT的功能
动态树link-cut-tree缩写为LCT
支持link(在线建边),cut(在线删边)的操作
下面看看这一道题
【题意】
一个图,有n个点,一开始图中没有边。
三种操作:
Connect u v:在点u和点v之间建一条边。保证所有Connect操作不会重复建边。
Destroy u v:摧毁点u到点v之间的边。保证所有Destroy操作将摧毁的是一条存在的边。
Query u v:询问点u和点v是否联通,是输出Yes,否则输出No
保证:在任何时刻,任何点到任何点最多只有一条路径。(就是叫你用树咯)
【输入格式】
第一行为两个正整数n和m,分别表示点的个数和操作的个数。
以下m行,一行表示一个操作。
每行开头是一个表示指令种类的字符串s(区分大小写),之后有两个整数u和v (1≤u, v≤n且u≠v) 分别表示两个点的编号。
【输出格式】
对每个Query操作,输出点u和点v是否连通:是输出Yes,否则输出No。
【样例输入】
200 5
Query 123 127
Connect 123 127
Query 123 127
Destroy 127 123
Query 123 127
【样例输出】
No
Yes
No
【样例输入】
3 5
Connect 1 2
Connect 3 1
Query 2 3
Destroy 1 3
Query 2 3
【样例输出】
Yes
No
【数据说明 】
10%的数据满足n≤1000, m≤20000
20%的数据满足n≤2000, m≤40000
30%的数据满足n≤3000, m≤60000
40%的数据满足n≤4000, m≤80000
50%的数据满足n≤5000, m≤100000
60%的数据满足n≤6000, m≤120000
70%的数据满足n≤7000, m≤140000
80%的数据满足n≤8000, m≤160000
90%的数据满足n≤9000, m≤180000
100%的数据满足n≤10000, m≤200000
【来源】SDOI2008
LCT的结构
如果这一题没有前两个操作,我们就可以用树链剖分来解决这一系列的操作
但是,建边和删边两个操作改变了树的结构。因此,我们就要用新的数据结构LCT来做。
LCT因为是动态的,所以建立在了数据结构splay上面,如果没有学过splay,就赶紧去学吧。
言归正传,LCT中那棵splay是由树中的每一个节点的深度作为权值进行操作的。
这样有什么好处呢?
父亲节点和儿子节点有边相连(不然就没有这种关系),而且深度正好相差1,而splay中的节点都有这样一个性质——左边的都比我小,右边的都比我大,所以这样构建splay还可以找到树中节点和节点的关系了。
看到这里,你可能会问,如果这棵树不是二叉树,而是森林的话,要怎么处理?
这里又要用到了splay的性质——将最新访问过的点转到根节点。
看一个图:
我们会发现这颗树中有粗边和细边
和树链剖分一样,我们把由粗边连起来的点用一颗splay维护
但是,因为树中的点都是相通的,所以在splay中,有一个新的机制:(很重要)
假如两个点是由粗边相连的,比如说1和2 ,那么
tr[2].f = 1 并且 tr[1].son[0] = 2
假如两个点是由细边相连的,比如说2和3,那么
tr[3].f = 2 但是 tr[2].son[0] = 0 ,而不是=2
还有,根据题目,LCT实际上是一颗无根树,并没有固定的根,而是通过splay的操作将最新访问过的点变成这颗无根树第1层的点(只有一个,可以说是根)
LCT代码的实现
LCT的实现建立在一个函数access(x)上面,这个函数的功能就是将x到x所在树(注意:指的不只是所在的splay)的根之间的路径全部变为粗边,顺便维护一下之前的节点(会改变与这些节点细边和粗边)
access函数:
inline void access ( int x ) {
int y = 0 ; //y记录上一个x
while ( x ) {
//如果没有跳出这一棵树
splay ( x , 0 ) ; //这里可以理解为:x是这条链的最低端,是没有右儿子的,所以直接旋转到最高处,以便和tr[x].f所在的链进行连接
tr[x].son[1] = y ;
if ( y != 0 ) tr[y].f = x ; //将两颗splay合并成一颗splay
y = x ; x = tr[x].f ; //记录x,然后x通过细边跳往下一棵splay
}
}
//access中令人发懵的splay
int tmp[N] ;
inline void splay ( int x , int rt ) {
int s = 0 , i = x ;
while ( tr[i].f && (tr[tr[i].f].son[0]==i||tr[tr[i].f].son[1]==i) ) {
//如果没有跳出这一棵splay
tmp[++s] = i ; i = tr[i].f ;
}
tmp[++s] = i ;
while ( s ) {
i = tmp[s] ; s -- ;
if ( tr[i].fz ) reverse(i) ; //维护splay的翻转
} //这个操作是将整颗splay存放在tmp中并维护splay中的翻转标记
//为什么会由翻转,等一下即可见分晓
while ( tr[x].f!=rt && (tr[tr[x].f].son[0]==x||tr[tr[x].f].son[1]==x) ) {
//确保是在一棵splay中
int f = tr[x].f , ff = tr[f].f ;
if ( ff == rt || (tr[ff].son[0]!=f&&tr[ff].son[1]!=f) ) {
if ( x == tr[f].son[0] ) rotate(x,1) ;
else rotate(x,0) ;
}
else {
if ( tr[ff].son[0] == f && tr[f].son[0