【LCT】一步步地解释Link-cut Tree

简介

  Link-cut Tree,简称LCT。

  干什么的?它是树链剖分的升级版,可以看做是动态的树剖。

  树剖专攻静态树问题;LCT专攻动态树问题,因为此时的树剖面对动态树问题已经无能为力了(动态树问题通常夹杂着树的操作,如删边与连边。这是线段树无法应对的)。

  LCT难写吗?不难写啊!真的没有200行......

  让我们用简洁的写法来搞定LCT:只需要一些基础函数,再疯狂调用这些函数就好啦。


1. LCT概念

  树链剖分把树分成若干条重链,对于每条重链,用线段树来维护信息。利用各线段树的信息来得到答案。

  模仿一下:

    • LCT把树分成若干条重链

       这是假的重链!树剖是挑选重儿子来延续重链;而LCT的重链是随缘的......

       我们先不管这里的重链是怎么确定的,因为在LCT中,重链是可以随时更改的!(不要畏惧更改操作,一切都可以用基础函数的简单调用搞定)

    • $access(u)$,这是我们的更改操作。作用是将$u$到根节点的一路都变成重链(根节点的定义在下一节描述,此处先不必纠结),同时,原本的重链将会被断开,如图:

      

    • 对于每条重链,我们用一棵Splay来维护信息,利用各Splay的信息来得到答案。

2. 存储方式

  LCT是怎么存储的?

  很简单,我们不要把树看成树剖一样的形式,分开若干条重链用线段树维护,又拼起来。

  我们的每条重链的Splay,都是连在一起的,但又是相互独立的!看图:

  

  橙色边为每棵Splay,灰色边表示的是Splay之间的连接边。

  每棵Splay储存照常,Splay的中序遍历即重链节点从浅到深的排列。每棵Splay内节点的关系可能和原树不同,但是与其他Splay连边的节点没有改变。

  但只有每棵Splay的根节点能连向其他Splay的某个节点(灰色边)。Splay根节点$root$记录它的父亲是谁(有的Splay根节点$root$没有父亲),而它的父亲并不记录自己有这个儿子$root$。

  发现,每一个节点,都能够通过一直走父亲,走到某一个点,这个点就是上节提到的根节点,不同于Splay的根节点。

  


3. 基础函数(以下基本都是经典函数)

  我们需要一个函数来判断当前节点$u$是否为所属Splay的根节点:

    

bool isSplayRoot(int u){
    return ch[fa[u]][0]!=u&&ch[fa[u]][1]!=u;
}

  即父亲的左右儿子都不是自己,说明此节点是Splay的根节点,它的父亲并不记录自己。

  需要一个函数判断当前节点$u$是父亲节点的左儿子还是右儿子:

  

int who(int u){
    return ch[fa[u]][1]==u;
} 

  如果是左儿子,返回0;否则返回1。

  更新Splay信息函数,作用是收集左右儿子的信息。不需要对LCT如何在这么多数据结构间保证正确性表示质疑,只需大胆在里面写上自己要的更新函数,这里以最大值举例:

void update(int u){
    if(!u) return;
    inf[u]=max(w[u],max(inf[ch[u][0]],inf[ch[u][1]]));
}

  

  经典的Splay翻转打标记函数reverse、单次下传函数pushdownOnce、一路下传函数pushdown、旋转函数rotate和伸展函数splay,没有什么特殊的地方:

  

void reverse(int u){
    rev[u]^=1;
    swap(ch[u][0],ch[u][1]);
}//为u打上翻转标记
void pushdownOnce(int u){
    if(rev[u]){
        if(ch[u][0]) reverse(ch[u][0]);
        if(ch[u][1]) reverse(ch[u][1]);
        rev[u]=0;
    }
}//单次下传
void pushdown(int u){
    if(!isroot(u)) pushdown(fa[u]);
    pushdownOnce(u);
}//从当前Splay的根节点一路下传到u,把一路的翻转都处理掉
void rotate(int u){
    int f=fa[u],g=fa[f],c=who(u);
    if(!isroot(f))
        ch[g][who(f)]=u;
    fa[u]=g;
    ch[f][c]=ch[u][c^1]; fa[ch[f][c]]=f;
    ch[u][c^1]=f; fa[f]=u;
    update(f); update(u);
}//将当前节点u旋转到父亲节点
void splay(int u){
    pushdown(u);
    while(!isroot(u)){
        if(!isroot(fa[u]))
            rotate(who(fa[u])==who(u)?fa[u]:u);
        rotate(u);
    }
}//将u旋转到当前Splay的根节点

4. 重要函数: 

  $access(u)$,更改函数,把$u$到LCT根节点一路变成重儿子,同时断开一路上原来的重儿子:

  

void access(int u){
    for(int v=0;u;v=u,u=fa[u]){
        splay(u);
        ch[u][1]=v;
        update(u);
    }
}

  什么意思呢?外层for循环负责迭代从$u$一直到Splay根节点的路径,同时用$v$记录是从哪里来到$u$的。

  每到达一个点$u$,我们将$u$提到树根,这时$u$的右儿子就是在原本重链上$u$的重儿子。我们把它替换成过来的节点,并更新信息即可。

  $makeRoot(u)$,换根操作,使$u$成为LCT的根节点:

  

void makeRoot(int u){
    access(u);
    splay(u);
    reverse(u);
}

  换根换根,实际上影响到的是哪些因素呢?

  换根,仅仅是$u$到LCT根节点一路上的信息发生了父子反向,对于其它的Splay并没有影响。

  于是神奇的调用来了:

  1. 我们把$u$到LCT根节点一路变为重链,即把它们放到一棵Splay中;
  2. 将$u$旋转到Splay的根节点;
  3. 为$u$打上翻转标记(不要纠结怎么维护翻转标记,我们的基础函数已经保证了标记的下传不会乱、出错)。

  这样就为$u$到根节点的信息完成了父子反向操作。

  (我们之后可以慢慢体会到LCT的调用的巧妙和精美)

  

  $link(a,b)$,连接操作,更改树形,连接a和b两个节点,即连接a和b所在的两棵LCT(前提是a和b不在同一棵LCT中):

  

void link(int a,int b){
    makeRoot(a);
    fa[a]=b;
}

  我们将$a$变为$a$的LCT的根,然后将$a$的父亲设为$b$。这样就将$a$的整棵LCT连接到了$b$所在的LCT。

  $cut(a,b)$,切割操作,更改树形,分离a和b两个节点,即分割出两棵独立的LCT(前提是a和b在同一棵LCT中且a和b相邻):

  

void cut(int a,int b){
    makeRoot(a);
    access(b);
    splay(b);
    fa[a]=0;
    ch[b][0]=0;
    update(b);
}

  我们将$a$变成LCT的根,然后将$b$到LCT根节点(也就是$a$)一路变为重链,再将$b$旋转到所在Splay的根。

  由于$a$和$b$同在一棵Splay中且$a$一定是$b$的父亲,所以Splay中$b$的左儿子一定是$a$,断开即可,记得更新,因为有了父子关系变化。

  $isConnect(a,b)$,实现判断a和b是否在同一棵LCT中:

bool isConnect(int a,int b){
    if(a==b) return true;
    makeRoot(a);
    access(b);
    splay(b);
    return fa[a];
}

  我们将$a$变成LCT的根,然后将$b$到LCT根节点(也就是$a$)一路变为重链,再将$b$旋转到所在Splay的根。(怎么和上面很像呢哈哈)

  如果$a$和$b$不在同一棵LCT中,执行$makeRoot(a)$后,$a$的父亲应该为空($makeRoot$最后有一个$splay(u)$的操作将$u$旋转到树根)。

  除非什么情况呢?除非a和b在同一棵LCT中,在$access(b)$并$splay(b)$后,$a$与$b$应该在同一棵Splay中,既然$b$为Splay根,那么$a$肯定不为Splay根,一定有一个父亲存在。

  至此,LCT的最常用函数已经介绍完毕,下面我们来总结一下最根本的核心思想:

  可以发现$access(u)$和$splay(u)$总是配套出现,有时在前面配上$makeRoot$。这一套COMBO可以将$u$转到Splay树根,然后进行如同Splay一样的便捷操作。

   比如想求$a$到$b$的点权之和,我们可以$makeRoot(a)  +  access(b)  +  splay(b)$,此时$a$和$b$一定在同一条重链、同一棵Splay中,然后我们统计$b$和$b$的左子树的点权之和就可以了。

  


总结

  久仰LCT,总觉得特别难,但是理解以后就会觉得很有意思。一些处理信息、调用函数的思想,值得我们更多地推敲。

时间: 2024-10-24 15:39:15

【LCT】一步步地解释Link-cut Tree的相关文章

LuoguP3690 【模板】Link Cut Tree (动态树) LCT模板

P3690 [模板]Link Cut Tree (动态树) 题目背景 动态树 题目描述 给定n个点以及每个点的权值,要你处理接下来的m个操作.操作有4种.操作从0到3编号.点从1到n编号. 0:后接两个整数(x,y),代表询问从x到y的路径上的点的权值的xor和.保证x到y是联通的. 1:后接两个整数(x,y),代表连接x到y,若x到y已经联通则无需连接. 2:后接两个整数(x,y),代表删除边(x,y),不保证边(x,y)存在. 3:后接两个整数(x,y),代表将点x上的权值变成y. 输入输出

AC日记——【模板】Link Cut Tree 洛谷 P3690

[模板]Link Cut Tree 思路: LCT模板: 代码: #include <bits/stdc++.h> using namespace std; #define maxn 300005 int n,m,val[maxn]; int top,ch[maxn][2],f[maxn],xr[maxn],q[maxn],rev[maxn]; inline void in(int &now) { int if_z=1;now=0; char Cget=getchar(); while

脑洞大开加偏执人格——可持久化treap版的Link Cut Tree

一直没有点动态树这个科技树,因为听说只能用Splay,用Treap的话多一个log.有一天脑洞大开,想到也许Treap也能从底向上Split.仔细思考了一下,发现翻转标记不好写,再仔细思考了一下,发现还是可以写的,只需要实时交换答案二元组里的两棵树,最后在吧提出来的访问节点放回去就行了.本着只学一种平衡树的想法,脑洞大开加偏执人格的开始写可持久化Treap版的Link Cut Tree... 写了才发现,常数硕大啊!!!代码超长啊!!!因为merge是从上到下,split从下到上,pushdow

HDOJ 题目3966 Aragorn&#39;s Story(Link Cut Tree成段加减点权,查询点权)

Aragorn's Story Time Limit: 10000/3000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Submission(s): 5505    Accepted Submission(s): 1441 Problem Description Our protagonist is the handsome human prince Aragorn comes from The Lor

Link Cut Tree学习笔记

从这里开始 动态树问题和Link Cut Tree 一些定义 access操作 换根操作 link和cut操作 时间复杂度证明 Link Cut Tree维护链上信息 Link Cut Tree维护子树信息 小结 动态树问题和Link Cut Tree 动态树问题是一类要求维护一个有根树森林,支持对树的分割, 合并等操作的问题. Link Cut Tree(林可砍树?简称LCT)是解决这一类问题的一种数据结构. 一些无聊的定义 Link Cut Tree维护的是动态森林中每棵树的任意链剖分. P

P3690 【模板】Link Cut Tree (动态树)

P3690 [模板]Link Cut Tree (动态树) https://www.luogu.org/problemnew/show/P3690 分析: LCT模板 代码: 注意一下cut! 1 #include<cstdio> 2 #include<algorithm> 3 4 using namespace std; 5 6 const int N = 300100; 7 8 int val[N],fa[N],ch[N][2],rev[N],sum[N],st[N],top;

Codeforces Round #339 (Div. 2) A. Link/Cut Tree

A. Link/Cut Tree Programmer Rostislav got seriously interested in the Link/Cut Tree data structure, which is based on Splay trees. Specifically, he is now studying the expose procedure. Unfortunately, Rostislav is unable to understand the definition

bzoj2049 [Sdoi2008]Cave 洞穴勘测 link cut tree入门

link cut tree入门题 首先说明本人只会写自底向上的数组版(都说了不写指针.不写自顶向下QAQ……) 突然发现link cut tree不难写... 说一下各个函数作用: bool isroot(int x):判断x是否为所在重链(splay)的根 void down(int x):下放各种标记 void rotate(int x):在x所在重链(splay)中将x旋转到fa[x]的位置上 void splay(int x):在x坐在重链(splay)中将x旋转到根 void acce

link cut tree 入门

鉴于最近写bzoj还有51nod都出现写不动的现象,决定学习一波厉害的算法/数据结构. link cut tree:研究popoqqq那个神ppt. bzoj1036:维护access操作就可以了. #include<cstdio> #include<cstring> #include<cctype> #include<algorithm> #include<queue> using namespace std; #define rep(i,s,

Link Cut Tree(无图慎入)

类似树链剖分(其实直接记住就可以了),提前放代码 1 #include<cstdio> 2 #include<cstdlib> 3 #include<iostream> 4 #include<algorithm> 5 #include<cstring> 6 #include<climits> 7 #include<cmath> 8 #define N (int)(3e5+5) 9 using namespace std;