• LCT学习笔记


    这回是真的林克卡特树了(

    简述

    LCT 基于实链剖分,维护对象是一个森林。

    对于一个节点,向某一个儿子的连边划分为实边,其他儿子的连边为虚边。虚实可以动态变化,所以要用 Splay 来维护每一条由若干实边连接成的实链。

    LCT 支持以下操作:

    • 查询、修改链信息
    • 换根
    • 动态加减边,合并/分离两棵树
    • 动态维护连通性
    • 其他

    LCT 的主要性质:

    • 每个 Splay 维护一条从上到下,按照在原树中的深度严格递增的路径,且中序遍历得到的深度序列严格递增。
    • 每个节点属于且只属于一个 Splay
    • 边分为实边和虚边,实边在 Splay 中,虚边由一棵 Splay 指向该 Splay 中序遍历最靠前的点在原树中的父亲。
    • 由于性质2,当一个点有多个儿子时,显然只能连一条实边,而其他儿子不在这个 Splay 中,那么为了保持树的性质,其他儿子的边要成为虚边,由儿子所属 Splay 的根节点连向该点。

    具体操作

    其实就是几个基本操作拼来拼去啦QWQ 简单易上手的拼接玩具LCT,你值得拥有

    access

    操作目的:得到一个 Splay ,其中序遍历的起点是根,终点是指定点。

    实现方式:

    定义 ( ext{splay}(x)) 为将节点 (x) 转到当前 Splay 中的根。

    循环进行如下步骤,直到操作点成为根节点:

    1. 一次 ( ext{splay}) 将当前点转到根
    2. 换儿子,将实边所指的儿子换到当前点
    3. 更新信息
    4. 当前点更换成虚边指向的父亲。(也就是当前 Splay 往上走)
    void Access( int x ) 
    { 
    	for ( int y=0; x; x=fa[y=x] ) 
    		Splay(x),rc=y,Pushup(x); 
    }
    

    makeroot

    操作目的:换根,让指定点成为原树的根。

    实现方式:

    注意到 ( ext{access}) 之后,当前点 (x)Splay 中是深度最大的点;而 ( ext{splay}(x)) 之后,(x)Splay 中没有右子树(因为性质一,不然不能保证中序遍历深度的严格递增)。所以翻转整个 Splay (也就是交换左右子树,打标记)之后,(x) 就会成为深度最小的点(根节点)。

    void MakeRoot( int x ) 
    { 
    	Access(x); Splay(x); Flip(x); 
    } 
    

    findroot

    操作目的:找到 (x) 所在原树的树根,主要用来判定连通性。

    实现方式:

    根据性质一,我们只需要不断走一个点的左儿子,深度是单调递减的。所以先 ( ext{access}) 一下,然后 ( ext{splay}) 一遍转到根,不断往左子树走就好了。最后把得到的 (x)( ext{splay}) 上去即可。

    int FindRoot( int x )		//找原树上的树根
    {
    	Access(x); Splay(x);			//取出路径,转到根
    	while ( lc ) Pushdown( x ),x=lc;		//每次往深度小的左子树走
    	Splay(x); return x;	
    }
    

    split

    操作目的:拉出 (x o y) 的路径,使之成为一个 Splay (不妨设最后的根是 (y) ).

    实现方式:先 ( ext{makeroot})(x) 成为根,然后对 (y) 进行 ( ext{access}) ,这样就拿出了这条路径,最后为了让 (y) 成为根,( ext{splay}(y)) 即可。

    void Split( int x,int y ) 
    { 
    	MakeRoot(x); Access(y); Splay(y); 
    }	//取出路径
    

    操作目的:加一条 ((x,y)) 的边。(这里是让 (x) 的父亲指向 (y) ,连一条轻边)

    实现方式:先 ( ext{makeroot}(x)) ,然后令 (f[x]=y) 即可。

    注意,如果题目没有保证连边一定合法,要先判一遍 (x,y) 的连通性。

    void Link( int x,int y ) 
    { 
    	MakeRoot(x); 
    	if ( FindRoot(y)^x ) fa[x]=y; 
    } //加边
    

    cut

    操作目的:将边 ((x,y)) 断开。

    如果保证合法显然不难:

    先搞一个 ( ext{split}) 拿出 ((x,y)) ,然后直接断掉 (y) 的左儿子,断掉 (x) 的父亲即可。最后不要忘记 ( ext{pushup}) .

    如果不一定存在呢?

    首先 ( ext{makeroot}(x)) ,然后用 ( ext{findroot}(y)) 判断连通性。接下来再判断 (x,y) 是否有父子关系,和 (y) 是否有左儿子(因为如果没有直接连边,中序遍历中左儿子会在 (x,y) 之间)。判断完之后就断掉 (x) 的右子树和 (y) 的父亲即可。最后还是要 ( ext{pushup}(x)) .

    void Cut( int x,int y ) 	//删边
    {
    	MakeRoot(x);	//分别判连通性,父子关系,左儿子
    	if ( FindRoot(y)==x && fa[y]==x && !tr[y][0] ) 
    	{ 
    		fa[y]=tr[x][1]=0; Pushup(x); 
    	}
    }
    

    模板

    题目链接

    维护四种操作:加边,删边,查询路径异或和,单点改点权。

    代码略有压行(

    //Author:RingweEH
    //P3690 【模板】Link Cut Tree
    #define lc tr[x][0]
    #define rc tr[x][1]
    const int N=3e5+10;
    int fa[N],tr[N][2],val[N],sum[N],n,m,sta[N];
    bool tag[N];
    
    bool IsRoot( int x ) { return (tr[fa[x]][0]==x) || (tr[fa[x]][1]==x); }	
    //判断Splay的根,如果是虚边显然不会是父亲的儿子
    void Pushup( int x ) { sum[x]=sum[lc]^sum[rc]^val[x]; }
    void Flip( int x ) { int tmp=lc; lc=rc; rc=tmp; tag[x]^=1; } //Splay翻转
    void Pushdown( int x )		//标记下传,默认自己已经翻转完了
    {
    	if ( tag[x] )
    	{
    		if ( lc ) Flip( lc );
    		if ( rc ) Flip( rc );
    		tag[x]=0;
    	}
    }
    void Rotate( int x )
    {
    	int y=fa[x],z=fa[y],kid=tr[y][1]==x,oth=tr[x][!kid];
    	if ( IsRoot(y) ) tr[z][tr[z][1]==y]=x;
    	tr[x][!kid]=y; tr[y][kid]=oth;
    	if ( oth ) fa[oth]=y;
    	fa[y]=x; fa[x]=z; Pushup(y);
    }
    void Splay( int x )
    {
    	int y=x,top=0; sta[++top]=y;
    	while ( IsRoot(y) ) sta[++top]=y=fa[y];
    	while ( top ) Pushdown(sta[top--]);
    	while ( IsRoot(x) )
    	{
    		y=fa[x]; top=fa[y];
    		if ( IsRoot(y) ) Rotate((tr[y][0]==x)^(tr[top][0]==y) ? x : y );
    		Rotate(x);
    	}
    	Pushup(x);
    }
    void Access( int x ) { for ( int y=0; x; x=fa[y=x] ) Splay(x),rc=y,Pushup(x); } //取到根
    void MakeRoot( int x ) { Access(x); Splay(x); Flip(x); }	//换根 
    int FindRoot( int x )		//找原树上的树根
    {
    	Access(x); Splay(x);			//取出路径,转到根
    	while ( lc ) Pushdown( x ),x=lc;		//每次往深度小的左子树走
    	Splay(x); return x;	
    }
    void Split( int x,int y ) { MakeRoot(x); Access(y); Splay(y); }	//取出路径
    void Link( int x,int y ) { MakeRoot(x); if ( FindRoot(y)^x ) fa[x]=y; } //加边
    void Cut( int x,int y ) 	//删边
    {
    	MakeRoot(x);	//分别判连通性,父子关系,左儿子
    	if ( FindRoot(y)==x && fa[y]==x && !tr[y][0] ) { fa[y]=tr[x][1]=0; Pushup(x); }
    }
    
    int main()
    {
    	n=read(); m=read();
    	for ( int i=1; i<=n; i++ ) val[i]=read();
    	while ( m-- )
    	{
    		int opt=read(),x=read(),y=read();
    		if ( opt==0 ) Split( x,y ),printf("%d
    ",sum[y] );
    		else if ( opt==1 ) Link(x,y);
    		else if ( opt==2 ) Cut(x,y);
    		else if ( opt==3 ) Splay(x),val[x]=y;
    	}
    
    	return 0;
    }
    

    应用:维护链上信息

    举个例子?模板就是。

    弹飞绵羊

    (n) 个带点权的点,在走到第 (i) 个点时会跳到第 (i+k_i) 个,如果不存在那么就飞了。

    维护单点修改,查询从第 (i) 个点开始几次会飞。


    建图一眼题。显然是 ((i,i+k_i)) 连边,这里有两种写法:

    第一种,把飞的情况视为终点,那么就是一棵树,原封不动套板子就行。

    第二种,把飞的边忽略掉,那么查询步数就是查询点到原树根节点的路径长度。发现我们根本不需要换根这个操作,那么就可以对板子进行一些魔改:

    • 对于连边,由于是一棵树,所以直接改父亲,连轻边。
    • 对于删边,用 ( ext{access})( ext{splay}) 来确定父亲位置,这时候父亲一定在左子树中,直接双向断开即可。
    • 对于查询,直接 ( ext{access}) 然后 ( ext{splay}) ,输出 (siz) 即可。

    这里是第二种的参考代码。

    //Author:RingweEH
    //P3203 [HNOI2010]弹飞绵羊
    bool IsRoot( int x ) { return (tr[fa[x]][0]==x) || (tr[fa[x]][1]==x); } 
    void Pushup( int x ) { sum[x]=sum[lc]+sum[rc]+1; }
    void Rotate( int x )
    {
        int y=fa[x],z=fa[y],kid=tr[y][1]==x,oth=tr[x][!kid];
        if ( IsRoot(y) ) tr[z][tr[z][1]==y]=x;
        tr[x][!kid]=y; tr[y][kid]=oth;
        if ( oth ) fa[oth]=y;
        fa[y]=x; fa[x]=z; Pushup(y);
    }
    void Splay( int x )
    {
        int y,z;
        while ( IsRoot(x) )
        {
            y=fa[x]; z=fa[y];
            if ( IsRoot(y) ) Rotate((tr[y][0]==x)^(tr[z][0]==y) ? x : y );
            Rotate(x);
        }
        Pushup(x);
    }
    void Access( int x ) { for ( int y=0; x; x=fa[y=x] ) Splay(x),rc=y,Pushup(x); }
    
    int main()
    {
        n=read(); 
        for ( int i=1,k; i<=n; i++ )
        {
            sum[i]=1; k=read();
            if ( i+k<=n ) fa[i]=i+k;
        }
        m=read();
        while ( m-- )
        {
            int opt=read(),x=read(),y;		//注意标号从0开始
            if ( opt&1 ) { x++; Access(x); Splay(x); printf( "%d
    ",sum[x] ); }
            else
            {
                x++; y=read(); Access(x); Splay(x);
                tr[x][0]=fa[tr[x][0]]=0;		//直接双向断边
                if ( x+y<=n ) fa[x]=x+y; 		//加新边
                Pushup(x);
            }
        }
    
        return 0;
    }
    

    应用:维护连通性

    如果只是维护连通性,直接 ( ext{findroot}) 就好了。

    不过 sk 的首页写着 我单方面宣布 findroot × ?不太懂,可能是复杂度出了什么大问题,下次问问他

    upd:好像 ( ext{findroot}) 复杂度确实不行……能用并查集尽量并查集吧.

    如果要维护双连通分量,就套个并查集,然后加边时如果不连通就合并,如果联通就说明有环,缩点合并即可。

    部落冲突

    维护一棵树,支持加边,删边,询问连通性。


    无脑 LCT 好啊。注意,题目虽然保证了一打东西,但是 ( ext{Cut}) 还是要特判,不然会五颜六色……

    上次交这道题是在去年暑假了,开 O21.44s ,但是这次没开就直接 1.36s 了,开完之后直接 927ms/cy .

    #define PII pair<int,int>
    PII event[N];
    
    int main()
    {
        n=read(); m=read(); int u,v,tot=0;
        for ( int i=1; i<n; i++ ) u=read(),v=read(),Link(u,v);
    
        while ( m-- )
        {
            char opt[5]; scanf( "%s",opt );
            if ( opt[0]=='Q') u=read(),v=read(),MakeRoot(u),puts( (u^FindRoot(v)) ? "No" : "Yes" );
            else if ( opt[0]=='C' ) u=read(),v=read(),event[++tot]=make_pair(u,v),Cut(u,v);
            else u=read(),Link( event[u].first,event[u].second ); 
        }
    }
    

    应用:维护链上的边权信息

    两个字:拆边

    把边看做一个点,向两个端点连边。由于不需要维护真正的点权,所以代表点的点权设空。写的时候直接 ( ext{link/cut}) 两次就好了。

    最小差值生成树

    求边权最大值与最小值的差值最小的生成树。


    从小到大对边权排序,每次枚举一条边,没连通就连上,否则就替换环上最小的一条边,有了生成树之后每次更新答案。

    维护最小边就直接搞个指针就好了。不知道为什么这份代码松得不行……随手一交就最优解 rank3 了虽然开O2一点用都没有

    我学习的博客的那个博主开了 fread + O2 松不过我一个快读不开 O2 的大常数老年选手(

    //Author:RingweEH
    #define lc tr[x][0]
    #define rc tr[x][1]
    const int N=5e4+10,M=2e5+10,P=N+M,INF=0x3f3f3f3f;
    int f[P],tr[P][2],mn[P],fa[N],val[P],n,m,ans;
    bool tag[P],vis[M];
    struct Edge
    {
        int fro,to,len;
        bool operator < ( Edge tmp ) { return len<tmp.len; }
    }e[M];
    bool IsRoot( int x ) { return tr[f[x]][0]==x || tr[f[x]][1]==x; }
    int vmin( int x,int y ) { return val[x]<val[y] ? x : y; }
    void Pushup( int x ) { mn[x]=vmin(x,vmin(mn[lc],mn[rc])); }
    void Pushdown( int x ) { if ( tag[x] ) { int tmp=lc; tag[lc=rc]^=1; tag[rc=tmp]^=1; tag[x]=0; } }
    void Pushall( int x ) { if ( IsRoot(x) ) Pushall(f[x]); Pushdown(x); }
    void Rotate( int x ) 
    {
        int y=f[x],z=f[y],kid=tr[y][1]==x,oth=tr[x][!kid];
        if ( IsRoot(y) ) tr[z][tr[z][1]==y]=x;
        tr[x][!kid]=y; tr[y][kid]=oth; f[oth]=y; f[y]=x; f[x]=z;
        Pushup(y);
    }
    void Splay( int x )
    {
        int y=x; Pushall(x);
        while ( IsRoot(x) )
        {
            if ( IsRoot(y=f[x]) ) Rotate( (tr[y][0]==x)^(tr[f[y]][0]==y) ? x : y );
            Rotate(x);
        }
        Pushup(x);
    }
    void Access( int x ) { for ( int y=0; x; x=f[y=x] ) Splay(x),rc=y,Pushup(x); }
    void MakeRoot( int x ) { Access(x); Splay(x); tag[x]^=1; }
    void Link( int i ) { MakeRoot(e[i].fro); f[f[e[i].fro]=N+i]=e[i].to; }
    void Cut( int x ) { Access(e[x-N].fro); Splay(x); lc=rc=f[lc]=f[rc]=0; }
    int Find( int x ) { return x==fa[x] ? x : fa[x]=Find(fa[x]); }
    
    int main()
    {
        n=read(); m=read();
        for ( int i=0; i<=n; i++ ) fa[i]=i,val[i]=INF;
        for ( int i=1; i<=m; i++ ) e[i].fro=read(),e[i].to=read(),e[i].len=read();
    
        sort( e+1,e+1+m ); int cnt=1,pt=1,x,y;
        for ( int i=1; i<=m; i++ )
        {
            val[i+N]=e[i].len;
            if ( Find(x=e[i].fro)^Find(y=e[i].to) )
            {
                vis[i]=1; Link(i); fa[fa[x]]=fa[y]; cnt++;
                if ( cnt==n ) ans=e[i].len-e[pt].len;
            }
            else
            {
                if ( x==y ) continue;
                vis[i]=1; MakeRoot(x); Access(y); Splay(y); vis[mn[y]-N]=0; 
                while ( !vis[pt] ) pt++;
                Cut(mn[y]); Link(i);
                if ( cnt==n ) bmin( ans,e[i].len-e[pt].len );
            }
        }
        printf( "%d
    ",ans );
        return 0;
    }
    

    应用:维护虚子树信息总和与原树信息总和

    由于原树的边只有实边和虚边两种,所以子树也可以分成实子树和虚子树,而实子树可以通过 Splay 得到(显然实子树就是一条实链),那么我们需要维护的就是虚子树和,总和可以通过这两个东西得到。

    下面用 (si) 表示虚子树信息和,用 (s) 表示原树信息和。

    显然 (s) 就是实加虚,所以不难得出 ( ext{pushup}) 的代码:

    void Pushup( int x ) { s[x]=s[lc]+s[rc]+si[x]+v[x]; }
    

    然后来考虑每个操作对虚实边的影响。

    ( ext{access}) :每一次 ( ext{splay}) 之后,更改了右子树,得到了一个虚儿子,失去了一个,总和没变。那么就直接让 (si) 加上原来右儿子的 (s) ,再减去新的右儿子的 (s) 即可。(如果 ( ext{pushup}) 只维护 (s) 可以直接删去)

    void Access( int x )
    {
    	for ( int y=0; x; x=f[y=x] )
    	{
    		Splay(x);
    		si[x]+=s[c[x][1]]; si[x]-=s[c[x][1]=y];
    		pushup(x);
    	}
    }
    

    ( ext{link})(y) 多了一个虚边指向儿子 (x) ,那么 (s[y],si[y]+=s[x]) ,而且 (y) 在 LCT 中的祖先的 (x) 同样要累加,所以一开始要把 (y) 转到根。

    //保证合法
    void Link( int x,int y )
    {
    	Split( x,y ); si[fa[x]=y]+=s[x]; Pushup(y);
    }
    //不保证
    bool Link( int x,int y ) 
    {
    	MakeRoot( x );
    	if ( FindRoot(y)==x ) return 0;
    	si[fa[x]=y]+=s[x]; Pushup(y); return 1;
    }
    

    大融合

    构建一棵树,在加边过程中动态查询经过某条边的路径数量。


    仔细想想就会发现,路径数就是 ( ext{split}(x,y)) 之后两棵虚子树 (siz+1) 的乘积。直接上板子就好了。

    //Author:RingweEH
    //P4219 [BJOI2014]大融合
    #define lc tr[x][0]
    #define rc tr[x][1]
    const int N=1e5+10;
    int fa[N],tr[N][2],s[N],si[N],n,m;
    bool tag[N];
    bool IsRoot( int x ) { return (tr[fa[x]][0]==x) || (tr[fa[x]][1]==x); }	
    void Pushup( int x ) { s[x]=s[lc]+s[rc]+si[x]+1; }
    void Pushdown( int x )	{ if ( tag[x] ) { int tmp=lc; lc=rc; rc=tmp; tag[lc]^=1; tag[rc]^=1; tag[x]=0; } }
    void Pushall( int x ) { if ( IsRoot(x) ) Pushall(fa[x]); Pushdown(x); }
    void Rotate( int x ) 
    {
        int y=fa[x],z=fa[y],kid=tr[y][1]==x,oth=tr[x][!kid];
        if ( IsRoot(y) ) tr[z][tr[z][1]==y]=x;
        tr[x][!kid]=y; tr[y][kid]=oth; fa[oth]=y; fa[y]=x; fa[x]=z;
        Pushup(y);
    }
    void Splay( int x )
    {
        int y=x; Pushall(x);
        while ( IsRoot(x) )
        {
            if ( IsRoot(y=fa[x]) ) Rotate( (tr[y][0]==x)^(tr[fa[y]][0]==y) ? x : y );
            Rotate(x);
        }
        Pushup(x);
    }
    void Access( int x ) { for ( int y=0; x; x=fa[y=x] ) Splay(x),si[x]+=s[rc],si[x]-=s[rc=y]; } 
    void MakeRoot( int x ) { Access(x); Splay(x); tag[x]^=1; }	 
    void Split( int x,int y ) { MakeRoot(x); Access(y); Splay(y); }	
    void Link( int x,int y ) { Split(x,y); si[fa[x]=y]+=s[x]; Pushup(y); } 
    
    int main()
    {
    	n=read(); m=read();
    	for ( int i=1; i<=n; i++ ) s[i]=1;
    	while ( m-- )
    	{
    		char opt[5]; scanf( "%s",opt ); int x=read(),y=read();
    		if ( opt[0]=='A' ) Link(x,y);
    		else Split(x,y),printf("%lld
    ",1ll*(si[x]+1)*(si[y]+1) );
    	}
    
    	return 0;
    }
    

    学习博客

    Flash_Hu概念篇 应用篇

    想要更多习题也可以看这里QWQ

  • 相关阅读:
    基于智能学习与业务感知的工控安全监测体系建设
    深度解析工控网络流量特点
    Studio 从入门到精通 (一)
    AndroidStudio权威教程 AS添加第三方库的6种方式(Jar module so等)
    最全的资源教程-前端涉及的所有知识体系
    Android 屏幕适配(二)增强版百分比布局库(percent-support-lib)
    Android 屏幕适配(一)百分比布局库(percent-support-lib) 解析与扩展
    Android 手势识别类 ( 三 ) GestureDetector 源码浅析
    Android 手势识别类 ( 二 ) GestureDetector 源码浅析
    Android 手势识别类 ( 一 ) GestureDetector 基本介绍
  • 原文地址:https://www.cnblogs.com/UntitledCpp/p/LinkCutTree_Study.html
Copyright © 2020-2023  润新知