識替罪羊樹之算法乃吾生之幸也!

0x00 扯淡

知乎上面有個問題問最優雅的算法是什么,我覺得暴力即是優雅

 

當然這里說的暴力並不是指那種不加以思考的無腦的暴力,而是說用繁瑣而技巧性的工作可以實現的事,我用看似簡單的思想和方法,也可以達到近似於前者的空間復雜度和時間復雜度,甚至可以更優,而其中也或多或少的夾雜着一些"LESS IS MORE"的思想在其中。

 

以下文章需要對普通二叉搜索樹Treap樹(可選)有一定的了解,可以自行百度也可以等我出的一篇有關這個的文章。

0x01 替罪羊樹[Scapegoat Tree]

對於一棵二叉搜索樹,最重要的事情就是維護他的平衡,以保證對於每次操作(插入,查找,刪除)的時間均攤下來都是O(logN)乃至O(lgN)紅黑樹,但是常數大而且難寫,此處不展開介紹)。

 

為了維護樹的平衡,各種平衡二叉樹絞盡腦汁方法五花八門,但幾乎都是通過旋轉的操作來實現(AVL 樹紅黑樹Treap 樹(經@GadyPu正,可持久化Treap樹不需要旋轉) Splay…),只不過是在判斷什么時候應該旋轉上有所不同。但替罪羊樹就是那么一棵特立獨行的豬,哦不,是一只特立獨行的樹。

0x02 各種嘿嘿嘿的操作

  • 重構

重構允許重構整棵替罪羊樹,也允許重構替罪羊樹其中的一棵子樹。

重構這個操作看似高端,實則十分暴力(真)。主要操作就是把需要重構的子樹拍平(由於子樹一定是二叉搜索樹,所以拍平之后的序列一定也是有序的),然后拎起序列的中點,作為根部,剩下的左半邊序列為左子樹,右半邊序列為右子樹,接着遞歸對左邊和右邊進行同樣的操作,直到最后形成的樹中包含的全部為點而不是序列(這樣形成的一定是一棵完全二叉搜索樹,也是最優的方案)。

 

這是一棵需要維護的子樹,雖然目前不知道基於什么判斷條件,但這棵是明顯需要維護的。
O(n)拍平之后的結果,直接遍歷即可。 子樹的重構就完成了。

 

  • 插入
插入操作一開始和普通的二叉搜索樹無異,但在插入操作結束以后,從插入位置開始一層一層往上回溯的時候,對於每一層都進行一次判斷h(v) > log(1/\alpha )(size(tree)),一直找到最后一層不滿足該條件的層序號(也就是從根開始的第一層不滿足該條件的層序號),然后從該層開始重構以該層為根的子樹一個節點導致樹的不平衡,就要導致整棵子樹被拍扁,估計這也是“替罪羊”這個名字的由來吧

 

每次插入操作的復雜度為O(log_{n}),每次重構樹的復雜度為O(n),但由於不會每次都要進行重構,也不會每次都重構一整棵樹,所以均攤下來的復雜度還是O(log_{n})

\alpha 在這里是一個常數,可以通過調整\alpha 的大小來控制樹的平衡度,使程序具有很好的可控性

-------------2016/5/30日更新-------------

 

為了測試\alpha 值的選取對於程序性能的影響,枚舉了(0.5,1)這個區間內\alpha 的值,性能繪制成圖標如下(數據采用BZOJ 6,7,8三組數據的3倍)

 

(測試結果如上)

 

由此可見,(0.5,1)區間內\alpha 的取值對於程序性能並沒有很大的影響,當然也有可能是我測試方法不當,

-------------2016/6/1日更新-------------

 

@dashgua 把測試數據進行了更改,全部改為1000000個節點按次序插入和逆序刪除。

 

(測試結果如上)

對於取值越靠近兩端的確速度越慢,但中間貌似還是沒有什么差異。如果有好的數據構造方法希望能提出,一定會再次嘗試,謝謝。


  • 刪除(惰性刪除)
我覺得刪除操作是替罪羊樹中最好玩的地方,替罪羊樹的刪除節點並不是真正的刪除,而是惰性刪除(即給節點增加一個已經刪除的標記,刪除后的節點與普通節點無異,只是不參與查找操作而已)。當刪除的數量超過樹的節點數的一半時,直接重構!(屌絲和暴力屬性MAX),可以證明均攤下來的復雜度還是O(log_{n})(作者太傻證明不來)。
 
  • 查找第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

您需要寫一種數據結構(可參考題目標題),來維護一些數,其中需要提供以下操作:

  1. 插入x數2. 刪除x數(若有多個相同的數,因只刪除一個)
  2. 查詢x數的排名(若有多個相同的數,因輸出最小的排名)
  3. 查詢排名為x的數
  4. 求x的前驅(前驅定義為小於x,且最大的數)
  5. 求x的后繼(后繼定義為大於x,且最小的數)

Input

第一行為n\leq 100000,表示操作的個數,下面n行每行有兩個數optxopt表示操作的序號(1\leq opt\leq 6)。

Output

對於操作\left\{ 3,4,5,6 \right\}每行輸出一個數,表示對應答案。

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 }

 

200ms,速度我已經很滿意了。

 再放一道POJ例題:1442 -- Black Box 有興趣可以試試。

轉載於:https://zhuanlan.zhihu.com/p/21263304