【首先聲明:LCT≠動態樹,前者是一種數據結構,而後者是一類問題,即:LCT—解決—>動態樹】 Link-cut-tree(下文統稱LCT)是一種強大的數據結構,不僅可以像樹鏈剖分一樣對樹上的兩點進行詢問(權值和、權值的最值……),還可以維護森林的連通性。 學習LCT首推楊哲神犇的《QTREE解法的 ...
【首先聲明:LCT≠動態樹,前者是一種數據結構,而後者是一類問題,即:LCT—解決—>動態樹】
Link-cut-tree(下文統稱LCT)是一種強大的數據結構,不僅可以像樹鏈剖分一樣對樹上的兩點進行詢問(權值和、權值的最值……),還可以維護森林的連通性。
學習LCT首推楊哲神犇的《QTREE解法的一些研究》,很詳細地解釋了LCT的概念及實現
本文則以ZOJ2114一題為例,分析LCT實現過程中的一些事項,並且力求讀者對LCT有一個“不次於‘感性’的認識”
敘述過程中會直接引用論文中的術語
題目大意:
N個點(N<=2e4),M次操作(M<=4e4),維護N個點構成的森林
最初N個點兩兩不連通
U x y z:新建一條權值為z的邊,把x所在樹的根節點連到y所在的樹上
Q x y:詢問x到y的距離。如果x和y未連通,輸出Not connected.
思路:
對於Q操作,如果x和y所屬的根節點不同,那麼x和y必然是不連通的
對於U操作,首先需要找到x和y的根節點(分別記作u和v),然後把u連到v上
查詢根節點可以用並查集
我們需要對森林進行修改(合併樹)及詢問操作,樹鏈剖分是無法勝任的(樹剖只能用於靜態樹),所以我們考慮LCT
我們用Splay維護Auxiliary Tree(如果不知道這是什麼,請看論文):
1 enum { Left=0,Right=1 }; 2 3 struct SplayNode; 4 SplayNode* vir; 5 6 struct SplayNode 7 { 8 int dist; 9 int totDist; 10 bool isRoot; 11 SplayNode* parent; 12 SplayNode* child[2]; 13 };
(為閱讀方便,分割代碼時作了一些修改)
vir的作用是代替空指針,從而免去很多不必要的判斷
SplayNode類的成員變數如下:
dist:在森林(原圖)中,該節點與父節點之間邊的權值。
totDist:Auxiliary Tree中,以該節點為根的子樹的dist之和
isRoot:該節點是否為所在Auxiliary Tree的根節點
child[]:該節點在Auxiliary Tree中的左右孩子。
parent:既可以是Auxiliary Tree內部的父節點,也可以是Auxiliary Tree整體的path-parent
(1)如果isRoot==false,那麼parent表示該節點在Auxiliary Tree中的父節點
(2)如果isRoot==true,那麼parent表示其所在Auxiliary Tree的path-parent,即其在森林中的父節點
也就是說,對於某個節點u,可能有多個節點以u為parent,但其中只有至多兩個與u構成Auxiliary Tree。在編程實現中,這是一個值得註意的問題
如圖,ABCDE構成森林中的一個連通分量,實線表示Preferred Child
相應的Auxiliary Tree(Series)。實線表示Preferred Child關係,虛線表示Path Parent關係
在Auxiliary Tree中,我們“隱式”地把節點的深度作為BST的關鍵字。也就是說,中序遍歷一棵Auxiliary Tree,得到的就是一條從上到下的Preferred Path鏈
(如果你不能理解從圖1到圖2的轉換,複習一下LCT的相關定義)
之後是Splay的一些操作(成員函數):
1 void init() 2 { 3 dist=totDist=0; 4 isRoot=true; 5 parent=child[0]=child[1]=vir; 6 } 7 8 void update() 9 { 10 this->totDist = 11 child[Left]->totDist+child[Right]->totDist+this->dist; 12 } 13 14 void rotate(int dir) 15 //If dir==Right then rotate clockwise, else rotate counter-clockwise 16 { 17 if(parent==parent->parent->child[Left]) 18 parent->parent->child[Left]=this; 19 else if(parent==parent->parent->child[Right]) 20 parent->parent->child[Right]=this; 21 22 parent->child[dir^1]=this->child[dir]; 23 child[dir]->parent=this->parent; 24 25 child[dir]=parent; 26 parent=parent->parent; 27 child[dir]->parent=this; 28 } 29 30 void zigzig(int dir) 31 { 32 parent->rotate(dir); 33 this->rotate(dir); 34 35 child[dir]->child[dir]->update(); 36 child[dir]->update(); 37 this->update(); 38 } 39 40 void zigzag(int dir) //dir depends on first rotation 41 { 42 rotate(dir); 43 rotate(dir^1); 44 45 child[Left]->update(); 46 child[Right]->update(); 47 this->update(); 48 } 49 50 void zig(int dir) 51 { 52 rotate(dir); 53 child[dir]->update(); 54 this->update(); 55 } 56 57 void splay() 58 { 59 while(!isRoot) 60 { 61 int status=0; 62 if(this==parent->child[Left]) status|=1; 63 else status|=2; 64 65 if(!parent->isRoot) 66 { 67 if(parent->parent->isRoot) this->isRoot=true; 68 if(parent==parent->parent->child[Left]) status|=4; 69 else status|=8; 70 } 71 else 72 this->isRoot=true; 73 74 switch(status) 75 { 76 case 1: zig(Right); 77 child[Right]->isRoot=false; 78 break; 79 case 2: zig(Left); 80 child[Left]->isRoot=false; 81 break; 82 case 5: zigzig(Right); 83 if(isRoot) child[Right]->child[Right]->isRoot=false; 84 break; 85 case 6: zigzag(Left); 86 if(isRoot) child[Right]->isRoot=false; 87 break; 88 case 9: zigzag(Right); 89 if(isRoot) child[Left]->isRoot=false; 90 break; 91 case 10:zigzig(Left); 92 if(isRoot) child[Left]->child[Left]->isRoot=false; 93 break; 94 default:break; 95 } 96 } 97 }
各函數的說明:
init:初始化。令該節點成為獨立的一個連通分量。
update:(在旋轉以後)更新節點的totDist值。
rotate:將節點上旋。如果dir==Right,那麼沿順時針方向上旋;如果dir==Left,那麼沿逆時針方向上旋。
zig,zigzig,zigzag:Splay的單雙旋
splay:提根,將當前節點提到所在Auxiliary Tree的根部
17 if(parent==parent->parent->child[Left]) 18 parent->parent->child[Left]=this; 19 else if(parent==parent->parent->child[Right]) 20 parent->parent->child[Right]=this;
這裡的判斷需要特別註意。必須寫成else if的形式(還記得我們是怎樣定義parent的嗎?)
另外splay的過程中涉及到isRoot的變化
除此之外其他的部分和普通的SplayTree大同小異,在這裡就不多說了,詳見代碼。
至此,LCT的基礎——SplayNode就已經打好了,接下來就是LCT的精華:Access(也有的版本稱為Expose)操作:
1 int access(SplayNode* u) 2 { 3 SplayNode* v=vir; 4 int res=0; 5 while(u!=vir) 6 { 7 u->splay(); 8 u->child[Right]->isRoot=true; 9 res=u->child[Right]->totDist; 10 u->child[Right]=v; 11 v->isRoot=false; 12 u->update(); 13 v=u; u=u->parent; 14 } 15 return res + v->child[Right]->totDist; 16 } 17 18 inline int query(SplayNode* u,SplayNode* v) 19 { 20 access(u); 21 return access(v); 22 }
比起SplayNode的實現是不是短了許多?(斜眼笑
然而,正所謂濃縮就是精華,Access的原理可以說是不容小視的(畢竟是LCT所有操作的基石)
接下來我們以此圖(以下稱為原圖)為例,演示access(H)的全過程。圖為access(H)之前
access(H)之後的效果圖,可以看到途經的所有節點,其原來的Preferred Path都被切斷了,取而代之的是“直達”H的Preferred Path
access(H)之前的Auxiliary Tree,也是我們操作的對象。
首先我們splay(H),把H提到所在Auxiliary Tree的根部
此時H的右子樹對應原圖中的Preferred Path
由於新的Preferred Path要“切斷”沿途所有舊的Preferred Child,而H的右子樹對應H原來的Preferred Child
所以H和I之間要斷開,表示新的Preferred Path到H就中止了,以I為根的子樹成為一棵新的Auxiliary Tree
之後別忘了update一下H的totDist
為了將A和H用Preferred Path相連,我們還要對H的Path Parent,也就是對C“開刀”
首先還是splay(C)。當然C已經是Auxiliary Tree的根節點了,所以對於本例,splay(C)什麼都沒做
然後依然將C與右子樹D斷開。
之後,我們要將C的Auxiliary Tree與H的Auxiliary Tree相連。由於H的Auxiliary Tree在原圖中處於C的下方,所以H應該成為C的新的右孩子
如圖所示,A和E已經用Preferred Path相連了。至此access(H)過程已經完成。不妨把此時的Auxiliary Tree和上面的效果圖對比一下
當然在編程時我們需要一個判斷條件:C的parent為vir,再往上走就沒有任何意義了,所以access操作停止
……
那麼,access的返回值是幹嘛的?totDist到底有什麼卵用?
舉個慄子:詢問D到H的距離。我們已經進行了access(H)
我們給每條邊賦予一個權值(圖中用黑色標註)
但在LCT的Auxiliary Tree中,這個權值實際上是存儲到“下方”的節點中的(圖中以紅色標註)。括弧中是totDist,括弧外是dist
然後我們access(D)
這是access(D)之後的Auxiliary Tree
在原圖中,D到F的距離可以分解成D到C的距離和C到H的距離。之所以選擇C是因為C是D和F的最近公共祖先(LCA)
而在Auxiliary Tree中,這恰好對應C的新、舊右孩子的totDist之和
H是C的”舊“右孩子,被D取代後,D就成了C的”新“右孩子
在編程過程中,我們需要設法保存”舊“右孩子的totDist值(要不然就失聯了( ̄▽ ̄")),access函數中的res變數恰好勝任了這一要求
query函數中進行了兩次access操作,第一次的返回值因為沒有意義所以被丟棄了,而第二次的返回值就是詢問的結果
問題已經解決了一半,接下來就是兩個子樹的合併(並不難):
1 const int maxN=20005; 2 int center[maxN]; 3 4 int getCenter(int x) 5 { 6 return center[x]==x ? x : 7 center[x]=getCenter(center[x]); 8 } 9 10 inline void link(int u,int v,int len) 11 { 12 access(node+u); 13 node[u].splay(); 14 center[u]=v; 15 node[u].parent=node+v; 16 node[u].dist=len; 17 node[u].update(); 18 }
center就是並查集,合併之前,首先要用getCenter函數找到兩棵樹的根節點u和v,然後把u連到v上
首先access(u),然後splay(u)
splay的目的是讓u的parent成為vir,這樣在連接u和v時,不會切斷u所在的Auxiliary Tree
於是這道題用LCT完美解決(而且效率很高),附上完整代碼:
1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 5 enum { Left=0,Right=1 }; 6 7 struct SplayNode; 8 SplayNode* vir; 9 10 struct SplayNode 11 { 12 int dist; 13 int totDist; 14 bool isRoot; 15 SplayNode* parent; 16 SplayNode* child[2]; 17 18 void init() 19 { 20 dist=totDist=0; 21 isRoot=true; 22 parent=child[0]=child[1]=vir; 23 } 24 25 void update() 26 { 27 this->totDist = 28 child[Left]->totDist+child[Right]->totDist+this->dist; 29 } 30 31 void rotate(int dir) 32 //If dir==Right then rotate clockwise, else rotate counter-clockwise 33 { 34 if(parent==parent->parent->child[Left]) 35 parent->parent->child[Left]=this; 36 else if(parent==parent->parent->child[Right]) 37 parent->parent->child[Right]=this; 38 39 parent->child[dir^1]=this->child[dir]; 40 child[dir]->parent=this->parent; 41 42 child[dir]=parent; 43 parent=parent->parent; 44 child[dir]->parent=this; 45 } 46 47 void zigzig(int dir) 48 { 49 parent->rotate(dir); 50 this->rotate(dir); 51 52 child[dir]->child[dir]->update(); 53 child[dir]->update(); 54 this->update(); 55 } 56 57 void zigzag(int dir) //dir depends on first rotation 58 { 59 rotate(dir); 60 rotate(dir^1); 61 62 child[Left]->update(); 63 child[Right]->update(); 64 this->update(); 65 } 66 67 void zig(int dir) 68 { 69 rotate(dir); 70 child[dir]->update(); 71 this->update(); 72 } 73 74 void splay() 75 { 76 while(!isRoot) 77 { 78 int status=0; 79 if(this==parent->child[Left]) status|=1; 80 else status|=2; 81 82 if(!parent->isRoot) 83 { 84 if(parent->parent->isRoot) this->isRoot=true; 85 if(parent==parent->parent->child[Left]) status|=4; 86 else status|=8; 87 } 88 else 89 this->isRoot=true; 90 91 switch(status) 92 { 93 case 1: zig(Right); 94 child[Right]->isRoot=false; 95 break; 96 case 2: zig(Left); 97 child[Left]->isRoot=false; 98 break; 99 case 5: zigzig(Right); 100 if(isRoot) child[Right]->child[Right]->isRoot=false; 101 break; 102 case 6: zigzag(Left); 103 if(isRoot) child[Right]->isRoot=false; 104 break; 105 case 9: zigzag(Right); 106 if(isRoot) child[Left]->isRoot=false; 107 break; 108 case 10:zigzig(Left); 109 if(isRoot) child[Left]->child[Left]->isRoot=false; 110 break; 111 default:break; 112 } 113 } 114 } 115 }; 116 117 int access(SplayNode* u) 118 { 119 SplayNode* v=vir; 120 int res=0; 121 while(u!=vir) 122 { 123 u->splay(); 124 u->child[Right]->isRoot=true; 125 res=u->child[Right]->totDist; 126 u->child[Right]=v; 127 v->isRoot=false; 128 u->update(); 129 v=u; u=u->parent; 130 } 131 return res + v->child[Right]->totDist; 132 } 133 134 inline int query(SplayNode* u,SplayNode* v) 135 { 136 access(u); 137 return access(v); 138 } 139 140 void initVir() 141 { 142 vir=new SplayNode; 143 vir->init(); 144 } 145 146 const int maxN=20005; 147 148 SplayNode node[maxN]; 149 int center[maxN]; 150 int N,Q; 151 int lastRes=0; 152 153 inline void link(int u,int v,int len) 154 { 155 access(node+u); 156 node[u].splay(); 157 center[u]=v; 158 node[u].parent=node+v; 159 node[u].dist=len; 160 node[u].update(); 161 } 162 163 inline void initNode() 164 { 165 for(int i=1;i<=N;i++) node[i].init(); 166 for(int i=1;i<=N;i++) center[i]=i; 167 } 168 169 int getCenter(int x) 170 { 171 return center[x]==x ? x : 172 center[x]=getCenter(center[x]); 173 } 174 175 #define DEBUG 176 #undef DEBUG 177 178 #ifdef DEBUG 179 int qc=0; 180 #endif 181 182 void solve() 183 { 184 lastRes=0; 185 scanf("%d%d",&N,&Q); 186 initNode(); 187 char cmd; 188 int v1,v2,len; 189 while(Q--) 190 { 191 do cmd=getchar(); while(cmd==' ' || cmd=='\n'); 192 if(cmd=='Q') 193 { 194 scanf("%d%d",&v1,&v2); 195 if(getCenter(v1)==getCenter(v2)) 196 printf("%d\n",lastRes=query(node+v1,node+v2)); 197 else printf("Not connected.\n"); 198 #ifdef DEBUG 199 printf("#%d query successful.\n",++qc); 200 #endif 201 } 202 else 203 { 204 scanf("%d%d%d",&v1,&v2,&len); 205 #ifndef DEBUG 206 v1=(v1-lastRes-1)%N; 207 if(v1<=0) v1+=N; 208 v2=(v2-lastRes-1)%N; 209 if(v2<=0) v2+=N; 210 #endif 211 link(getCenter(v1),getCenter(v2),len); 212 } 213 } 214 } 215 216 int main() 217 { 218 initVir(); 219 int X; scanf("%d",&X); 220 while(X--) solve(); 221 return 0; 222 }Problem:ZOJ P2114