You had me at hello.|

_Youngxy

园龄:3年7个月粉丝:5关注:28

动态树 LCT (Link-Cut-Tree)

什么是动态树?

动态维护⼀个森林,动态的⽀持 2 个操作——添加边、删除边的数据结构。

它还可以维护树上路径的⼀些信息

时间复杂度单独操作:logn

虽然树连剖分的复杂度比动态树大,但是常数比动态树

回忆⼀下,树连剖分是维护一些重链来维护⼀些树,动态树也是类似的。

树链剖分可以将边分为重边和虚边,动态树可以维护实边和虚边

具体这样表述:任意⼀个点最多只有一条实边

我们可以定义一条与实边对应的路径

如下图所示:


孤⽴点我们也单独当作实边处理

Spaly 维护的所有实边路径
红圈圈出来的就是一个 splay

Q: 具体怎么维护?
A: splay 中序遍历就是这个路径从上到下的遍历

splay 维护的是当前联通的所有实边的极⼤路径
splay 通过其中的后继与前驱关系,来维护原树中的⽗⼦关系。

Q: 树跟树之间的关系如何维护?
A:splay中,rtfa信息还没利⽤上,所以⽤ splayrt 的结点来维护

提示,x 的⽗节点不⼀定是 y,因为 x 不⼀定是 spaly 中的 rt
举个例⼦,假设 xfa 是整个 splayrt,那么t[r].p=y

Q:如何判断实边虚边?

A:因为每个结点最多只有⼀个实边

如果是虚边

在链接 splay 的时候只有只有这个 splayrtfa(点A)指向了某个点(点B)

但是这个点B的儿子却不是点A

虚边:子结点知道父结点是谁,但是父结点不知道⼦结点是谁

实边:⽗⼦之间相互知道

基于这种情况,我们只需要修改一次父子关系

我们可以很轻松的完成实边和虚边的转换

即:只需要确定父亲的儿子是谁

LCT基本操作

换边操作

将结点 fa 的后继改为想要变成实边的那个点 x


注意,得fa 转到根节点

此时fa的右⼦树是空的(因为 fa 的序号最⼤)

然后x 点接到 fa 的右⼦树

access(x)

核⼼操作:将 rtx 的路径全部变成实边

(建⽴⼀条 rtx 的实边路径)

注意这个实边路径只能包含 rtx 的实边路径

这个过程是从下往上做的

⾸先需要将 x 旋到当前实边的 rt

接下来希望将 xy 的连边边成实边

因为 y 是最⼤的点,那么直接将 y 旋到 rt

此时 y 没有右⼦树,直接将 x 接到 y 的后继节点上

那么直接将 x 插到 y 的右节点即可

此时,我们以 yrtsplay 维护了整个路径

同样的道理对于 z 来讲,先把 z 旋转到 rt

此时 z 有左右子树(因为原树的有⼦树)

解决⽅案很简单

基于之前的换边理论,我们直接将y挂到z的右⼦树上

此时z就是这个 splayrt

总结⼀下:节点直接旋上去。挂到右⼦树。递归做即可。

make_root(x)

将x变为根节点

利⽤第一个操作 access(x) 建立一条rt到x的实边路径。
对于一个无根树而言,我们将路径翻转是不会影响这个树的拓扑结构的。

所以直接将 x 转到 rt,然后将整个路径翻转即可。

拓扑结构不变:翻转前后 任意两点 xy 经过的路径点不会发⽣变化。

路径翻转是 splay 的⼀个经典操作,直接将某个区间翻转即可。

利⽤ lazytag 实现。

提示:此处2个关系别搞混了,⼀个是从原树的,⼀个是 splay

⼀个疑惑:区间翻转后为什么不会破坏其他边的偏序关系

因为父子关系不会发⽣改变

find_root(x)

找到 x 所在的树的根节点

  1. 建⽴ xrt 的实边路径(调⽤ access

  2. x 旋转到 rt

  3. 整个路径深度最小的点就是根节点

只需要找到整个树的最左的节点

做两遍 findrt 即可

进阶操作:判断 x 和 y 是否在同⼀个 splay 之中

split(x,y)

将从 xy 的路径变为⼀条实边路径

⾸先通过 makertx 变为 rt

access(y) 这样就实现了split

link(x,y)

如果 x y 不连通,则加(x,y)这⼀条边
具体操作:

  1. 判断连通,makert(x)x 变为 rt

findrt(y) 是否为 x

如果findrt(y)!=x

则说明他们不连通

  1. 由于在判断连通时x 已经是 rt

所以若需要加边,只需要将 xfa 记为 y

cut(x,y)

x y 有边,则删掉该边
操作:

  1. x 变为 rt

  2. findrt(y) 找到y所在的根节点

  3. 考虑第⼆个操作的副作⽤:当 findrt(y) 后,除了找到y的根节点外的同时,会将y所在
    树的 rt 旋转到 y 所在实边 splayrt 上(在 findrt 后,会从左⼀直⾛找到 rt,然后 splay 操作为了保证时间复杂度,最后还会 splay ⼀次把 rt 旋上去。)y 所在实边splay 的根节点就应该是整个树的根节点,也就是 x

  4. 如果x y有边,则意味着 y 应是 x 的后继。所以只需要判断 y 是否为 x 的后继即可( y 是否为 x 的后继的判断标准:y 是不是 x 的右⼦树,同时 y 的左⼦树是否为空。
    这样就🆗了

isrt(x)

判断 x 是否为所在 splayrt(注意不是原树的 rt
如果 x 不是 rt,则 x 必然存在父节点。则x必然是其fa的左儿子或右儿子。
因此,若 x 既不是其 fa 的左儿子,也不是其 fa 的右儿子。那么 x ⼀定是 splayrt

code

#include<bits/stdc++.h> 
using namespace std;
const int N=1e5+2;
int n,m,s[N],op,x,y;
struct node{
    int ch[2],p,a,sum,tag;
}t[N];
void pushtag(int x){
    swap(t[x].ch[0],t[x].ch[1]);
    t[x].tag^=1;
}
void pushup(int x){
    t[x].sum=t[t[x].ch[0]].sum^t[x].a^t[t[x].ch[1]].sum;
}
void pushdown(int x){
    if(t[x].tag){
        pushtag(t[x].ch[0]);
        pushtag(t[x].ch[1]);
        t[x].tag=0;
     }
}
bool isrt(int x){
    return t[t[x].p].ch[0]!=x&&t[t[x].p].ch[1]!=x;
}
void rotate(int x){//x上旋 
    int y=t[x].p,z=t[y].p;
    int k=(t[y].ch[1]==x);//x原来的位置 
    if(!isrt(y)) t[z].ch[(t[z].ch[1]==y)]=x;//x代替y位置 
    t[x].p=z;
    t[y].ch[k]=t[x].ch[k^1];
    t[t[x].ch[k^1]].p=y;
    t[x].ch[k^1]=y;
    t[y].p=x;
    pushup(y);
    pushup(x);
}
void splay(int x){
    int top=0,r=x;
    s[++top]=r;
    while(!isrt(r)){
        s[++top]=r=t[r].p;
    }
    while(top) pushdown(s[top--]);
    while(!isrt(x)){
        int y=t[x].p,z=t[y].p;
        if(!isrt(y)){
            if((t[y].ch[1]==x)^(t[z].ch[1]==y)) rotate(x);
            else rotate(y);
        }
        rotate(x);
     }
     pushup(x);
}
void access(int x){// 建1条从根到x的路径,同时将x变成splay的根节点
    int z=x;
    for(int y=0;x;y=x,x=t[x].p){
        splay(x);
        t[x].ch[1]=y;//右子树 
        pushup(x);
    }
    splay(z);
}
void makert(int x){// 将x变成原树的根节点
    access(x);
    pushtag(x);
}
int findrt(int x) {// 找到x所在原树的根节点, 再将原树的根节点旋转到splay的根节点
    access(x);
    while(t[x].ch[0]){
        pushdown(x);
        x=t[x].ch[0];
    }
    //路径深度最小 
    splay(x);
    return x;
}
void split(int x,int y){ // 给x和y之间的路径建1个splay,其根节点是y
    makert(x);
    access(y);
}
void link(int x,int y){//如果x和y不连通,加1条x和y之间的边
    makert(x);
    if(findrt(y)!=x){//不连通 
        t[x].p=y;
    }
}
bool back(int x,int y){//y是否是x的后继 
    return t[x].ch[1]==y&&!t[y].ch[0];
}
void cut(int x,int y){// 如果x和y之间存在边,则删除该边
    makert(x);//将x变为root
    if(findrt(y)==x&&back(x,y)){
        t[x].ch[1]=t[y].p=0;
        pushup(x);
    }
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%d",&t[i].a); 
    } 
    while(m--){
        scanf("%d%d%d",&op,&x,&y);
        if(op==0){
            split(x,y);
            printf("%d\n",t[y].sum);
        }else if(op==1){
            link(x,y);
        }else if(op==2){
            cut(x,y); 
        }else{
            splay(x);
            t[x].a=y;
            pushup(x);
        }
    }
    return 0;
}

一些题

「HNOI2010」弹飞绵羊

就是将自己和可以跳到的点连边,询问就size咯

[SHOI2014]三叉神经树

每次更改一个子节点的信息,它的父亲能改变权值

当且仅当它的父亲还有 1 或 2 个点权值为 1

(这里的 1 或 2 个权值取决于叶子结点改变的权值)

那么再开一个 val 数组记录其儿子权值为 1 的个数

那么我们维护这棵树中叶子结点到根节点深度最大的 val1 或者 val2 的。

这个可以用 LCT ,每次维护的 splay 中右子树的深度大于他,左子树的深度小于他

那么直接在右子树中修改,这个点单调修改就行了(连续修改的区间)

用一个 tag 来维护是否需要修改

每次 access(x),这个时候 x 到根节点是一个 splay

直接在 x 上做文章就行了

要注意的细节:

  1. 每次都是从 fax​ 开始 splay 的,如果从叶子节点开始的话,那么维护的最大深度的 val 就没有任何意义了(叶子结点不应该维护 val )

  2. 这个 pushup 操作是跟 lsonrson 的顺序有关的,在 pushup 之前一定要 pushdown

「NOI2014」魔法森林

根据库鲁斯卡尔生成树算法,我们先按a关键字将边排序,动态加边

LCT里就存b最大值,动态更新答案

「WC2006」水管局长

我们发现删边特别难处理,那么怎样转化一下呢?

倒着处理所有询问,于是删边变成了加边。

然后查询所有路径上最大值的最小值,不难发现就是要维护这个图的最小生成树

然后就可以直接查询树路径上的最大值(用 lct

那么问题转化为了动态维护最小生成树

我们此时已经将询问倒过来处理了

那么假设我们已经维护好了一棵mst

每加一条边 uv,肯定会形成一个

因为是最小生成树,所以我们肯定要在环上去掉一条最大的边

于是处理加边操作流程如下:

  1. 查询uv链上最大边权mx

  2. 比较新加的边权 wmx 的大小关系,如果 w>mx ,则不做任何操作;

否则删去边权为 mx 的边(cut),加上uv这条边(link)。

那么查询就直接查链上最大值即可

本文作者:Yvette的博客

本文链接:https://www.cnblogs.com/yvette1217/p/17080643.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   _Youngxy  阅读(113)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起