Link Cut Tree詳解

2022-08-05 14:27:08 字數 3823 閱讀 2093

lct樹是一類動態樹,其用於動態維護多個連通無環圖之間的關係,允許動態刪除邊或者增加邊(新增的邊不允許構成環)。在我眼中,lct是類似線段樹和splay樹的萬金油資料結構,非常強悍,值得一學。lct的所有操作的攤還時間複雜度均為o(log2(n)),其甚至優於更加簡單的樹鏈剖分。

假設我們有一組獨立的樹(即連通無環圖)。lct中的邊分為兩類,偏好邊(prefered)和普通邊。如果一個結點x下某個子結點y被訪問,我們稱y為x的偏好結點,連線父結點和偏好結點的邊稱為偏好邊,x對y的偏好一直保持到另外一個x的兒子z被訪問,這是x的偏好結點成為z,而x與y之間的邊也就成為了普通邊。一個結點最多隻有一個偏好結點。就像splay樹的核心操作是splay一樣,lct也有自己的核心操作,稱為access,用於訪問某株樹中的某個結點和其所有父結點。

實際上一株樹上會有多條由偏好邊相連得到的路徑,每條路徑都沒有交集(原因是偏好結點只允許有一個)。可以發現一條偏好路徑上是沒有相同深度的結點的,這是一個有用的資訊,我們可以以偏好路徑上結點的深度作為關鍵字用splay樹儲存整條路徑上的所有結點資訊,每條偏好路徑都對於一個splay樹。允許存在只有一個結點的路徑,那麼可以保證所有樹中的結點都被維護在某株splay樹中。這樣我們發現繼續維護整株原樹的資訊有些多餘,我們可以通過為每個結點新增一個屬性routefather來表示其所在路徑最高點的實際父結點(原樹中)。一個結點在原樹的父結點或者是該結點在其所在偏好路徑對應的spaly樹上的前驅結點,或者是其routefather。

下面說明如何實現lct的各項關鍵操作:

access(x)用於訪問所有x和所有原樹中x的祖先結點,這會導致在x和x所在原樹中的根結點r之間構建一條偏好路徑。access還有沒提到的效果,就是會移除x對所有子結點的偏好,也就是access構建的路徑的兩端為x和r,這在對路徑做操作時會尤其有用。

access(x)實現需要預先將x旋轉到頂部,並移除其右孩子(其偏好結點)。之後對x的父親結點y做splay操作,旋轉到頂端,之後移除y的右子樹,並將x作為y的右子樹,之後對父親結點y做相同操作,直到遇到抵達原樹根所在路徑。

access(x)

y =nil

while(x !=nil)

splay(x)

x.right.father =nil

x.right.routefather =x

x.right =y

y.father =x

y =x

x = x.routefather

findroot(x)用於尋找x所在原樹中的根。可以先access(x)以在x與原樹根r之間建立路徑,之後沿著路徑尋找最小的結點,該結點必定是r。

findroot(x)

access(x)

splay(x)

while(x.left !=nil)

x =x.left

splay(x)

//執行splay以攤還費用

return x

makeroot(x)用於將x作為原樹的根。

注意到我們樹的資訊實際上是通過splay樹儲存的,而所謂的根就是最小的結點。我們可以通過access操作在x和原樹根r之間建立一條偏好路徑,之後翻轉這條路徑即可。

makeroot(x)

access(x)

splay(x)

reverse(x)

其中reverse可以使用惰性標記來實現,以降低時間複雜度。但是不要忘記在直接訪問左右孩子之前需要下壓標記。

cut(x, y)用於刪除x與y之間的邊。

需要先將makeroot(x),將x作為根,之後access(y),建立起從x到y的路徑。之後旋轉y到頂部,此時x為y的左孩子,切斷二者的聯絡,同時將y作為新樹的根。

cut(x, y)

makeroot(x)

access(y)

splay(y)

y.left.father =nil

y.left = nil

join(x, y)建立x與y之間的邊。

需要先makeroot(x),將x作為其所在原樹的根,並將x的routefather設定為y,從而將x連線到y之下。

join(x, y)

makeroot(x)

splay(x)

x.routefather = y

getroute(x, y)用於獲取x與y之間的路徑,返回該路徑對於splay樹的根結點。這個方法的返回值用於對路徑做操作。

可以這樣實現,先通過makeroot將x作為根,之後access(y),此時建立了x與y之間的路徑,在splay(x)將x旋轉為樹根並返回。

getroute(x, y)

makeroot(x)

access(y)

splay(x)

return x

不難發現所有lct操作,如果移除了access方法後,實際上就是對splay樹的操作,即攤還時間複雜度為o(log2(n))。因此我們只需要證明access方法的時間複雜度即可。

先說明access的次數。這裡假設我們已經瞭解了樹鏈剖分的基於輕重鏈的時間複雜度的分析,如果不瞭解,可以檢視我的另外一篇部落格《樹鏈剖分詳解》。我們瞭解到每次access內部迴圈發生時,必然會發生偏好結點切換,偏好結點由輕孩子切換為輕孩子,或者由輕孩子切換為重孩子,或者由重孩子切換為輕孩子。由於在access構建的路徑上最多有log2(n)個輕孩子,因此我們能保證偏好結點由輕孩子切換為輕孩子和由重孩子切換為輕孩子的次數至多為log2(n)次。接下來說明輕孩子切換為重孩子的次數,在整個程式流程中,重孩子切換髮生的次數的上限為輕孩子切換為重孩子的次數加上邊的總數(可以假設初始時所有重孩子都是偏好孩子,之後每次重孩子要再次變成偏好結點,必定對應之前發生的某次該重孩子變成非偏好結點)。假設發生了k次操作,則重孩子切換最多發生n+klog2(n)次,平攤下來,每次操作重孩子切換最多發生(n/k+log2(n))次,而由於建立樹的操作存在(即將原本不相連的孤立結點通過邊連線為一株樹),因此k>=n-1,從而重孩子切換的平攤次數為o(1+log2(n))=o(log2(n))次。到此說明了每次access操作內部迴圈平攤下來發生了o(log2(n))次。

接下來說明一次access中對應的o(log2(n))次splay操作的總時間複雜度為o(log2(n))。對於splay的時間複雜度的說明可以看我的另外一篇部落格《splay樹分析》,這裡不加贅述。我們認為所有routefather引用也是splay樹的一條邊,此時勢能的定義依舊,s(x)為x代表的splay子樹所有結點的數目,d(x)則等價於log2(s(x))。我們記x0=x,x1=x0.routefather,....,xk=xk-1.father,而x'i表示xi經過splay操作後對應的結點。而第i次splay操作的攤還時間複雜度為

$$ t_i\le 3\left(d\left(x_i'\right)-d\left(x_i\right)\right)+o\left(1\right) $$

因此我們對時間複雜度進行加總得到

$$ \sum_^k\le 3\sum_^k+o\left(\log_2\left(n\right)\right) $$ $$ \le 3\sum_^k\right)\right)}+o\left(\log_2\left(n\right)\right) $$ $$ =d\left(x'_k\right)-d\left(x'_0\right)+o\left(\log_2\left(n\right)\right)=o\left(\log 2\left(n\right)\right) $$

到此時間複雜度的證明完成。而一次access操作的攤還時間複雜度為o(log2(n))*o(1)+o(log2(n))=o(log2(n))。對應的所有lct的操作的時間複雜度也是o(log2(n))。考慮到lct是完全建立在splay之上的,而splay已經擁有常數大的特徵了,因此lct的常數是更甚,需要小心。

Link Cut Tree

樹剖,也被稱作 靜態樹 是用線段樹維護樹上每條鏈的資訊 而link cut tree用splay森林維護樹上的動態資訊 先明確幾個定義 1 重兒子 這裡指推廣之後的重兒子,滿足i,ii兩條性質 i 重兒子和它的父親在同一棵splay中 ii 一個節點最多有一個重兒子 2 重邊 連線重兒子和它父親的邊...

LCT(Link Cut Tree)

link cut tree lct 可以理解為樹鏈剖分 splay 給出如下定義 access x 訪問x節點 perferred child 若以x為根的子樹中最後被訪問的節點在以x的兒子y為根的子樹中,則稱y為節點x的preferred child preferred edge 節點x與其pre...

Link Cut Tree入門

link cut tree的主要作用是維護一個動態森林中點與點間的路徑動態資訊。 相比於樹鏈剖分,link cut tree能支援動態刪連邊,這是因為樹剖用線段樹來維護區間,而lct用splay的旋轉與區間翻轉的特性來維護點與點間的相對關係。 那麼首先,我們需要像樹剖那樣,把一棵樹分成若干條樹鏈 專...