Splay伸展樹
有篇Splay入門必看文章 —— CSDN鏈接
經典引文

- 節點x是父節點p的左孩子還是右孩子
- 節點p是不是根節點,如果不是
- 節點p是父節點g的左孩子還是右孩子



自學筆記
今天開始自己動手寫Splay。身邊的小伙伴大多是用下標和數組來維系各個結點的聯系,但是我還是一如既往的喜歡C++的指針(❤ ω ❤)。
結點以一個struct結構體的形式存在。
struct node { int value; node *father; node *son[2]; node (int v = 0, node *f = NULL) { value = v; father = f; son[0] = NULL; son[1] = NULL; } };
其中,son[0]代表左兒子,son[1]代表右兒子。
用一個函數來判斷子節點是父節點的哪個兒子。
inline bool son(node *f, node *s) { return f->son[1] == s; }
返回值就是son[]數組的下標,這個函數很方便。
最關鍵的是旋轉操作,有別於常見的zig,zag旋轉,我喜歡用一個函數實現其兩者的功能,即rotate(x)代表將x旋轉到其父節點的位置上。
inline void rotate(node *t) { node *f = t->father; node *g = f->father; bool a = son(f, t), b = !a; f->son[a] = t->son[b]; if (t->son[b] != NULL) t->son[b]->father = f; t->son[b] = f; f->father = t; t->father = g; if (g != NULL) g->son[son(g, f)] = t; else root = t; }
函數會自行判斷x實在父節點的左兒子上還是右兒子上,並自動左旋或右旋。這里要注意改變祖父結點的兒子指針,以及結點的父親指針,切忌馬虎漏掉。同時還要事先做好特判,放止訪問非法地址。這里用指針相較於用下標的一個好處就是,如果你不小心訪問了NULL即空指針,也是下標黨常用的0下標,指針寫法一定會RE,而下標寫法可能就不會崩潰,因而不易發現錯誤,導致一些較為復雜而智障的錯誤。
然后是核心函數——Splay函數,貌似也有人叫Spaly的樣子,然而我並沒有考證什么。Splay(x,y)用於將x結點旋轉到y結點的某個兒子上。特別地,Splay(x,NIL)代表將x旋轉到根節點的位置上。根節點的父親一般是NIL或0。
inline void spaly(node *t, node *p) { while (t->father != p) { node *f = t->father; node *g = f->father; if (g == p) rotate(t); else { if (son(g, f) ^ son(f, t)) rotate(t), rotate(t); else rotate(f), rotate(t); } } }
這里值得注意的是兩種雙旋。如果t(該節點),f(父親節點),g(祖父節點)形成了一條單向的鏈,即[右→右]或[左→左]這樣子,那么就先對父親結點進行rotate操作,再對該節點進行rotate操作;否則就對該節點連續進行兩次rotate操作。據稱單旋無神犇,雙旋O(logN),這句話我也沒有考證,個人表示不想做什么太多的探究,畢竟Splay的復雜度本來就挺玄學的了,而且專門卡單旋Splay的題也沒怎么聽說過。對了,這個雙旋操作和AVL的雙旋是不是有那么幾分相似啊,雖然還是不太一樣的吧,好吧其實也不怎么像╮(╯-╰)╭。
接下來談談插入操作。插入操作就和普通的二叉搜索樹類似,先找到合適的葉子結點,然后在空着的son[]上新建結點,把值放入。不同的是需要把新建的結點Splay到根節點位置,復雜度需要,不要問為什么。
inline void insert(int val) { if (root == NULL) root = new node(val, NULL); for (node *t = root; t; t = t->son[val >= t->value]) { if (t->value == val) { spaly(t, NULL); return; } if (t->son[val >= t->value] == NULL) t->son[val >= t->value] = new node(val, t); } }
注意,這個插入函數實現的是非重集合。
與之對應的就是刪除操作,相對的復雜一些。刪除一個元素,需要先在樹中找到這個結點,然后把這個結點Splay到根節點位置,開始分類討論。如果這個結點沒有左兒子(左子樹),直接把右兒子放在根的位置上即可;否則的話就需要想方設法合並左右子樹:在左子樹種找到最靠右(最大)的結點,把它旋轉到根節點的兒子上,此時它一定沒有右兒子,因為根節點的左子樹中不存在任何一個元素比它更大,那么把根節點的右子樹接在這個結點的右兒子上即可。
inline void erase(int val) { node *t = root; for ( ; t; ) { if (t->value == val) break; t = t->son[val > t->value]; } if (t != NULL) { spaly(t, NULL); if (t->son[0] == NULL) { root = t->son[1]; if (root != NULL) root->father = NULL; } else { node *p = t->son[0]; while (p->son[1] != NULL) p = p->son[1]; spaly(p, t); root = p; root->father = NULL; p->son[1] = t->son[1]; if (p->son[1] != NULL) p->son[1]->father = p; } } }
相較於insert()確實復雜了不少。
以上就是Splay的框架了,是Splay必不可少的部分,在此基礎上可以加入許多新的功能。
例如,手動實現垃圾回收,這樣新建結點的常數會小很多,畢竟C++的new是很慢的。
node tree[siz], *stk[siz]; int top; inline node *newnode(int v, node *f) { node *ret = stk[--top]; ret->size = 1; ret->value = v; ret->father = f; ret->son[0] = NULL; ret->son[1] = NULL; ret->reverse = false; return ret; } inline void freenode(node *t) { stk[top++] = t; }
有的時候需要我們維護子樹大小。
inline int size(node *t) { return t == NULL ? 0 : t->size; } inline void update(node *t) { t->size = 1; t->size += size(t->son[0]); t->size += size(t->son[1]); }
簡單,安全。
維護區間翻轉的時候需要用到打標記的方式。
inline bool tag(node *t) { return t == NULL ? false : t->reverse; } inline void reverse(node *t) { if (t != NULL) t->reverse ^= true; } inline void pushdown(node *t) { if (tag(t)) { std::swap(t->son[0], t->son[1]); reverse(t->son[0]); reverse(t->son[1]); t->reverse ^= true; } }
還有更為簡潔的rotate函數。
inline void connect(node *f, node *s, bool k) { if (f == NULL) root = s; else f->son[k] = s; if (s != NULL) s->father = f; } inline void rotate(node *t) { node *f = t->father; node *g = f->father; bool a = son(f, t), b = !a; connect(f, t->son[b], a); connect(g, t, son(g, f)); connect(t, f, b); update(f); update(t); }
@Author: YouSiki