LCT学习笔记

最近自学了一下LCT(Link-Cut-Tree),参考了Saramanda及Yang_Zhe等众多大神的论文博客,对LCT有了一个初步的认识,LCT是一种动态树,可以处理动态问题的算法。对于树分治中的树链剖分,只能处理静态的数据或者在轻重链上的边或点的权值,对于其他动态的处理就毫无办法了。因此我们就需要引入LCT这个东西。那么问题来了,LCT到底是什么呢?我弄了很久总算是理解了LCT,打算总结一下LCT的基本操作。

①浅谈对LCT的初步印象

LCT用来维护动态的森林,以及一些链上操作,是处理节点无序的有根树组成的森林,进行一些列操作(例如合并,剪切,翻转,更新·····)

对于一棵树的操作,我们可以用splay维护;同理,对于一片森林,也可以用多棵splay维护,而LCT就是要把这些森林的splay联系在一起。而LCT的核心就是access操作啦!

看完之后我们知道,LCT和静态的树链剖分很像。怎么说呢?这两种树形结构都是由若干条长度不等的“重链”和“轻边”构成(名字可以不同,大概就是这个意思),“重链”之间由”轻边”连接。就像这样:

LCT学习笔记

可以想象为一棵树被人为的砍成了一段段。

  LCT和树链剖分不同的是,树链剖分的链是不会变化的,所以可以很方便的用线段树维护。但是,既然是动态树,那么树的结构形态将会发生改变,所以我们要用更加灵活的维护区间的结构来对链进行维护,不难想到Splay可以胜任。如何分离树链也是保证时间效率的关键(链的数量和长度要平衡),树链剖分的“重儿子”就体现了前人博大精深的智慧。

  在这里解释一下为什么要把树砍成一条条的链:我们可以在logn的时间内维护长度为n的区间(链),所以这样可以极大的提高树上操作的时间效率。在树链剖分中,我们把一条条链放到线段树上维护。但是LCT中,由于树的形态变化,所以用能够支持合并、分离、翻转等操作的Splay维护LCT的重链(注意,单独一个节点也算是一条重链)。

  这时我们注意到,LCT中的轻边信息变得无法维护。为什么呢?因为Splay只维护了重链,没有维护重链之间的轻边;而LCT中甚至连根都可以不停的变化,所以也没法用点权表示它父边的边权(父亲在变化)。所以,如果在LCT中要维护边上信息,个人认为最方便的方法应该是把边变成一个新点和两条边。这样可以把边权的信息变成点权维护,同时为了不影响,把真正的树上节点的点权变成0,就可以用维护点的方式维护边。

②再谈LCT的各种操作

  LCT中用Splay维护链,这些Splay叫做“辅助树“。辅助树以它上面每个节点的深度为关键字维护,就是辅助树中每个节点左儿子的深度小于当前节点的深度,当前节点的深度小于右儿子的深度。

  可以把LCT认为是一个由Splay组成的森林,就像这样:(三角形代表一棵Splay,对应着LCT上一条链)

LCT学习笔记

箭头是什么意思呢?箭头记录着某棵Splay对应的链向上由轻边连着哪个节点,可以想象为箭头指向“Splay 的父亲”。但是,Splay的父亲并不记录有这个儿子,即箭头是单向的。同时,每个节点要记录它是否是它所在的Splay的根。这样,Splay构成的森林就建成了。

这个是我的Splay节点最基本的定义:(如果要维护更多信息就像Splay维护区间那样加上更多标记)

 struct node{
int fa,ch[]; //父亲和左右儿子。
bool reverse,is_root; //区间反转标记、是否是所在Splay的根
}T[maxn];

LCT中基本的Splay上操作:

 int getson(int x){
return x==T[T[x].fa].ch[];
}
void pushreverse(int x){
if(!x)return;
swap(T[x].ch[],T[x].ch[]);
T[x].reverse^=;
}
void pushdown(int x){
if(T[x].reverse){
pushreverse(T[x].ch[]);
pushreverse(T[x].ch[]);
T[x].reverse=false;
}
}
void rotate(int x){
if(T[x].is_root)return;
int k=getson(x),fa=T[x].fa;
int fafa=T[fa].fa;
pushdown(fa);pushdown(x); //先要下传标记
T[fa].ch[k]=T[x].ch[k^];
if(T[x].ch[k^])T[T[x].ch[k^]].fa=fa;
T[x].ch[k^]=fa;
T[fa].fa=x;
T[x].fa=fafa;
if(!T[fa].is_root)T[fafa].ch[fa==T[fafa].ch[]]=x;
else T[x].is_root=true,T[fa].is_root=false;
//update(fa);update(x); //如果维护了信息,就要更新节点
}
void push(int x){
if(!T[x].is_root)push(T[x].fa);
pushdown(x);
}
void Splay(int x){
push(x); //在Splay到根之前,必须先传完反转标记
for(int fa;!T[x].is_root;rotate(x)){
if(!T[fa=T[x].fa].is_root){
rotate((getson(x)==getson(fa))?fa:x);
}
}
}

access操作:

这是LCT最核心的操作。其他所有操作都要用到它。

他的含义是”访问某节点“。作用是:对于访问的节点x,打通一条从树根(真实的LCT树)到x的重链;如果x往下是重链,那么把x往下的重边改成轻边。可以理解为专门开辟一条x到根的路径,由一棵Splay维护这条路径。

access之前:(粗的是重链)

LCT学习笔记

access之后:

LCT学习笔记

access实现的方式很简单:先把x旋转到所在Splay的根,然后把x的右孩子的is_root设为true(此时右孩子对应的是x下方的重链,这样就断开了x和下方的重链)。用y记录上一次的x(初始化y=0),把y接到x的右孩子上,这样就把上一次的重链接到了当前重链一起,同时记得T[y].is_root=false。记录y=x,然后x=T[x].fa,把x上提。重复上面的步骤直到x=0。

实现代码如下:

 void access(int x){
int y=;
do{
Splay(x);
T[T[x].ch[]].is_root=true;
T[T[x].ch[]=y].is_root=false;
//update(x); //如果维护了信息记得更新。
x=T[y=x].fa;
}while(x);
}

mroot操作:

  这个操作的作用是把某个节点变成树根(这里的根指的是整棵LCT的根)。加上access操作,就可以方便的提取出LCT上两点之间的路径。提取u到v的路径只需要mroot(u),access(v),然后v所在的Splay对应的链就是u到v的路径。

mroot实现的方式:

  由于LCT是Splay组成的森林,所以要把x变成根就只需要让所有Splay的父亲最终指向x所在Splay。所以先access(x),Splay(x),把现在的根和将成为根的x链在一棵Splay中,并转到根即可。但是我们注意到,由于x成为了新的根,所以它和原来的根所在的Splay中深度作为关键字的性质遭到了破坏:新根x应该是Splay中深度最小的,但是之前的操作并不会改变x的深度(也就是目前x依旧是当前Splay中深度最深的)。所以,我们需要把所在的这棵Splay翻转过来。

(粗的是重链,y是原来的根)

翻转前:

LCT学习笔记

翻转后:

LCT学习笔记

这时候x才真正变成了根。

实现代码如下:

 void mroot(int x){
access(x);
Splay(x);
pushreverse(x);
}

link操作:

这个操作的作用是连接两棵LCT。对于link(u,v),表示连接u所在的LCT和v所在的LCT;

link实现的方式:

很简单,只需要先mroot(u),然后记录T[u].fa=v就可以了,就是把一个Splay森林连到另一个上。

实现代码如下:

void link(int u,int v){
mroot(u);
T[u].fa=v;
}

cut操作:

  这个操作的作用是分离出两棵LCT。

实现代码如下:

 void cut(int u,int v)
mroot(u); //先把u变成根
access(v);Splay(v); //连接u、v
pushdown(v); //先下传标记
T[u].fa=T[v].ch[]=;
//v的左孩子表示v上方相连的重链
//update(v); //记得维护信息
}

以上这些就是LCT的基本操作啦。

③例题分析

先来一道简单的入门题

Time Limit: 851MS   Memory Limit: 1572864KB   64bit IO Format: %lld & %llu

Submit Status

Description

You are given a tree (an acyclic undirected connected graph) with N nodes, and edges numbered 1, 2, 3...N-1.

We will ask you to perfrom some instructions of the following form:

  • CHANGE i ti : change the cost of the i-th edge to ti
    or
  • QUERY a b : ask for the maximum edge cost on the path from node a to node b

Input

The first line of input contains an integer t, the number of test cases (t <= 20). t test cases follow.

For each test case:

  • In the first line there is an integer N (N <= 10000),
  • In the next N-1 lines, the i-th line describes the i-th edge: a line with three integers a b c denotes an edge between ab of cost c (c <= 1000000),
  • The next lines contain instructions "CHANGE i ti" or "QUERY a b",
  • The end of each test case is signified by the string "DONE".

There is one blank line between successive tests.

Output

For each "QUERY" operation, write one integer representing its result.

Example

Input:
1 3
1 2 1
2 3 2
QUERY 1 2
CHANGE 1 3
QUERY 1 2
DONE Output:
1
3

Hint

Thanh-Vy Hua Date: 2005-06-08 Time limit: 0.851s Source limit: 15000B Memory limit: 1536MB Cluster: Cube (Intel G860) Languages: ADA ASM BASH BF C C# C++ 5 CLPS LISP sbcl LISP clisp D FORT HASK ICON ICK JAVA LUA NEM NICE CAML PAS gpc PAS fpc PERL PHP PIKE PRLG PYTH 2.7 RUBY SCM qobi SCM guile ST TEXT WSPC

Submit Status

咦,这道题不是树链剖分的入门题么?  对的,不过,也可以用LCT做(树链剖分只处理静态的树和有关轻重边的动态更新,而LCT就是用来解决动态树问题)

题意:

给定一棵树,给定每条边a,b,w  (w权值)

完成两个操作:

1.把第i条边权值改成w

2.查询u到v路径上的最大权值

介绍一下access操作:查询根到u这条路径,并把这条路径更新为偏爱路径(之前的偏爱边可能会有改变)被访问到的u,不再有偏爱儿子。

每次access访问,都会把这条路径上的点用一棵splay维护,splay维护的关键词是点的深度,保证左边的子树比当前点深度小,右边的子树比当前点深度大。

并且这棵树把u作为根节点之后不再有右儿子(u没有偏爱儿子).

而splay要维护的是一条偏爱路径上的点。所有的splay会构成一个森林的集合。

要注意的是,在rotate判断ff的时候,有可能ff的左右儿子都不是f,然而没有进行判断就会跑出神奇的错误。

LCT学习笔记

实现代码如下:

 #include<cstdio>
#include<iostream>
#include<cstring>
#include<cstdlib>
#include<queue>
#include<cmath>
#include<algorithm>
using namespace std;
/*
LCT的思想类似于轻重链剖分
是用偏爱路径划分,用splay维护一条偏爱路径,然后像森林操作一样搞
u-v边的权值记录在v点上
*/
#define maxn 10005
int n;
int fa[maxn];
struct edge
{
int v,w;
edge *next;
edge(int _v,int _w,edge *_next)
{
v=_v;
w=_w;
next=_next;
}
}*head[maxn];
struct node
{
node *f;
node *ch[];
bool root;//是否是所在辅助树的根节点
int cost;
int maxcost; }tree[maxn],*null,Tnull;
void init(node *u)
{
u->f=u->ch[]=u->ch[]=null;
u->root=true;
u->cost=u->maxcost=;
}
void pushup(node *p)
{
p->maxcost=max(max(p->ch[]->maxcost,p->ch[]->maxcost),p->cost);
}
void rotate(node *u)
{
node *f=u->f;
node *ff=f->f;
int d=u==f->ch[]; f->ch[d]=u->ch[d^];
if(u->ch[d^]!=null)u->ch[d^]->f=f; u->f=ff;
if(ff!=null)
{
if(ff->ch[] == f)
{
ff->ch[] = u;
}
else if(ff->ch[] == f)//一定要用Else If,如果直接用Else会出现错误,因为树本身可能不是二叉树,虽然生成的Splay Tree是
{
ff->ch[] = u;
}
} u->ch[d^]=f;
f->f=u; pushup(f);
pushup(u);
if(f->root)
{
u->root=true;
f->root=false;
}
}
void splay(node *u)
{
if(u==null)return ;
while(!u->root)
{
node *f=u->f;
if(f->root)
{
rotate(u);
}
else
{
node *ff=f->f;
int d=u==f->ch[];
int dd=f==ff->ch[];
if(d==dd)rotate(f);
else rotate(u);
rotate(u);
}
}
pushup(u);
}
vector<pair<int,int> >E;
char ss[];
void access(node *u,bool flag)
{
node *v=null;
while(u!=null)
{
splay(u);
if(flag)
{
if(u->f==null)
{
printf("%d\n",max(u->ch[]->maxcost,v->maxcost));
}
}
u->ch[]->root=true;
u->ch[]=v;
v->root=false;
pushup(u);
v=u;
u=u->f;
}
}
void query(int u,int v)
{
access(tree+u,);
access(tree+v,);
}
void change(int u,int w)
{
access(tree+u,);
splay(tree+u);
tree[u].cost=w;
pushup(tree+u);
}
void bfs()
{
memset(fa,-,sizeof(fa));
queue<int >q;
q.push();
fa[]=;
while(!q.empty())
{
int u=q.front();
q.pop();
for(edge *i=head[u];i;i=i->next)
{
if(fa[i->v]==-)
{
fa[i->v]=u;
tree[i->v].f=tree+u;
tree[i->v].cost=tree[i->v].maxcost=i->w;
q.push(i->v);
}
}
}
}
int main()
{
int T;
scanf("%d",&T);
null=&Tnull;
init(null);
while(T--)
{
scanf("%d",&n);
for(int i=;i<=n;i++)
{
head[i]=NULL;
init(&tree[i]);
}
E.clear();
for(int i=;i<n;i++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
head[a]=new edge(b,c,head[a]);
head[b]=new edge(a,c,head[b]);
E.push_back(make_pair(a,b));
}
bfs();
while(scanf("%s",ss)!=EOF)
{
if(ss[]=='D')break;
if(ss[]=='Q')
{
int u,v;
scanf("%d%d",&u,&v);
query(u,v);
}
else if(ss[]=='C')
{
int a,w;
scanf("%d%d",&a,&w);
a--;
int u=E[a].first;
int v=E[a].second;
if(fa[u]==v)
{
change(u,w);
}
if(fa[v]==u)
{
change(v,w);
}
}
}
}
return ;
}

一道入门题怎么够呢?那就再来一道操作多一点的~~~

Query on The Trees

Time Limit: 10000/5000 MS (Java/Others)    Memory Limit: 65768/65768 K (Java/Others)
Total Submission(s): 6250    Accepted Submission(s): 2504

Problem Description
We have met so many problems on the tree, so today we will have a query problem on a set of trees.
There are N nodes, each node will have a unique weight Wi. We will have four kinds of operations on it and you should solve them efficiently. Wish you have fun!
 
Input
There are multiple test cases in our dataset.
For each case, the first line contains only one integer N.(1 ≤ N ≤ 300000) The next N‐1 lines each contains two integers x, y which means there is an edge between them. It also means we will give you one tree initially.
The next line will contains N integers which means the weight Wi of each node. (0 ≤ Wi ≤ 3000)
The next line will contains an integer Q. (1 ≤ Q ≤ 300000) The next Q lines will start with an integer 1, 2, 3 or 4 means the kind of this operation.
1. Given two integer x, y, you should make a new edge between these two node x and y. So after this operation, two trees will be connected to a new one.
2. Given two integer x, y, you should find the tree in the tree set who contain node x, and you should make the node x be the root of this tree, and then you should cut the edge between node y and its parent. So after this operation, a tree will be separate into two parts.
3. Given three integer w, x, y, for the x, y and all nodes between the path from x to y, you should increase their weight by w.
4. Given two integer x, y, you should check the node weights on the path between x and y, and you should output the maximum weight on it.
 
Output
For each query you should output the correct answer of it. If you find this query is an illegal operation, you should output ‐1.
You should output a blank line after each test case.
 
Sample Input

5

1 2

2 4

2 5

1 3

1 2 3 4 5

6

4 2 3

2 1 2

4 2 3

1 3 5

3 2 1 4

4 1 4

Sample Output
3
-1
7
Hint

We define the illegal situation of different operations:
In first operation: if node x and y belong to a same tree, we think it's illegal.
In second operation: if x = y or x and y not belong to a same tree, we think it's illegal.
In third operation: if x and y not belong to a same tree, we think it's illegal.
In fourth operation: if x and y not belong to a same tree, we think it's illegal.

Source

题意:

给定一棵树,维护4个操作:

1.给定u,v,如果不在一棵树上,它们之间连边。

2.给定u,v如果u!=v且在同一棵树上,把u设为整棵树的根,再把v和它的父亲断开。

3.给定u,v,w,如果u,v在同一条树上,把他们之间的点的权值+w

4.给定u,v,如果u,v在同一棵树上,求出他们路径上的最大值。

违法操作输出-1

思路:

连边操作,访问u,把u旋到根,把它左右翻转即可(翻转前splay上u一定没有右节点(没有偏爱儿子),翻转后一定没有左儿子,说明它就是从根到u路径上深度最小的了,就成为了根),把它接到v上,设为整棵树的根:一样的先access(u),splay(u)再rev(u)。

断开:access(u),splay(u),但不翻转,这样u没有右儿子但有左儿子,和左子树断开即可。

对于3、4操作,可以先把u设为根,再access(v)然后找到u-v这条路径上splay的根节点在哪里,信息都记录在那个节点上!

实现代码如下:

 #include<cstdio>
#include<iostream>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<queue>
#include<cmath>
using namespace std;
const int maxn=+;
const int inf=0x3f3f3f3f;
struct node
{
node *f;
node *ch[];
bool rev;
int add;
int mm;
int key;
}tree[maxn],*null,*cur;
void init()
{
null=tree;
null->f=null->ch[]=null->ch[]=null;
null->rev=;
null->add=;
null->mm=null->key=-inf;
cur=tree+;
}
node *newnode(int key)
{
cur->f=cur->ch[]=cur->ch[]=null;
cur->mm=cur->key=key;
cur->add=;
cur->rev=;
return cur++;
}
bool isroot(node *x)
{
return x==null||x->f->ch[]!=x&&x->f->ch[]!=x;
}
void pushup(node *u)
{
u->mm=max(u->key,max(u->ch[]->mm,u->ch[]->mm));
}
void pushdown(node *u)
{
if(u==null)return ;
if(u->rev)
{
swap(u->ch[],u->ch[]);
if(u->ch[]!=null)u->ch[]->rev^=;
if(u->ch[]!=null)u->ch[]->rev^=;
u->rev=;
}
if(u->add)
{
if(u->ch[]!=null)
{
u->ch[]->add+=u->add;
u->ch[]->mm+=u->add;
u->ch[]->key+=u->add;
}
if(u->ch[]!=null)
{
u->ch[]->add+=u->add;
u->ch[]->mm+=u->add;
u->ch[]->key+=u->add;
}
u->add=;
}
}
void rotate(node *u)
{
node *f=u->f;
node *ff=f->f;
int d=u==f->ch[]; if(u->ch[d^]!=null)u->ch[d^]->f=f;
f->ch[d]=u->ch[d^]; u->f=ff;
if(ff!=null)
{
if(f==ff->ch[])ff->ch[]=u;
else if(f==ff->ch[])ff->ch[]=u;
} u->ch[d^]=f;
f->f=u; pushup(f);
pushup(u);
}
node *sta[maxn];
int cnt;
void splay(node *u)
{
if(u==null)return ;
cnt=;
sta[]=u;
for(node *y=u;!isroot(y);y=y->f)
{
sta[cnt++]=y->f;
}
while(cnt)pushdown(sta[--cnt]);
while(!isroot(u))
{
node *f=u->f;
node *ff=f->f;
if(isroot(f))
{
rotate(u);
}
else
{
int d=u==f->ch[];
int dd=f==ff->ch[];
if(d==dd)rotate(f);
else rotate(u);
rotate(u);
}
}
pushup(u);
}
node *access(node *u)
{
node *v=null;
while(u!=null)
{
splay(u);
v->f=u;
u->ch[]=v;
pushup(u);
v=u;
u=u->f;
}
return v;
}
void link(node *u,node *v)
{
access(u);
splay(u);
u->rev=;
u->f=v;
}
bool sam(node *u,node *v)
{
while(u->f!=null)u=u->f;
while(v->f!=null)v=v->f;
return u==v;
}
void changeroot(node *u)
{
access(u)->rev^=;
}
void cut(node *u)
{
access(u);
splay(u);//access+旋转之后左子树的点都比u小,一定会有u的父亲
u->ch[]=u->ch[]->f=null;
pushup(u);
}
node *getroot(node *u)
{
access(u);
splay(u);
while(u->f!=null)u=u->f;
splay(u);
return u;
}
int n,m;
int det[maxn];
struct edge
{
int u,v;
}E[maxn];
int main()
{
while(scanf("%d",&n)!=EOF)
{
init();
for(int i=;i<n;i++)
{
scanf("%d%d",&E[i].u,&E[i].v);
}
for(int i=;i<=n;i++)
{
scanf("%d",&det[i]);
newnode(det[i]);
}
for(int i=;i<n;i++)
{
link(tree+E[i].u,tree+E[i].v);
}
scanf("%d",&m);
for(int i=;i<=m;i++)
{
int k,a,b;
scanf("%d",&k);
if(k==)
{
scanf("%d%d",&a,&b);
if(sam(tree+a,tree+b))printf("-1\n");
else
{
link(tree+a,tree+b);
}
}
else if(k==)
{
scanf("%d%d",&a,&b);
if(a==b||!sam(tree+a,tree+b))printf("-1\n");
else
{
changeroot(tree+a);
cut(tree+b);
}
}
else if(k==)
{
int w;
scanf("%d%d%d",&w,&a,&b);
if(!sam(tree+a,tree+b))printf("-1\n");
else
{
changeroot(tree+a);
access(tree+b);
node *q=getroot(tree+b);
q->add+=w;
q->mm+=w;
q->key+=w;
}
}
else if(k==)
{
scanf("%d%d",&a,&b);
if(!sam(tree+a,tree+b))printf("-1\n");
else
{
changeroot(tree+a);
access(tree+b);
node *q=getroot(tree+b);
printf("%d\n",q->mm);
}
}
}
printf("\n");
}
return ;
}

④习题推荐

我推荐几个LCT的练习题练习下上述的知识点吧!

  • BZOJ 2049 SDOI2008洞穴勘探,模板题,只需要link和cut,然后询问连通性。
  • BZOJ 2002 HNOI2010弹飞绵羊,模板题,需要link和询问某点到根的路径长度。

  • BZOJ 3669 NOI2014魔法森林,LCT的综合应用。

相信做完上述例题,你也能对LCT有了个初步的认识吧,加油!

⑤参考资料

上一篇:SPLAY,LCT学习笔记(四)


下一篇:最新Idea,Webstrom,PhpStrom,PyCharm,Clion等 JetBrains激活