1. 程式人生 > 實用技巧 >LCT(Link-Cut-Tree)

LCT(Link-Cut-Tree)

LCT(Link-Cut-Tree)

LCT維護一個森林,即把每個節點用splay維護,可以進行許多操作:

  • 查詢、修改鏈上的資訊

  • 隨意指定原樹的根(即換根)

  • 動態連邊、刪邊

  • 合併兩棵樹、分離一棵樹

  • 動態維護連通性

主要性質

  1. 每一個Splay維護的是一條從上到下按在原樹中深度嚴格遞增的路徑,且中序遍歷Splay得到的每個點的深度序列嚴格遞增。
  2. 每個節點僅包含於一個splay中。
  3. 邊分為實邊和虛邊,實邊記錄 sonfa,包含在一個 splay 中。為了維護 splay 樹形,虛邊僅記錄 fa。不過虛邊是由 splay(根) 指向父親的,不一定是原節點。

操作

access

access 操作是指將一個點到樹根的路徑打通,即把根節點和該節點搞到一個 splay 上。

我們從 x 向上爬。

  • 每次將所在節點 splay(轉到 splay 的根節點)
  • 將該 splay 所指向的節點的兒子換為 splay 的根節點。
  • 更新資訊。
  • 將操作點切換到父節點,重複操作直到節點的父親是0。
void access(int x){
    for(int y=0;x;y=x,x=fa[x]){
        splay(x);son[x][1]=y;pushup(x);
	}
}

makert

makert 操作可以將一個節點變成整棵樹的根。

  • 將該節點 access
  • 將該節點 splay
  • 將該節點打上子樹翻轉標記。

正確性為,access 操作後將該節點到原來的根的路徑打通併成為一個 splay 後,整條路徑的 dfs 序都會反轉,而其他節點的 dfs 序都不會變。

inline void rev(const int &x){tag[x]^=1,swap(son[x][0],son[x][1]);}
void makert(int x){access(x),splay(x),rev(x);}

findrt

findrt 操作可以找到一個節點在其樹內的根。

  • 將該節點 access
  • 將該節點 splay
  • 一直跳左兒子,則找到 dfs 序最小的節點,也就是根。
int findrt(int x){access(x),splay(x);while(son[x][0])x=son[x][0];splay(x);return x;}

注意,上面的程式碼中如果不在找到根後 splay 複雜度是假的。

link 操作將兩個連通塊進行連邊。

  • 若要在連邊之前判斷兩者是否已經聯通,可以將一個節點變成根,查詢另一個節點的根進行判斷。
  • 一般連邊是將一個節點變成另一個節點的虛兒子,也就是連虛邊。這種方式適用於虛兒子貢獻較為簡單計算的情況。設這兩個節點為 x 和 y,我們將 y makert ,將 x splay,然後將 y 的 fa 改成 x 即可。(如果要統計子樹資訊的話,將兩個節點都改為根,然後連邊時順便統計字數貢獻)
  • 當然也可以直接連成實邊。
void link(int x,int y){
    makert(x);
    if(findrt(y)!=x) fa[x]=y;
}
inline void link(int x,int y){
    splay(x);fa[x]=y;
    access(y),splay(y);
    son[y][1]=x;pushup(y);
}

cut

cut 操作將兩個點間進行刪邊。

  • 若要判斷兩個點原先是否有邊相連,先將一個節點設成根然後判斷連通性,再判斷兩點間的 dfs 序是否連續。
  • 然後直接將上面節點的兒子和下面節點的父親設為 0 即可。別忘了更新資訊。
inline void cut(int x,int y){
    makert(x);
    if(findrt(y)==x and fa[y]==x and !son[y][0]) rs=fa[y]=0,pushup(x);
}

模板

維護鏈上最大值。

struct LCT{
    #define ls son[x][0]
    #define rs son[x][1]
    int tag[maxm],fa[maxm],st[maxm],mx[maxm],id[maxm],son[maxm][2];
    inline bool notrt(int x){return son[fa[x]][0]==x or son[fa[x]][1]==x;}
    inline int getw(int x){return son[fa[x]][1]==x;}
    inline void rev(int x){if(x)swap(ls,rs),tag[x]^=1;}
    inline void pushup(int x){
        if(mx[ls]>mx[rs])mx[x]=mx[ls],id[x]=id[ls];
        else mx[x]=mx[rs],id[x]=id[rs];
        if(val[x]>mx[x])mx[x]=val[x],id[x]=x;
    }
    inline void pushdown(int x){if(tag[x])tag[x]=0,rev(ls),rev(rs);}
    inline void rotate(int x){
        int y=fa[x],z=fa[y],w=getw(x),s=son[x][!w];
        if(notrt(y))son[z][getw(y)]=x;
        son[x][!w]=y;son[y][w]=s;
        if(s)fa[s]=y;fa[x]=z,fa[y]=x;
        pushup(y);pushup(x);
    }
    inline void splay(int x){
        int y,top=1;
        for(y=x;notrt(st[++top]=y);y=fa[y]);
        while(top)pushdown(st[top--]);
        while(notrt(x)){
            y=fa[x];
            if(notrt(y)) rotate((getw(x)^getw(y))?x:y);
            rotate(x);
        }
        pushup(x);
    }
    inline void access(int x){
        for(int y=0;x;y=x,x=fa[x])
            splay(x),rs=y,pushup(x);
    }
    inline int findroot(int x){
        access(x),splay(x);
        while(ls)x=ls;
        splay(x);
        return x;
    }
    inline void makeroot(int x){access(x),splay(x),rev(x);}
    inline void split(int x,int y){makeroot(x);access(y),splay(y);}
    inline void link(int x,int y){makeroot(x);if(findroot(y)!=x)fa[x]=y;}
    inline void cut(int x,int y){
        makeroot(x);
        if(findroot(y)==x and fa[y]==x and !son[y][0])
            fa[y]=rs=0,pushup(x);
    }
    #undef ls
    #undef rs
}L;

進階

維護子樹資訊

LCT 可以維護子樹資訊,但是隻能做到查詢而做不到修改。簡單來說,維護的方式就是每次給一個 splay 新增一個虛兒子的時候,需要多開一個數據結構記錄虛兒子的貢獻。然後在上傳的時候考慮虛兒子即可。

P4219 [BJOI2014]大融合

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cctype>
#include<cstring>
#include<cmath>
using namespace std;
inline int read(){
	int w=0,x=0;char c=getchar();
	while(!isdigit(c))w|=c=='-',c=getchar();
	while(isdigit(c))x=x*10+(c^48),c=getchar();
	return w?-x:x;
}
namespace star
{
	const int maxn=1e5+10;
	int n,m;
	struct LCT{
		#define ls son[x][0]
		#define rs son[x][1]
		int tag[maxn],son[maxn][2],fa[maxn],siz[maxn],siz2[maxn],st[maxn];
		inline bool getw(int x){return son[fa[x]][1]==x;}
		inline void rev(int x){if(x)tag[x]^=1,swap(ls,rs);}
		inline void pushup(int x){siz[x]=siz[ls]+siz[rs]+siz2[x]+1;}
		inline void pushdown(int x){if(tag[x])tag[x]=0,rev(ls),rev(rs);}
		inline bool notrt(int x){return son[fa[x]][0]==x or son[fa[x]][1]==x;}
		inline void rotate(int x){
			int y=fa[x],z=fa[y],w=getw(x),s=son[x][!w];
			if(notrt(y))son[z][getw(y)]=x;son[y][w]=s;son[x][!w]=y;
			if(s)fa[s]=y;fa[y]=x,fa[x]=z;
			pushup(y);
		}
		inline void splay(int x){
			int y;int top=0;
			for(y=x;notrt(st[top++]=y);y=fa[y]);
			while(top--)pushdown(st[top]);
			while(notrt(x)){
				y=fa[x];
				if(notrt(y)) rotate(getw(x)^getw(y)?x:y);
				rotate(x);
			}
			pushup(x);
		}
		inline void access(int x){for(int y=0;x;y=x,x=fa[x])splay(x),siz2[x]+=siz[rs]-siz[y],rs=y,pushup(x);}
		inline void makert(int x){access(x),splay(x),rev(x);}
		inline int findrt(int x){access(x),splay(x);while(ls)x=ls;splay(x);return x;}
		inline void split(int x,int y){makert(x);access(y),splay(y);}
		inline void link(int x,int y){makert(x);if(findrt(y)!=x)fa[x]=y,siz2[y]+=siz[x],splay(y);}
		inline void cut(int x,int y){
			makert(x);
			if(findrt(y)==x and fa[y]==x and !son[y][0]) rs=fa[y]=0,pushup(x);
		}
		#undef ls
		#undef rs
	}L;
	inline void work(){
		n=read(),m=read();
		int x,y;
		while(m--){
			char c=getchar();
			while(!isalpha(c))c=getchar();
			if(c=='A')L.link(read(),read());
			else L.split(x=read(),y=read()),printf("%lld\n",1ll*(L.siz2[x]+1)*(L.siz2[y]+1));
		}
	}
}
signed main(){
	star::work();
	return 0;
}

動態求LCA

LCT 本來就是動態的,如何求兩個點的 LCA 呢?

將其中一個點 access ,然後將另外一個點 access ,並記錄最後一次 splay 前找到的節點(即最後的程式碼中的y)

利用LCT的結構

LCT 是一種優秀的暴力,它的結構有時候可以幫我們做一些很強的題目(雖然一般都想不到這個模型)

P3703 [SDOI2017]樹點塗色

思路:觀察操作,有“將一個點到根節點的路徑染成同一種新的顏色”,發現同一顏色的連通塊都是一條鏈,那麼我們很快想到 LCT 的模型。維護的答案是該節點到根的 splay 個數。那麼我們在改變 access 的時候,即改變兒子虛實的時候,需要將虛兒子子樹內所有節點的答案都增加,將實兒子子樹內所有節點都減少,這個可以用線段樹進行維護。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cctype>
#include<cstring>
#include<cmath>
using namespace std;
inline int read(){
	int w=0,x=0;char c=getchar();
	while(!isdigit(c))w|=c=='-',c=getchar();
	while(isdigit(c))x=x*10+(c^48),c=getchar();
	return w?-x:x;
}
namespace star
{
	const int maxn=1e5+10;
	int n,m;
	int dfn[maxn],dep[maxn],id[maxn],fa[maxn],son[maxn],siz[maxn],top[maxn];
	int ecnt,head[maxn],nxt[maxn<<1],to[maxn<<1];
	inline void addedge(int a,int b){
		to[++ecnt]=b,nxt[ecnt]=head[a],head[a]=ecnt;
		to[++ecnt]=a,nxt[ecnt]=head[b],head[b]=ecnt;
	}
	void dfs1(int x,int f){
		fa[x]=f,dep[x]=dep[f]+1,siz[x]=1;
		for(int u,i=head[x];i;i=nxt[i]) if((u=to[i])!=f){
			dfs1(u,x);
			siz[x]+=siz[u];
			if(siz[u]>siz[son[x]]) son[x]=u;
		}
	}
	void dfs2(int x,int topf){
		top[x]=topf;dfn[x]=++dfn[0],id[dfn[0]]=x;
		if(!son[x])return;
		dfs2(son[x],topf);
		for(int u,i=head[x];i;i=nxt[i]) if((u=to[i])!=fa[x] and u!=son[x]) dfs2(u,u);
	}
	inline int LCA(int x,int y){
		while(top[x]!=top[y]) if(dep[top[x]]>dep[top[y]]) x=fa[top[x]];
		else y=fa[top[y]];
		return dep[x]<dep[y]?x:y;
	}
	struct SegmentTree{
		#define ls (ro<<1)
		#define rs (ro<<1|1)
		#define mid ((l+r)>>1)
		int mx[maxn<<2],tag[maxn<<2];
		inline void pushup(const int &ro){mx[ro]=max(mx[ls],mx[rs]);}
		inline void pushdown(const int &ro){tag[ls]+=tag[ro],tag[rs]+=tag[ro];mx[ls]+=tag[ro],mx[rs]+=tag[ro];tag[ro]=0;}
		void build(const int &ro=1,const int &l=1,const int &r=n){
			if(l==r)return mx[ro]=dep[id[l]],tag[ro]=0,void();
			build(ls,l,mid),build(rs,mid+1,r);
			pushup(ro);
		}
		void update(const int &x,const int &y,const int &k,const int &ro=1,const int &l=1,const int &r=n){
			if(x==l and y==r) return tag[ro]+=k,mx[ro]+=k,void();
			if(tag[ro])pushdown(ro);
			if(y<=mid) update(x,y,k,ls,l,mid);
			else if(x>mid) update(x,y,k,rs,mid+1,r);
			else update(x,mid,k,ls,l,mid),update(mid+1,y,k,rs,mid+1,r);
			pushup(ro);
		}
		int query(const int &x,const int &y,const int &ro=1,const int &l=1,const int &r=n){
			if(x==l and y==r)return mx[ro];
			if(tag[ro])pushdown(ro);
			if(y<=mid) return query(x,y,ls,l,mid);
			if(x>mid) return query(x,y,rs,mid+1,r);
			return max(query(x,mid,ls,l,mid),query(mid+1,y,rs,mid+1,r));
		}
		#undef ls
		#undef rs
		#undef mid
	}T;
	struct LCT{
		#define ls son[x][0]
		#define rs son[x][1]
		int son[maxn][2],fa[maxn];
		inline bool notrt(int x){return son[fa[x]][0]==x or son[fa[x]][1]==x;}
		inline int getw(int x){return son[fa[x]][1]==x;}
		inline void rotate(int x){
			int y=fa[x],z=fa[y],w=getw(x),s=son[x][!w];
			if(notrt(y)) son[z][getw(y)]=x;son[y][w]=s,son[x][!w]=y;
			if(s) fa[s]=y;fa[y]=x,fa[x]=z;
		}
		inline void splay(int x){
			while(notrt(x)){
				int y=fa[x];
				if(notrt(y))rotate(getw(x)^getw(y)?x:y);
				rotate(x);
			}
		}
		inline int findrt(int x){while(ls)x=ls;return x;}
		inline void access(int x){
			for(int u,y=0;x;y=x,x=fa[x]){
				splay(x);
				if(rs) u=findrt(rs),T.update(dfn[u],dfn[u]+siz[u]-1,1);
				if(rs=y) u=findrt(rs),T.update(dfn[u],dfn[u]+siz[u]-1,-1);
			}
		}
		#undef ls
		#undef rs
	}S;
	inline void work(){
		n=read(),m=read();
		for(int i=1;i<n;i++) addedge(read(),read());
		dfs1(1,0);dfs2(1,1);
		for(int i=1;i<=n;i++) S.fa[i]=fa[i];
		T.build();
		while(m--)
		switch(read()){
			case 1:S.access(read());break;
			case 2:{
				int x=read(),y=read(),lca=LCA(x,y);
				printf("%d\n",T.query(dfn[x],dfn[x])+T.query(dfn[y],dfn[y])-2*T.query(dfn[lca],dfn[lca])+1);
				break;
			}
			case 3:{
				int x=read();
				printf("%d\n",T.query(dfn[x],dfn[x]+siz[x]-1));
			}
		}
	}
}
signed main(){
	star::work();
	return 0;
}

P6292 區間本質不同子串個數也用到了這個 trick。

更多trick

待耕。