zoukankan      html  css  js  c++  java
  • LCT 模板及套路总结

    这一个月貌似已经考了无数次(LCT)了.....
    保险起见还是来一发总结吧.....

    A. LCT 模板

    (LCT) 是由大名鼎鼎的 (Tarjan) 老爷发明的。
    主要是用来维护树上路径问题的。 它的神奇之处在于可以直接把一条路径抠出来维护。
    其实就是维护树链剖分中的重链与轻链。
    网上相关教程很多,直接给板子(其实是我懒得打E_E)

    0. Splay代码:

    (LCT)是以(Splay) 为实现基础的:

    IL bool Son(RG int x){return ch[fa[x]][1] == x;}
    IL bool Isroot(RG int x){return ch[fa[x]][1]!=x && ch[fa[x]][0]!=x; }
    
    IL void Rot(RG int x){
    	RG int y = fa[x] , z = fa[y] , c = Son(x);
    	if(!Isroot(y))ch[z][Son(y)] = x; fa[x] = z;
    	ch[y][c] = ch[x][!c]; fa[ch[y][c]] = y;
    	ch[x][!c] = y; fa[y] = x; PushUp(y);
    }
    IL void Splay(RG int x){
    	RG int top = 0; stk[++top] = x;
    	for(RG int i = x; !Isroot(i); i = fa[i])stk[++top] = fa[i];
    	while(top)PushDown(stk[top--]);
    	for(RG int y = fa[x]; !Isroot(x) ; Rot(x) , y = fa[x])
    		if(!Isroot(y))Son(x) ^ Son(y) ? Rot(x) : Rot(y);
    	PushUp(x); return;
    }
    

    1. Access(x) 操作

    作用是把 (x) 到根节点的这条路径变为重链。

    void Access(int x){
         for(RG int y=0;x;y=x,x=fa[x])
             Splay(x) , ch[x][1] = y , PushUp(x);
         }
    }
    

    2. MakeRoot(x) 操作

    作用是使 (x) 节点成为原树中的根。

    IL void Makeroot(RG int x){Access(x); Splay(x); Reverse(x);}
    

    对应的辅助函数:

    IL bool Reverse(RG int x){swap(ch[x][1],ch[x][0]); rev[x] ^= 1;}
    
    IL void PushDown(RG int x){
        if(rev[x])
            rev[x] ^= 1 ,
            Reverse(ch[x][0]) , Reverse(ch[x][1]) ;
    }
    

    3. FindRoot(x) 操作

    作用是找到(x)所在原树的根节点。

    IL int FindRoot(RG int x){
        Access(x); Splay(x); 
        while(ch[x][0])x=ch[x][0]; return x;
    }
    

    4. Split(x,y) 操作

    作用是分离出(x)(y)这条路径。

    IL void Split(RG int x,RG int y){MakeRoot(x); Access(y); Splay(y);}
    

    那么此时路径信息都存在节点 (y) 上了,要查询什么直接查即可。

    5. Link(x,y) 操作

    作用是连接两个结点(x) , (y)

    IL void Link(RG int x,RG int y){if((!x)||(!y))return; MakeRoot(x); fa[x] = y; }
    

    6. Cut(x,y) 操作

    作用是删除(x) , (y) 之间的连边

    IL void Cut(RG int x,RG int y){if((!x)||(!y))return; Split(x,y); ch[y][0] = fa[x] = 0; PushUp(y);}
    

    7. PushUp(x) PushDown(x) 操作

    其实是与线段树一样的,直接维护题目所求即可。
    代码略,以实际题目为准(注意翻转左右儿子的(PushDown)是必须有的)。

    8.贴一发完整的(LCT)模版

    
    namespace Link_Cut_Tree{
    	
    	bool Son(RG int x){return ch[fa[x]][1] == x;}
    	bool Isroot(RG int x){return ch[fa[x]][1]!=x && ch[fa[x]][0]!=x; }
    	bool Reverse(RG int x){swap(ch[x][1],ch[x][0]); rev[x] ^= 1;}
    
    	void PushUp(RG int x){ ....... }
    	void PushDown(RG int x)
            {if(rev[x])Reverse(ch[x][0]) , Reverse(ch[x][1]) , rev[x] ^= 1;}
    
    	void Rot(RG int x){
    		RG int y = fa[x] , z = fa[y] , c = Son(x);
    		if(!Isroot(y))ch[z][Son(y)] = x; fa[x] = z;
    		ch[y][c] = ch[x][!c]; fa[ch[y][c]] = y;
    		ch[x][!c] = y; fa[y] = x; PushUp(y);
    	}
    	void Splay(RG int x){
    		RG int top = 0; stk[++top] = x;
    		for(RG int i = x; !Isroot(i) ; i = fa[i])stk[++top] = fa[i];
    		while(top)PushDown(stk[top--]);
    		for(RG int y = fa[x]; !Isroot(x) ; Rot(x) , y = fa[x])
    			if(!Isroot(y))Son(x) ^ Son(y) ? Rot(x) : Rot(y);
    		PushUp(x); return;
    	}
    	
    	void Access(RG int x)
    	    { for(RG int y = 0; x; y = x,x = fa[x])Splay(x),ch[x][1] = y,PushUp(x); }
        void MakeRoot(RG int x){Access(x); Splay(x); Reverse(x);}
        int FindRoot(RG int x){Access(x); Splay(x); while(ch[x][0])x=ch[x][0]; return x;}
        void Split(RG int x,RG int y){MakeRoot(x); Access(y); Splay(y);}
        void Link(RG int x,RG int y){if((!x)||(!y))return; MakeRoot(x); fa[x] = y;  }
        void Cut(RG int x,RG int y){if((!x)||(!y))return; Split(x,y); ch[y][0] = fa[x] = 0; PushUp(y);}
    
    }
    

    B. LCT 维护 边权 信息

    具体的实现方法有几种,这里只讲一种最简单、最常用的方法。
    对于每一条边,新建一个点,然后连向对应的两个原树节点。
    那么常见的维护方法是 代表点的节点不赋值,只有代表边的节点才赋值。
    具体可以见这一道非常经典的题目: [NOI2014]魔法森林
    伪代码为:
    (Link)操作直接进行即可。

    IL void Link(Node u,Node v)
       NewNode(val_edge) ---(give)---> code_edge
       Link(u,code_edge);  Link(v,code_edge);
    

    (Cut)操作则需要找到这条边对应的两个节点,然后直接删除即可。

    IL void Cut(Edge e)
        Edge e ---(find)--> Node u1,u2 ;  
        Delete(u1,code_e);  Delete(u2,code_e);
    

    这里注意 :
    一定要找到对应的节点,而不是code_e的左右儿子!! ,不然保准你 WA 的怀疑人生。
    然后一个非常经典的应用 就是 动态求图的割边(桥):
    具体见这题:[AHOI2005]LANE 航线规划 ;;;;;;;;

    C. LCT 维护 子树 信息

    理论上来说(LCT)是不适合维护子树信息的,但总有一些毒瘤的出题人**..
    给一篇写的非常详细的博客:http://blog.csdn.net/neither_nor/article/details/52979425
    怎么搞呢?
    先明确一下概念。
    (LCT) 链剖后,一些路径变为了 单独的 一棵(Splay)
    我们类似树链剖分,把节点划为实儿子、虚儿子。

    实儿子就是在同一棵(Splay)里的那个儿子,其它则为虚儿子。
    那么实儿子对应的子树就为实子树,虚儿子对应的子树就为虚子树。
    .
    其实我们原来的(LCT)就是维护了实子树对吧?
    所以我们在维护子树信息时,只要再维护一下虚子树的信息即可。
    以求子树大小为例。
    我们假设(sz[u])表示(u)虚子树大小 , (sum[u])表示整个子树大小,
    那么显然,我们(PushUp)时,只需要更新(sum) , (sz)我们手动维护。

    IL void PushUp(RG int x){ sum[x] = sum[ch[x][0]]+sum[ch[x][1]]+sz[x]+1; }
    

    那么考虑什么情况下会改变虚子树的大小,其实只有两个:

    1. Access'(x) 操作

    我们会把 (x) 原来的实儿子变为虚儿子,把另一个虚儿子变为实儿子。
    我们对应的修改一下(x)(sz)值,然后(PushUp)修改(sum)值即可。

    IL void Access(RG int x){
    	for(RG int y = 0; x; y = x,x = fa[x]){
    		Splay(x);
    		sz[x] += sum[ch[x][1]] - sum[y];
    		ch[x][1] = y; PushUp(x); 
    	}return;
    }
    

    2. Link'(x,y) 操作

    我们把(x)变为(y)的儿子。
    那么显然(x)以及其所有祖先的(sz)值都会收到影响。
    解决办法非常简单,把(y)(MakeRoot) 一下即可使得(x)只影响 (y)
    注意一下由于我们修改了(y)(sz)值,所以要(PushUp)一下(y)节点。

    IL void Link(RG int x,RG int y){
            if((!x)||(!y))return; 
    	MakeRoot(x); MakeRoot(y);
    	fa[x] = y; sz[y] += sum[x]; PushUp(y); 
    }
    

    3.应用

    应用一般就是求解会不断换根的子树信息问题。
    最经典的一道题目是:「BJOI2014」大融合

    D.LCT 维护图上信息

    理论上来说(LCT)也是不适合维护图上信息的,但总有一些毒瘤的出题人..
    图上信息的维护是不支持删边操作的。
    其实图与树的差别就是可能有重边、环之类的。
    那么既然我们只需要支持加边操作,那么只要用并查集维护缩点即可。
    怎么维护呢? 这其实也是维护双联通分量的套路了。
    .
    开两个并查集:
    (1)(bzj[u][1]),用于维护连通性
    每次(Link)前,先查找一下两个点是否联通,如果不连通,则直接相连即可。
    如果联通,则需要用到第二个并查集:
    (2)(bzj[u][2]),用于维护双联通性
    如果两个点已经双联通了,那么再加边其实对双联通性没有影响。
    如果两个点的(bzj2)未相连,那么把两个的(bzj2)相连,然后缩点。
    如果两个点的(bzj2)已相连,那么说明这两个点已经缩在一起了,所以无需连边。
    .
    那么此时(bzj2)其实就是每个点对应的缩点后的点(id)了。
    然后是实现,实现的时候,想一想(LCT)的性质,我们可以发现:
    只有向上跳父亲的时候,缩点才会对操作有影响
    为了方便描述,定义一个(F)函数用于找到一个点(u)的对应(id)

    int Find(RG int x,RG int od){return(bzj[x][od]==x)?x:Find(bzj[x][od],od);}
    int F(RG int x){return Find(x,2); }
    

    然后正常的(LCT)操作都遵循上面那个结论,找父亲是(Find)一下即可。
    (Splay)为例(什么(Access)之类的都是一样的):

    IL void Splay(RG int x){
        RG int top = 0; stk[++top] = x;
        for(RG int i = x; !Isroot(i); i = F(fa[i]))stk[++top] = F(fa[i]); 
        while(top)PushDown(stk[top--]);
        for(RG int y = F(fa[x]); !Isroot(x); Rot(x),y = F(fa[x]))
            if(!Isroot(y))Son(x) ^ Son(y) ? Rot(x) : Rot(y);
        PushUp(x);  return;
    }
    

    关键:(Add(x,y)) 操作

    关键在于链接两个点时的操作,这时候是不能直接执行(Link)操作的。
    先给代码:

    IL void Add(RG int u,RG int v){
        RG int x = F(u) , y = F(v);
        RG int f1 = Find(x,1) , f2 = Find(y,1);
        if(f1 != f2){
            bzj[f1][1] = f2;
            Link(x,y);  return;
        }
        else{
            if(x == y)return;
            Split(x,y); Merge(y,y);   //缩点
            ch[y][0] = ch[y][1] = 0;  //!!!!!!!!
        }return;
    }
    

    显然就是按照上面的并查集维护原则连边。
    然后给一下缩点的代码:

    IL void Merge(RG int u,RG int rt){
        if(u ^ rt)
            sum[rt] += sum[u], bzj[F(u)][2] = rt, sum[u] = fig[u] = 0;
        if(ch[u][0])Merge(ch[u][0],rt);
        if(ch[u][1])Merge(ch[u][1],rt);
    }
    

    其中(sum)为结点自身的信息(本身值),(fig)为子树信息(计算值)。

    应用:

    这个就依题目而定吧。
    请记住一个最明显的特征:只支持加边,不支持删边!
    (LCT)维护图上信息最经典的一题为:[BZOJ 2959]长跑 , 友情提示注意卡常。

    有根LCT

    自行yy,唯一一个需要注意的地方:
    Cut(u,Fa) 的时候,必须把(fa)转到(Splay)顶端!(不能把(u)转到顶端),然后剪切。

    IL void Cut(int u , int Fa) {
        Access(u) ; Splay(Fa) ;
        ch[1][Fa] = fa[u] = 0 ; PushUp(Fa) ; return ; 
    }
    

    因为如果转(u)到顶端的话,剪切后整棵树的根就会产生变化,转移到(u)所属(Splay)中。

  • 相关阅读:
    多态
    没有抽象方法的抽象类有什么意义
    抽象类继承(雇员练习)
    怎样在win7中 安装Tomcat7.0
    继承训练
    Java的接口和抽象类
    jQuery插件的学习
    jQuery学习之路-A
    android之路-android事件处理-OnTouchListener
    丢弃的东西,还能否找回?
  • 原文地址:https://www.cnblogs.com/GuessYCB/p/8330024.html
Copyright © 2011-2022 走看看