[2018.12.19]动态树LCT

戏子无情 提交于 2020-02-11 04:44:18

总算学会了...NOIp2018之前就开始学了...模板一直过不去...

需要先学会Splay

现在切入正题。

先放模板题链接

LCT的概况

一种数据结构。(废话)

可以均摊\(O(logn)\)维护一个森林,支持的树上任意路径的查询,两棵树的连接、断开,单点的修改。

LCT的实现基于Splay。

大概长成下面这样↓

(图片来自这位dalao的博客,下同)

LCT中的边分为实边和虚边,每一个节点连向它的孩子的边中只有一条实边。

实边需要记录在这条边的两个端点上(即孩子记父亲,父亲记孩子),虚边只需要记录在孩子上(即孩子记父亲即可)。

其中实边(实线的边)连接的一组点构成一个Splay,大小依据为深度。

Splay的根的父亲指针指向这棵Splay中深度最小的节点在原树中的父亲(这是一条虚边)。

比如上面这个LCT的Splay长这样(当然和Splay的形态没关系)↓

具体操作

LCT中的Splay必须维护的是翻转标记,其他的视题目而定。

首先是一个前置操作,判断点\(y\)是否是Splay的根(因为即使是Splay的根也可能有在别的Splay中有父亲,所以并不是没有父亲的才是根):

bool Isroot(int x){
    int y=t[x].f;
    return !(t[y].c[0]==x||t[y].c[1]==x);
}

本文的Splay节点定义如下:

struct node{
    int f,c[2],tag,...;
    //f是父亲,c[0/1]是左/右孩子,tag是翻转标记
}t[Size];

Splay的操作有些会有所修改,如下:

上/下传(并没有不一样):

void Upd(int x){//视具体情况而定
    //something...
}
void Psd(int x){//主要是下传翻转标记
    if(t[x].tag){
        swap(t[x].c[0],t[x].c[1]);
        if(t[x].c[0])t[t[x].c[0]].tag^=1;
        if(t[x].c[1])t[t[x].c[1]].tag^=1;
        t[x].tag=0;
    }
}

旋转:

void Rotate(int x){
    int y=t[x].f,z=t[y].f;
    Psd(y);
    Psd(x);
    int c=t[y].c[1]==x;
    if(!Isroot(y)){//注意这里要判断y是否为Splay的根(否则z可能在另一颗Splay中或者不存在)
        int gc=t[z].c[1]==y;
        t[z].c[gc]=x;
    }
    t[x].f=z;
    t[y].c[c]=t[x].c[c^1];
    t[t[x].c[c^1]].f=y;
    t[x].c[c^1]=y;
    t[y].f=x;
    Upd(y);
    Upd(x);
}

伸展:

void Splay(int x){
    Psd(x);
    while(!Isroot(x)){//这里同上
        int y=t[x].f,z=t[y].f;
        if(Isroot(y))Rotate(x);
        else{
            Psd(z);
            Psd(y);
            int c=t[y].c[1]==x,gc=t[z].c[1]==y;
            if(c==gc)Rotate(y);
            else Rotate(x);
            Rotate(x);
        }
    }
}

接下来终于开始讲LCT了

1.访问操作\(Access(x)\)

使\(x\)与当前LCT的根存在由实边组成的路径,并且\(x\)是路径中深度最大的点。

之后所有操作的基础。

我们从\(x\)开始,一路向父亲寻找,每次将节点伸展为所在Splay的根,将该节点的父亲的所有连向儿子的边变成虚边,将该节点的父亲和节点本身的边变成实边(该节点是其父亲的右儿子,因为深度大于它的父亲)。

当然为了保证\(x\)是深度最大的,一开始要把\(x\)的右儿子断开。

我个人喜欢递归实现,调用方式为\(Access(x,0)\)

code:

void Access(int x,int lst){
    if(!x)return;
    Splay(x);
    t[x].c[1]=lst;
    Upd(x);
    Access(t[x].f,x);
}

2.换根操作\(Makeroot(x)\)

\(x\)换为LCT的根。

我们需要\(Access(x)\),此时\(x\)与LCT的根直接相连,而且是它到根的路径上深度最大的。

我们要让它变成根,就是路径中深度最小的点,怎么办?

直接翻转Splay不就好了。。。

为了确定翻转标记标记的位置,我们再将\(x\)换为所在Splay的根。

code:

void Makeroot(int x){
    Access(x,0);
    Splay(x);
    t[x].tag^=1;
}

3.??操作\(Split(x,y)\)

难以概括的操作。。。就是将\(x\)\(y\)的路径提取出来方便询问。

\(x\)换为LCT的根,再\(Access(y)\),就得到了\(x\)\(y\)的路径。

\(Splay(y)\),就可以在\(y\)节点上获取路径信息了。

code:

void Split(int x,int y){
    Makeroot(x);
    Access(y,0);
    Splay(y);
}

4.找根操作\(Findroot(x)\)

找到\(x\)所在LCT的根,主要用于判断两点联通性。

\(Access(x)\),将\(x\)和它在LCT中的根放在同一个Splay中,再\(Splay(x)\),将\(x\)变为所在Splay的根。

然后\(x\)所在LCT的根必然是深度最小的,所以一直跳左孩子即可。

(记得下传标记哦)

code:

int Findroot(int x){
    Access(x,0);
    Splay(x);
    Psd(x);
    while(t[x].c[0]){
        x=t[x].c[0];
        Psd(x);
    }
    Splay(x);//维持Splay的平衡
    return x;
}

5.连接操作\(Link(x,y)\)

\(x,y\)之间连一条边。

首先如果\(x,y\)已经联通(\(Findroot(x)=Findroot(y)\)),操作不合法,直接退出即可。

否则,让\(x\)成为所在LCT的根,将它和\(y\)之间连一条虚边(即让\(x\)的父亲为\(y\))。

code:

void Link(int x,int y){
    Makeroot(x);
    if(Findroot(y)!=x)t[x].f=y;
}

6.断边操作\(Cut(x,y)\)

断开\(x\)\(y\)之间的边(如果有的话)。

\(Makeroot(x)\)后,\(x\)是LCT的根,而\(y\)深度必然大于\(x\),所以会在右子树中,深度相差1(如果存在这条边),即在Splay中\(x\)的右孩子。

如果\(y\)\(x\)直接连边,那么首先判断联通性,然后看看\(y\)是否是\(x\)的右孩子,并且\(y\)没有左孩子(如果有左孩子说明\(x\)\(y\)之间还有点)。

存在的话直接断边就好了。

code:

void Cut(int x,int y){
    Makeroot(x);
    Psd(x);
    if(Findroot(y)==x&&t[y].f==x&&!t[y].c[0]){
        t[x].c[1]=t[y].f=0;
        Upd(x);
    }
}

结束。

最后给出模板题代码:

#include<bits/stdc++.h>
using namespace std;
struct node{
    int f,c[2],v,xs,tag;
}t[300010];
int n,m,op,u,v;
void PrintLCT(){
    for(int i=1;i<=n;i++){
        cout<<t[i].f<<"<-node "<<i<<" l->"<<t[i].c[0]<<" r->"<<t[i].c[1]<<"\n";
    }
}
bool Isroot(int x){
    int y=t[x].f;
    return !(t[y].c[0]==x||t[y].c[1]==x);
}
void Upd(int x){
    t[x].xs=t[x].v^t[t[x].c[0]].xs^t[t[x].c[1]].xs;
}
void Psd(int x){
    if(t[x].tag){
        swap(t[x].c[0],t[x].c[1]);
        if(t[x].c[0])t[t[x].c[0]].tag^=1;
        if(t[x].c[1])t[t[x].c[1]].tag^=1;
        t[x].tag=0;
    }
}
void Rotate(int x){
    int y=t[x].f,z=t[y].f;
    Psd(y);
    Psd(x);
    int c=t[y].c[1]==x;
    if(!Isroot(y)){
        int gc=t[z].c[1]==y;
        t[z].c[gc]=x;
    }
    t[x].f=z;
    t[y].c[c]=t[x].c[c^1];
    t[t[x].c[c^1]].f=y;
    t[x].c[c^1]=y;
    t[y].f=x;
    Upd(y);
    Upd(x);
}
void Splay(int x){
    Psd(x);
    while(!Isroot(x)){
        int y=t[x].f,z=t[y].f;
        if(Isroot(y))Rotate(x);
        else{
            Psd(z);
            Psd(y);
            int c=t[y].c[1]==x,gc=t[z].c[1]==y;
            if(c==gc)Rotate(y);
            else Rotate(x);
            Rotate(x);
        }
    }
}
void Access(int x,int lst){
    if(!x)return;
    Splay(x);
    t[x].c[1]=lst;
    Upd(x);
    Access(t[x].f,x);
}
void Makeroot(int x){
    Access(x,0);
    Splay(x);
    t[x].tag^=1;
}
void Split(int x,int y){
    Makeroot(x);
    Access(y,0);
    Splay(y);
}
int Findroot(int x){
    Access(x,0);
    Splay(x);
    Psd(x);
    while(t[x].c[0]){
        x=t[x].c[0];
        Psd(x);
    }
    Splay(x);
    return x;
}
void Link(int x,int y){
    Makeroot(x);
    if(Findroot(y)!=x)t[x].f=y;
}
void Cut(int x,int y){
    Makeroot(x);
    Psd(x);
    if(Findroot(y)==x&&t[y].f==x&&!t[y].c[0]){
        t[x].c[1]=t[y].f=0;
        Upd(x);
    }
}
int Query(int x,int y){
    Split(x,y);
    return t[y].xs;
}
void Change(int x,int y){
    Splay(x);
    t[x].v=y;
    Upd(x);
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%d",&t[i].v);
        t[i].xs=t[i].v;
    }
    for(int i=1;i<=m;i++){
        scanf("%d%d%d",&op,&u,&v);
        if(op==0)printf("%d\n",Query(u,v));
        else if(op==1)Link(u,v);
        else if(op==2)Cut(u,v);
        else Change(u,v);
    }
    return 0;
}
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!