1. 程式人生 > 其它 >平衡樹入門——FHQ_Treap

平衡樹入門——FHQ_Treap

平衡樹入門——FHQ_Treap

1 簡介

FHQ treap,也有人稱之為無旋 Treap,因為沒有旋轉,所以可以支援可持久化,也可以支援序列操作,常數略大,速度比 Splay 快。用兩個操作——插入和刪除就完成了對整個 Treap 的維護。自我感覺程式碼複雜度比 Treap,Splay 很低。容易理解,理解後記憶程式碼十分容易。

2 資料結構解析

2.1 節點資訊

FHQ Treap 和 Treap 的節點資訊是一樣的——除了一點以外:

struct node{
    int val,size,key,l,r;
};
node p[N];

相信讀者已經發現了,FHQ Treap 沒有記錄相同權值節點的個數,也就是說,就算有相同權值的節點,也不能夠把這些節點當做一個節點來處理。這帶來了一定意義上空間的浪費,不過少維護一些東西可以讓程式設計複雜度降低。

這裡 \(val\) 是權值,\(size\) 是子樹大小,\(key\) 是 Treap 的隨機值,\(l,r\) 分別是左右兒子。

2.2 創造新節點

    inline int new_node(int val){
        p[++tot].val=val;p[tot].key=random(INF);
        p[tot].l=p[tot].r=0;p[tot].size=1;
        return tot;
    }

其中:

inline int random(int n){
    return rand()*rand()%n+1;
}

這裡 \(tot\)

是節點總量,\(val\) 是所插入節點的權值。這段程式碼不作講解。

2.3 合併資訊

    inline void pushup(int k){
        p[k].size=p[p[k].l].size+p[p[k].r].size+1;
    }

太過於簡單,不作講解。

2.4 分裂

inline void split(int k,int val,int &x,int &y){//depend on val
    if(k==0){
        x=y=0;return;
    }
    if(p[k].val<=val){
        x=k;
        split(p[k].r,val,p[k].r,y);
    }
    else{
        y=k;
        split(p[k].l,val,x,p[k].l);
    }
    pushup(k);
}

使用 split(k,val_,x,y) 就相當於完成了這樣一件事情:把以 \(k\) 為根的子樹按照權值分裂,其中權值小於等於 val_ 的最終會到以 \(x\) 為根的樹中,大於 val_ 的權值會到以 \(y\) 為根的樹中。注意到 \(x\)\(y\) 是通過引用傳回來的。

我們現在重點關注一下他是如何分裂的,首先,如果 \(k=0\) 那麼這就代表已經分裂結束或者這顆樹為空,不管是哪一種情況,這個時候,讓引用的 \(x,y\) 等於 \(0\) 是不影響結果的,因為這個時候並沒有那一顆子樹被分到了以 \(x\) 為根或以 \(y\) 為根的子樹上去。

否則,如果這個節點的權值是小於等於 \(val\) 的,說明 \(k\) 這個節點和 \(k\) 這個節點的左子樹都會被劃分到 \(x\) 這顆子樹上去,而此時此刻,\(k\) 這個節點的右子樹還沒有被劃分,所以我們在去劃分一下 \(k\) 的右子樹,注意我們是帶引用的去進行遞迴的,所以如果有要劃分到 \(x\) 這個子樹上的節點,我們就把它掛到右子樹上去,這也是為什麼我們把第三個引數變為 p[k].r 。第 \(9\) 到第 \(12\) 行同理。

這裡的引用有效降低了程式設計複雜度。

2.5 合併

    inline int merge(int x,int y){
        if(x==0||y==0) return x+y;
        if(p[x].key>p[y].key){
            p[x].r=merge(p[x].r,y);
            pushup(x);
            return x;
        }
        else{
            p[y].l=merge(x,p[y].l);
            pushup(y);
            return y;
        }
    }

這一段程式碼完成的是把以 \(x\) 為根的子樹與以 \(y\) 為根的子樹合併,注意這裡保證以 \(x\) 為根的子樹的權值最大值要小於以 \(y\) 為根的子樹的權值最小值。注意這裡我們需要維護優先順序。因為有上面那個性質,所以我們不用判斷節點權值大小而可以直接合並,最後這段程式碼傳回的是合併完兩顆子樹後的根節點。

不難理解這段程式碼:如果 \(x\) 的優先值大於 \(y\) 的優先值,如圖:

不難發現的是,\(x\) 的左子樹就不需要進行合併的,需要在進行合併的是 \(x\) 的右子樹和 \(y\) 這顆子樹,遞迴進行就可以。\(y\) 的優先順序更大是同理的。如果其中一顆子樹為空,那麼我們直接返回 \(x+y\) 就可以,如果另一顆子樹不為空那麼返回的就是另一顆子樹的根節點,如果兩顆子樹都為空也不失正確性。

2.6 插入

其實有了上面這兩個操作其它操作的實現就非常簡單了。

    inline void insert(int val){
        int x,y;
        split(root,val-1,x,y);
        root=merge(merge(x,new_node(val)),y);
    }

這個函式能夠往平衡樹中插入一個權值為 \(val\) 的節點。

如何實現的呢?我們按照權值 \(val-1\) 來進行分裂,分裂之後,權值小於等於 \(val-1\) 的節點都在以 \(x\) 為根的子樹中,其他節點在以 \(y\) 為根的子樹中,然後我們先把 \(x\) 與我們新建的節點合併,然後再合併整棵樹。

2.7 刪除

    inline void delete_(int val){
        int x,y,z;
        split(root,val,x,z);
        split(x,val-1,x,y);
        if(y){
            y=merge(p[y].l,p[y].r);
        }
        root=merge(merge(x,y),z);
    }

不難發現在分裂之後,以 \(y\) 為根的子樹裡只有權值等於 \(val\) 的節點,我們合併左右子樹——刪除根就可以。

如果要刪除所有權值為 \(val\) 的節點,就不用寫第 \(6\) 到第 \(9\) 行。

刪完之後,我們把整棵樹重新合併。

2.8 查詢排名

inline int getrank(int val){
    int x,y,ans;
    split(root,val-1,x,y);
    ans=p[x].size+1;
    root=merge(x,y);
    return ans;
}

某個數的排名就是比他小的數的個數 \(+1\) ,所以不難理解上面這個程式碼。

2.9 查詢值

inline int getval(int rank){
    int k=root;
    while(k){
        if(p[p[k].l].size+1==rank) break;
        else if(p[p[k].l].size>=rank) k=p[k].l;
        else rank-=p[p[k].l].size+1,k=p[k].r;
    }
    return k==0?INF:p[k].val;
}

這個和普通的平衡樹查詢值是一樣的,不作講解。

2.10 查詢前驅後繼

inline int getpre(int val){
    int x,y,k,ans;
    split(root,val-1,x,y);
    k=x;
    while(p[k].r) k=p[k].r;
    ans=p[k].val;
    root=merge(x,y);
    return ans;
}
inline int getnext(int val){
    int x,y,k,ans;
    split(root,val,x,y);
    k=y;while(p[k].l) k=p[k].l;
    ans=p[k].val;
    root=merge(x,y);
    return ans;
}

如果要查詢前驅,我們就按照 \(val-1\) 分裂整顆樹,然後取 \(x\) 子樹最靠右的節點就可以了,查詢後繼同理。

2.11 優化

因為在一定程度上 FHQ Treap 有點浪費空間,所以我們可以開一個棧,把所以刪除的節點編號存一下,然後插入新節點時我們優先使用這些被刪除的節點。程式碼就看這篇部落格,同時這篇部落格還講解了如何判斷某一個節點是否存在,以及返回整顆輸的大小等操作,相信有了分裂和合並,這些操作不難實現。

引用

當你想要結束的時候,想想你為什麼開始!