平衡樹入門——替罪羊樹
平衡樹入門——替罪羊樹
1 簡介
替罪羊樹是一顆重量平衡樹,不需要旋轉,但是非常暴力,據說常數很小,但是我寫的替罪羊樹跑不過 Treap ,可能常數比較大。。。
2 資料結構解析
2.1 節點結構體
struct node{
int val,l,r,cnt,size,allsize,not_dele_size;
};
node p[N];
\(val\) 是節點權值,\(l,r\) 是左右兒子,\(cnt\) 是權值相同節點個數,\(size\) 是子樹大小,注意 \(size\) 並沒有重複計數權值相同的節點,也就是子樹節點個數,\(allsize\) 是算上了權值相同節點的大小。
那麼 \(not\_dele\_size\) 是什麼呢?請注意,替罪羊樹的刪除是惰性刪除,也就是說這個節點就算已經被刪除了它還是在那裡,我們刪除的時候只把 \(cnt-1\) ,而不管其他東西,所以 \(not\_dele\_size\) 值得就是不算被刪除的節點,子樹節點個數是多少。
換句話說,\(size\) 裡面算上了被刪除節點,而 \(allsize\) 因為統計的是 \(cnt\),被刪除節點的 \(cnt\) 是 \(0\) ,所以 \(allsize\) 裡面其實也沒有計數被刪除節點。
2.2 重構程式碼
沒有旋轉,替罪羊樹是如何判斷並維護整顆樹是否平衡呢?我們考慮不平衡一定是一棵樹有一顆子樹很大,而另一顆子樹很小,那麼我們如何判斷這件事情?我們引入平衡係數—— \(\alpha\)
如果使這棵樹變得平衡呢?暴力重構整棵樹就可以做。我們對這棵樹進行中序排序,那麼怎樣建這顆樹是最平衡的?我們取最中間的節點作為樹根就是最平衡的,因為平衡樹中序排序後的序列是有序的,所以這麼做正確性顯然。
在重構的時候我們順便去掉所有已經被刪除的節點。
首先是判斷是否平衡的程式碼:
inline bool can_rest(int k){ return (p[k].cnt)&&(alpha*(dd)p[k].size<=max((dd)p[p[k].l].size,dd(p[p[k].r].size))||((dd)p[k].not_dele_size<=alpha*(dd)p[k].size)); }
請注意,因為替罪羊樹是惰性刪除,所以要時刻注意如何處理被刪除節點,不能讓被刪除節點產生影響。除了判斷子樹大小,如果沒有被刪除的節點太多,也影響效率,我們也進行重構。
接下來是中序排序的程式碼和重構的程式碼:
inline void mid_travel(int &tail,int k){
if(!k) return;
mid_travel(tail,p[k].l);
if(p[k].cnt) mid_tra[tail++]=k;
mid_travel(tail,p[k].r);
}
inline int rest_build(int l,int r){
if(l>=r) return 0;
int mid=l+r>>1;
p[mid_tra[mid]].l=rest_build(l,mid);
p[mid_tra[mid]].r=rest_build(mid+1,r);
pushup(mid_tra[mid]);return mid_tra[mid];
}
需要注意的是,這裡的 \(rest\_build\) 函式是把 \([l,r)\) 這段區間進行重構。最終 \(rest\_build\) 會返回整棵樹的根節點。
而第 \(4\) 行我們保證了去掉被刪除節點。
呼叫:
inline void rest(int &k){
int tail=0;
mid_travel(tail,k);
k=rest_build(0,tail);
}
呼叫完後,整顆以 \(k\) 為根的子樹被徹底重構。
2.3 新節點與合併資訊
inline void pushup(int k){
p[k].size=p[p[k].l].size+p[p[k].r].size+1;
p[k].allsize=p[p[k].l].allsize+p[p[k].r].allsize+p[k].cnt;
p[k].not_dele_size=p[p[k].l].not_dele_size+p[p[k].r].not_dele_size+(p[k].cnt!=0);
}
inline int new_node(int val){
tot++;p[tot].cnt=p[tot].size=p[tot].allsize=p[tot].not_dele_size=1;
p[tot].val=val;p[tot].l=p[tot].r=0;return tot;
}
其中 \(tot\) 是節點總數,包括被刪除節點。這比較顯然,不作講解。
2.4 插入
inline void insert(int &k,int val){
if(!k){
k=new_node(val);
return;
}
if(val==p[k].val) p[k].cnt++;
else if(val<p[k].val) insert(p[k].l,val);
else insert(p[k].r,val);
pushup(k);if(can_rest(k)) rest(k);
return;
}
插入比較簡單,只需要在需要重構的時候重構,注意先合併再重構。
2.5 刪除
inline void delete_(int &k,int val){
if(!k) return;
if(p[k].val==val){
if(p[k].cnt) p[k].cnt--;
}
else if(val<p[k].val) delete_(p[k].l,val);
else delete_(p[k].r,val);
pushup(k);if(can_rest(k)) rest(k);
return;
}
因為替罪羊樹是惰性刪除,所以刪除也比較顯然,注意不要在第 \(4\) 行後直接寫return;
因為節點 \(k\) 需要合併。
2.6 查詢後繼排名和前驅排名
inline int upper_rank(int k,int val){
if(!k) return 1;
else if(p[k].val==val&&p[k].cnt) return p[p[k].l].allsize+1+p[k].cnt;
else if(val<p[k].val) return upper_rank(p[k].l,val);
else return p[p[k].l].allsize+p[k].cnt+upper_rank(p[k].r,val);
}
inline int lower_rank(int k,int val){
if(!k) return 0;
if(p[k].val==val&&p[k].cnt) return p[p[k].l].allsize;
else if(p[k].val<val) return p[p[k].l].allsize+p[k].cnt+lower_rank(p[k].r,val);
else return lower_rank(p[k].l,val);
}
這裡的坑點比較多,但是實現比較巧妙。注意第 \(3,9\) 行不要忘記判斷被刪除節點,\(4,5\) ,\(10,11\) 行不能交換,這涉及到如果 p[k].val==val
並且 p[k].cnt==0
,對於查後繼排名來說,它需要進入 p[k].r
,對於查前驅排名來說,它需要進入 p[k].l
。所以兩個判斷不能交換,其他都比較顯然。
2.7 查詢值
inline int getval(int k,int rank){
if(!k) return 0;
if(p[p[k].l].allsize<rank&&rank<=p[p[k].l].allsize+p[k].cnt) return p[k].val;
else if(p[p[k].l].allsize+p[k].cnt<rank) return getval(p[k].r,rank-p[p[k].l].allsize-p[k].cnt);
else return getval(p[k].l,rank);
}
這個比較顯然,注意第三行已經去除了被刪除節點的影響。
2.8 查詢排名,找前驅後繼
inline int getrank(int k,int val){
int ans=lower_rank(k,val);
return ans+1;
}
inline int getpre(int k,int val){
return getval(k,lower_rank(k,val));
}
inline int getnext(int k,int val){
return getval(k,upper_rank(k,val));
}
這個比較顯然,也不作講解。