1. 程式人生 > >伸展樹(Splay)學習筆記

伸展樹(Splay)學習筆記

二叉排序樹能夠支援多種動態集合操作,它可以被用來表示有序集合,建立索引或優先佇列等。因此,在資訊學競賽中,二叉排序樹應用非常廣泛。 作用於二叉排序樹上的基本操作,其時間複雜度均與樹的高度成正比,對於一棵有 $n$ 個節點的二叉樹,這些操作在最有情況下執行時間為 $O( \log_2 n)$。 但是,如果二叉樹退化成了一條 $n$ 個節點組成的線性連結串列,則這些操作在最壞情況下的執行時間為 $O(n)$。 有些二叉排序樹的變形,其基本操作的效能在最壞情況下依然很好,如平衡樹(AVL)等。但是,它們需要額外的空間來儲存平衡資訊,且實現起來比較複雜。同時,如果訪問模式不均勻,平衡樹的效率就會受到影響,而伸展樹卻可以克服這些問題。 伸展樹(Splay Tree),是對二叉排序樹的一種改進。雖然它並不能保證樹一直是“平衡”的,但對於它的一系列操作,可以證明其每一步操作的“平攤時間”複雜度都是 $O(\log_2 n)$ 。平攤時間是指在一系列最壞情況的操作序列中單次操作的平均時間。所以,從某種意義上來說,伸展樹也是一種平衡的二叉排序樹。而在各種樹形資料結構中,伸展樹的空間複雜度(不需要記錄用於平衡的冗餘資訊)和程式設計複雜度也都是很優秀的。 獲得較好平攤效率的一種方法就是使用“自調整”的資料結構,與平衡結構或有明確限制的資料結構相比,自調整的資料結構有一下幾個優點: 1. 從平攤角度上,它們忽略常數因子,因此絕對不會差於有明確限制的資料結構,而且它們可以根據具體使用情況進行調整,所以在使用模式不均勻的情況下更加有效; 2. 由於無需儲存平衡資訊或者其他限制資訊,所以所需的儲存空間更小; 3. 它們的查詢和更新的演算法與操作都很簡單,易於實現。 當然,自調整的資料結構也有其潛在的缺點: 1. 它們需要更多的區域性調整,尤其在查詢期間,而那些有明確限制的資料結構僅需要在更新期間進行調整,查詢期間則不需要; 2. 一系列查詢操作中的某一個可能會耗時較長,這在實時處理的應用程式中可能是一個不足之處。 ## 1. 伸展樹的主要操作 伸展樹是對二叉排序樹的一種改進。與二叉排序樹一樣,伸展樹也具有有序性,即伸展樹中的每一個節點 $x$ 都滿足:該節點左子樹中的每一個元素都小於 $x$,而其右子樹中的每一個元素都大於 $x$。 但是,與普通二叉排序樹不同的是,伸展樹可以“自我調整”,這就要依靠伸展樹的核心操作 —— $\text{Splay(x, S)}$。 ### 1.1 伸展操作 伸展操作 $\text{Splay(x, S)}$ 是在保持伸展樹有序的前提下,通過一系列旋轉,將伸展樹 $\text{S}$ 中的元素 $\text{x}$ 調整至數的根部。在調整的過程中,要分以下三種情況分別處理。 #### 情況一:節點 $\text{x}$ 的父節點 $\text{y}$ 是根節點。 此時, - 若 $\text{x}$ 是 $\text{y}$ 的左兒子,則我們進行一次右旋操作 $\text{Zig(x)}$; - 若 $\text{x}$ 是 $\text{y}$ 的右兒子,則我們進行一次左旋操作 $\text{Zag(x)}$。 經過旋轉,使 $\text{x}$ 成為二叉排序樹 $S$ 的根節點,且依然滿足二叉排序樹的性質。 $\text{Zig}$ 操作和 $\text{Zag}$ 操作如圖所示: ![](https://img2020.cnblogs.com/blog/1878967/202008/1878967-20200808092025709-1015616445.png) #### 情況二:節點 $\text{x}$ 的父節點 $\text{y}$ 不是根節點,且 $\text{x}$ 和 $\text{y}$ 同為各自父節點的左兒子,或同為各自父節點的右兒子。 此時,我們設 $\text{z}$ 為 $\text{y}$ 的父節點, - 若 $\text{x}$ 和 $\text{y}$ 同時是各自父節點的左兒子,則進行一次 $\text{Zig-Zig}$ 操作; - 若 $\text{x}$ 和 $\text{y}$ 同時是各自父節點的右兒子,則進行一次 $\text{Zag-Zag}$ 操作。 如圖所示: ![](https://img2020.cnblogs.com/blog/1878967/202008/1878967-20200808093748870-820303649.png) #### 情況三:節點 $\text{x}$ 的父節點 $\text{y}$ 不是根節點,且 $\text{x}$ 和 $\text{y}$ 中的一個是其父節點的左兒子,另一個是其父節點的右兒子。 此時,我們設 $\text{z}$ 為 $\text{y}$ 的父節點, - 若 $\text{x}$ 是 $\text{y}$ 的左兒子,$\text{y}$ 是 $\text{z}$ 的右兒子,則進行一次 $\text{Zig-Zag}$ 操作; - 若 $\text{x}$ 是 $\text{y}$ 的右兒子,$\text{y}$ 是 $\text{z}$ 的左二子,則進行一次 $\text{Zag-Zig}$ 操作。 ![](https://img2020.cnblogs.com/blog/1878967/202008/1878967-20200808095317393-501056272.png) ![](https://img2020.cnblogs.com/blog/1878967/202008/1878967-20200808095912599-30596745.png) 下面舉一個例子來體會上面的伸展操作。 如下圖所示,最左邊的一個單鏈先執行 $\text{Splay(1, S)}$,我們將元素 $1$ 調整到了伸展樹的根部。 ![](https://img2020.cnblogs.com/blog/1878967/202008/1878967-20200808103454522-1224295219.png) 執行幾次 $\text{Splay(1, S)}$ 的效果 然後再執行 $\text{Splay(2, S)}$,將元素 $2$ 調整到伸展樹 $\text{S}$ 的根部。如下圖所示: ![](https://img2020.cnblogs.com/blog/1878967/202008/1878967-20200808104038181-1680770944.png) 執行幾次 $\text{Splay(2, S)}$ 的效果 ### 1.2 伸展樹的基本操作 利用伸展樹 Splay ,我們可以在伸展樹 $S$ 上進行如下幾種基本操作。 #### (1) $\text{Find(x, S)}$:判斷元素 $\text{x}$ 是否在伸展樹 $\text{S}$ 表示的有序集中。 首先,與在二叉排序樹中進行查詢操作操作一樣,在伸展樹中查詢元素 $\text{x}$。如果 $\text{x}$ 在樹中,則再執行 $\text{Splay(x, S)}$ 調整伸展樹。 #### (2) $\text{Insert(x, S)}$:將元素 $\text{x}$ 插入到伸展樹 $S$ 表示的有序集中。 首先,與在二叉排序樹中進行插入操作一樣,將 $\text{x}$ 插入到伸展樹 $\text{S}$ 中的相應位置,再執行 $\text{Splay(x, S)}$ 調整伸展樹。 #### (3) $\text{Join(S1, S2)}$:將兩棵伸展樹 $\text{S1}$ 與 $\text{S2}$ 合併成為一棵伸展樹。其中,$S1$ 的所有元素都小於 $S2$ 的所有元素。 首先,找到伸展樹 $S1$ 中最大的一個元素 $\text{x}$,再通過 $\text{Splay(x, S1)}$ 將 $\text{x}$ 調整到伸展樹 $S1$ 的根部。然後將 $S2$ 作為 $\text{x}$ 節點的右子樹插入,這樣就得到了新的伸展樹 $S$,如圖所示: ![](https://img2020.cnblogs.com/blog/1878967/202008/1878967-20200808111536091-1717629423.png) $\text{Join(S1, S2)}$ 的兩個步驟 #### (4)$\text{Delete(x, S)}$:將元素 $x$ 從伸展樹 $S$ 所表示的有序集中刪除。 首先,執行 $\text{Find(x, S)}$ 將 $\text{x}$ 調整為根節點,然後再對左右子樹執行 $\text{Join(S1, S2)}$ 操作即可。 #### (5)$\text{Split(x, S)}$:以 $x$ 為界,將伸展樹 $S$ 分離為兩棵伸展樹 $S1$ 和 $S2$,其中,$S1$ 的所有元素都小於 $x$,$S2$ 的所有元素都大於 $x$。 首先,執行 $\text{Find(x, S)}$ 將 $\text{x}$ 調整為根節點,則 $\text{x}$ 的左子樹就是 $\text{S1}$,右子樹就是 $\text{S2}$。如圖所示: ![](https://img2020.cnblogs.com/blog/1878967/202008/1878967-20200808112315448-202830434.png) 除了上述介紹的 $5$ 種基本操作外,伸展樹還支援求最大值、最小值、求前趨、求後繼等多種操作,這些操作也都是建立在伸展樹操作 $\text{Splay}$ 的基礎之上的。 ## 2. 伸展樹的演算法實現 _注:這裡的程式碼並不是最簡單的程式碼,而是基於上述思想實現的程式碼,更方便我們結合之前分析的內容來理解。_ 下面給出伸展樹的各種操作的演算法實現,它們都是基於如下伸展樹的型別定義: ```c++ int lson[maxn], // 左兒子編號 rson[maxn], // 右兒子編號 p[maxn], // 父節點編號 val[maxn], // 節點權值 sz; // 編號範圍 [1, sz] struct Splay { int rt; // 根節點編號 void zag(int x); // 左旋 void zig(int x); // 右旋 void splay(int x); // 伸展操作:將x移到根節點 int func_find(int v); // 查詢是否存在值為v的節點 void func_insert(int v); // 插入 void func_delete(int v); // 刪除 int get_max(); // 求最大值 int get_min(); // 求最小值 int get_pre(int v); // 求前趨 int get_suc(int v); // 求後繼 int join(int rt1, int rt2); // 合併 } tree; ``` #### 1. 左旋操作 ```c++ void Splay::zag(int x) { int y = p[x], z = p[y], a = lson[x]; lson[x] = y; p[y] = x; rson[y] = a; p[a] = y; p[x] = z; if (z) { if (lson[z] == y) lson[z] = x; else rson[z] = x; } } ``` ![](https://img2020.cnblogs.com/blog/1878967/202008/1878967-20200808143324013-4128882.png) Zag(x)操作 #### 2. 右旋操作 ```c++ void Splay::zig(int x) { int y = p[x], z = p[y], a = rson[x]; rson[x] = y; p[y] = x; lson[y] = a; p[a] = y; p[x] = z; if (z) { if (lson[z] == y) lson[z] = x; else rson[z] = x; } } ``` ![](https://img2020.cnblogs.com/blog/1878967/202008/1878967-20200808143132024-806042686.png) Zig(x)操作 #### 3. 伸展操作 ```c++ void Splay::splay(int x) { while (p[x]) { int y = p[x], z = p[y]; if (!z) { if (x == lson[y]) zig(x); else zag(x); } else if (lson[y] == x) { if (lson[z] == y) { // zig-zig zig(y); zig(x); } else { // zig-zag zig(x); zag(x); } } else { // rson[y] == x if (lson[z] == y) { // zag-zig zag(x); zig(x); } else { // zag-zag zag(y); zag(x); } } } rt = x; } ``` #### 4. 查詢 ```c++ int Splay::func_find(int v) { int x = rt; while (x) { if (val[x] == v) { rt = x; splay(x); return x; } else if (v < val[x]) x = lson[x]; else x = rson[x]; } return 0; // 返回0說明沒找到 } ``` #### 5. 插入 ```c++ void Splay::func_insert(int v) { val[++sz] = v; if (rt == 0) { rt = sz; return; } int x = rt; while (true) { if (v < val[x]) { if (lson[x]) x = lson[x]; else { lson[x] = sz; p[sz] = x; break; } } else { if (rson[x]) x = rson[x]; else { rson[x] = sz; p[sz] = x; break; } } } splay(rt = sz); } ``` #### 6. 刪除(會用到下面定義的join操作) ```c++ void Splay::func_delete(int v) { int x = func_find(v); if (!x) return; int ls = lson[x], rs = rson[x]; lson[x] = rson[x] = 0; p[ls] = p[rs] = 0; rt = join(ls, rs); } ``` #### 7. 求最大值 ```c++ int Splay::get_max() { if (!rt) return 0; int x = rt; while (rson[x]) x = rson[x]; splay(rt = x); return x; } ``` #### 8. 求最小值 ```c++ int Splay::get_min() { if (!rt) return 0; int x = rt; while (lson[x]) x = lson[x]; splay(rt = x); return x; } ``` #### 9. 求前趨 ```c++ int Splay::get_pre(int v) { if (!rt) return 0; int x = rt, ans = 0; while (true) { if (val[x] <= v) { if (!ans || val[ans] < val[x]) ans = x; if (rson[x]) x = rson[x]; else break; } else { if (lson[x]) x = lson[x]; else break; } } if (ans) splay(rt = ans); return ans; } ``` #### 10. 求後繼 ```c++ int Splay::get_suc(int v) { if (!rt) return 0; int x = rt, ans = 0; while (true) { if (val[x] >= v) { if (!ans || val[ans] > val[x]) ans = x; if (lson[x]) x = lson[x]; else break; } else { if (rson[x]) x = rson[x]; else break; } } if (ans) splay(rt = ans); return ans; } ``` #### 11. 合併 ```c++ int Splay::join(int rt1, int rt2) { if (!rt1) return rt2; if (!rt2) return rt1; Splay tree1; tree1.rt = rt1; rt1 = tree1.get_max(); assert(rson[rt1] == 0); rson[rt1] = rt2; p[rt2] = rt1; return rt1; } ``` 示例程式碼(對應題目:《怪物倉庫管理員(二)》): ```c++ #include
using namespace std; const int maxn = 500050; int lson[maxn], // 左兒子編號 rson[maxn], // 右兒子編號 p[maxn], // 父節點編號 val[maxn], // 節點權值 sz; // 編號範圍 [1, sz] struct Splay { int rt; // 根節點編號 void zag(int x); // 左旋 void zig(int x); // 右旋 void splay(int x); // 伸展操作:將x移到根節點 int func_find(int v); // 查詢是否存在值為v的節點 void func_insert(int v); // 插入 void func_delete(int v); // 刪除 int get_max(); // 求最大值 int get_min(); // 求最小值 int get_pre(int v); // 求前趨 int get_suc(int v); // 求後繼 int join(int rt1, int rt2); // 合併 } tree; /** zag(int x) 左旋 */ void Splay::zag(int x) { int y = p[x], z = p[y], a = lson[x]; lson[x] = y; p[y] = x; rson[y] = a; p[a] = y; p[x] = z; if (z) { if (lson[z] == y) lson[z] = x; else rson[z] = x; } } /** zig(int x) 右旋 */ void Splay::zig(int x) { int y = p[x], z = p[y], a = rson[x]; rson[x] = y; p[y] = x; lson[y] = a; p[a] = y; p[x] = z; if (z) { if (lson[z] == y) lson[z] = x; else rson[z] = x; } } /** splay(int x) 伸展操作 */ void Splay::splay(int x) { while (p[x]) { int y = p[x], z = p[y]; if (!z) { if (x == lson[y]) zig(x); else zag(x); } else if (lson[y] == x) { if (lson[z] == y) { // zig-zig zig(y); zig(x); } else { // zig-zag zig(x); zag(x); } } else { // rson[y] == x if (lson[z] == y) { // zag-zig zag(x); zig(x); } else { // zag-zag zag(y); zag(x); } } } rt = x; } int Splay::func_find(int v) { int x = rt; while (x) { if (val[x] == v) { rt = x; splay(x); return x; } else if (v < val[x]) x = lson[x]; else x = rson[x]; } return 0; // 返回0說明沒找到 } void Splay::func_insert(int v) { val[++sz] = v; if (rt == 0) { rt = sz; return; } int x = rt; while (true) { if (v < val[x]) { if (lson[x]) x = lson[x]; else { lson[x] = sz; p[sz] = x; break; } } else { if (rson[x]) x = rson[x]; else { rson[x] = sz; p[sz] = x; break; } } } splay(rt = sz); } void Splay::func_delete(int v) { int x = func_find(v); if (!x) return; int ls = lson[x], rs = rson[x]; lson[x] = rson[x] = 0; p[ls] = p[rs] = 0; rt = join(ls, rs); } int Splay::get_max() { if (!rt) return 0; int x = rt; while (rson[x]) x = rson[x]; splay(rt = x); return x; } int Splay::get_min() { if (!rt) return 0; int x = rt; while (lson[x]) x = lson[x]; splay(rt = x); return x; } int Splay::get_pre(int v) { if (!rt) return 0; int x = rt, ans = 0; while (true) { if (val[x] <= v) { if (!ans || val[ans] < val[x]) ans = x; if (rson[x]) x = rson[x]; else break; } else { if (lson[x]) x = lson[x]; else break; } } if (ans) splay(rt = ans); return ans; } int Splay::get_suc(int v) { if (!rt) return 0; int x = rt, ans = 0; while (true) { if (val[x] >= v) { if (!ans || val[ans] > val[x]) ans = x; if (lson[x]) x = lson[x]; else break; } else { if (rson[x]) x = rson[x]; else break; } } if (ans) splay(rt = ans); return ans; } int Splay::join(int rt1, int rt2) { if (!rt1) return rt2; if (!rt2) return rt1; Splay tree1; tree1.rt = rt1; rt1 = tree1.get_max(); assert(rson[rt1] == 0); rson[rt1] = rt2; p[rt2] = rt1; return rt1; } int n, op, x; int main() { cin >> n; while (n --) { cin >> op; if (op != 3 && op != 4) cin >> x; if (op == 1) tree.func_insert(x); else if (op == 2) tree.func_delete(x); else if (op == 3) cout << val[tree.get_min()] << endl; else if (op == 4) cout << val[tree.get_max()] << endl; else if (op == 5) cout << val[tree.get_pre(x)] << endl; else cout << val[tree.get_suc(x)] << endl; } return