本文共 14845 字,大约阅读时间需要 49 分钟。
最近自学了一下LCT(Link-Cut-Tree),参考了Saramanda及Yang_Zhe等众多大神的论文博客,对LCT有了一个初步的认识,LCT是一种动态树,可以处理动态问题的算法。对于树分治中的树链剖分,只能处理静态的数据或者在轻重链上的边或点的权值,对于其他动态的处理就毫无办法了。因此我们就需要引入LCT这个东西。那么问题来了,LCT到底是什么呢?我弄了很久总算是理解了LCT,打算总结一下LCT的基本操作。
LCT用来维护动态的森林,以及一些链上操作,是处理节点无序的有根树组成的森林,进行一些列操作(例如合并,剪切,翻转,更新·····)
对于一棵树的操作,我们可以用splay维护;同理,对于一片森林,也可以用多棵splay维护,而LCT就是要把这些森林的splay联系在一起。而LCT的核心就是access操作啦!
看完之后我们知道,LCT和静态的树链剖分很像。怎么说呢?这两种树形结构都是由若干条长度不等的“重链”和“轻边”构成(名字可以不同,大概就是这个意思),“重链”之间由”轻边”连接。就像这样:
可以想象为一棵树被人为的砍成了一段段。
LCT和树链剖分不同的是,树链剖分的链是不会变化的,所以可以很方便的用线段树维护。但是,既然是动态树,那么树的结构形态将会发生改变,所以我们要用更加灵活的维护区间的结构来对链进行维护,不难想到Splay可以胜任。如何分离树链也是保证时间效率的关键(链的数量和长度要平衡),树链剖分的“重儿子”就体现了前人博大精深的智慧。
在这里解释一下为什么要把树砍成一条条的链:我们可以在logn的时间内维护长度为n的区间(链),所以这样可以极大的提高树上操作的时间效率。在树链剖分中,我们把一条条链放到线段树上维护。但是LCT中,由于树的形态变化,所以用能够支持合并、分离、翻转等操作的Splay维护LCT的重链(注意,单独一个节点也算是一条重链)。
这时我们注意到,LCT中的轻边信息变得无法维护。为什么呢?因为Splay只维护了重链,没有维护重链之间的轻边;而LCT中甚至连根都可以不停的变化,所以也没法用点权表示它父边的边权(父亲在变化)。所以,如果在LCT中要维护边上信息,个人认为最方便的方法应该是把边变成一个新点和两条边。这样可以把边权的信息变成点权维护,同时为了不影响,把真正的树上节点的点权变成0,就可以用维护点的方式维护边。
LCT中用Splay维护链,这些Splay叫做“辅助树“。辅助树以它上面每个节点的深度为关键字维护,就是辅助树中每个节点左儿子的深度小于当前节点的深度,当前节点的深度小于右儿子的深度。
可以把LCT认为是一个由Splay组成的森林,就像这样:(三角形代表一棵Splay,对应着LCT上一条链)
箭头是什么意思呢?箭头记录着某棵Splay对应的链向上由轻边连着哪个节点,可以想象为箭头指向“Splay 的父亲”。但是,Splay的父亲并不记录有这个儿子,即箭头是单向的。同时,每个节点要记录它是否是它所在的Splay的根。这样,Splay构成的森林就建成了。
这个是我的Splay节点最基本的定义:(如果要维护更多信息就像Splay维护区间那样加上更多标记)
1 struct node{2 int fa,ch[2]; //父亲和左右儿子。3 bool reverse,is_root; //区间反转标记、是否是所在Splay的根4 }T[maxn];
LCT中基本的Splay上操作:
1 int getson(int x){ 2 return x==T[T[x].fa].ch[1]; 3 } 4 void pushreverse(int x){ 5 if(!x)return; 6 swap(T[x].ch[0],T[x].ch[1]); 7 T[x].reverse^=1; 8 } 9 void pushdown(int x){10 if(T[x].reverse){11 pushreverse(T[x].ch[0]);12 pushreverse(T[x].ch[1]);13 T[x].reverse=false;14 }15 }16 void rotate(int x){17 if(T[x].is_root)return;18 int k=getson(x),fa=T[x].fa;19 int fafa=T[fa].fa;20 pushdown(fa);pushdown(x); //先要下传标记21 T[fa].ch[k]=T[x].ch[k^1];22 if(T[x].ch[k^1])T[T[x].ch[k^1]].fa=fa;23 T[x].ch[k^1]=fa;24 T[fa].fa=x;25 T[x].fa=fafa;26 if(!T[fa].is_root)T[fafa].ch[fa==T[fafa].ch[1]]=x;27 else T[x].is_root=true,T[fa].is_root=false;28 //update(fa);update(x); //如果维护了信息,就要更新节点29 }30 void push(int x){31 if(!T[x].is_root)push(T[x].fa);32 pushdown(x);33 }34 void Splay(int x){35 push(x); //在Splay到根之前,必须先传完反转标记36 for(int fa;!T[x].is_root;rotate(x)){37 if(!T[fa=T[x].fa].is_root){38 rotate((getson(x)==getson(fa))?fa:x);39 }40 }41 }
access操作:
这是LCT最核心的操作。其他所有操作都要用到它。
他的含义是”访问某节点“。作用是:对于访问的节点x,打通一条从树根(真实的LCT树)到x的重链;如果x往下是重链,那么把x往下的重边改成轻边。可以理解为专门开辟一条x到根的路径,由一棵Splay维护这条路径。
access之前:(粗的是重链)
access之后:
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。
实现代码如下:
1 void access(int x){ 2 int y=0; 3 do{ 4 Splay(x); 5 T[T[x].ch[1]].is_root=true; 6 T[T[x].ch[1]=y].is_root=false; 7 //update(x); //如果维护了信息记得更新。 8 x=T[y=x].fa; 9 }while(x);10 }
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是原来的根)
翻转前:
翻转后:
这时候x才真正变成了根。
实现代码如下:
1 void mroot(int x){2 access(x);3 Splay(x);4 pushreverse(x);5 }
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。
实现代码如下:
1 void cut(int u,int v)2 mroot(u); //先把u变成根3 access(v);Splay(v); //连接u、v4 pushdown(v); //先下传标记5 T[u].fa=T[v].ch[0]=0;6 //v的左孩子表示v上方相连的重链7 //update(v); //记得维护信息8 }
以上这些就是LCT的基本操作啦。
先来一道简单的入门题
Time Limit: 851MS | Memory Limit: 1572864KB | 64bit IO Format: %lld & %llu |
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:
The first line of input contains an integer t, the number of test cases (t <= 20). t test cases follow.
For each test case:
There is one blank line between successive tests.
For each "QUERY" operation, write one integer representing its result.
Input:131 2 12 3 2QUERY 1 2CHANGE 1 3QUERY 1 2DONEOutput:13
Hint
咦,这道题不是树链剖分的入门题么? 对的,不过,也可以用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,然而没有进行判断就会跑出神奇的错误。
实现代码如下:
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 using namespace std; 9 /* 10 LCT的思想类似于轻重链剖分 11 是用偏爱路径划分,用splay维护一条偏爱路径,然后像森林操作一样搞 12 u-v边的权值记录在v点上 13 */ 14 #define maxn 10005 15 int n; 16 int fa[maxn]; 17 struct edge 18 { 19 int v,w; 20 edge *next; 21 edge(int _v,int _w,edge *_next) 22 { 23 v=_v; 24 w=_w; 25 next=_next; 26 } 27 }*head[maxn]; 28 struct node 29 { 30 node *f; 31 node *ch[2]; 32 bool root;//是否是所在辅助树的根节点 33 int cost; 34 int maxcost; 35 36 }tree[maxn],*null,Tnull; 37 void init(node *u) 38 { 39 u->f=u->ch[0]=u->ch[1]=null; 40 u->root=true; 41 u->cost=u->maxcost=0; 42 } 43 void pushup(node *p) 44 { 45 p->maxcost=max(max(p->ch[0]->maxcost,p->ch[1]->maxcost),p->cost); 46 } 47 void rotate(node *u) 48 { 49 node *f=u->f; 50 node *ff=f->f; 51 int d=u==f->ch[1]; 52 53 f->ch[d]=u->ch[d^1]; 54 if(u->ch[d^1]!=null)u->ch[d^1]->f=f; 55 56 u->f=ff; 57 if(ff!=null) 58 { 59 if(ff->ch[0] == f) 60 { 61 ff->ch[0] = u; 62 } 63 else if(ff->ch[1] == f)//一定要用Else If,如果直接用Else会出现错误,因为树本身可能不是二叉树,虽然生成的Splay Tree是 64 { 65 ff->ch[1] = u; 66 } 67 } 68 69 u->ch[d^1]=f; 70 f->f=u; 71 72 pushup(f); 73 pushup(u); 74 if(f->root) 75 { 76 u->root=true; 77 f->root=false; 78 } 79 } 80 void splay(node *u) 81 { 82 if(u==null)return ; 83 while(!u->root) 84 { 85 node *f=u->f; 86 if(f->root) 87 { 88 rotate(u); 89 } 90 else 91 { 92 node *ff=f->f; 93 int d=u==f->ch[1]; 94 int dd=f==ff->ch[1]; 95 if(d==dd)rotate(f); 96 else rotate(u); 97 rotate(u); 98 } 99 }100 pushup(u);101 }102 vector >E;103 char ss[20];104 void access(node *u,bool flag)105 {106 node *v=null;107 while(u!=null)108 {109 splay(u);110 if(flag)111 {112 if(u->f==null)113 {114 printf("%d\n",max(u->ch[1]->maxcost,v->maxcost));115 }116 }117 u->ch[1]->root=true;118 u->ch[1]=v;119 v->root=false;120 pushup(u);121 v=u;122 u=u->f;123 } 124 }125 void query(int u,int v)126 {127 access(tree+u,0);128 access(tree+v,1);129 }130 void change(int u,int w)131 {132 access(tree+u,0);133 splay(tree+u);134 tree[u].cost=w;135 pushup(tree+u);136 }137 void bfs()138 {139 memset(fa,-1,sizeof(fa));140 queue q;141 q.push(1);142 fa[1]=0;143 while(!q.empty())144 {145 int u=q.front();146 q.pop();147 for(edge *i=head[u];i;i=i->next)148 {149 if(fa[i->v]==-1)150 {151 fa[i->v]=u;152 tree[i->v].f=tree+u;153 tree[i->v].cost=tree[i->v].maxcost=i->w;154 q.push(i->v);155 }156 }157 }158 }159 int main()160 {161 int T;162 scanf("%d",&T);163 null=&Tnull;164 init(null);165 while(T--) 166 {167 scanf("%d",&n);168 for(int i=1;i<=n;i++)169 {170 head[i]=NULL;171 init(&tree[i]);172 }173 E.clear();174 for(int i=1;i
一道入门题怎么够呢?那就再来一道操作多一点的~~~
Time Limit: 10000/5000 MS (Java/Others) Memory Limit: 65768/65768 K (Java/Others)Total Submission(s): 6250 Accepted Submission(s): 2504
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
题意:
给定一棵树,维护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的根节点在哪里,信息都记录在那个节点上!
实现代码如下:
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 using namespace std; 9 const int maxn=300000+20; 10 const int inf=0x3f3f3f3f; 11 struct node 12 { 13 node *f; 14 node *ch[2]; 15 bool rev; 16 int add; 17 int mm; 18 int key; 19 }tree[maxn],*null,*cur; 20 void init() 21 { 22 null=tree; 23 null->f=null->ch[0]=null->ch[1]=null; 24 null->rev=0; 25 null->add=0; 26 null->mm=null->key=-inf; 27 cur=tree+1; 28 } 29 node *newnode(int key) 30 { 31 cur->f=cur->ch[0]=cur->ch[1]=null; 32 cur->mm=cur->key=key; 33 cur->add=0; 34 cur->rev=0; 35 return cur++; 36 } 37 bool isroot(node *x) 38 { 39 return x==null||x->f->ch[0]!=x&&x->f->ch[1]!=x; 40 } 41 void pushup(node *u) 42 { 43 u->mm=max(u->key,max(u->ch[0]->mm,u->ch[1]->mm)); 44 } 45 void pushdown(node *u) 46 { 47 if(u==null)return ; 48 if(u->rev) 49 { 50 swap(u->ch[0],u->ch[1]); 51 if(u->ch[0]!=null)u->ch[0]->rev^=1; 52 if(u->ch[1]!=null)u->ch[1]->rev^=1; 53 u->rev=0; 54 } 55 if(u->add) 56 { 57 if(u->ch[0]!=null) 58 { 59 u->ch[0]->add+=u->add; 60 u->ch[0]->mm+=u->add; 61 u->ch[0]->key+=u->add; 62 } 63 if(u->ch[1]!=null) 64 { 65 u->ch[1]->add+=u->add; 66 u->ch[1]->mm+=u->add; 67 u->ch[1]->key+=u->add; 68 } 69 u->add=0; 70 } 71 } 72 void rotate(node *u) 73 { 74 node *f=u->f; 75 node *ff=f->f; 76 int d=u==f->ch[1]; 77 78 if(u->ch[d^1]!=null)u->ch[d^1]->f=f; 79 f->ch[d]=u->ch[d^1]; 80 81 u->f=ff; 82 if(ff!=null) 83 { 84 if(f==ff->ch[0])ff->ch[0]=u; 85 else if(f==ff->ch[1])ff->ch[1]=u; 86 } 87 88 u->ch[d^1]=f; 89 f->f=u; 90 91 pushup(f); 92 pushup(u); 93 } 94 node *sta[maxn]; 95 int cnt; 96 void splay(node *u) 97 { 98 if(u==null)return ; 99 cnt=1;100 sta[0]=u;101 for(node *y=u;!isroot(y);y=y->f)102 {103 sta[cnt++]=y->f;104 }105 while(cnt)pushdown(sta[--cnt]);106 while(!isroot(u))107 {108 node *f=u->f;109 node *ff=f->f;110 if(isroot(f))111 {112 rotate(u);113 }114 else115 {116 int d=u==f->ch[1];117 int dd=f==ff->ch[1];118 if(d==dd)rotate(f);119 else rotate(u);120 rotate(u);121 }122 }123 pushup(u);124 }125 node *access(node *u)126 {127 node *v=null;128 while(u!=null)129 {130 splay(u);131 v->f=u;132 u->ch[1]=v;133 pushup(u);134 v=u;135 u=u->f;136 }137 return v;138 }139 void link(node *u,node *v)140 {141 access(u);142 splay(u);143 u->rev=1;144 u->f=v;145 }146 bool sam(node *u,node *v)147 {148 while(u->f!=null)u=u->f;149 while(v->f!=null)v=v->f;150 return u==v;151 }152 void changeroot(node *u)153 {154 access(u)->rev^=1;155 }156 void cut(node *u)157 {158 access(u);159 splay(u);//access+旋转之后左子树的点都比u小,一定会有u的父亲 160 u->ch[0]=u->ch[0]->f=null;161 pushup(u);162 }163 node *getroot(node *u)164 {165 access(u);166 splay(u);167 while(u->f!=null)u=u->f;168 splay(u);169 return u;170 }171 int n,m;172 int det[maxn];173 struct edge174 {175 int u,v;176 }E[maxn];177 int main()178 {179 while(scanf("%d",&n)!=EOF)180 {181 init();182 for(int i=1;i add+=w;230 q->mm+=w;231 q->key+=w;232 }233 }234 else if(k==4)235 {236 scanf("%d%d",&a,&b);237 if(!sam(tree+a,tree+b))printf("-1\n");238 else239 {240 changeroot(tree+a);241 access(tree+b);242 node *q=getroot(tree+b);243 printf("%d\n",q->mm);244 } 245 }246 }247 printf("\n");248 }249 return 0;250 }
我推荐几个LCT的练习题练习下上述的知识点吧!
BZOJ 2002 HNOI2010弹飞绵羊,模板题,需要link和询问某点到根的路径长度。
BZOJ 3669 NOI2014魔法森林,LCT的综合应用。
相信做完上述例题,你也能对LCT有了个初步的认识吧,加油!