扯
學校清明竟然給放兩天假期,心血來潮突然想去學習平衡樹。
可是我太弱了學不會有旋轉操作的treap和splay,這可怎么辦啊qaq。
誒?我以前好像看過一種叫做替罪羊樹的平衡樹可以不用旋轉操作,那還是學這個吧……
替罪羊樹
用處
替罪羊樹是一種平衡樹,支持插入,刪除,查找第k小元素,查找元素的排名等操作
什么數據結構優雅?暴力即是優雅!
替罪羊樹就是一種暴力平衡樹,旋轉?不存在的!
替罪羊樹保持平衡的方法就是暴力重構,即當樹不平衡時拍扁重新建樹,那么如何才能知道一棵樹是否平衡呢?
在替罪羊樹中選用了一種叫做平衡因子的東西,聽起來很高端,其實就是一個0.5~1之間的任意浮點數,保持平衡的方法是這樣的:
如果一棵樹的左子樹/右子樹的存在的節點數量大於這棵樹的存在的節點數量*旋轉因子,那么就要重構這棵樹
為什么我特意標出了是存在的節點數呢?是因為替罪羊樹的刪除不是真正的刪除,而是惰性刪除。
所以我們就可以寫出代表替罪羊樹的每個節點的結構體
1 const double alpha = 0.75; //旋轉因子 2 struct Node { 3 Node* ch[2]; //左右子節點 4 int key,siz,cover; //key是值,siz是以該節點為根的樹的存在的節點數,cover是所有節點數量 5 bool exist; //exist標志該節點是否被刪除 6 void pushup() { //更新函數 7 this->siz=ch[0]->siz+ch[1]->siz+(int)exist; 8 this->cover=ch[0]->cover+ch[1]->cover+1; 9 } 10 int isbad() { //判斷是否要重構 11 return (ch[0]->cover>this->cover*alpha+5)||(ch[1]->cover>this->cover*alpha+5); 12 } 13 };
(這種東西還是很好理解的吧,幾乎所有平衡樹都要寫這種東西)
內部操作
內部操作指實現操作的函數需要調用的子函數,這些函數不會在主程序中調用
總的來說就只有四種內部操作:新節點,插入,刪除,重構
新節點有什么可講的?不就是一個new Node就行的事嗎?
不不不,動態分配內存的速度可以說是非常慢了,所以我們要手寫內存池。
1 Node mempol[maxn]; //內存池 2 Node *tail; //tail為指向內存池元素的指針 3 Node *bc[maxn]; //內存回收池(棧) 4 int bc_top; //內存回收池(棧)頂指針
分配機制很簡單,如果*bc為空,那么從mempol里獲得一個節點的內存,當刪除節點時,把節點存入*bc,需要時再從*bc中取出即可。
另外還有兩個不可缺少的東西,根節點和null節點,為Node*類型。
(null是為了代替NULL的,看到錯誤就像砸電腦,所以就自己模擬一個空節點)
有了這些東西,新節點操作也就不難寫出:
1 Node* newnode(int key) { //返回一個新節點 2 Node* p=bc_top?bc[--bc_top]:tail++; //分配內存 3 p->ch[0]=p->ch[1]=null; 4 p->cover=p->siz=p->exist=1; 5 p->key=key; 6 return p; 7 }
接下來就是插入操作了,插入操作和二叉搜索樹是一樣的,如果當前節點小於等於要插入的節點權值,那么遞歸操作左子樹,反之遞歸操作右子樹
同時維護siz和cover
1 Node** insert(Node*& p,int val) { //返回指向距離根節點最近的一棵不平衡的子樹的指針 2 if(p==null) { 3 p=newnode(val); 4 return &null; 5 } else { 6 p->siz++,p->cover++; //維護節點數 7 Node** res=insert(p->ch[val>=p->key],val); 8 if(p->isbad()) res=&p; 9 return res; 10 } 11 }
這里插入也是有返回值的,返回的是指向距離根節點最近的一棵不平衡的子樹的指針,因為我們定義的樹是指針實現的,所以這個地方要用到指向指針的指針,即雙重指針。那么返回這么個拗口的東西又有什么用呢?前面有說過重構平衡樹的方法,找到距離根節點最近的不平衡的子樹就是為了能夠一遍將樹重構成平衡樹,而不用多遍。
刪除比較簡單,因為不用像其他樹一樣真正的刪除然后旋轉,替罪羊樹的刪除操作只用將exist置為false然后更新siz值即可
還有一點就是這里的刪除操作並不是刪除值為k的節點,而是刪除第k小的節點。
求第k小的節點要用到siz,根據二叉搜索樹的性質,我們可以知道一個節點的左子樹的值都要小於這個節點,所以當k小於左子樹的節點時遞歸操作左子樹,反之遞歸操作右子樹,同時減去左子樹的節點數即可
1 void erase(Node*& p,int k) { 2 p->siz--; //維護siz 3 int offset=p->ch[0]->siz+p->exist; //計算左子樹的存在的節點總數 4 if(p->exist&&k==offset) { //判斷當前節點權值是否第k小 5 p->exist=false; //刪除節點 6 } else { 7 if(k<=offset) erase(p->ch[0],k); //如果k小於等於offset,遞歸操作左子樹 8 else erase(p->ch[1],k-offset); //反之遞歸操作右子樹,不要忘記將k減去offset 9 } 10 }
插入和刪除都說完了,接下來是有些難度的重構操作。
重構需要用到三個函數,第一個是將樹轉化成有序序列的函數,第二個是建樹的函數,而第三個就是將前兩個函數連接在一起的函數
轉化成序列的函數是怎么做到有序的呢?別忘了替罪羊樹也是一顆二叉搜索樹,所以我們可以跑一邊中序遍歷,把結果存入vector中,序列就一定有序
前面說過的刪除標記exist在這里起到了作用,把樹拍扁的同時判斷節點是否存在,如果不存在直接扔到內存回收池中就好
所以說替罪羊樹的刪除操作只在重構操作中執行,erase函數只是惰性刪除而已
1 void travel(Node* p,vector<Node*>& x) { //將一棵樹轉化成序列,保存在vector中 2 if(p==null) return; //如果是空樹則退出 3 travel(p->ch[0],x); //遞歸操作左子樹 4 if(p->exist) x.push_back(p); //如果該節點存在則放入序列中 5 else bc[bc_top++]=p; //回收內存,將不用的節點扔到內存回收池(棧)中 6 travel(p->ch[1],x); //遞歸操作右子樹 7 }
建樹函數就不用說啦,這個是基本操作吧,取序列的中間值作為樹的根,然后遞歸操作左子樹和右子樹即可,顯然能保證平衡
1 Node* divide(vector<Node*>& x,int l,int r) { //返回建好的樹 2 if(l>=r) return null; //序列為空不用建樹 3 int mid=(l+r)>>1; 4 Node* p=x[mid]; //mid保證平衡 5 p->ch[0]=divide(x,l,mid); //遞歸操作 6 p->ch[1]=divide(x,mid+1,r); //遞歸操作 7 p->pushup(); //維護節點信息 8 return p; 9 }
接下來用一個函數把前兩個函數連接,就是一個完整的重構函數了
1 void rebuild(Node*& p) { 2 static vector<Node*> v; 3 v.clear(); 4 travel(p,v); //拍扁 5 p=divide(v,0,v.size()); //建樹 6 }
至此所有內部函數已經全部實現,替罪羊樹的全部操作函數可以由以上函數完成。
操作函數
哈哈,想不到吧,接下來才是實現替罪羊樹操作的真正函數
有了上面的內部函數,操作函數的實現就顯得非常簡單
初始化
首先是初始化函數,其實沒有什么可以初始化的,只是定義了一下空節點和根節點而已
把初始化函數放到構造函數中可以自動調用
1 void init() { 2 tail=mempol; //tail指向內存池的第一個元素 3 null=tail++; //為null指針分配內存 4 null->ch[0]=null->ch[1]=null; //null的兩個兒子也是null 5 null->cover=null->siz=null->key=0; //null的所有標記都是0 6 root=null; //初始化根節點 7 bc_top=0; //清空棧 8 } 9 10 STree() { 11 init(); 12 }
插入
插入函數的實現只有兩行,調用內部插入函數,然后判斷是否需要重建,如果需要調用重建函數即可
1 void insert(int val) { 2 Node** res=insert(root,val); 3 if(*res!=null) rebuild(*res); 4 }
求值為val的節點的名次
和內部刪除的算法差不多,但是因為只是求rank而不是刪除,所以沒必要用遞歸函數,用循環實現即可
1 int rank(int val) { 2 Node* now=root; 3 int ans=1; 4 while(now!=null) { 5 if(now->key>=val) now=now->ch[0]; 6 else { 7 ans+=now->ch[0]->siz+now->exist; 8 now=now->ch[1]; 9 } 10 } 11 return ans; 12 }
求第k小元素
由判斷值變成判斷名次,根據siz判斷即可,思想和求rank差不多,用循環可以解決
1 int kth(int val) { 2 Node* now=root; 3 while(now!=null) { 4 if(now->ch[0]->siz+1==val&&now->exist) return now->key; 5 else if(now->ch[0]->siz>=val) now=now->ch[0]; 6 else val-=now->ch[0]->siz+now->exist,now=now->ch[1]; 7 } 8 }
刪除
刪除操作分兩種,刪除第k小和刪除值為val的元素,但不是只刪除就行的。
為了不讓無用的節點過多,我們可以在有用節點與全部節點的比值小於平衡因子的的情況時重構整棵樹,以提高效率。
1 void erase(int k) { //刪除值為k的元素 2 erase(root,rank(k)); 3 if(root->siz<root->cover*alpha) rebuild(root); 4 } 5 6 void erase_kth(int k) { //刪除第k小 7 erase(root,k); 8 if(root->siz<root->cover*alpha) rebuild(root); 9 }
替罪羊樹的全部操作到此已經結束,其他操作就可以根據題目自己yy了。
模板
板子題:洛谷 P3369 【模板】普通平衡樹(Treap/SBT)
其實把上面所有代碼連成一串就是板子了qaq
代碼:
1 #include <cstdio> 2 #include <vector> 3 using std::vector; 4 5 namespace Scapegoat_Tree { 6 const int maxn = 100000 + 10; 7 const double alpha = 0.75; //旋轉因子 8 struct Node { 9 Node* ch[2]; //左右子節點 10 int key,siz,cover; //key是值,siz是以該節點為根的樹的存在的節點數,cover是所有節點數量 11 bool exist; //exist標志該節點是否被刪除 12 void pushup() { //更新函數 13 this->siz=ch[0]->siz+ch[1]->siz+(int)exist; 14 this->cover=ch[0]->cover+ch[1]->cover+1; 15 } 16 int isbad() { //判斷是否要重構 17 return (ch[0]->cover>this->cover*alpha+5)||(ch[1]->cover>this->cover*alpha+5); 18 } 19 }; 20 struct STree { 21 protected: 22 Node mempol[maxn]; //內存池 23 Node *tail,*null,*root; //tail為指向內存池元素的指針 24 Node *bc[maxn]; //內存回收池(棧) 25 int bc_top; //內存回收池(棧)頂指針 26 27 Node* newnode(int key) { 28 Node* p=bc_top?bc[--bc_top]:tail++; 29 p->ch[0]=p->ch[1]=null; 30 p->cover=p->siz=p->exist=1; 31 p->key=key; 32 return p; 33 } 34 35 void travel(Node* p,vector<Node*>& x) { //將一棵樹轉化成序列,保存在vector中 36 if(p==null) return; //如果是空樹則退出 37 travel(p->ch[0],x); //遞歸操作左子樹 38 if(p->exist) x.push_back(p); //如果該節點存在則放入序列中 39 else bc[bc_top++]=p; //回收內存,將不用的節點扔到內存回收池(棧)中 40 travel(p->ch[1],x); //遞歸操作右子樹 41 } 42 43 Node* divide(vector<Node*>& x,int l,int r) { //返回建好的樹 44 if(l>=r) return null; //序列為空不用建樹 45 int mid=(l+r)>>1; 46 Node* p=x[mid]; //mid保證平衡 47 p->ch[0]=divide(x,l,mid); //遞歸操作 48 p->ch[1]=divide(x,mid+1,r); //遞歸操作 49 p->pushup(); //維護節點信息 50 return p; 51 } 52 53 void rebuild(Node*& p) { 54 static vector<Node*> v; 55 v.clear(); 56 travel(p,v); //拍扁 57 p=divide(v,0,v.size()); //建樹 58 } 59 60 Node** insert(Node*& p,int val) { //返回指向距離根節點最近的一棵不平衡的子樹的指針 61 if(p==null) { 62 p=newnode(val); 63 return &null; 64 } else { 65 p->siz++,p->cover++; //維護節點數 66 Node** res=insert(p->ch[val>=p->key],val); 67 if(p->isbad()) res=&p; 68 return res; 69 } 70 } 71 72 void erase(Node*& p,int k) { 73 p->siz--; //維護siz 74 int offset=p->ch[0]->siz+p->exist; //計算左子樹的存在的節點總數 75 if(p->exist&&k==offset) { //判斷當前節點權值是否第k小 76 p->exist=false; //刪除節點 77 } else { 78 if(k<=offset) erase(p->ch[0],k); //如果k小於等於offset,遞歸操作左子樹 79 else erase(p->ch[1],k-offset); //反之遞歸操作右子樹 80 } 81 } 82 83 void iod(Node* p) { 84 if(p!=null) { 85 iod(p->ch[0]); 86 printf("%d ",p->key); 87 iod(p->ch[1]); 88 } 89 } 90 public: 91 void init() { 92 tail=mempol; //tail指向內存池的第一個元素 93 null=tail++; //為null指針分配內存 94 null->ch[0]=null->ch[1]=null; //null的兩個兒子也是null 95 null->cover=null->siz=null->key=0; //null的所有標記都是0 96 root=null; //初始化根節點 97 bc_top=0; //清空棧 98 } 99 100 STree() { 101 init(); 102 } 103 104 void insert(int val) { 105 Node** res=insert(root,val); 106 if(*res!=null) rebuild(*res); 107 } 108 109 int rank(int val) { 110 Node* now=root; 111 int ans=1; 112 while(now!=null) { 113 if(now->key>=val) now=now->ch[0]; 114 else { 115 ans+=now->ch[0]->siz+now->exist; 116 now=now->ch[1]; 117 } 118 } 119 return ans; 120 } 121 122 int kth(int val) { 123 Node* now=root; 124 while(now!=null) { 125 if(now->ch[0]->siz+1==val&&now->exist) return now->key; 126 else if(now->ch[0]->siz>=val) now=now->ch[0]; 127 else val-=now->ch[0]->siz+now->exist,now=now->ch[1]; 128 } 129 } 130 131 void erase(int k) { //刪除值為k的元素 132 erase(root,rank(k)); 133 if(root->siz<root->cover*alpha) rebuild(root); 134 } 135 136 void erase_kth(int k) { //刪除第k小 137 erase(root,k); 138 if(root->siz<root->cover*alpha) rebuild(root); 139 } 140 141 void iod() { //調試用的中序遍歷 142 Node* p=root; 143 iod(p); 144 } 145 }; 146 } 147 148 using namespace Scapegoat_Tree; 149 STree st; 150 int main(int argc,char** argv) { 151 int n,opt,que; 152 scanf("%d",&n); 153 while(n--) { 154 scanf("%d%d",&opt,&que); 155 if(opt==1) st.insert(que); 156 if(opt==2) st.erase(que); 157 if(opt==3) printf("%d\n",st.rank(que)); 158 if(opt==4) printf("%d\n",st.kth(que)); 159 if(opt==5) printf("%d\n",st.kth(st.rank(que)-1)); 160 if(opt==6) printf("%d\n",st.kth(st.rank(que+1))); 161 if(opt==7) st.iod(); 162 } 163 return 0; 164 }
--完--
