zoukankan      html  css  js  c++  java
  • 浅谈 LCT

    实链剖分和树链剖分的区别

    树链剖分有一个更专业的名称 :轻重链剖分,即为根据子节点的子树大小来剖,虽然树链剖分有很好的性质 ,但是还是存在缺陷的。例如 : 树链剖分将树剖完之后是静态的,(无法进行修改了,但不代表就不能换根了。)也就是说树链剖分只能针对于树的结构不变的情况下操作。

    实链剖分 : 将树的边分为两种,一种是实边,一种是虚边,维护的时候则是对实边进行维护。

    我们发现实链剖分很不固定,将树的边划分实虚的话,也无法保证形态。如果我们用一个灵活的数据结构,那么我们发现,其实这个树完全可以动起来,因为任意转化实虚边都可以维护,也就是说,删除一条边,加上一条边,对于实链剖分来说,都可以。

    然后我们将实链剖分剖出来的链用 (Splay) 维护,这种数据结构叫做 (LCT)(Link-Cut-Tree)

    不知道为什么 (LCT) 的一些博客讲解中以 (Splay) 去维护轻重链剖分的链。


    LCT 的一些浅显的概念理解

    大概有辅助树, (Splay) 与辅助树的关系之类。

    辅助树

    可以简单的理解为一些 (Splay) 构成了辅助树。我们给出一张图来理解一下其结构 :

    通过对比第一个图和第二个图,我们可以知道原树中的实链对应着辅助树的实链。无论怎么变换都是一条条的实链都是不会变得。

    同时,因为我们选择用 (Splay) 维护一条实链,那么我们也就可以认为左边绿框框也就是一个 (Splay) , 然后我们显然可以知道这些 (Splay) 是通过虚边连接起来的(也就是红边连接起来的)。

    然后我们考虑是怎么构造的这一颗辅助树 :
    首先我们通过实链构造出一颗颗的 (Splay) , 即为 :

    ({A - D - C } ,{ E - C } , { F }) 总共三个 (Splay)

    然后我们令 (E , F) 去寻找他在原树中的父亲,也就是 (A , C) , 然后通过虚边连接起来。
    这里有一个不成文的规定 : 认父不认子

    最后就构造完了。


    辅助树和原树的区别

    • 辅助树的根不一定是原树的根。
    • 原树父亲的指向不等同于辅助树父亲的指向
    • 辅助树是可以在 (Splay) 的帮助下,实现任意换根。
    • 辅助树中不存在节点指向子节点的情况。(但可以有节点统计子节点的情况)

    LCT 的一些性质

    • 每一个 (Splay) 维护的是一条在原树中深度严格递增的树链,且中序遍历 (Splay) 得到的每一个点的深度组成的序列也是严格递增的。

    • 每一个节点包含且仅包含于一个 (Splay)

    • 认父不认子

    边分为实边和虚边,实边包含在 (Splay) 中,而虚边总是由一棵 (Splay) 指向另一个节点(指向该 (Splay) 中中序遍历最靠前的点在原树中的父亲)。
    因为性质 (2),当某点在原树中有多个儿子时,只能向其中一个儿子拉一条实链(只认一个儿子),而其它儿子是不能在这个 (Splay) 中的。
    那么为了保持树的形状,我们要让到其它儿子的边变为虚边,由对应儿子所属的 (Splay) 的根节点的父亲指向该点,而从该点并不能直接访问该儿子(认父不认子)。


    LCT 的一些操作


    Access(x) 操作

    因为性质 (3) ,建立了虚边,而我们选择维护的却是实链,所以会导致根节点 (以下均称为 (rt) ) 到 (x) 的路径经过所有的边不一定全都是实边,即 (rt)(x) 的路径不通。
    (Access(x)) 的意思为 将 (rt)(x) 的路径打通,也就是将 (rt o x) 的路径上所有的经过的边都转化为实边。

    这是 (LCT) 最核心的部分 (就属 (Splay) 的代码最长)

    这里以 (FlashHu) 大佬的博文 LCT总结——概念篇 中的例子予以说明。 他讲的特别详细,我不认为我能比他讲的还要详细。


    有一棵树,假设一开始实边和虚边是这样划分的(虚线为虚边)

    那么所构成的 (LCT) 可能会长这样(绿框中为一个 (Splay),可能不会长这样,但只要满足中序遍历按深度递增(性质 (1))就对结果无影响)

    现在我们要 (Access(N)),把 (A−N) 的路径拉起来变成一条 (Splay)
    因为性质 (2) ,该路径上其它链都要给这条链让路,也就是把每个点到该路径以外的实边变虚。
    所以我们希望虚实边重新划分成这样。

    然后怎么实现呢?
    我们要一步步往上拉。
    首先把 (splay(N)),使之成为当前 (Splay) 中的根。
    为了满足性质 (2),原来 (N−O) 的重边要变轻。
    因为按深度O在N的下面,在 (Splay) 中O在 (N) 的右子树中,所以直接单方面将 (N) 的右儿子置为 (0)(认父不认子)
    然后就变成了这样——

    我们接着把 (N) 所属 (Splay) 的虚边指向的 (I)(在原树上是 (L) 的父亲)也转到它所属 (Splay) 的根,(splay(I))
    原来在 (I) 下方的重边 (I−K) 要变轻(同样是将右儿子去掉)。
    这时候 (I−L) 就可以变重了。因为 (L) 肯定是在 (I) 下方的(刚才 (L) 所属 (Splay) 指向了(I)),所以I的右儿子置为 (N),满足性质 (1)
    然后就变成了这样——

    (I) 指向 (H),接着 (splay(H))(H) 的右儿子置为 (I)

    (H) 指向 (A),接着 (splay(A))(A) 的右儿子置为 (H)

    (A−N) 的路径已经在一个 (Splay) 中了,大功告成!
    代码其实很简单。。。。。。循环处理,只有四步——

    归根到底,其实就是 :
    (u) 的右儿子为 (v) 的时候,我们就认为 (u - v) 是一条实边。
    显然 (Splay) 是维护实链的,如果我们 (1 o n) 是连通的,那么我们直接查询举行了。

    同样的。如果不连通,那么就以为着我们需要将这一条链赋值成实链。我们按照上面图的模拟过程来即可。

    模拟过程可以简化为 :

    • 旋转到当前 (Splay) 的根。
    • 建立和父亲的实边关系。
    • 更新节点维护的信息。

    (Question) : 我们需不需要考虑当前和 (u) 这个点连接的实链,把他置换成虚边呢?
    (Answer) : 不需要,这个时候就体现出我们认父不认子的好处了,我们直接将 (u) 这个点的右儿子替换掉,就代表 (u) 这个点的右儿子已经处理完了。

    qaq void Access(int u) {
    	for(qwq int y = 0 ; u ; u = f[y = u]) 
    	 Splay(u) , ch[u][1] = y , pushup(u) ; 
    	// 先旋转到当前 Splay 的根,然后通过 f[u] 建立的虚边找到父亲节点,同时将父亲节点
    	// 的右儿子赋为当前的这个点,形成实边,同时连接该节点和父亲所在的 Splay  。
    	// pushup 即为更新维护的信息 
    }
    

    MakeRoot(x) 操作

    就像他的意译一样, (MakeRoot) ,使成为根。缺宾语

    那么如何操作呢 。我们上文已经知道了如何将打通一个点到根的路径了。

    这时候用到 (Access(x))(Splay(x)) 操作了。

    我们这个 (Splay) 满足性质 (1), 所以 (Access(x)) 之后 , (x) 还是深度最大的点。

    我们将其 (Splay) 旋转一下,本来它就是最大的,显然 (x) 在这个 (Splay) 中没有右子树。

    于是我们翻转整个 (Splay) , 使得所有点的深度都倒过来,(x) 没有了左子树,它成了深度最小的点,那 (x) 其实不就是树根了嘛。

    qaq void MakeRoot(int u) {
    	Access(u) , Splay(u) , PushOver(u) ; 
    	// PushOver(u) 就是翻转操作
    }
    

    FindRoot 操作

    找树根 。
    (Access(x)) 之后 (x) 不就是深度最大的点了嘛,我们就不断去找左子树左子树,也就是去寻找深度最小的点,当节点 (u) 没有左子树的时候,他的深度也就是最小的了,那么 (u) 就是树根了。

    当然,其中有可能会有 (tag) 标记,也就是区间翻转标记,我们这里直接下传即可,不下传无法保证 (u) 一定是树根。 解释的话,分析上一个操作。

    qaq void FindRoot(int u) {
    	Access(u) ; 
    	while(ch[u][0]) pushdown(u) , u = ch[u][0] ; 
    	return u ; 
    } 
    

    (LCT) 中加入一条 (u - v) 的边。

    (u) 成为树根 , 然后建立虚边。

    这个地方需要特判一下,因为树上显然不能出现环,所以 (FindRoot(v) eq u) ,这样才让 (u)(v) 认父。如果不知为什么 (u)(v) 认父,则建议重新审视一下 (Access(x)) 的模拟过程。

    qaq void Link(int u , int v) {
    	MakeRoot(u) ; 
    	if(FindRoot(v) != u) f[u] = v ; 
    }
    

    Split 操作

    (Split(u,v))代表是抽出 (u - v) 这条路径成为实链。

    这时候我们有 (Link) 的启发,我们就可以直接让 (u) 成为树根。然后通过 (Access(v)) 打通(u - v) 的路径即可。

    qaq void Split(int u , int v) {
    	MakeRoot(u) , Access(v) , Splay(v) ;
    }
    

    Cut 操作

    删除 (u , v) 这一条边。

    如果题目保证断边合法,倒是很方便。

    使 (u) 为根后 , (v) 的父亲一定会指向 (u) , 且深度相差 (1) , 当 (Access(v) , Splay(v)) 之后,因为 (u) 深度小,所以 (u) 一定是 (v) 的左儿子。直接断开连接。

    qaq void CUT(int u , int v) {
    	Split(u , v) ; f[u] = ch[v][0] = 0 ; pushup(v) ;
    }
    

    如果题目不保证断边合法,也就是不一定会存在该边。

    那么我们也按照上面一样,去特判一下。首先使得 (u) 成为 (Splay) 的根,然后去判断一下 (u , v) 是否在一个子树内,如果不在,则不存在。接着去判断一下 (v) 的父亲是否是 (u) ,如果不是,不存在,最后去判断一下 (v) 是否有左儿子,如果没有,也不行。

    qaq void Cut(int u , int v) {
    	MakeRoot(u) ; 
    	if(FindRoot(v) == u && f[v] = u && !ch[v][0]) 
    	f[v] = ch[u][1] = 0 ,pushup(u); 
    } 
    

    Splay , Rorate,pushdown,其他操作

    和普通平衡树很相似,但是有几处是不同的。

    这里就直接给出代码了

    qaq bool check(int x) {//判断节点是否为一个Splay的根(与普通Splay的区别1)
    	return ch[f[x]][1] == x || ch[f[x]][0] == x ;
    }//原理很简单,如果连的是轻边,他的父亲的儿子里没有它
    qaq bool jd(int x) {
    	return ch[f[x]][1] == x ; 
    }
    qaq void PushOver(int u) {
    	swap(ch[u][1] , ch[u][0]) ;
    	tag[u] ^= 1 ; 
    }
    qaq void pushdown(int u) {
    	if(tag[u]) 
    	{
    		tag[u] = 0 ; 
    		if(ch[u][0]) PushOver(ch[u][0]) ; 
    		if(ch[u][1]) PushOver(ch[u][1]) ;
    	}
    }
    qaq void Rorate(int x) {
    	int y = f[x] , z = f[y] , k = ch[y][1] == x , w = ch[x][k ^ 1] ; 
    	if(check(y)) ch[z][ch[z][1] == y] = x ; ch[x][k ^ 1] = y ; ch[y][k] = w ; 
    	//额外注意if(check(y))语句,此处不判断会引起致命错误(与普通Splay的区别2)
    	if(w) f[w] = y ;f[y] = x ; f[x] = z ; pushup(y) ; 
    }
    qaq void Splay(int x) {//只传了一个参数,因为所有操作的目标都是该Splay的根(与普通Splay的区别3)
    	int y = x, z = 0 ; sta[++z] = y ; //sta为栈,暂存当前点到根的整条路径,pushdown时一定要从上往下放标记(与普通Splay的区别4)
    	while(check(y)) sta[++z] = y = f[y] ; 
    	while(z) pushdown(sta[z--]) ; 
    	while(check(x)) 
    	{
    		y = f[x] , z = f[y] ; 
    		if(check(y)) Rorate(jd(x) ^ jd(y) ? x : y) ; 
    		Rorate(x) ; 
    	}
    	pushup(x) ; 
    }
    

    囊括了几乎上文所有内容 。

    因为上文都说了,所以这里也是直接给出代码了。不过这个是早写的,所以用的是结构体存的,不过没什么两样

    //
    /*
    Author : Zmonarch
    Knowledge :
    */
    #include <bits/stdc++.h>
    #define int long long
    #define inf 2147483647
    #define qwq register
    #define qaq inline
    using namespace std ;
    const int kmaxn = 1e6 + 10 ;
    qaq int read() {
    	int x = 0 , f = 1 ; char ch = getchar() ;
    	while(!isdigit(ch)) {if(ch == '-') f = - 1 ; ch = getchar() ;}
    	while( isdigit(ch)) {x = x * 10 + ch - '0' ; ch = getchar() ;}
    	return x * f ;
    }
    int n , m ; 
    int f[kmaxn] , rt[kmaxn] , sum[kmaxn] , s[kmaxn]; 
    struct SPLAY {
    	int val , sum ; 
    	bool tag ; // 区间翻转的标记 
    	int ch[2] ; 
    	SPLAY() {
    	 tag = ch[1] = ch[0] = 0 ; 
    	}
    }st[kmaxn << 1];
    qaq bool check(int x) {
    	return (st[f[x]].ch[0] == x) || (st[f[x]].ch[1] == x) ; 
    }
    qaq void pushup(int u) {
    	st[u].sum = st[u].val ^ st[st[u].ch[0]].sum ^ st[st[u].ch[1]].sum ;
    }
    qaq void Pushover(int u) {
    	swap(st[u].ch[1] , st[u].ch[0]) ; 
    	st[u].tag ^= 1 ; 
    }
    qaq void pushdown(int u) {
    	if(st[u].tag)
    	{
    		if(st[u].ch[0]) Pushover(st[u].ch[0]) ; 
    		if(st[u].ch[1]) Pushover(st[u].ch[1]) ;
    		st[u].tag = 0 ;
    	} 
    }
    qaq void Rorate(int x) {
    	int y = f[x] , z = f[y] , k = (st[y].ch[1] == x) , w = st[x].ch[!k] ;
    	if(check(y)) st[z].ch[st[z].ch[1] == y] = x ;
    	st[x].ch[!k] = y ; st[y].ch[k] = w ; 
    	if(w) f[w] = y ; f[y] = x ; f[x] = z ; pushup(y) ; 
    }
    qaq void Splay(int x) {
    	int y = x , z = 0 ; rt[++z] = y ; 
    	while(check(y)) rt[++z] = y = f[y] ; 
    	while(z) pushdown(rt[z--]) ; 
    	while(check(x)) 
    	{
    		y = f[x] ; z = f[y] ; 
    		if(check(y)) Rorate((st[y].ch[0] == x) ^ (st[z].ch[0] == y) ? x : y) ;
    		Rorate(x) ; 
    	} 
    	pushup(x) ;
    }
    qaq void Access(int u) {
    	for(qwq int y = 0 ; u ; y = u , u = f[u]) 
    	 Splay(u) , st[u].ch[1] = y , pushup(u) ; 
    // 通过虚链指定父亲,将这个父亲旋转到当前父亲所在的 Splay 的根上,更新 u 这个点的右儿子。 
    }
    qaq void MakeRoot(int u) { // 指定 u 为原树的根 
    	Access(u) ; Splay(u) ; Pushover(u) ; 
    }
    qaq int FindRoot(int u) {
    	Access(u) ; Splay(u) ; 
    	while(st[u].ch[0]) pushdown(u) , u = st[u].ch[0] ; 
    	Splay(u) ; return u ;   
    }
    qaq void Split(int u , int v) { // 使得 u , v 这一条链能在一个 Splay 中 
    	MakeRoot(u) ; Access(v) ; Splay(v) ; 
    	// 先让 u 成为根,然后直接 Access 打通 v 到根 
    }
    qaq void Link(int u , int v) { // 判断连一条 u , v 的边是否合法  
    	MakeRoot(u) ; 
    	if(FindRoot(v) != u)  f[u] = v ; // u -> v 的边
    	// u 已经是 Splay 的了,根据认父不认子,所以直接向这个根连 
    }
    // 这是保证存在该边的情况 
    qaq void Cut(int u , int v) { // 断开 u - v 这条边 
    	Split(u , v) ; f[u] = st[v].ch[1] = 0 ; pushup(u) ;  
    }
    // 这是不保证存在该边的情况
    qaq void Pre_Cut(int u , int v) {
    	MakeRoot(u) ; 
    	if(FindRoot(v) == u && f[v] == u && !st[v].ch[0]) f[v] = st[u].ch[1] = 0 , pushup(u) ;  
    }
    signed main() {
    	n = read() , m = read() ; 
    	for(qwq int i = 1 ; i <= n ; i++) st[i].val = read() ; 
    	for(qwq int i = 1 ; i <= m ; i++) 
    	{
    		int opt = read() , x = read() , y = read() ; 
    		if(opt == 0) Split(x , y) , printf("%lld
    " , st[y].sum) ; 
    		if(opt == 1) Link(x , y) ;
    		if(opt == 2) Pre_Cut(x , y) ; 
    		if(opt == 3) Splay(x) , st[x].val = y ; 
    	}
    	return 0 ;
    }
    

    题单

    这里就是照搬 (FlashHu) 大佬的 LCT总结——应用篇(附题单)(LCT) 这篇博客了。

    维护链信息(LCT上的平衡树操作)

    P3690 【模板】Link Cut Tree
    P3203 [HNOI2010]弹飞绵羊
    P1501 [国家集训队]Tree II
    P2486 [SDOI2011]染色
    P4332 [SHOI2014]三叉神经树


    动态维护连通性&双联通分量

    P2147 [SDOI2008] 洞穴勘测
    P3950 部落冲突
    P2542 [AHOI2005]航线规划
    BZOJ4998 星球联盟
    BZOJ2959 长跑


    维护边权(常用于维护生成树)

    P4172 [WC2006]水管局长
    UOJ274温暖会指引我们前行
    P4180 [BJWC2010]严格次小生成树
    P4234 最小差值生成树
    P2387 [NOI2014] 魔法森林


    维护子树信息

    P4219 [BJOI2014]大融合
    U19482 山村游历(Wander)
    #3510. 首都
    SP2939 QTREE5 - Query on a tree V
    #558. 「Antileaf's Round」我们的 CPU 遭到攻击


    维护树上染色联通块

    P2173 [ZJOI2012]网络
    P3703 [SDOI2017]树点涂色
    SP16549 QTREE6 - Query on a tree VI
    SP16580 QTREE7 - Query on a tree VII
    #3914. Jabby's shadows


    特殊题型

    #207. 共价大爷游长沙
    P3348 [ZJOI2016]大森林
    P4338 [ZJOI2018]历史
    #2289. 「THUWC 2017」在美妙的数学王国中畅游


    (ans so on…)


    鸣谢

    LCT总结——概念篇
    OI-Wiki -- Link Cut Tree
    LCT总结——应用篇(附题单)(LCT)

  • 相关阅读:
    webpy安装
    windows 上jenkins slave 执行脚本提示成功,但是没有运行
    jenkins slave上执行脚本报错
    python selenium2 动态调试
    maven配置阿里云国内仓库
    jenkins部署报404错误
    elipse常用插件下载
    jenkins部署
    国内开源镜像站
    最大公约数
  • 原文地址:https://www.cnblogs.com/Zmonarch/p/15105246.html
Copyright © 2011-2022 走看看