详解动态树(LCT)

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
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值