动态树之Link-Cut-Tree(LCT)详解

在学习动态树之前,你需要能够熟练掌握Splay算法树链剖分的原理和模板,并知道每一步的细节,如果还比较模糊,可以直接点击超链接,看我的博客(给自己打个广告)

        动态树是指一类问题,通常要求对一片森林进行路径查找与修改、增删边、维护连通性、路径上的权值和等信息等操作,它与树链剖分的区别在于需要维护边的删除和添加。动态树有很多种解决方法,我们今天只介绍适用范围最广、最常用的一种解法:Link-Cut-Tree(LCT)。

(插个眼,等我以后学了其他方法也更在这个博客里吧,到时候再改个名字)

来看下面这条例题:(洛谷P3690 【模板】动态树) LCT同样也是用树链剖分的思想,将森林划分成若干链的形式,为了不与树链剖分混淆,我们用实边、虚边来表示每条边的划分方案(实链剖分):

1、对于某一个节点u,我们可以通过自定义的方式选择一条子边作为实边,其他边作为虚边

2、一个节点最多有一条与孩子相连的实边,可以没有实边。

3、相邻实边连接作为一条实链

4、没有孩子的叶节点也作为一条实链

5、实链中深度最浅的点被称为顶点

6、顶点与其父节点连接的边必定是虚边

7、实边在操作过程中是可变的,当需要将实边变化时,我们只需要更改交换两个改变的孩子即可

实链剖分的强大在于链的划分是可变的,动态的,对于链上求权值和我们需要找一个同样灵活可变的数据结构来维护,那必然是我Splay了! 维护方案:

1、每一条链由一棵splay维护

2、splay的中序遍历即为实链的上下顺序

3、每个节点在splay中的前驱是它的父节点,后继则是它的子节点

4、无论splay如何旋转,都不会影响实链的先后顺序

利用原树中的虚边,我们可以将splay们连成一棵树:

新树的每个节点都是一棵splay维护的实链,由于原本的splay的根结点没有父亲节点,那么我们就可以把虚边的关系存在splay的根结点中,注意:对于每条虚边,我们只存储了父节点在splay的根结点中,并没有存子节点的关系,也就是只认父亲不认孩子

新树的性质:

1、每个节点都是一棵splay树,对应原树中的实链

2、所有的虚边关系由splay的根结点保存

3、新树只保存了不同实链之间的关系,故可以在满足性质的情况下随意换根

4、虚实边的变化在新树上可以很轻松的维护,所以我们所有的操作都在新树上完成即可

LCT的操作函数:

1、access(int x):在原树中建立一条根结点到x的实链
2、update(int x):把x的标记下传到整个splay树
3、make_root(int x):将x变为原树中的根结点
4、find_root(int x):找到x所在原树的根节点, 再将原树的根节点旋转到splay的根节点
5、split(int x, int y):将x到y的路径变为实链
6、link(int x, int y):如果x与y不连通,加入新边x,y
7、cut(intx, int y):删除x与y连接的边
8、isRoot(int x):判断x是否为splay的根结点

我们从代码角度介绍一下每个函数的目的和实现方法 :

1、access(int x):在原树中建立一条根结点到x的实链

       我们可以先把x转到splay的根结点,那么它的父节点y和它的边一定为虚边,将这条边变成实边也就是将x更新为y的后继,于是我们再把y旋转到y所在splay的根结点,由于y在原链中是最低的点,也就是说y原本是没有后继的,所以y旋转到根结点后它的右子树一定是空的,此时我们将x所在splay插在y的右子树即可。依次从下往上操作直到根结点位置。

这个操作是LCT的核心操作,后续的很多操作都是以该函数为基础的。

void access(int x) {  //建立一条从根到x的路径,同时将x变成splay的根节
    int y = 0,u = x;
    while(u) {
        splay(u);
        tr[u].s[1] = y, pushup(u);
        y = u; u = tr[u].p;
    }
    splay(x);
}

2、update(int x):把x的标记下传到整个splay树

3、make_root(int x):将x变为原树中的根结点

        为了将x变为原树中的根结点,我们可以先access一遍x,此时x作为实链的最后,根结点作为开头,只要翻转一下链即可。

void pushrev(int x) { // 翻转x为顶点的实链
    swap(tr[x].s[0], tr[x].s[1]);
    tr[x].rev ^= 1;
}

void makeroot(int x) { // 将x变成原树的根节点
    access(x);
    pushrev(x);
}

4、find_root(int x):// 找到x所在原树的根节点, 再将根节点旋转到splay的根节点

        首先用一遍access将x与根结点放在同一条实链上,由于根结点在原来的链上是顶点,所以只需要找x作为splay的根结点的最左边的节点即可。记得更新自下而上地更新标记。

int findroot(int x) { // 找到x所在原树的根节点, 再将原树的根节点旋转到splay的根节点
    access(x);
    while (tr[x].s[0]) pushdown(x), x = tr[x].s[0];
    splay(x);
    return x;
}

5、split(int x, int y):将x到y的路径变为实链

        有了函数4实现这一步就特别简单,直接将x移到根,然后access一遍即可。

void split(int x, int y) { // 给x和y之间的路径建立一个splay,其根节点是y
    makeroot(x);
    access(y);
}

6、link(int x, int y):如果x与y不连通,加入新边x,y

         主要是判断x与y是否在同一条链上。可以先将x移到原树的根,再findroot一下y的根是否与x相等即可。

void link(int x, int y) { // 如果x和y不连通,则加入一条x和y之间的边
    makeroot(x);
    if (findroot(y) != x) tr[x].p = y;
}

7、cut(intx, int y):删除x与y连接的边

        删除操作与连接操作类似,只需要把父子关系指针置0即可。

void cut(int x, int y) { // 如果x和y之间存在边,则删除该边
    makeroot(x);
    if (findroot(y) == x && tr[y].p == x && !tr[y].s[0])
    {
        tr[x].s[1] = tr[y].p = 0;
        pushup(x);
    }
}

8、isRoot(int x):判断x是否为splay的根结点

        根据我们存储虚边的特性,儿子是知道父亲是谁,但是父亲不知道儿子,那么我们就判断x的父亲的两个儿子是否是x即可。

bool isroot(int x) {
    return tr[tr[x].p].s[0] != x && tr[tr[x].p].s[1] != x;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值