史上最詳盡的平衡樹 splay 講解與模板

2021-07-09 20:07:20 字數 3787 閱讀 1047

首先宣告:萬分感謝gty大哥的幫助!這年頭能找到簡單易懂的陣列版平衡樹模板只能靠學長了!

變數宣告:f[i]表示i的父結點,ch[i][0]表示i的左兒子,ch[i][1]表示i的右兒子,key[i]表示i的關鍵字(即結點i代表的那個數字),cnt[i]表示i結點的關鍵字出現的次數(相當於權值),size[i]表示包括i的這個子樹的大小;sz為整棵樹的大小,root為整棵樹的根。

再介紹幾個基本操作:

【clear操作】:將當前點的各項值都清0(用於刪除之後)

inline void clear(int x)
【get操作】:判斷當前點是它父結點的左兒子還是右兒子

inline int get(int x)
【update操作】:更新當前點的size值(用於發生修改之後)

inline void update(int x)

}

下面boss來了:

【rotate操作**詳解】

這是原來的樹,假設我們現在要將d結點rotate到它的父親的位置。

step 1:

找出d的父親結點(b)以及父親的父親(a)並記錄。判斷d是b的左結點還是右結點。

step 2:

我們知道要將drotate到b的位置,二叉樹的大小關係不變的話,b就要成為d的右結點了沒錯吧?

咦?可是d已經有右結點了,這樣不就衝突了嗎?怎麼解決這個衝突呢?

我們知道,d原來是b的左結點,那麼rotate過後b就一定沒有左結點了對吧,那麼正好,我們把g接到b的左結點去,並且這樣大小關係依然是不變的,就完美的解決了這個衝突。

這樣我們就完成了一次rotate,如果是右兒子的話同理。step 2的具體操作:

我們已經判斷了d是b的左兒子還是右兒子,設這個關係為k;將d與k關係相反的兒子的父親記為b與k關係相同的兒子(這裡即為d的右兒子的父親記為b的左兒子);將d與k關係相反的兒子的父親即為b(這裡即為把g的父親記為b);將b的父親即為d;將d與k關係相反的兒子記為b(這裡即為把d的右兒子記為b);將d的父親記為a。

最後要判斷,如果a存在(即rotate到的位置不是根的話),要把a的兒子即為d。

顯而易見,rotate之後所有牽涉到變化的父子關係都要改變。以上的樹需要改變四對父子關係,bg dg bd ab,需要三個操作(bg bd ab)。

step 3:update一下當前點和各個父結點的各個值

【**】

inline void rotate(int x)
【splay操作】

其實splay只是rotate的發展。伸展操作只是在不停的rotate,一直到達到目標狀態。如果有乙個確定的目標狀態,也可以傳兩個參。此**直接splay到根。

splay的過程中需要分類討論,如果是三點一線的話(x,x的父親,x的祖父)需要先rotate x的父親,否則需要先rotate x本身(否則會形成單旋使平衡樹失衡)

inline void splay(int x)
【insert操作】

其實插入操作是比較簡單的,和普通的二叉查詢樹基本一樣。

step 1:如果root=0,即樹為空的話,做一些特殊的處理,直接返回即可。

step 2:按照二叉查詢樹的方法一直向下找,其中:

如果遇到乙個結點的關鍵字等於當前要插入的點的話,我們就等於把這個結點加了乙個權值。因為在二叉搜尋樹中是不可能出現兩個相同的點的。並且要將當前點和它父親結點的各項值更新一下。做一下splay。

如果已經到了最底下了,那麼就可以直接插入。整個樹的大小要+1,新結點的左兒子右兒子(雖然是空)父親還有各項值要一一對應。並且最後要做一下他父親的update(做他自己的沒有必要)。做一下splay。

inline void insert(int v)

int now=root,fa=0;

while (1)

fa=now;

now=ch[now][key[now]

【find操作】查詢x的排名

初始化:ans=0,當前點=root

和其它二叉搜尋樹的操作基本一樣。但是區別是:

如果x比當前結點小,即應該向左子樹尋找,ans不用改變(設想一下,走到整棵樹的最左端最底端排名不就是1嗎)。

如果x比當前結點大,即應該向右子樹尋找,ans需要加上左子樹的大小以及根的大小(這裡的大小指的是權值)。

不要忘記了再splay一下

inline int find(int v)}}

【求x的前驅(後繼),前驅(後繼)定義為小於(大於)x,且最大(最小)的數】

這類問題可以轉化為將x插入,求出樹上的前驅(後繼),再將x刪除的問題。

其中insert操作上文已經提到。

【pre/next操作】

這個操作十分的簡單,只需要理解一點:在我們做insert操作之後做了一遍splay。這就意味著我們把x已經splay到根了。求x的前驅其實就是求x的左子樹的最右邊的乙個結點,後繼是求x的右子樹的左邊乙個結點(想一想為什麼?)

inline int pre()

inline int next()

【del操作】

刪除操作是最後乙個稍微有點麻煩的操作。

step 1:隨便find一下x。目的是:將x旋轉到根。

step 2:那麼現在x就是根了。如果cnt[root]>1,即不只有乙個x的話,直接-1返回。

step 3:如果root並沒有孩子,就說名樹上只有乙個x而已,直接clear返回。

step 4:如果root只有左兒子或者右兒子,那麼直接clear root,然後把唯一的兒子當作根就可以了(f賦0,root賦為唯一的兒子)

剩下的就是它有兩個兒子的情況。

step 5:我們找到新根,也就是x的前驅(x左子樹最大的乙個點),將它旋轉到根。然後將原來x的右子樹接到新根的右子樹上(注意這個操作需要改變父子關係)。這實際上就把x刪除了。不要忘了update新根。

inline void del(int x)

//only one point

if (!ch[root][0]&&!ch[root][1])

//only one child

if (!ch[root][0])

else if (!ch[root][1])

//two children

int leftbig=pre(),oldroot=root;

splay(leftbig);

f[ch[oldroot][1]]=root;

ch[root][1]=ch[oldroot][1];

clear(oldroot);

update(root);

return;

}

【總結】

平衡樹的本質其實是二叉搜尋樹,所以很多操作是基於二叉搜尋樹的操作。

splay的本質是rotate,旋轉其實只是為了保證二叉搜尋樹的平衡性。

所有的操作一定都滿足二叉搜尋樹的性質,所有改變父子關係的操作一定要update。

關鍵是理解rotate,splay的原理以及每乙個操作的原理。

完整**:

史上最詳盡的LCT講解

摘自popoqqq 優秀的學姐 lct能幹嘛 1 維護乙個序列,支援下列操作 區間求和 區間求最值 區間修改 求連續子段和 這個線段樹就可以解決 具體做法不加累述了 2 維護乙個序列,支援下列操作 區間求和 區間求最值 區間修改 求連續子段和 新增一段區間 刪除一段區間 翻轉一段區間 splay的基...

史上最詳盡的斜率優化!

最近被欽定要寫教材,負責斜率優化那一塊,就把寫的內容搬了些上來。3.6.1斜率優化dp的基本思想 考慮這樣乙個問題 現在要給n個數a 1 a 2 a n 分組,每分出一組,你的代價為該組所有數的和的平方 乙個常數m 即設你分出的一組數字是2,3,4,m 5,則你分出該組的代價為 2 3 4 2 5 ...

平衡樹之splay

在每次查詢之後對樹進行重構 把被查詢的條目搬移到 離樹根近一些的地方。伸展樹應運而生。伸展樹是一種自調整形式的二叉查詢樹,它會 沿著從某個節點到樹根之間的路徑 通過一系列的旋轉把這個節點搬移到樹根去。大家只需要記住,每次進行插入 查詢的時候,都要把插入 查詢的元素通過旋轉變到根的位置,splay 的...