快速入门Splay
\(splay\) :伸展樹(shù)(\(Splay Tree\)),也叫分裂樹(shù),是一種二叉排序樹(shù),它能在\(O(log n)\)內(nèi)完成插入、查找和刪除操作。它由\(Daniel Sleator\)和\(Robert Tarjan\)創(chuàng)造,后勃?jiǎng)倢?duì)其進(jìn)行了改進(jìn)。它的優(yōu)勢(shì)在于不需要記錄用于平衡樹(shù)的冗余信息。在伸展樹(shù)上的一般操作都基于伸展操作。
先讓我們看一下一棵二叉搜索樹(shù)(\(Binary\) \(Search\) \(Tree\))是什么樣子的。
如圖所示,對(duì)任意一棵\(BST\),它有以下性質(zhì):
- 是一棵空樹(shù),或者是具有下列性質(zhì)的二叉樹(shù)
- 若它的左子樹(shù)不空,則左子樹(shù)上所有結(jié)點(diǎn)的值均小于它的根結(jié)點(diǎn)的值;
- 若它的右子樹(shù)不空,則右子樹(shù)上所有結(jié)點(diǎn)的值均大于它的根結(jié)點(diǎn)的值;
根據(jù)定義,我們會(huì)發(fā)現(xiàn):
- 這棵樹(shù)的中序遍歷,與其壓成數(shù)組的升序排序等效
在這樣一棵樹(shù)中,我們可以很容易地維護(hù)以下信息:
- 查詢\(x\)數(shù)的排名
- 查詢排名為\(x\)的數(shù)
- 求\(x\)的前驅(qū)(前驅(qū)定義為小于\(x\),且最大的數(shù))
- 求\(x\)的后繼(后繼定義為大于\(x\),且最小的數(shù))
同樣的我們會(huì)發(fā)現(xiàn),對(duì)于一個(gè)固定的數(shù)列,它可以形成很多種不同類(lèi)型的\(BST\)。如果這棵樹(shù)恰好不太優(yōu)美,每次維護(hù)的復(fù)雜度可能會(huì)被卡到\(O(N)\)(一條鏈)。
所以,平衡樹(shù)這種偉大的數(shù)據(jù)結(jié)構(gòu)就誕生啦!
顧名思義,平衡樹(shù)就是一棵可以保持全樹(shù)平衡的二叉搜索樹(shù),以此避免復(fù)雜度退化為\(O(N)\)。比較經(jīng)典的一種平衡樹(shù)是\(Treap\),它基于的是對(duì)一個(gè)有序數(shù)列,隨機(jī)出的\(BST\)期望復(fù)雜度是\(O(logN)\),通過(guò)利用堆的性質(zhì)來(lái)維護(hù)其隨機(jī)性,這個(gè)東西我的上一篇博客已經(jīng)介紹過(guò),不再展開(kāi)介紹。今天我們要介紹的是另一種經(jīng)典的平衡樹(shù)——\(Splay\)。
既然是平衡樹(shù),\(Splay\)是如何實(shí)現(xiàn)其樹(shù)體平衡的呢?
在\(Splay\)的每一個(gè)維護(hù)操作中,維護(hù)結(jié)束后當(dāng)前被維護(hù)的點(diǎn)都會(huì)被旋轉(zhuǎn)成為樹(shù)的根節(jié)點(diǎn),這個(gè)過(guò)程叫做樹(shù)的伸展。\((Splay)\)。伸展是\(Splay\)的核心操作。與\(Treap\)利用隨機(jī)出來(lái)的優(yōu)先級(jí)進(jìn)行堆的維護(hù)不同,\(Splay\)的大多數(shù)操作都要基于伸展操作,這也決定了\(Splay\)相比前者具有更廣泛的適用性。
那\(Splay\)是怎么保證其復(fù)雜度不退化成\(O(N)\)的呢?來(lái)看個(gè)例子。
在這一棵已經(jīng)退化成鏈的\(BST\)中,我們對(duì)最底下那個(gè)節(jié)點(diǎn)進(jìn)行了一次維護(hù)。在這之后,這個(gè)節(jié)點(diǎn)就開(kāi)始了向根節(jié)點(diǎn)的漫漫伸展之路~
所以在伸展過(guò)程結(jié)束后,這棵樹(shù)就再次自發(fā)地進(jìn)化回了一棵正常的樹(shù)。如果深度更深會(huì)更加明顯,在一次\(Splay\)以后,它會(huì)從\(N\)級(jí)別的深度進(jìn)化為\(logN\)級(jí)別。
接著讓我們貪心地想一想,假如現(xiàn)在這棵樹(shù)非常的不優(yōu)秀。我想要把它卡掉,就應(yīng)該總是訪問(wèn)它最不優(yōu)秀的節(jié)點(diǎn)。如果最開(kāi)始它還有很多超級(jí)長(zhǎng)的鏈,那么經(jīng)過(guò)幾次貪心的訪問(wèn)之后,它的所有鏈中的最大深度就已經(jīng)回到\(logN\)了。不管常數(shù)怎么樣,均攤一下復(fù)雜度是沒(méi)有問(wèn)題了。
既然這些操作\(Treap\)也能做,為什么不用又快又好寫(xiě)的\(Treap\)呢?因?yàn)?span id="ozvdkddzhkzd" class="math inline">\(Splay\)在區(qū)間操作和\(LCT\)中有其不可替代的作用。具體是什么作用,我也沒(méi)有學(xué)到,等到學(xué)了在拿出來(lái)講吧QwQ
講過(guò)了原理,我們可以來(lái)看一下代碼實(shí)現(xiàn)了Qw
inline void push_up (int u) {t[u].sz = t[u].cnt;t[u].sz += t[t[u].ch[0]].sz;t[u].sz += t[t[u].ch[1]].sz; }inline void rotate (int x) {int y = t[x].fa;int z = t[y].fa;int d1 = t[y].ch[1] == x;int d2 = t[z].ch[1] == y;connect (z, x, d2);connect (y, t[x].ch[!d1], d1);connect (x, y , !d1); push_up (y);push_up (x); }這里\(connect\)是一個(gè)連邊的函數(shù),旋轉(zhuǎn)的原理和\(Treap\)一樣,都是要保證其\(BST\)的性質(zhì),可以手畫(huà)一下示意圖就明白啦~
inline void splay (int x, int goal) {if (x == 0) return;while (t[x].fa != goal) {int y = t[x].fa;int z = t[y].fa;int d1 = t[y].ch[1] == x;int d2 = t[z].ch[1] == y;if (z != goal) {if (d1 == d2) {rotate (y);} else {rotate (x);}}rotate (x);}if (goal == 0) {root = x;} }核心操作——伸展,可以思考一下:為什么是把\(x\)旋轉(zhuǎn)為\(goal\)的子節(jié)點(diǎn)?
剩下的操作,作者很懶,就只貼上代碼啦~
inline void find (int key) {int u = root;if (u == 0) return;while (t[u].key != key && t[u].ch[key > t[u].key]) {u = t[u].ch[key > t[u].key];}//找到key對(duì)應(yīng)的節(jié)點(diǎn),并把它旋轉(zhuǎn)到根。splay (u, 0); }inline void Insert (int key) {int u = root, fa = 0;while (u != 0 && t[u].key != key) {fa = u;//記得記錄父親if (key > t[u].key) {u = t[u].ch[1];} else {u = t[u].ch[0];}}if (u != 0) {//已有(能查到)++t[u].sz;++t[u].cnt;} else {//新增u = ++max_size;t[u].sz = 1;t[u].cnt = 1;t[u].key = key;connect (fa, u, key > t[fa].key); }splay (u, 0); } inline int Next (int key, int dir) {//dir = 0 -> 前驅(qū)//dir = 1 -> 后繼find (key);int u = root;if (dir == 0 && t[u].key < key) return u;if (dir == 1 && t[u].key > key) return u; //如果key值并沒(méi)有存在于樹(shù)中:u = t[u].ch[dir];while (t[u].ch[!dir]) {u = t[u].ch[!dir];}//e.g 如果要找前驅(qū),就先往左一步(保證一定比當(dāng)前值更小),再一直向右(最大的那個(gè))。return u; }inline void Delete (int key) {int _pre = Next (key, 0);int _nxt = Next (key, 1);splay (_pre, 0000);splay (_nxt, _pre);//當(dāng)前鍵值key的前驅(qū)是_pre, 后繼是_nxt//_pre被旋轉(zhuǎn)到根節(jié)點(diǎn),_nxt成為_(kāi)pre的子節(jié)點(diǎn)(顯然是右)//那么當(dāng)前點(diǎn)一定在_nxt的左邊,而且底下沒(méi)有任何一個(gè)點(diǎn)。int u = t[_nxt].ch[0];if (t[u].cnt > 1) {--t[u].cnt;splay (u, 0);} else {t[_nxt].ch[0] = 0;} }inline int kth (int k) {int u = root;if (u == 0) return 0;while (u != 0) {int ls = t[u].ch[0];int rs = t[u].ch[1];if (k > t[ls].sz + t[u].cnt) {k -= t[ls].sz + t[u].cnt;u = rs;//格外注意不要寫(xiě)反順序} else if (k <= t[ls].sz) {u = ls;} else {return t[u].key;}}return false; }inline int get_rnk (int key) {find (key);return t[t[root].ch[0]].sz; }還有一點(diǎn)需要注意的,\(splay\)在使用前要先\(insert\)一個(gè)極大值和一個(gè)極小值。否則在\(Next\)函數(shù)的查找中,比如只有一個(gè)點(diǎn)的話,會(huì)出現(xiàn)找不到前驅(qū)和后繼的情況,也就會(huì)導(dǎo)致出莫名其妙的鍋。當(dāng)然,加上極大極小值之后要格外注意對(duì)答案的處理。下面給出完整代碼,題目P3369 【模板】普通平衡樹(shù)。
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> #define N 100010 #define INF 0x7fffffff using namespace std;struct Splay_Tree {int root, max_size;struct Splay_Node {int sz, fa, cnt, key, ch[2];}t[N];Splay_Tree () {root = max_size = 0;memset (t, 0, sizeof (t));}inline void connect (int u, int v, int dir) {t[u].ch[dir] = v;t[v].fa = u;}inline void push_up (int u) {t[u].sz = t[u].cnt;t[u].sz += t[t[u].ch[0]].sz;t[u].sz += t[t[u].ch[1]].sz;}inline void rotate (int x) {int y = t[x].fa;int z = t[y].fa;int d1 = t[y].ch[1] == x;int d2 = t[z].ch[1] == y;connect (z, x, d2);connect (y, t[x].ch[!d1], d1);connect (x, y , !d1); push_up (y);push_up (x);} inline void splay (int x, int goal) {if (x == 0) return;while (t[x].fa != goal) {int y = t[x].fa;int z = t[y].fa;int d1 = t[y].ch[1] == x;int d2 = t[z].ch[1] == y;if (z != goal) {if (d1 == d2) {rotate (y);} else {rotate (x);}}rotate (x);}if (goal == 0) {root = x;} }inline void find (int key) {int u = root;if (u == 0) return;while (t[u].key != key && t[u].ch[key > t[u].key]) {u = t[u].ch[key > t[u].key];}splay (u, 0);}inline void Insert (int key) {int u = root, fa = 0;while (u != 0 && t[u].key != key) {fa = u;if (key > t[u].key) {u = t[u].ch[1];} else {u = t[u].ch[0];}}if (u != 0) {++t[u].sz;++t[u].cnt;} else {u = ++max_size;t[u].sz = 1;t[u].cnt = 1;t[u].key = key;connect (fa, u, key > t[fa].key); }splay (u, 0);} inline int Next (int key, int dir) {find (key);int u = root;if (dir == 0 && t[u].key < key) return u;if (dir == 1 && t[u].key > key) return u; u = t[u].ch[dir];while (t[u].ch[!dir]) {u = t[u].ch[!dir];}return u;}inline void Delete (int key) {int _pre = Next (key, 0);int _nxt = Next (key, 1);splay (_pre, 0000);splay (_nxt, _pre);int u = t[_nxt].ch[0];if (t[u].cnt > 1) {--t[u].cnt;splay (u, 0);} else {t[_nxt].ch[0] = 0;}}inline int kth (int k) {int u = root;if (u == 0) return 0;while (u != 0) {int ls = t[u].ch[0];int rs = t[u].ch[1];if (k > t[ls].sz + t[u].cnt) {k -= t[ls].sz + t[u].cnt;u = rs;//格外注意 } else if (k <= t[ls].sz) {u = ls;} else {return t[u].key;}}return false;}inline int get_rnk (int key) {find (key);return t[t[root].ch[0]].sz;} }st;int n, x, opt;int main () { // freopen ("splay.in", "r", stdin);scanf ("%d", &n);st.Insert (+INF);st.Insert (-INF);for (int i = 1; i <= n; ++i) {scanf ("%d %d", &opt, &x);if (opt == 1) {st.Insert (x);} if (opt == 2) {st.Delete (x);}if (opt == 3) {printf ("%d\n", st.get_rnk (x));}if (opt == 4) {printf ("%d\n", st.kth (x + 1));}if (opt == 5) {printf ("%d\n", st.t[st.Next (x, 0)].key);}if (opt == 6) {printf ("%d\n", st.t[st.Next (x, 1)].key);}} }轉(zhuǎn)載于:https://www.cnblogs.com/maomao9173/p/10297014.html
總結(jié)
- 上一篇: MySQL的库表详细操作
- 下一篇: Gitlab常用命令