【暖*墟】#数据结构# LCT的学习与练习

 一. 概念总结

【 Link-Cut Tree 】一种 动态维护森林上的信息 的数据结构,适用于动态树问题。

采用类似树链剖分的轻重边路径剖分,把树边分为实边和虚边,并用 Splay 来维护每一条实路径。

LCT用很多个splay维护森林的信息。因为splay是二叉树,所以要将原森林”剖分”成很多个二叉树。

于是就有实边和虚边。用实边连接起来的一棵树就是原森林中的一棵树,我们称它为原树。

按每个点在原树中的深度为优先级,将每个点以优先级的中序遍历放到splay上。

那么:每一个Splay维护的是一条从上到下按在原树中深度严格递增的路径,

如果中序遍历Splay,得到的每个点的深度序列严格递增。

我们一般将原树所对应的splay称为辅助树,原森林就对应一个辅助树森林。

那么 Link-Cut Tree 的基本操作复杂度为均摊 O(log⁡n) 。            ----- 来自 各种 dalao orz

显然原树中同一个深度的点是不可能在一个splay里的,因此每个splay里面就是维护了原树中的一条链。

每棵 Splay 之间都用"虚边"连接(下图灰边),每棵 Splay 中的结点都用"实边"链接起来(下图黑边)。

假如我们现在有一个例子:(用 红色圈圈 圈在一起的结点是一个 Splay 中的结点)

LCT

一开始,每个结点都是一颗 Splay,就像这样:

LCT

如果将1,2连接起来,那么1,2就在同一个 Splay 中:

LCT

二. 基本定义

fa[x]:结点x的爸爸(father)

v[x]:结点x的权值(value)

sum[x]:结点x及它的子树的权值和(sum)

rev[x]:结点x的翻转情况(rev)

ch[x][0/1]:结点x的左/右儿子

三. 相关操作

  • Link-Cut Tree 支持的基本操作

Access(x):将x到根节点的路径上全部变成实边,并弃掉自己所有的儿子。

(变成虚边:认父不认子)(每一个父结点对于自己的每个子结点只有一条实边)

findroot(x):找出x所在的原树的根结点(实际上就是上图的一号点)。

makeroot(x):将x点变为原树的根节点;split(x,y):将x,y节点放在一个 Splay 中,方便操作。

link(x,y):将x和y所在原树合并起来(树的连接);cut(x,y):将x和y所在原树拆开(树的切断) 。

  • Access(x)

将点x到原树中根结点root之间的链,放到一个辅助树splay中。

即:将x到根节点的路径上全部变成实边,并弃掉自己所有的儿子。

LCT

执行 Access(6) 。即:将{1--3,3--6}变成实边,1-2变成虚边

假设6有一儿子n,之间用实边连着,那么这条边也将变成虚边

即:每次将 xxx 点 splay 到当前所在辅助树的根节点,将它的右儿子更新为上一个 xxx ,

然后令 xxx 跳到它的父节点,不断重复进行......特别的,第一个 xxx 的右儿子设为0(NULL)。

   Q:为什么是右儿子而不是左儿子呢?

   A:因为f[x]的深度小于x,而在Splay里面f[x]是x的爸爸,所以x在Splay中是f[x]的右儿子。

所以就变成了这样:

LCT

1.转到根。 2.换右儿子。3.更新信息。4.当前操作点切换为轻边所指的父亲,转1。

我真的不知道自己到底是哪里来的勇气...觉得自己能学会LCT???

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
#include<queue>
#include<vector>
#include<cmath>
#include<map>
using namespace std;
typedef long long ll;

/* p3690【模板】Link Cut Tree(动态树) */

/* 给定n个点以及每个点的权值,处理m个操作。
0:询问从x到y路径上的点权xor值。保证x到y是联通的。
1:连接x到y。若x到y已经联通,则无需连接。
2:删除边(x,y)。不保证边(x,y)存在。3:将点x上的权值变成y。 */

void reads(int &x){ //读入优化(正负整数)
    int fx=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')fx=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
    x*=fx; //正负号
}

const int N=300019;

int fa[N],ch[N][2],v[N],s[N],sta[N]; bool r[N];

//【维护の基本操作】////////////////////////////////////

bool nroot(int x){ //判断节点是否为一个Splay的根(与普通Splay的区别1)
    return ch[fa[x]][0]==x||ch[fa[x]][1]==x;
} //原理:如果连的是轻边,他的父亲的儿子里没有它。

void push_up(int x){ s[x]=s[ch[x][0]]^s[ch[x][1]]^v[x]; } //上传信息
    
void push_rev(int x){int t=ch[x][0];ch[x][0]=ch[x][1],ch[x][1]=t;r[x]^=1;} //翻转操作

void push_down(int x){ //判断并释放懒标记
    if(r[x]){ if(ch[x][0])push_rev(ch[x][0]);
        if(ch[x][1])push_rev(ch[x][1]); r[x]=0; } }

//【splay基本操作】/////////////////////////////////////

void rotate(int x){ //一次旋转
    int y=fa[x],z=fa[y],k=(ch[y][1]==x),w=ch[x][!k];
    if(nroot(y)) ch[z][ch[z][1]==y]=x; ch[x][!k]=y; ch[y][k]=w;
    //↑↑额外注意if(nroot(y))语句,此处不判断会引起致命错误(与普通Splay的区别2)
    if(w) fa[w]=y; fa[y]=x; fa[x]=z; push_up(y);
}

void splay(int x){ //所有操作的目标都是该Splay的根(与普通Splay的区别3)
    int y=x,z=0; sta[++z]=y;
    //sta为栈,暂存当前点到根的整条路径,push_down时一定要从上往下放标记(与普通Splay的区别4)
    while(nroot(y)) sta[++z]=y=fa[y];
    while(z) push_down(sta[z--]);
    while(nroot(x)){ y=fa[x];z=fa[y];
        if(nroot(y)) rotate((ch[y][0]==x)^(ch[z][0]==y)?x:y); rotate(x);
    } push_up(x);
}

//【1】///////////////////////////////////////////////

void access(int x){ for(int y=0;x;x=fa[y=x]) splay(x),ch[x][1]=y,push_up(x);}

/*  access(x):将根节点到x上的边都变为实边。
    1.将节点转到所属Splay的根。//splay(x)
    2.将其右儿子删除-->删一个Splay​的根节点。//ch[x][1]=y
    3.更新节点信息。//push_up(x)
    4.将当前点变为虚边所指的父亲,转回步骤1。//x=fa[y=x]    */

//【2】///////////////////////////////////////////////

void makeroot(int x){ access(x); splay(x); push_rev(x); }

/*  makeroot(x):将x成为[原树]的根节点(换根)。
    1.进行access(x),得到一条从根节点到x的链,x在这个Splay中深度最大。
    2.这个Splay中,没有比x深度更大的点,即x没有右子树。
      直接翻转整个Splay,使得所有点的深度都倒过来。//splay(x);
      那么x就没有了左子树,x成为深度最小的点,也就是根节点。
    3.最后,给这个Splay​打上翻转标记。//push_rev(x);           */

//【3】///////////////////////////////////////////////

int findroot(int x){ //找在原树中的根
    access(x); splay(x);
    while(ch[x][0]) push_down(x),x=ch[x][0];
    splay(x); return x; }

/*  findroot(x):找到原树中的根,用于判断两点之间的连通性。 
    1.一棵树的根节点一定是深度最小的点。用access(x)把x和根连成一条链。
    2.用splay(x)将x旋转到Splay的根节点。//splay(x);
    3.根节点一定是x不断往左走得到的(越往左深度越小)。//x=ch[x][0];
    4.在往左走的过程中,一定要下传标记。//push_down(x);             */

//【4】///////////////////////////////////////////////

void split(int x,int y){ makeroot(x),access(y),splay(y); } //(0)

/*  split(x,y):提取路径。得到x到y的一条路径,其中y是此路径所在Splay的根节点。
    1.先把x作为根节点; 2.得到根节点到y的链; 3.将y旋转到Splay的根。 */

//【5】///////////////////////////////////////////////

void link(int x,int y){ makeroot(x); if(findroot(y)!=x) fa[x]=y; } //(1)

/*  link(x,y):连一条虚边(x,y)(如果已经连通则不操作)。
    1.将x变成原树的根,将x的父节点直接设为y(这样x、y就相连了)。
    2.在findroot(y)中执行了access(y)和splay(y),y已经是所在Splay​的根节点。
    3.连通性的检查:x成为根节点后,如果findroot(y)==x,则说明x,y连通。      */

//【6】///////////////////////////////////////////////

void cut(int x,int y){ makeroot(x);
    if(findroot(y)==x&&fa[y]==x&&!ch[y][0])
     { fa[y]=ch[x][1]=0; push_up(x); } } //(2)

/*  cut(x,y):切断边(x,y)(如果没有边则不进行操作)。
    先把x变成根节点。如果存在边(x,y),那么x的深度一定比y小,则x是y的左儿子。
    
    【判断】若不存在边(x,y)​:1.x,y不在同一棵树内;2.x的父亲不是y,即x,y没有连边。
    3.x的右子树非空,那么以y为根的中序遍历中x和y不相邻,即没有边相连。*/

//【主程序】///////////////////////////////////////////////

int main(){
    int n,m; reads(n),reads(m);
    for(int i=1;i<=n;i++) reads(v[i]);
    while(m--){ int op,x,y; reads(op),reads(x),reads(y);
        if(op==0) split(x,y),printf("%d
",s[y]);
        if(op==1) link(x,y); //连一条虚边(x,y)
        if(op==2) cut(x,y); //切断边(x,y)
        if(op==3) splay(x),v[x]=y; //把x旋转到根再修改
    }
}

                                  ——时间划过风的轨迹,那个少年,还在等你

原文地址:https://www.cnblogs.com/FloraLOVERyuuji/p/10419422.html