前置技能

splay: 必须

树链剖分: 可选,知道树链剖分会容易理解一些。

以下大部分图片来自https://blog.csdn.net/saramanda/article/details/55253627

Link Cut Tree(LCT)

LCT,又叫林克-卡特树,可以用来解决动态树问题。

LCT显然是一棵树,它长这样:

lct-1

这上面有一些粗一点的边,我们把它称为重边;还有一些细一点的,我们把它称为轻边。(就像树链剖分的定义)

每个点连向儿子的重边最多只能有1条(可以没有),因此所有的重边能构成树上的一条一条链,叫做重链

连到重边的儿子叫做嫡长子**重儿子**。

例如上面这个图,1-2-5, 3-7, 4, 6, 8就分别是这棵树的重链。

但是与树链剖分不同的是,LCT中的重边和轻边是可以不断变化的,甚至边/根也可以不断变化,因此我们需要一种灵活一点的数据结构来维护重链,那就是splay啦。

LCT节点的最基本的定义:

struct node
{
    node *son[2],*fa;
    int rev;
};

splay的维护

一棵splay维护LCT的一条重链,splay维护的关键字是点的深度,一个点深度越浅,在平衡树中的位置就越靠左。

那么LCT就可以视为一棵splay森林,如下:

lct-2

其中三角形代表一棵splay,也代表了原树中的一条重链。

箭头什么意思呢?箭头指向重链最顶端的点的父亲,但是起始点是代表重链的splay的root,可以理解为splay的父亲,但是splay的父亲不记录它有这个儿子。(这条边是单向的)

例如在第一个图中,3-7这条重链的splay的父亲就是1,但是1的左儿子或右儿子并没有记录3-7这条重链的任何一个点。

那么如何判定一个节点是所在splay的根呢?

我们可以写一个isroot函数,返回1代表是所在splay的根,0代表不是:

int isroot(int x)
{
    if(x->fa==NULL)
    {
        return 1;
    }
    return !((x->fa->son[0]==x)||(x->fa->son[1]==x));
}

access

这个词好像没啥好的翻译,不妨称为废嫡立庶

用法:access(x)

含义:专门打通一条从真实的*LCT树上开辟的从根到x的重链,专门用一棵splay维护。与这条路径上的点相连的任何其他重边都会被强行变成轻边*。

做法:

  1. 记录一个y,初始时y为NULL.

  2. 取x所在splay,将x旋到根后,删除右儿子。

    这样做的目的是为了删除x的重儿子。(废嫡

  3. 将y接到x的右儿子上,这样就把x和y所在的重链接到一起了(立庶

  4. 将y=x,x变成所在splay的父亲。回到第2步。

这样就打通了一条从root到x的重链。

代码:

int access(node *x)
{
  node *y=NULL;
  while(x!=NULL)
    {
      splay::splay_root(x);
      splay::pushdown(x);
      x->son[1]=y; //这里用y自动把x的右儿子冲掉了
      if(y!=NULL)
        {
          y->fa=x;
        }
      //如果LCT维护了信息,记得在这里更新
      //splay::updata(x);
      y=x;
      x=y->fa;
    }
  return 0;
}

makeroot

用法:makeroot(x)

含义:将x变成整棵LCT的根,一般用于与access配合,形成打通任意两个点的效果。

方法:

将x变成根,只需要将从root到x的父子关系反向。

首先access(x),这样就有一条从root到x的重链。

makeroot-1

如图,可以发现这棵树上root到x的点构成了一棵splay,由于splay的关键字是深度,因此只需要将splay翻转,就将x变成了LCT的根。

makeroot-2

代码:

int makeroot(node *x)
{
  access(x);
  splay::splay_root(x); //将x提到splay的根,这样只需要在x处打一个reverse标记就代表翻转了splay
  x->rev^=1;
  return 0;
}

用法:link(x,y)

含义:新建一条边x-y,将x所在LCT和y所在LCT连起来。

方法:

首先makeroot(y),这时y所在LCT的根就变成了y,然后直接将y的父亲指向x。

代码:

  int link(node *x,node *y)
  {
    makeroot(x);
    x->fa=y;
    return 0;
  }

cut

用法:cut(x,y)

含义:删除边x-y,必须保证x-y这条边存在。(不存在的话加个特判就可以了)

方法:

将x变成根,access(y),这时x和y所在splay中只有x,y两个点。可以直接断开这两个点的联系。

代码:

int cut(node *x,node *y)
{
  makeroot(x);
  access(y); //现在x和y的splay中只有x,y两个点了
  splay::splay_root(y); //将y作为splay的根
  splay::pushdown(y);
  y->son[0]=x->fa=NULL; //删掉x和y之间的边
  //splay::updata(y); //如果lct维护了信息,记得在这里更新
  return 0;
}

getval

用法:getval(x,y)

含义:查询x到y的树上路径中存储的信息

方法:

makeroot(y),access(x),此时x,y的splay中的所有节点就是x-y路径上的节点。

代码:

int getval(node *x,node *y)
{
  makeroot(y);
  access(x);
  splay::splay_root(x);
  return x->val;
}

推荐的题目

BZOJ 2049 [Sdoi2008]Cave 洞穴勘测

这题只有link和cut两个操作,比较水

BZOJ 3282 Tree

LCT真板子题

Qtree系列

据说都可以用LCT写?