1. 程式人生 > 實用技巧 >FHQ-Treap(無旋Treap)

FHQ-Treap(無旋Treap)

FHQ Treap

treap是一種基於隨機索引值的二叉搜尋樹,因其在重構樹的過程中也同時按隨機索引值來維護該二叉搜尋樹具有二叉堆的性質

故得名Treap(Tree + Heap)

fhq treap則是一種不依賴於旋轉操作的平衡樹(所以好寫

其結點一般儲存左右兒子索引、當前結點值、堆索引、當前結點大小(包含子樹)

struct node{
int l, r;
int key, val;
int size;
};
const int maxn = 1e5 + 50;
node fhq[maxn];
int cnt, root; //計數器&根節點編號

提前開記憶體池,避免動態申請記憶體的額外開銷。

inline int newnode(int val){
int now = ++cnt;
fhq[now].val = val;
fhq[now].l = fhq[now].r = 0;
fhq[now].size = 1;
fhq[now].key = rnd();
return now;
}

上面的key是堆索引,可以使用rand()取隨機數

這裡使用random庫的mt19937隨機數生成器生成隨機數

#include<random>
std::mt19937 rnd(233);

其核心操作有二:

按值val分裂整棵樹 一棵樹的全部結點值 ≤ val 另一棵樹的全部結點值 > val

inline void split(int now, int val, int &x, int &y){
if(!now) x = y = 0;
else{
if(fhq[now].val <= val){
x = now;
split(fhq[now].r, val, fhq[now].r, y);
}else{
y = now;
split(fhq[now].l, val, x, fhq[now].l);
}
update(now);
}
}

思路是這樣的

邊界條件:當前已經遞迴到空結點了,那麼x = y = 0(對空結點分裂大家肯定都空)

不然的話就比較當前結點和分裂值的大小

1.當前結點值≤val then 該結點以及該結點的左子樹應該都劃給結點x,然後遞迴地分裂當前結點的右子樹(且結合二叉搜尋樹性質, 右子樹中≤val的應當作為x結點的右子樹)

2.同理,對於>當前結點的值的,則將該結點的右子樹全部劃分給結點y,然後遞迴地分裂該結點的左子樹

在分裂完之後重新更新結點的size,因為會遞迴到所有size發生變化的結點,因此update只需要把size向上pushup即可。

inline void update(int now){
fhq[now].size = fhq[fhq[now].l].size + fhq[fhq[now].r].size + 1;
}

第二個操作是合併(分裂完了得給人家拼起來啊……

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

合併merge是基於隨機索引key來維護二叉堆性質來對該二叉搜尋樹進行合併的

怎麼理解經過merge幾乎是隨機合併得到的新二叉搜尋樹能夠維持平衡樹的平衡而使得二叉搜尋樹的常規操作維持在O(logN)級別呢?

可以這麼理解: 如果一組資料是按大小順序插入二叉搜尋樹的

比如1 2 3 4 5 那麼該二叉搜尋樹將退化成連結串列

但如果我們插入時是經過隨機化的,即變成比如3 5 2 4 1的順序插入

那麼將是一棵非常漂亮的平衡樹。

如果將此處的

if(fhq[x].key > fhq[y].key)

換成

if(rnd() % 2)

treap的效能會被較大程度地影響(可能效能會更好…),但仍然能夠在大多數情況下維持平衡。

FHQ Treap的插入、刪除、求第k大、求x的排名均可通過split和merge操作來實現

……to be continued (寫作業去)

方便起見:

我們宣告三個全域性變數用來存放以下操作所需要的樹的根節點

int x, y, z;

插入操作insert

插入一個值為val的結點

可以用split和merge來實現

先將整棵樹按值val分裂,然後將左子樹和新結點merge,再將新的樹和y merge

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

刪除操作del

刪除一個值為val的結點

可以先將樹按值val分裂成x和y

再將樹x按值val-1分裂成x和z

這樣分裂兩次樹z中僅有值為val的結點

我們只需要通過

z = merge(fhq[z].l, fhq[z].r);

即可將z樹的根節點刪除

所以del的程式碼就很容易寫了-刪除掉根節點之後再將三部分merge起來就OK了

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

求一個數的前驅

inline void pre(int val, int &res){
    split(root, val-1, x, y);
    int now = x;
    while(fhq[now].r) now = fhq[now].r;
    res = fhq[now].val;
    root = merge(x, y);
}

思路比較顯然:將樹按val-1分裂為x和y

再在較小的樹上找到最大的值即為該數的前驅

求一個數的字尾

inline void nxt(int val, int &res){
    split(root, val, x, y);
    int now = y;
    while(fhq[now].l) now = fhq[now].l;
    res = fhq[now].val;
    root = merge(x, y);
}

思路和求pre類似,按值val分裂成x, y

然後在較大樹上找最小值即為val的字尾

求排名為k的數

以第k小為例子,其他的排名規則思想一致

主要利用二叉搜尋樹的性質

1.如果當前結點+左子樹的size 恰好等於k 則該結點的值即為第k小的數

2.如果當前結點+左子樹size小於k,則第k大應在該結點的左子樹上,在左子樹上遞迴查詢第k大

3.如果當前結點+左子樹size大於k,則第k大應在結點的右子樹上,在右子樹上找第k - 左子樹+根節點大小 大的數即可。

inline int getnum(int rank){
    int now = root;
    while(now){
        if(fhq[fhq[now].l].size + 1 == rank) break;
        else if(fhq[fhq[now].l].size + 1 > rank){
            now = fhq[now].l;
        }else now = fhq[fhq[now].r], rank -= (fhq[fhq[now].l].size + 1);
    }
    return fhq[now].val;
}

求數val的排名

排名定義依然採取越小排名越高的規則。

將樹按值val-1分裂

在分裂得到的較小樹的大小即為比val小的數字個數

再+1即為val的排名

inline void getrank(int val, int &res){
    split(root, val-1, x, y);
    res = fhq[x].size + 1;
    merge(x, y);
}