1. 程式人生 > 其它 >平衡樹入門——替罪羊樹

平衡樹入門——替罪羊樹

平衡樹入門——替罪羊樹

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\)

,這個平衡係數選在 \(0.5\)\(1\) 之間,通常取 \(0.7\) ,如果有一顆子樹的大小佔了整棵樹的 \(\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));
    }

這個比較顯然,也不作講解。

引用

OI wiki