Link-Cut-Tree 動態樹算法

Link-Cut-Tree 動態樹算法總結

動態樹是一類要求維護森林連通性的算法總稱,其中最常用的就是lct (Link-Cut-Tree).

lct 支持一下操作

鏈上求和 
鏈上求最值
鏈上修改 (前三項均可用樹鏈剖分+線段樹實現)
斷開樹上的一條邊
連接兩個點,保證連接後仍然是一棵樹
判斷森林連通性

說到lct,自然需要引入一些概念:
Preferred Child:重兒子(爲了便於理解這裏沿用樹鏈剖分中的命名),重兒子與父親節點同在一棵Splay中,一個節點最多隻能有一個重兒子
Preferred Edge:重(zhòng)邊,連接父親節點和重兒子的邊
Preferred Path:重鏈,由重邊及重邊連接的節點構成的鏈
由一條重鏈上的所有節點所構成的Splay稱作這條鏈的輔助樹
每個點的關鍵值爲這個點的深度,即這棵Splay的中序遍歷是這條鏈從鏈頂到鏈底的所有節點構成的序列
輔助樹的根節點的父親指向鏈頂的父親節點,然而鏈頂的父親節點的兒子並不指向輔助樹的根節點


原樹與輔助樹的關係
原樹中的重鏈 -> 輔助樹中兩個節點位於同一棵Splay中
原樹中的輕鏈 -> 輔助樹中子節點所在Splay的根節點的father指向父節點(參見上圖)
注意原樹與輔助樹的結構並不相同
輔助樹的根節點≠原樹的根節點
輔助樹中的father≠原樹中的father
由於要維護的信息已經都在輔助樹中維護了,所以LCT無需維護原樹,只維護輔助樹即可,也就是在實際程序中是不存在原樹的,即使是原樹的換根操作也只是通過輔助樹來間接完成。
輕鏈和重鏈並不是固定的,隨着算法的進行,輕鏈和重鏈隨算法需要和題目要求而變化,然而無論怎麼變化,由這棵輔助樹一定能生成原樹,並滿足輔助樹的所有性質

這裏重點說一下換根操作,也是我剛開始卡住的地方
換根實際上是完成下圖的操作:

注意這轉的是原樹!!!!
那麼假設1,2,3這三個點在一個splay樹中,因爲splay是以深度爲關鍵字建樹的,所有隨着原樹中根的改變,本身的深度就發生了變化,原先最深的是3號節點,而換根之後最深的就變成了1號節點,那麼我們對splay進行翻轉操作,將他的左右子樹進行交換,就可以實現深度的更新,很神奇啊!


附代碼: bzoj 2049 洞穴勘測
#include<iostream>  
#include<cstdio>  
#include<cstring>  
#define N 2000003  
using namespace std;  
int n,m;  
int ch[N][3],fa[N],next[N],size[N],st[N],rev[N],top;  
int isroot(int x)  //是否是輔助樹的鏈頂,即當前splay 的根 
{  
  return ch[fa[x]][0]!=x&&ch[fa[x]][1]!=x;//他父親的左右兒子都不是他,輔助樹的根節點的父親指向鏈頂的父親節點,然而鏈頂的父親節點的兒子並不指向輔助樹的根節點  
}  
void update(int x)  
{  
 size[x]=size[ch[x][0]]+size[ch[x][1]]+1;  
}  
void pushdown(int x)  
{  
 if (!x) return;  
 if (rev[x])  
 {  
 swap(ch[x][0],ch[x][1]);  
 rev[ch[x][0]]^=1; rev[ch[x][1]]^=1; rev[x]=0;  
 }  
}  
int get(int x)  
{  
  return ch[fa[x]][1]==x;  
}  
void rotate(int x)  
{  
    int y=fa[x],z=fa[y],l,r;  
    l=get(x); r=l^1;  
    if(!isroot(y))  
      ch[z][ch[z][1]==y]=x;   
    ch[y][l]=ch[x][r]; fa[ch[y][l]]=y;//很神奇,這兩行放到if前就會TLE   
    ch[x][r]=y; fa[y]=x; fa[x]=z;//因爲更改了fa[y]的緣故,單純的splay if 語句中判斷的是z是否爲根,所有不影響,但是lct 中splay與單純的splay有細節上的差別  
}  
void splay(int x)  
{  
  top=0; st[++top]=x;  
  for (int i=x;!isroot(i);i=fa[i])  
   st[++top]=fa[i];  
  for (int i=top;i>=0;i--) pushdown(st[i]);//由於找節點並非自上至下,故操作之前需預先將節點到輔助樹根的標記全下傳一遍,注意翻轉標記只會影響當前這顆樹,不會改變整顆樹中的順序。   
  while(!isroot(x))    
   {    
    int y=fa[x];   
    if (!isroot(y)) //判斷y 是否是輔助樹中的根節點   
     rotate(get(x)==get(y)?y:x);  //splay 之字形旋轉
     rotate(x);    
   }    
}  
void access(int x) //將一個點與原先的重兒子切斷,並使這個點到根路徑上的邊全都變爲重邊,執行Access(x)函數後這個節點到根的路徑上的所有節點形成了一棵Splay,便於操作或查詢節點到根路徑上的所有節點 
{  
  int t=0;  
  while (x)  
  {  
   splay(x);//將x 轉到輔助樹的根節點  
   ch[x][1]=t;  //將x 原來的重兒子斬斷 ,但是x的重兒子並未斬斷與x的關係,也就是重兒子只是當前存儲了當前的路徑,是不斷改變的,下一次詢問時還可以重新通過fa記錄的關係得到一條新的重鏈,保證了原樹的信息
   t=x; x=fa[x];  
  }  
}  
void rever(int x) //換根,換根換的是原樹的根,是把x在原樹中正常轉動到根結點,在原樹轉動之後,那麼原樹中對應的深度也相應發生了變化,因爲splay維護的是原樹的信息,並且是以深度爲關鍵字建樹,所有樹的形態發生翻轉,以保證可以通過splay還原原樹。有一點需要注意就是原樹其實不需要維護,他是虛擬的不存在的 
{  
  access(x); splay(x); rev[x]^=1;  //注意Access(x)之後x不一定是Splay的根節點 所以Access之後通常還要Splay一下 
}  
void cut(int x,int y)  //先把x轉到他所在鏈的根, 
{  
  rever(x); //這裏之所以要把x轉到他所在子樹的根是因爲lct可以維護多棵樹,並支持合併,但是如果連接是x不是他所在樹的根的話,那麼他之前一定有一個父親節點,連接時就會發生混亂。 
  access(y); splay(y); ch[y][0]=fa[x]=0;  //因爲原樹換根後,x,y的位置關係發生了改變,所有y 砍掉的是左兒子,而不是右兒子!!
}  
void link(int x,int y)  //連接,建立新的父子關係
{  
  rever(x); fa[x]=y; splay(x);  
}  
int find (int x)  //判斷森林連通性,因爲一顆輔助splay的父親不一定是當前根的父親,而是重鏈的的鏈頂的父親,因爲splay是以深度爲關鍵字建樹,所有我們要不停的向左子樹方向尋找。
{  
  access(x); splay(x);  
  while(ch[x][0]) x=ch[x][0];  
  return x;  
}  
int main()  
{  
  scanf("%d%d",&n,&m);  
  for (int i=1;i<=m;i++)  
   {  
    char s[10]; int x,y; scanf("%s%d%d",s,&x,&y);  
    if (s[0]=='Q')  
     {  
       if (find(x)==find(y)) printf("Yes\n");  
       else printf("No\n");  
     }  
    else  
    if (s[0]=='C')  
     link(x,y);  
    else  
     cut(x,y);  
   }  
}   



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章