1. 程式人生 > 其它 >[資料結構-平衡樹]普通 FHQ_Treap從入門到精通(註釋比程式碼多系列)

[資料結構-平衡樹]普通 FHQ_Treap從入門到精通(註釋比程式碼多系列)

覺得Treap難打?不如來看看FHQ大佬的無旋Treap。 這注釋比程式碼還多,再也不用擔心看不懂了。 不用引用,變數再也不會搞亂了。

普通 FHQ_Treap從入門到精通(註釋比程式碼多系列)

前提說明,作者寫註釋太累了,文章裡的部分講解來源於Oi-wiki,並根據程式碼,有部分增改。本文僅僅釋出於部落格園,其他地方出現本文,均是未經許可的盜竊。

芝士前置

知識名 內容
二叉搜尋樹 一顆每個節點的左兒子val都比自己小,右兒子val都比自己大的樹
Treap 堆和平衡樹的結合(Tree+Heap),其中的pri變數滿足堆的性質(父親的比自己大),val變數滿足平衡樹的性質

芝士引入

節點定義

struct FHQ_Node
{
    int sze, val, pri;
    int lc, rc;
    FHQ_Node()
    {
        sze = 1; //注意0號節點不能這樣
        pri = rand();
    }
} FHQ_Tree[N];

基本操作

FHQ_Treap不同於傳統Treap,FHQ_Treap是基於以下兩個基本操作分裂合併,而不是旋轉。所以也有叫無旋Treap

基本思路很簡單

操作 含義
分裂 把一個樹按一個界限分裂,通常是按val值分裂。小於val的節點放一顆樹,大於val的節點放另一棵樹。
合併 把兩個樹合併,通常情況下合併的兩樹有嚴格的大小關係(一般是A樹的每個節點值,均小於B樹)。

操作解釋

分裂

分裂操作是基於遞迴實現的。

分裂過程接受兩個引數:根指標\(u\)​ 、關鍵值\(key\)​ 。結果為將根指標指向的 treap 分裂為兩個 treap,第一個 treap 所有結點的關鍵值小於$key \(​,第二個 treap 所有結點的關鍵值大於等於\)

key \(​。該過程首先判斷\)key$​ 是否大於 \(u\)​的關鍵值,若大於,則說明 \(u\)​及其左子樹全部屬於第一個 treap(當然也有一部分右子樹屬於,一部分不屬於,所以需要遞迴進去),否則說明\(u\)​及其右子樹全部屬於第二個 treap(同理,也需要遞迴進去)。根據此判斷決定應向左子樹遞迴還是應向右子樹遞迴,繼續分裂子樹。待子樹分裂完成後按剛剛的判斷情況連線 的左子樹或右子樹到遞迴分裂所得的子樹中。

pair<int, int> split(int u, int _key)
{
    if (u == 0)
        return make_pair(0, 0); //如果我是個空節點,那我就算分割後的節點也是空的
    if (FHQ_Tree[u].val < _key) //當前節點小於臨界key,右兒子比我大,說不定有比key大(或者等於)的,左兒子比我小,一定不可能大於等於key了
    {
        pair<int, int> ret = split(FHQ_Tree[u].rc, _key); //但是右兒子裡可能有比我大的(甚至是大於key)
        FHQ_Tree[u].rc = ret.first;                       //沒有key的那作為的的新右兒子,隨我一起被切出去
        updata(u);                                        //我被切了,需要維護一下大小資訊
        return make_pair(u, ret.second);                  //我屬於小於key的部分,而被切除了小於key節點的右兒子當然就完全都是大於等於key了
    }
    else //當前節點大於等於臨界key,右兒子比我還大,肯定是也大於key的,不用管。
    {
        pair<int, int> ret = split(FHQ_Tree[u].lc, _key); //同理左兒子也有可能小於我的(甚至是小於key)
        FHQ_Tree[u].lc = ret.second;                      //比大於等於key的作為我的新左兒子,和我一起走
        updata(u);                                        //我被切了,需要維護一下大小資訊
        return make_pair(ret.first, u);                   //我清除掉了小於key的子樹,這部分小於key,而和我在一起的都要比key大
    }
}

合併

合併操作也是基於遞迴的

合併過程接受兩個引數:左 treap 的根指標\(x\) 、右 treap \(y\)的根指標 。必須滿足\(u\)中所有結點的關鍵值小於\(y\)中所有結點的關鍵值。因為兩個 treap 已經有序,我們只需要考慮\(pri\)​來決定哪個 treap 應與另一個 treap 的兒子合併。若\(x\)的根結點的\(pri\)大於\(y\)的,那麼 即為新根結點,\(y\)應與\(x\)的右子樹合併;反之,則\(y\)作為新根結點,然後讓\(x\)\(y\)的左子樹合併。不難發現,這樣合併所得的樹依然滿足\(pri\)的大根堆性質。

int merge(int x, int y) //把x和y拼在一起(前提是x裡所有節點都要比y所有節點小)
{
    if (!x || !y) //如果其中有一個節點是空的,那就別合併了,直接返回非空的那個就好了
        return x + y;
    if (FHQ_Tree[x].pri > FHQ_Tree[y].pri) //採用大根堆,x現在應該在y上面
    {
        //由於x都比y小,那麼y只能掛在x的右兒子上
        //                      x全部小於y,這裡順序別搞錯了
        FHQ_Tree[x].rc = merge(FHQ_Tree[x].rc, y); //考慮到x原本可能就有右兒子,先把x的右兒子和y拼在一起再說
        updata(x);                                 //y掛在了我身上,我肯定變大了,需要維護一些大小資訊
        return x;                                  //y在我下面,我才是這棵樹的老大
    }
    else //現在x應該在y下面了
    {
        //由於x都比y小,那麼y只能考慮掛在y的左兒子上
        //                      x全部小於y,這裡順序別搞錯了
        FHQ_Tree[y].lc = merge(x, FHQ_Tree[y].lc); //考慮到y原本可能就有左兒子,先把x和y的左兒子拼在一起再說
        updata(y);                                 //x掛在了我身上,我肯定變大了,需要維護一些大小資訊
        return y;                                  //x在我下面,我才是這棵樹的老大
    }
}

功能操作

插入

先在待插入的關鍵值處將整棵 treap 分裂,判斷關鍵值是否已插入過之後新建一個結點,包含待插入的關鍵值,然後進行兩次合併操作即可。

void ins(int _val) //插入一個點
{
    FHQ_Tree[++p].val = _val;               //先是建立一個這個樣的節點
    pair<int, int> ret = split(root, _val); //以val為分界線,把這棵樹分裂成兩部分
    //現在我們嘗試把這個新節點插入到樹裡
    int _new = merge(ret.first, p); //上文說道,split返回的第一個樹的每個節點一定比val小,這個時候就可以把這個比val小的樹和新的那個節點合併了
    root = merge(_new, ret.second); //由於新加入的節點的優先順序我們是未知的,有可能比原來的根節點大,導致在原來根節點上面,發生換根
}

刪除

將具有待刪除的關鍵值的結點從整棵 treap 中孤立出來(進行兩側分裂操作),刪除中間的一段(具有待刪除關鍵值),再將左右兩端合併即可。

void del(int _val) //刪除一個
{
    pair<int, int> ret = split(root, _val);            //按val為分界線,現在含有val的樹一定是ret.second了;
    pair<int, int> ret2 = split(ret.second, _val + 1); //再把ret.second裡的節點再分一遍,現在ret2.first裡的節點一定全是數為val的點
    //通常情況下,我們只刪除一個節點
    int _new = merge(FHQ_Tree[ret2.first].lc, FHQ_Tree[ret2.first].rc); //左兒子和右邊兒子合併,其實是其中一個優先順序較大的,跑出來當爹,原來的父親就會被孤立
    root = merge(merge(ret.first, _new), ret2.second);                  //同樣的,我們刪除的有可能就是原來的根,導致發生換根
}

獲取排名

把一個樹按分為小於\(val\)的,和大於等於\(val\)的,\(val\)的排名自然就是小於\(val\)的節點的數量+1了

int getrank(int _val) //獲取排名
{
    pair<int, int> ret = split(root, _val); //以val為分界線,這樣ret.first裡的東西都要比val小
    int rank = FHQ_Tree[ret.first].sze + 1; //比val小的樹節點全在裡面,val的排名自然就是他們的數量+1了
    root = merge(ret.first, ret.second);    //別忘了把原來拆分的樹合起來
    return rank;
}

通過排名取數字

和二叉搜尋樹一樣,不再贅述。

int getnum(int _rank) //通過排名取出這個數來,返回節點的編號
{
    int now = root; //同平衡二叉樹的方法一樣,從根節點向下找
    while (now)
    {
        //now的左節點全是比now小的,所以比now小的數量加上1(now自己),如果正好是我們要求的點的排名,那麼now就是我們要的點了
        if (FHQ_Tree[FHQ_Tree[now].lc].sze + 1 == _rank)
            break;
        else if (FHQ_Tree[FHQ_Tree[now].lc].sze >= _rank) //同理,如果比now小的數的數量大於等於我們的rank,那麼排名為rand的數必須要更小,只能在now的左子樹上
            now = FHQ_Tree[now].lc;
        else //rank的位置在now的後面,說明rank的數要比now還大,我們就要去右節點找
        {
            _rank -= FHQ_Tree[FHQ_Tree[now].lc].sze + 1; //由於我們要找到節點在now右節點上,右節點的排名是相對於now的排名,所以我們需要把now的排名減掉
            now = FHQ_Tree[now].rc;
        }
    }
    return now;
}

求前驅

用把原來的二叉樹分裂開,一部分全是小於\(val\)的,另一部分全是大於等於\(val\)。而前驅就是前一個二叉樹裡最大的,用二叉搜尋樹的性質就能得到。

int pre(int _val) //求前驅,返回節點的值
{
    pair<int, int> ret = split(root, _val); //以val為分界線分裂
    int now = ret.first;                    //ret.first的數值都比val小
    while (FHQ_Tree[now].rc)                //找比val小的數裡最大的,就是前驅了
        now = FHQ_Tree[now].rc;
    int ans = FHQ_Tree[now].val;
    root = merge(ret.first, ret.second); //別忘了把他倆合併了
    return ans;
}

求後繼

用把原來的二叉樹分裂開,一部分全是小於\(val+1\)的,另一部分全是大於等於\(val+1\)。而後繼就是後一個二叉樹裡最小的,用二叉搜尋樹的性質就能得到。

int nxt(int _val) //求後繼,返回節點的值
{
    pair<int, int> ret = split(root, _val + 1); //以val+1為分界線分裂,所有比val大的數全在ret.second裡
    int now = ret.second;                       //在比val的數裡找
    while (FHQ_Tree[now].lc)                    //找比val大的數裡最小了的,就是後繼了
        now = FHQ_Tree[now].lc;
    int ans = FHQ_Tree[now].val;
    root = merge(ret.first, ret.second); //別忘了把他倆合併了
    return ans;
}

呼叫方法

洛谷3369

int main()
{
    FHQ_Tree[0].sze = 0; //0號節點作為溢位點,不能有大小

    int n;
    cin >> n;
    while (n--)
    {
        int opt, val;
        cin >> opt >> val;
        switch (opt)
        {
        case 1:
            ins(val);
            break;
        case 2:
            del(val);
            break;
        case 3:
            cout << getrank(val) << endl;
            break;
        case 4:
            cout << FHQ_Tree[getnum(val)].val << endl;
            break;
        case 5:
            cout << pre(val) << endl;
            break;
        case 6:
            cout << nxt(val) << endl;
            break;
        }
    }
    return 0;
}

完整程式碼下載地址:https://files.cnblogs.com/files/blogs/694685/FHQ.7z

如果未來有時間我將會出一個支援序列的FHQTreap教程。