[Scapegoat Tree] & BZOJ3224
0x00 扯淡
知乎上面有個問題問最優雅的算法是什么,我覺得暴力即是優雅。
當然這里說的暴力並不是指那種不加以思考的無腦的暴力,而是說用繁瑣而技巧性的工作可以實現的事,我用看似簡單的思想和方法,也可以達到近似於前者的空間復雜度和時間復雜度,甚至可以更優,而其中也或多或少的夾雜着一些"LESS IS MORE"的思想在其中。
以下文章需要對普通二叉搜索樹和Treap樹(可選)有一定的了解,可以自行百度也可以等我出的一篇有關這個的文章。
0x01 替罪羊樹[Scapegoat Tree]
對於一棵二叉搜索樹,最重要的事情就是維護他的平衡,以保證對於每次操作(插入,查找,刪除)的時間均攤下來都是
乃至
(紅黑樹,但是常數大而且難寫,此處不展開介紹)。
為了維護樹的平衡,各種平衡二叉樹絞盡腦汁方法五花八門,但幾乎都是通過旋轉的操作來實現(AVL 樹,紅黑樹,Treap 樹(經@GadyPu指正,可持久化Treap樹不需要旋轉) ,Splay…),只不過是在判斷什么時候應該旋轉上有所不同。但替罪羊樹就是那么一棵特立獨行的豬,哦不,是一只特立獨行的樹。
0x02 各種嘿嘿嘿的操作
- 重構
重構允許重構整棵替罪羊樹,也允許重構替罪羊樹其中的一棵子樹。
重構這個操作看似高端,實則十分暴力(真)。主要操作就是把需要重構的子樹拍平(由於子樹一定是二叉搜索樹,所以拍平之后的序列一定也是有序的),然后拎起序列的中點,作為根部,剩下的左半邊序列為左子樹,右半邊序列為右子樹,接着遞歸對左邊和右邊進行同樣的操作,直到最后形成的樹中包含的全部為點而不是序列(這樣形成的一定是一棵完全二叉搜索樹,也是最優的方案)。
這是一棵需要維護的子樹,雖然目前不知道基於什么判斷條件,但這棵是明顯需要維護的。
拍平之后的結果,直接遍歷即可。
子樹的重構就完成了。
- 插入
,一直找到最后一層不滿足該條件的層序號(也就是從根開始的第一層不滿足該條件的層序號),然后從該層開始重構以該層為根的子樹(一個節點導致樹的不平衡,就要導致整棵子樹被拍扁,估計這也是“替罪羊”這個名字的由來吧)。
每次插入操作的復雜度為
,每次重構樹的復雜度為
,但由於不會每次都要進行重構,也不會每次都重構一整棵樹,所以均攤下來的復雜度還是
。
在這里是一個常數,可以通過調整
的大小來控制樹的平衡度,使程序具有很好的可控性
-------------
日更新-------------
為了測試
值的選取對於程序性能的影響,枚舉了
這個區間內
的值,性能繪制成圖標如下(數據采用BZOJ 6,7,8三組數據的3倍)

(測試結果如上)
由此可見,
區間內
的取值對於程序性能並沒有很大的影響,當然也有可能是我測試方法不當,
-------------
日更新-------------

(測試結果如上)
對於取值越靠近兩端的確速度越慢,但中間貌似還是沒有什么差異。如果有好的數據構造方法希望能提出,一定會再次嘗試,謝謝。
- 刪除(惰性刪除)
),可以證明均攤下來的復雜度還是
(作者太傻證明不來)。
- 查找第K大&查找數X的序號
0x03 代碼
以下是替罪羊樹的模板,大部分操作直接調用成員函數就可以了。
1 #include <vector>
2 using namespace std; 3
4 namespace Scapegoat_Tree { 5 #define MAXN (100000 + 10)
6 const double alpha = 0.75; 7 struct Node { 8 Node * ch[2]; 9 int key, size, cover; // size為有效節點的數量,cover為節點總數量
10 bool exist; // 是否存在(即是否被刪除)
11 void PushUp(void) { 12 size = ch[0]->size + ch[1]->size + (int)exist; 13 cover = ch[0]->cover + ch[1]->cover + 1; 14 } 15 bool isBad(void) { // 判斷是否需要重構
16 return ((ch[0]->cover > cover * alpha + 5) ||
17 (ch[1]->cover > cover * alpha + 5)); 18 } 19 }; 20 struct STree { 21 protected: 22 Node mem_poor[MAXN]; //內存池,直接分配好避免動態分配內存占用時間
23 Node *tail, *root, *null; // 用null表示NULL的指針更方便,tail為內存分配指針,root為根
24 Node *bc[MAXN]; int bc_top; // 儲存被刪除的節點的內存地址,分配時可以再利用這些地址
25
26 Node * NewNode(int key) { 27 Node * p = bc_top ? bc[--bc_top] : tail++; 28 p->ch[0] = p->ch[1] = null; 29 p->size = p->cover = 1; p->exist = true; 30 p->key = key; 31 return p; 32 } 33 void Travel(Node * p, vector<Node *>&v) { 34 if (p == null) return; 35 Travel(p->ch[0], v); 36 if (p->exist) v.push_back(p); // 構建序列
37 else bc[bc_top++] = p; // 回收
38 Travel(p->ch[1], v); 39 } 40 Node * Divide(vector<Node *>&v, int l, int r) { 41 if (l >= r) return null; 42 int mid = (l + r) >> 1; 43 Node * p = v[mid]; 44 p->ch[0] = Divide(v, l, mid); 45 p->ch[1] = Divide(v, mid + 1, r); 46 p->PushUp(); // 自底向上維護,先維護子樹
47 return p; 48 } 49 void Rebuild(Node * &p) { 50 static vector<Node *>v; v.clear(); 51 Travel(p, v); p = Divide(v, 0, v.size()); 52 } 53 Node ** Insert(Node *&p, int val) { 54 if (p == null) { 55 p = NewNode(val); 56 return &null; 57 } 58 else { 59 p->size++; p->cover++; 60
61 // 返回值儲存需要重構的位置,若子樹也需要重構,本節點開始也需要重構,以本節點為根重構
62 Node ** res = Insert(p->ch[val >= p->key], val); 63 if (p->isBad()) res = &p; 64 return res; 65 } 66 } 67 void Erase(Node *p, int id) { 68 p->size--; 69 int offset = p->ch[0]->size + p->exist; 70 if (p->exist && id == offset) { 71 p->exist = false; 72 return; 73 } 74 else { 75 if (id <= offset) Erase(p->ch[0], id); 76 else Erase(p->ch[1], id - offset); 77 } 78 } 79 public: 80 void Init(void) { 81 tail = mem_poor; 82 null = tail++; 83 null->ch[0] = null->ch[1] = null; 84 null->cover = null->size = null->key = 0; 85 root = null; bc_top = 0; 86 } 87 STree(void) { Init(); } 88
89 void Insert(int val) { 90 Node ** p = Insert(root, val); 91 if (*p != null) Rebuild(*p); 92 } 93 int Rank(int val) { 94 Node * now = root; 95 int ans = 1; 96 while (now != null) { // 非遞歸求排名
97 if (now->key >= val) now = now->ch[0]; 98 else { 99 ans += now->ch[0]->size + now->exist; 100 now = now->ch[1]; 101 } 102 } 103 return ans; 104 } 105 int Kth(int k) { 106 Node * now = root; 107 while (now != null) { // 非遞歸求第K大
108 if (now->ch[0]->size + 1 == k && now->exist) return now->key; 109 else if (now->ch[0]->size >= k) now = now->ch[0]; 110 else k -= now->ch[0]->size + now->exist, now = now->ch[1]; 111 } 112 } 113 void Erase(int k) { 114 Erase(root, Rank(k)); 115 if (root->size < alpha * root->cover) Rebuild(root); 116 } 117 void Erase_kth(int k) { 118 Erase(root, k); 119 if (root->size < alpha * root->cover) Rebuild(root); 120 } 121 }; 122 #undef MAXN
123
124 }
小小的封裝了一下。
如果對封裝不習慣的,這里有一個為封裝的:https://www.luogu.org/record/show?rid=14045715
0x04 例題
來看一道例題:P3369
您需要寫一種數據結構(可參考題目標題),來維護一些數,其中需要提供以下操作:
- 插入x數2. 刪除x數(若有多個相同的數,因只刪除一個)
- 查詢x數的排名(若有多個相同的數,因輸出最小的排名)
- 查詢排名為x的數
- 求x的前驅(前驅定義為小於x,且最大的數)
- 求x的后繼(后繼定義為大於x,且最小的數)
Input
第一行為
,表示操作的個數,下面n行每行有兩個數
和
,
表示操作的序號(
)。
Output
對於操作
每行輸出一個數,表示對應答案。
Sample Input
10 1 106465 4 1 1 317721 1 460929 1 644985 1 84185 1 89851 6 81968 1 492737 5 493598
Sample Output
106465 84185 492737
0x05 題解
模板題,套用上面的就可以了。
1 /************************************************************** 2 Problem: 3224 3 User: SillyVector 4 Language: C++ 5 Result: Accepted 6 Time:200 ms 7 Memory:4112 kb 8 ****************************************************************/
9
10 #include <iostream>
11 #include <cstdio>
12 #include <cstring>
13 #include <vector>
14 using namespace std; 15
16 /*
17 Template 18 */
19
20 #define INLINE __attribute__((optimize("O3"))) inline
21 INLINE char NC(void) 22 { 23 static char buf[100000], *p1 = buf, *p2 = buf; 24 if (p1 == p2) { 25 p2 = (p1 = buf) + fread(buf, 1, 100000, stdin); 26 if (p1 == p2) return EOF; 27 } 28 return *p1++; 29 } 30 INLINE void read(int &x) { 31 static char c; c = NC(); int b = 1; 32 for (x = 0; !(c >= '0' && c <= '9'); c = NC()) if(c == '-') b = -b; 33 for (; c >= '0' && c <= '9'; x = x * 10 + c - '0', c = NC()); x *= b; 34 } 35 using namespace Scapegoat_Tree; 36
37 STree _t; 38 int n, k, m; 39 int main(void) { 40 //freopen("in.txt", "r", stdin); 41 //freopen("out.txt", "w", stdout);
42 read(n); 43 while (n--) { 44 read(k), read(m); 45 switch (k) { 46 case 1: _t.Insert(m); break; 47 case 2: _t.Erase(m); break; 48 case 3: printf("%d\n", _t.Rank(m)); break; 49 case 4: printf("%d\n", _t.Kth(m)); break; 50 case 5: printf("%d\n", _t.Kth(_t.Rank(m) - 1)); break; 51 case 6: printf("%d\n", _t.Kth(_t.Rank(m + 1))); break; 52 } 53 /* DEBUG INFO 54 vector<Node *> xx; 55 _t.Travel(_t.root, xx); 56 cout << "::"; 57 for(int i = 0; i < xx.size(); i++) cout << xx[i]->key << ' '; cout << endl; 58 */
59 } 60 return 0; 61
62 }
,速度我已經很滿意了。
再放一道POJ例題:1442 -- Black Box 有興趣可以試試。
