替罪羊樹作為平衡樹家族里比較簡單的一員,效率還是很不錯的
只要不是維護序列之類的需要提取子樹進行操作的問題,選擇高效率的重量平衡樹是無可非議的
我們可以用一個標准:需不需要采用旋轉操作來對重量平衡樹進行一個簡單的分類:
沒有采用旋轉機制的有:跳表和替罪羊樹
采用旋轉機制的有:Treap
所有采用旋轉機制的平衡樹都有這么一個弊端:在平衡樹的每個節點上維護一個集合,來存儲子樹內部所有的數,此時單次旋轉操作可能有O(n)的時間復雜度
那么什么是重量平衡樹?如果你把勢能的概念引入到平衡樹的節點上面去,就比較容易理解了
在這種情況下,完全二叉樹的勢能是最低的,對於那些勢能高的子樹,我們或旋轉或拍平重構來使它接近甚至成為完全二叉樹結構,這應該就是重量平衡樹的本質了(其實吧,平衡樹的話,應該都是這樣的)
然后開始介紹正題:替罪羊樹的平衡機理:
對於某個0.5<=alpha<=1滿足size(lch(x))<=alpha*size(x)並且size(rch(x))<=alpha*size(x),即這個節點的兩棵子樹的size都不超過以該節點為根的子樹的size,那么就稱這個子樹(或節點)是平衡的
然后如果不平衡的話,直接拍平之后重構為完全二叉樹就好了
然后開始說代碼,我的數據結構的代碼風格,總體來說還是比較凌亂的,后期一定會修整的,一定會修整的。
const int INF=1000000000; const int maxn=2000005; const double al=0.75; int n; struct Tree { int fa; int size; int num; int ch[2]; }t[maxn]; int cnt; int root; int node[maxn]; int sum;
在這里al就是平衡因子,size是該子樹包含的節點個數,num是值(Splay中我用的是v),cnt記錄節點個數,node是個存點坐標的臨時數組,其下標用sum來指代
然后再來說一說建樹操作,一般我們的建樹操作是指給定一個裝滿了數的數組,然后調用建樹函數,以這個數組為依托遞歸建樹,就像這個函數:
int build(int l,int r) { if(l>r) return 0; int mid=(l+r)/2; int x=node[mid]; t[t[x].ch[0]=build(l,mid-1)].fa=x; t[t[x].ch[1]=build(mid+1,r)].fa=x; t[x].size=t[t[x].ch[0]].size+t[t[x].ch[1]].size+1; return x; }
但其實為了方便,我們大可直接一個一個數往數據結構里面插就好了
這種操作在替罪羊樹中只用於在重構子樹的時候臨時存一下子樹拍平之后的那些點,我們引出插入函數:
void insert(int x) { int o=root; int cur=++cnt; t[cur].size=1; t[cur].num=x; while(1) { t[o].size++; bool son=(x>=t[o].num); if(t[o].ch[son]) o=t[o].ch[son]; else { t[t[o].ch[son]=cur].fa=o; break; } } int flag=0; for(int i=cur;i;i=t[i].fa) if(!balance(i)) flag=i; if(flag) rebuild(flag); }
可以看到如果插入之后不平衡了,就要完成重構了,重構子樹為完全二叉樹
這里給出判斷是否需要重構的函數,就是本文開頭給出的那個公式
bool balance(int x) { return (double)t[x].size*al>=(double)t[t[x].ch[0]].size &&(double)t[x].size*al>=(double)t[t[x].ch[1]].size; }
如果真的需要重構的話,就調用重構函數進行重構,重構操作是拍成鏈然后重建子樹,之后還接回去
void rebuild(int x) { sum=0; recycle(x); int fa=t[x].fa; int son=(t[t[x].fa].ch[1]==x); int cur=build(1,sum); t[t[fa].ch[son]=cur].fa=fa; if(x==root) root=cur; }
這里給出拍成鏈的函數,這里就用到剛才說的那個node和sum了
void recycle(int x) { if(t[x].ch[0]) recycle(t[x].ch[0]); node[++sum]=x; if(t[x].ch[1]) recycle(t[x].ch[1]); }
插入的問題說完了,然后說說刪除,其實替罪羊這個名字就是因為這個刪除操作而來的
void erase(int x) { if(t[x].ch[0]&&t[x].ch[1]) { int cur=t[x].ch[0]; while(t[cur].ch[1]) cur=t[cur].ch[1]; t[x].num=t[cur].num; x=cur; } int son=(t[x].ch[0])?t[x].ch[0]:t[x].ch[1]; int k=(t[t[x].fa].ch[1]==x); t[t[t[x].fa].ch[k]=son].fa=t[x].fa; for(int i=t[x].fa;i;i=t[i].fa) t[i].size--; if(x==root) root=son; }
替罪羊樹中的刪除操作,就是用被刪除節點的左子樹的最后一個節點或者右子樹的第一個節點來頂替被刪除節點的位置
查詢操作的話,具備一般平衡樹的一些基本功能:
查詢x數的排名
查詢排名為x的數(這個Splay里面寫了,剩下兩個都沒有寫,以后再完善吧)
求x的前驅和后繼
int get_rank(int x) { int o=root,ans=0; while(o) { if(t[o].num<x) ans+=t[t[o].ch[0]].size+1,o=t[o].ch[1]; else o=t[o].ch[0]; } return ans; } int get_kth(int x) { int o=root; while(1) { if(t[t[o].ch[0]].size==x-1) return o; else if(t[t[o].ch[0]].size>=x) o=t[o].ch[0]; else x-=t[t[o].ch[0]].size+1,o=t[o].ch[1]; } return o; } int get_front(int x) { int o=root,ans=-INF; while(o) { if(t[o].num<x) ans=max(ans,t[o].num),o=t[o].ch[1]; else o=t[o].ch[0]; } return ans; } int get_behind(int x) { int o=root,ans=INF; while(o) { if(t[o].num>x) ans=min(ans,t[o].num),o=t[o].ch[0]; else o=t[o].ch[1]; } return ans; }
其實查詢操作對各個樹而言大同小異,只不過,我目前知道的,Splay干什么都要splay到根節點一下子
最后,我們給出替罪羊樹的完整實現:
1 #include<iostream> 2 #include<algorithm> 3 using namespace std; 4 const int INF=1000000000; 5 const int maxn=2000005; 6 const double al=0.75; 7 int n; 8 struct Tree 9 { 10 int fa; 11 int size; 12 int num; 13 int ch[2]; 14 }t[maxn]; 15 int cnt; 16 int root; 17 int node[maxn]; 18 int sum; 19 bool balance(int x) 20 { 21 return (double)t[x].size*al>=(double)t[t[x].ch[0]].size 22 &&(double)t[x].size*al>=(double)t[t[x].ch[1]].size; 23 } 24 void recycle(int x) 25 { 26 if(t[x].ch[0]) 27 recycle(t[x].ch[0]); 28 node[++sum]=x; 29 if(t[x].ch[1]) 30 recycle(t[x].ch[1]); 31 } 32 int build(int l,int r) 33 { 34 if(l>r) 35 return 0; 36 int mid=(l+r)/2; 37 int x=node[mid]; 38 t[t[x].ch[0]=build(l,mid-1)].fa=x; 39 t[t[x].ch[1]=build(mid+1,r)].fa=x; 40 t[x].size=t[t[x].ch[0]].size+t[t[x].ch[1]].size+1; 41 return x; 42 } 43 void rebuild(int x) 44 { 45 sum=0; 46 recycle(x); 47 int fa=t[x].fa; 48 int son=(t[t[x].fa].ch[1]==x); 49 int cur=build(1,sum); 50 t[t[fa].ch[son]=cur].fa=fa; 51 if(x==root) 52 root=cur; 53 } 54 void insert(int x) 55 { 56 int o=root; 57 int cur=++cnt; 58 t[cur].size=1; 59 t[cur].num=x; 60 while(1) 61 { 62 t[o].size++; 63 bool son=(x>=t[o].num); 64 if(t[o].ch[son]) 65 o=t[o].ch[son]; 66 else 67 { 68 t[t[o].ch[son]=cur].fa=o; 69 break; 70 } 71 } 72 int flag=0; 73 for(int i=cur;i;i=t[i].fa) 74 if(!balance(i)) 75 flag=i; 76 if(flag) 77 rebuild(flag); 78 } 79 int get_num(int x) 80 { 81 int o=root; 82 while(1) 83 { 84 if(t[o].num==x) 85 return o; 86 else 87 o=t[o].ch[t[o].num<x]; 88 } 89 } 90 void erase(int x) 91 { 92 if(t[x].ch[0]&&t[x].ch[1]) 93 { 94 int cur=t[x].ch[0]; 95 while(t[cur].ch[1]) 96 cur=t[cur].ch[1]; 97 t[x].num=t[cur].num; 98 x=cur; 99 } 100 int son=(t[x].ch[0])?t[x].ch[0]:t[x].ch[1]; 101 int k=(t[t[x].fa].ch[1]==x); 102 t[t[t[x].fa].ch[k]=son].fa=t[x].fa; 103 for(int i=t[x].fa;i;i=t[i].fa) 104 t[i].size--; 105 if(x==root) 106 root=son; 107 } 108 int get_rank(int x) 109 { 110 int o=root,ans=0; 111 while(o) 112 { 113 if(t[o].num<x) 114 ans+=t[t[o].ch[0]].size+1,o=t[o].ch[1]; 115 else 116 o=t[o].ch[0]; 117 } 118 return ans; 119 } 120 int get_kth(int x) 121 { 122 int o=root; 123 while(1) 124 { 125 if(t[t[o].ch[0]].size==x-1) 126 return o; 127 else if(t[t[o].ch[0]].size>=x) 128 o=t[o].ch[0]; 129 else 130 x-=t[t[o].ch[0]].size+1,o=t[o].ch[1]; 131 } 132 return o; 133 } 134 int get_front(int x) 135 { 136 int o=root,ans=-INF; 137 while(o) 138 { 139 if(t[o].num<x) 140 ans=max(ans,t[o].num),o=t[o].ch[1]; 141 else 142 o=t[o].ch[0]; 143 } 144 return ans; 145 } 146 int get_behind(int x) 147 { 148 int o=root,ans=INF; 149 while(o) 150 { 151 if(t[o].num>x) 152 ans=min(ans,t[o].num),o=t[o].ch[0]; 153 else 154 o=t[o].ch[1]; 155 } 156 return ans; 157 } 158 int main() 159 { 160 cnt=2; 161 root=1; 162 t[1].num=-INF,t[1].size=2,t[1].ch[1]=2; 163 t[2].num=INF,t[2].size=1,t[2].fa=1; 164 cin>>n; 165 int tmp,x; 166 for(int i=1;i<=n;i++) 167 { 168 cin>>tmp>>x; 169 if(tmp==1) 170 insert(x); 171 if(tmp==2) 172 erase(get_num(x)); 173 if(tmp==3) 174 cout<<get_rank(x)<<endl; 175 if(tmp==4) 176 cout<<t[get_kth(x+1)].num<<endl; 177 if(tmp==5) 178 cout<<get_front(x)<<endl; 179 if(tmp==6) 180 cout<<get_behind(x)<<endl; 181 } 182 }
替罪羊樹與同類的平衡樹相比,對查詢操作具有極大的優勢(重量平衡樹的特性,當然對於那些維護序列的操作,重量平衡樹沒有用武之地)
而且實驗證明在某種程度上是最快的?其實應該是因題而異
那么這個樹的真實應用是啥呢?
替罪羊樹可以優化無法旋轉的樹形結構的時間復雜度,即樹套樹
第一種情況是與K-D樹的嵌套,第二種情況是與線段樹的嵌套
平衡樹套線段樹通常不可寫,因為這樣嵌套之后平衡樹無法旋轉,但是用替罪羊樹套函數式線段樹是沒有任何問題的
在介紹樹套樹部分時,將着重介紹這部分內容