1. 程式人生 > >淺談Link-Cut Tree(LCT)

淺談Link-Cut Tree(LCT)

  • 0XFF 前言&概念

Link-Cut Tree 是一種用來維護動態森林連通性的資料結構,適用於動態樹問題。它採用類似樹鏈剖分的輕重邊路徑剖分,把樹邊分為實邊和虛邊,並用 Splay 來維護每一條實路徑。Link-Cut Tree 的基本操作複雜度為均攤O(logn),但常數因子較大,一般效率會低於樹鏈剖分。但是卻可以解決樹鏈剖分解決不了的問題(或者優化碼量) -----Menci dalao

動態樹LCT(link cut tree)是一個可以動態維護森林上各種資訊的東西(刪除查詢合併啥的都有吧),原來的森林我們稱為原森林,裡面有實邊和虛邊,為啥有這兩種邊呢,首先LCT是用很多個splay維護這個森林的資訊,那麼因為splay本來就是個二叉樹,所以我們要將原森林”剖分”成很多個二叉樹並且用splay來維護它,用實邊連線起來的一棵樹就是原森林中的一棵樹,我們稱它為原樹。

這個Splay會有些特殊,它的關鍵字是節點在樹裡面的深度。

這棵原樹我們也不是直接用splay維護,而是按每個點在原樹中的深度為優先順序,將每個點以優先順序的中序遍歷丟到splay上。我們一般將原樹所對應的splay稱為輔助樹,原森林就對應一個輔助樹森林。
-----quhengyi11 dalao

請務必先將上文讀清楚,再繼續下面的閱讀。

Splay是輔助樹,閱讀時不要將主的和輔的搞混了。

顯然原樹中同一個深度的點是不可能在一個splay裡的,因此每個splay裡面就是維護了原樹中的一條鏈

Link-Cut Tree 準確的說是一個 Splay 森林。每棵 Splay

都用"虛邊"連結(下圖灰邊表示虛邊),每棵 Splay 中的結點都用"實邊"連結起來(下圖用黑色表示實邊)。假如我們現在有一個栗子:(用紅色圈圈圈在一起的結點是一個 Splay 中的結點)

LCT

那麼現在每個結點都是一顆 Splay

就像這樣:

LCT

如果我們將1,2連線起來的話。

那麼1,2就是同一個 Splay 中的節點了。

那麼現在的情況就是這樣:

LCT

相信你一定對此有些瞭解了吧。

  • 0X01 一些基本的定義

f[x]:結點x的爸爸(father)

v[x]:結點x的權值(value)

s[x]:結點x及它的子樹的權值和(sum)

r[x]

:結點x的翻轉情況(rev)

ch[x][0/1]:結點x的左/右兒子

  • 0X02 一些操作

    Link-Cut Tree 支援以下幾種基本操作:

Access(x):將x到根節點的路徑上全部變成實邊,並棄掉自己所有的兒子(變成虛邊:認父不認子)(每一個父結點對於自己的每個子結點只有一條實邊)

findroot(x):找出x所在的原樹的根結點(實際上就是上圖的一號點)

makeroot(x):這個操作的意思是將x點變為原樹的根節點

split(x,y):將x,y搞在一個 Splay 中,以方便操作。

link(x,y):將x和y所在原樹合併起來(連結)

cut(x,y):將x和y所在原樹拆開(切斷)

  • Access(x):

    這是最基礎的操作,意思是將點x到原樹中根結點root之間的鏈丟到一個輔助樹splay裡面

比方說,現在森林的狀態是這樣的:

LCT

我們的 x 現在等於6。執行 Access(6)

那麼就會將{1-3,3-6}變成實邊,1-2變成虛邊,假設6有一兒子n,之間用實邊連著,那麼這條邊也將變成虛邊

每次將$x$點 splay 到當前所在輔助樹的根節點,將它的右兒子更新為上一個$x$,然後令$x$跳到它的父節點,特別的,第一個$x$的右兒子設為0(NULL)。

Q:為什麼是右兒子而不是左兒子呢?

A:因為f[x]的深度小於x,而在Splay裡面f[x]是x的爸爸,所以x在Splay中是f[x]的右兒子。

所以就變成了這樣:

LCT

我們將$x$旋轉到輔助樹的根節點,也就是將當前原樹這條鏈上深度小於$x$(在$x$上面的點)丟到了$x$的左子樹上,將$x$的右子樹設為上一個$x$點相當於將$x$原來的右子樹丟到了新的 splay 裡面(而它們之間用虛邊相連),並且將上一段鏈連線起來。

現在就可以了。這棵新 Splay 中只有這條鏈上的結點,沒有其他任何的結點。如果我們指定要這三個結點同時進行操作,可以直接下傳懶標記到這三個結點組成的 Splay 的根結點哦!到後面Splay的時候就可以直接下傳跟新結點資訊了。

總體過程:

LCT

虛邊:兒子認父,父不認子

實邊:兒子認父,父也認子

用FlashHu大佬的話來說,就是四步:

1.轉到根。

2.換兒子。

3.更新資訊。

4.當前操作點切換為輕邊所指的父親,轉1。

程式碼實現:

inline void Access(int x){
      for(register int y=0;x;y=x,x=f[x]){
            Splay(x);//轉到所在Splay的根節點
            ch[x][1]=y;//認兒子了
            pushup(x);//兒子有變化,更新
      }
}

  • findroot(x):

  • 首先要明白:
  • 根節點是的深度最小的

我們可以通過x向上找,用 Access 操作可以將x和x的根結點搞到一個 Splay 裡。

又因為有BST的性質:x的左子樹所有結點的權值 < x < x右子樹所有結點的權值。

而我們又知道,在執行完 Access 操作後,這課 Splay 裡面的結點權值最大的(深度最大的)就是x。

於是我們可以將x Splay 到這棵 Splay 的根結點,那麼現在最左邊的節點便是這課樹的根結點了。

程式碼實現:

inline int findroot(int x){
      Access(x);//Access將x和根結點搞到同一個Splay中
      Splay(x);//轉到Splay的根結點
      while(ch[x][0])pushdown(x),x=ch[x][0];//不斷的找左兒子&更新節點資訊
      return x;//最左邊的就是根結點了。
}

  • makeroot(x):

    將x到根結點的路徑上的點全部翻轉(即x變成了根節點)

具體操作是我們先將x點與原樹中的根打通一條鏈,那麼現在它們就在同一棵輔助樹裡面了,我們發現x一定是在它所在的輔助樹的中序遍歷的最後一個的(因為它是這條鏈上最深的點),我們把x點 splay 到輔助樹的根上,那麼x顯然是沒有右子樹的,我們要實現將x移到原樹的根上,也就是將x到根的這條鏈的深度全部翻轉一遍,在輔助樹上的體現就是將整棵樹翻轉一遍,我們可以寫個翻轉標記來減少複雜度。

程式碼實現:

inline void filp(int x){//Splay普通區間翻轉
      swap(ch[x][0],ch[x][1]);r[x]^=1;
} 
inline void makeroot(int x){
      Access(x);
      Splay(x);
      filp(x);//懶標記&翻轉區間
}

  • split(x,y)

這個操作是將x到y之間的那條路徑丟到一棵輔助樹裡,並且這棵輔助樹以y節點為根(方便處理資訊)。

Splay 維護的是原樹中的一條鏈,我們不能保證x,y會在同一條鏈裡。

所以我們可以先把x變成原樹的根節點(這下子Access(y)就會將x到y之間的所有節點丟到一個 Splay 中了)。

最後如上面所講的,最後來一個 Splay(y) 就大功告成了。

程式碼實現:

inline void split(int x,int y){
      makeroot(x);Access(y);Splay(y);
} 

  • link(x,y):

    將x和y所在原樹合併起來(連結)

首先將x點丟到原樹的根,然後去找找y的根是不是x,如果不是說明x,y不在一個原樹內,我們將x的父節點設為y,也就相當於從y到x連了一條虛邊。

程式碼實現:

inline void link(int x,int y){
      makeroot(x);//丟到根
      if(findroot(y)!=x)f[x]=y;//連結一條虛邊
//注意因為是虛邊,所以不能認兒子
}

  • cut(x,y):

    首先我們先把x,y之間的那條邊用split(x,y)拎出來,因為x,y是相鄰的,所以y的左兒子一定是x,將它們的父子關係消滅掉即可。

消滅父子關係時一定滿足以下條件:

1.x和y在一個原樹裡(不在一個樹裡面往哪兒切啊)

2.split之後x是y的左兒子

3.x的右兒子是空的(保證了中序遍歷中y緊跟在x的後面,即深度相鄰)(x的權值(深度)只比y小1,而x又正好是直接連著y的,所以我們無法再找到 >x 而又 <y 的整數了)

程式碼實現:

inline void cut(int x,int y){
      split(x,y);
      if(findroot(y)==x&&f[x]==y&&!ch[x][1]){//判斷各種條件
            f[x]=ch[y][0]=0;//徹底切斷關係
            pushup(y);//兒子變了,更新
      }return;
}

  • 0X03 Splay的改動:

  • 旋轉的改動:

這裡需要注意一下,如果x的父親節點的父親節點y已經不在當前的這棵輔助樹上,只需要連單向邊(也就是虛邊,認父不認子),否則正常連就行,這裡要和普通的rotate區分開來。

做個對比:

現在的rotate(x):

inline void rotate(int x){
      int y=f[x],z=f[y],k=chk(x),v=ch[x][!k]; 
      if(get(y))ch[z][chk(y)]=x;ch[x][!k]=y,ch[y][k]=v;
      if(v)f[v]=y;f[y]=x,f[x]=z;pushup(y),pushup(x);
}

普通的rotate(x):

inline void rotate(int x){
      int y=f[x],z=f[y],k=chk(x),v=ch[x][!k]; 
      ch[z][chk(y)]=x;ch[x][!k]=y,ch[y][k]=v;
      f[v]=y;f[y]=x,f[x]=z;pushup(y),pushup(x);
}
  • Splay的改動

同樣要注意一下只能Splay到輔助樹的根節點,Splay之前需先下傳一下這一條鏈上需操作的所有的點,用棧來完成即可,可以手寫棧來減少常數。

inline void Splay(int x){
      int y=x,top=0;hep[++top]=y;
      while(get(y))hep[++top]=y=f[y];
      while(top)pushdown(hep[top--]);

      while(get(x)){//基本普通的Splay
            y=f[x],top=f[y];
            if(get(y))
                  rotate((ch[y][0]==x)^(ch[top][0]==y)?x:y);
            rotate(x);
      }pushup(x);return; 
}


  • 0X04 一些題目程式碼:

就是上文講的。

Code:

#include<bits/stdc++.h>
#define ll long long
#define inf 0x3f3f3f3f
#define RI register int
#define A printf("A")
#define C printf(" ") 
using namespace std;
const int N=3e5+2;
template <typename Tp> inline void IN(Tp &x){
    int f=1;x=0;char ch=getchar();
    while(ch<'0'||ch>'9')if(ch=='-')f=-1,ch=getchar();
    while(ch>='0'&&ch<='9')x=x*10+ch-'0',ch=getchar();x*=f;
}int f[N],v[N],s[N],r[N],hep[N],ch[N][2];
inline int get(int x){
    return ch[f[x]][0]==x||ch[f[x]][1]==x;
}
inline void pushup(int x){
    s[x]=s[ch[x][1]]^s[ch[x][0]]^v[x];
}
inline void filp(int x){
    swap(ch[x][0],ch[x][1]);r[x]^=1;
} 
inline void pushdown(int x){
    if(!r[x])return;r[x]=0;
    if(ch[x][0])filp(ch[x][0]);
    if(ch[x][1])filp(ch[x][1]);
} 
inline void rotate(int x){
    int y=f[x],z=f[y],k=ch[y][1]==x,v=ch[x][!k]; 
    if(get(y))ch[z][ch[z][1]==y]=x;ch[x][!k]=y,ch[y][k]=v;
    if(v)f[v]=y;f[y]=x,f[x]=z;pushup(y);
}
inline void Splay(int x){
    int y=x,top=0;hep[++top]=y;
    while(get(y))hep[++top]=y=f[y];
    while(top)pushdown(hep[top--]);
    while(get(x)){
        y=f[x],top=f[y];
        if(get(y))
           rotate((ch[y][0]==x)^(ch[top][0]==y)?x:y);
        rotate(x);
    }pushup(x);return; 
}
inline void Access(int x){
    for(register int y=0;x;x=f[y=x])
       Splay(x),ch[x][1]=y,pushup(x);
}
inline void makeroot(int x){
    Access(x);Splay(x);filp(x);
}
inline int findroot(int x){
    Access(x);Splay(x);
    while(ch[x][0])pushdown(x),x=ch[x][0];
    return 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)f[x]=y;
}
inline void cut(int x,int y){
    makeroot(x);
    if(findroot(y)==x&&f[x]==y&&!ch[x][1]){
        f[x]=ch[y][0]=0;pushup(y);
    }return;
}int n,m,x,y,op;
int main(){
    scanf("%d%d",&n,&m);
    for(register int i=1;i<=n;++i)scanf("%d",&v[i]); 
    for(register int i=1;i<=m;++i){
        scanf("%d%d%d",&op,&x,&y);
        if(op==0)split(x,y),printf("%d\n",s[y]);
        else if(op==1)link(x,y);
        else if(op==2)cut(x,y);
        else Splay(x),v[x]=y;
    }return 0;
}

  • [SDOI2008]洞穴勘測

分析:題目只要求link(有一條新道路==連線)和cut(道路被摧毀了==cut)以及判斷連通性(直接findroot,一樣的話那麼就是聯通的)

就是LCT的板子,真的沒那麼難。

Code:

#include<bits/stdc++.h>
#define ll long long
#define inf 0x3f3f3f3f
#define RI register int
#define A printf("A")
#define C printf(" ") 
using namespace std;
const int N=2e5+2;
template <typename Tp> inline void IN(Tp &x){
    int f=1;x=0;char ch=getchar();
    while(ch<'0'||ch>'9')if(ch=='-')f=-1,ch=getchar();
    while(ch>='0'&&ch<='9')x=x*10+ch-'0',ch=getchar();x*=f;
}int n,m,f[N],r[N],hep[N],ch[N][2];
inline int get(int x){return ch[f[x]][0]==x||ch[f[x]][1]==x;}
inline void filp(int x){swap(ch[x][0],ch[x][1]);r[x]^=1;} 
inline void pushdown(int x){
    if(!r[x])return;r[x]=0;
    if(ch[x][0])filp(ch[x][0]);
    if(ch[x][1])filp(ch[x][1]);
}
inline void rotate(int x){
    int y=f[x],z=f[y],k=ch[y][1]==x,v=ch[x][!k]; 
    if(get(y))ch[z][ch[z][1]==y]=x;ch[x][!k]=y,ch[y][k]=v;
    if(v)f[v]=y;f[y]=x,f[x]=z;return; 
}
inline void Splay(int x){
    int y=x,top=0;hep[++top]=y;
    while(get(y))hep[++top]=y=f[y];
    while(top)pushdown(hep[top--]);
    while(get(x)){
        y=f[x],top=f[y];
        if(get(y))
           rotate((ch[y][0]==x)^(ch[top][0]==y)?x:y);
        rotate(x);
    }return; 
}
inline void Access(int x){
    for(register int y=0;x;x=f[y=x])
       Splay(x),ch[x][1]=y;
}
inline void makeroot(int x){
    Access(x);Splay(x);filp(x);
}
inline int findroot(int x){
    Access(x);Splay(x);
    while(ch[x][0])pushdown(x),x=ch[x][0];
    return 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)f[x]=y;
}
inline void cut(int x,int y){
    makeroot(x);
    if(findroot(y)==x&&f[x]==y&&!ch[x][1]){
        f[x]=ch[y][0]=0;
    }return;
}char op[16];
int main(){
    scanf("%d%d",&n,&m);
    for(register int x,y,i=1;i<=m;++i){
        scanf("%s%d%d",op,&x,&y);
        if(op[0]=='C')link(x,y);
        else if(op[0]=='D')cut(x,y);
        else if(op[0]=='Q'){
            if(findroot(x)==findroot(y))printf("Yes\n");
            else printf("No\n");
        }   
    }return 0;
}

再推存一道題目:P1501 [國家集訓隊]Tree II

這道題目主要就是懶標記的運用,建議在做這一道題之前先去做一做線段樹的模板2,其實道理差不多,相通的,並不難。(講乘法標記的正確下傳方法弄到Splay的下傳上即可)

當然這道題我也附上題解:題解 P1501【[國家集訓隊]Tree II】(Link-Cut-Tree)