1. 程式人生 > >非旋Treap總結 : 快過Splay 好用過傳統Treap

非旋Treap總結 : 快過Splay 好用過傳統Treap

非旋$Treap$

其高階名字叫$Fhq\ Treap$,既然叫$Treap$,它一定滿足了$Treap$的性質(雖然可能來看這篇的人一定知道$Treap$,但我還是多說幾句:$Fhp\ Treap$就是繼承了$Treap$的隨機系統,在二叉搜尋的基礎上,每個點加一個隨機化處理,這些隨機值滿足堆的性質……通俗一點講,就是$Fhp\ Treap$它每個點有至少兩個值,一個是val,即存的數值,這些數值滿足二叉搜尋樹,也就是父親比左孩子小/大,則右孩子比父親小/大;還有一個是key,是個隨機值,這些隨機值滿足堆的性質,即父親比兩個孩子都大/小),好了$Fhp\ Treap$和$Treap$的關係到此結束。但是,$Fhp\ Treap$還和左偏樹

笛卡爾樹,用到了笛卡爾樹的建樹左偏樹的合併,這使得它不用像$Treap$一樣旋轉即可做到上述樹的性質。

以上都是廢話,下面進入正題               PS:本文以大根堆來維持$key$值(第一遍)

 


 

$Fhq\ Treap$要維護的值

既然叫$Treap$,就有$Treap$要有的隨機值$Key$,還有和其他平衡樹一樣,要左孩子$l$,右孩子$r$,節點的值$val$和子樹大小$siz$、子樹節點權值和$sum$。必要的話,還有翻轉標記和區間標記。對於作者本人為什麼不記錄它的$father$,我覺得好像$father$在幾個操作中用處不大,好多題似乎沒怎麼用到$father$這一變數(可以看下面的程式碼,其實沒有$father$就可以做很多操作),不過有$father$確實是一個好習慣。(如有大神指出不對,或者知道哪些$Fhp\ Treap$要用的$father$的地方,一定要跟我講啊!!!)

當然,我們還要記錄當前整棵樹的根$rt$和節點編號$cnt$

int rt,cnt;
struct Tree
{
    int l,r,siz,sum,val,key;
    int lazychange;
    bool flagchange,flagreverse;
}t[MAXN];

兩個重要操作

可以說,在非旋$Treap$中,最核心的只有兩個:Merge(合併)和Split(分裂),$Fhq Treap$就是用這兩個操作打遍天下。

$Split$ 分裂操作

非旋$Treap$有兩種分裂操作

一種是按$val$值進行$Split$ (一般用於找該$val$值在樹中的位置、$Rank$值等):即分裂後$A$中的葉子節點都比給定的$Val$小(小於等於),$B$中則均大於等於(大於)$Val$。

還有一種則是按個數$Split$(一般用於找第$K$大、區間權值等):即分裂後$A$子樹的大小為給定的$Size$,而$B$則為$A$的補集。

其實兩種差別不大,我們先看第一種:

(1)按$Val\ Split$

一般$Split$有四個引數,$Now$表示我們現在在搜的子樹的根,$W$為給定的標準$val$,還有就是$u,\ v$ 表示$Split$以$Now$為根的子樹後得到的兩棵樹。($u$符合條件,$v$不符合)

1、如果這棵樹為空集,則 $Split$後兩棵樹也為空,$u\ =\ v\ =\ 0$

2、因為以$W$為界,我們只要把$Now$的$val$和$W$比較一下即可,若$Val_Now <= W$ (有些情況為$Val_Now < W$ ),則$Now$滿足標準。因為為二叉搜尋樹,所以$Now$的左孩子$Val$都小於自己,也必滿足條件,所以我們可以直接把$Now$和它的左孩子$Split$出來變成一個新的樹$u$,此時,我們只要繼續做$Now$的右孩子,而前面$Split$出的$u$的右孩子就是$Now$的右孩子$Split$出的滿足條件的樹,不符合就為$v$的孩子;若$Val_Now >= W$ (有些情況為$Val_Now > W$ ),則$Now$不滿足條件,同理$Now$的右孩子也不滿足條件,所以我們可以直接把$Now$和它的右孩子$Split$出來變成一個新的樹$v$,此時,我們只要繼續做$Now$的左孩子,而$Now$新的左孩子就是$Now$的左孩子$Split$出的不滿足條件的樹$v$,滿足則為$u$樹的孩子。

 

大概過程就是這樣,不過因為本人語文不太好,聽懂的人應該不多……看程式碼吧……

inline void Split(int Now,int W,int &u,int &v) // now現在搜的根 以w為界分為以u為根的樹和v為根的樹(本程式為u中所有節點的val小於等於w)
{
    if(!Now) u = v = 0;
    else
    {
        pushdown(Now);//如果必要時要先下傳標記
        if(t[Now].val <= W) u = Now,Split(t[Now].r,W,t[Now].r,v);
        else v = Now,Split(t[Now].l,W,u,t[Now].l);
        update(Now);//最後更新該根的資訊
    }
}     

用$&$符號來方便直接將後面的接到前面的左或右孩子上;

(2)按個數來$Split$

和上面的方法一樣,只是我們把$W$改成了$Size$

1、和上面一樣如果這棵樹為空集,則 $Split$後兩棵樹也為空,$u\ =\ v\ =\ 0$

2、以$Siz$為界,則只要比較$Now$左孩子的$siz_NowLeft$和$Siz$即可,若$siz_L\ >=\ Siz$,則$Now$必不符合條件,不然要有其左孩子,$Size$必要大於標準,所以只要搜$Now$的左孩子,$Split\ Now$和它的右孩子變為$v$即可,其他同上;反之若$siz_L\ <\ Siz$, 則加上$Now$必有$siz_L\ +\ 1\ <=\ Siz$,所以$Split\ Now$和它的左孩子變為$u$即可

inline void Split(int now,int siz,int &u,int &v)//now現在搜的根 以siz為界 Split後的樹的根為u,v(即子樹u的大小為siz)
{
    if(!now) u = v = 0;
    else
    {
        pushdown(now);
        if(t[t[now].l].siz >= siz) v = now,Split(t[now].l,siz,u,t[now].l);
        else u = now,Split(t[now].r,siz-t[t[now].l].siz-1,t[now].r,v);
        update(now);
    }
}

 

$Merge$ 合併操作

一般$Fhq Treap$的操作都是先$Split$再$Merge$之後的,$Split$使其$val$值以滿足二叉搜尋樹(即樹$A$所有的$val$值都比$B$中的小),所以只要處理它們的$Key$值,即堆的性質就好了

所以$Fhq Treap$的合併和左偏樹的合併非常相似,唯一不同的就是不能$swap$左右孩子,$swap$了那你前面的$Split$使$val$滿足二叉搜尋樹的就全白忙了……

合併的時候,我們傳進兩棵樹的根$u$和$v$

1、有一棵樹為空 :直接返回另一棵樹

2、合併兩棵樹:

因為$val$值已在$Merge$中滿足的前一棵樹的$val$值都比後一棵樹的小,所以我們只要按照$Key$值合併。若$u$的$Key$值比$v$的大,因為本文以大根堆來維持$key$值(第二遍),所以新的樹一定以$u$為根,定下了根,根據二叉搜尋樹的性質左邊比根小右邊比根大,而以$v$為根的子樹$val$均大於以$u$為根的樹,所以以$v$為根的子樹只能接到新的根$u$的右孩子,即$v$和$R_u$進行$Merge$變為新的$R_u$,而左孩子就是原來$u$的左孩子;反之若$u$的$Key$值比$v$的小,因為本文以大根堆來維持$key$值(第三遍),所以新的樹一定以$v$為根,同理根據二叉搜尋樹的性質左邊比根小右邊比根大,而以$u$為根的子樹$val$均小於以$v$為根的樹,所以以$v$為根的子樹必接到新根$v$的左孩子,即$u$和$L_v$進行$Merge$變為新的$L_v$,右孩子就是原來$v$的右孩子。

最後,更新一下新的樹根$u$或$v$並返回新得到的樹的根節點即可。

 

怕是還是沒太懂)看程式碼吧……

inline int Merge(int u,int v)
{
    if(!u||!v) return u + v;
    pushdown(u),pushdown(v);
    if(t[u].key < t[v].key)
    {
        t[v].l = Merge(u,t[v].l);
        return update(v),v;
    }
    else
    {
        t[u].r = Merge(t[u].r,v);
        return update(u),u;
    }
}

以上就是$Fhq\ Treap$的兩個最重要的操作,接下來來看幾個必要的操作:

$Build$建樹

$Fhq\ Treap$建樹和笛卡爾樹差不多。

為了保證$Fhq\ Treap$的中序遍歷是原數列(或為$val$值遞增$/$減),我們可以把它變成一條鏈!!!沒錯吧,建樹完成了,但顯然不優秀,所以就有了$Key$值的隨機化。

沒有學過笛卡爾樹的看這裡

首先我們用棧來存我們要做的點, 我們按順序彈入一個點,因為保證了在它前面的點(即在棧中的點)中序遍歷一定在它前面,所以它只能認左孩子或父親,而且只能根據$Key$值來認。

本文以大根堆來維持$key$值(第四遍)我們先取出棧頂的點,如果棧頂的$Key$值大於該插入的點的值,那麼我們直接把該點接在棧頂的右孩子處,當然此時它可能已經有一個右孩子了,很明顯原來的右孩子中序遍歷也該在應插入點的前面,那我們就把棧頂的右孩子變為它的左孩子,在將其接在棧頂的右孩子處;反之如果棧頂的$Key$值小於等於該插入的點的值 那麼它就應該變為該點的左孩子,我們只要彈出棧頂的點,然後繼續找到第一個比該插入的點$Key$值大的點即可;當然,萬一它是最大的,那我們只要把原棧底的點接在它的左孩子處即可。

這樣一來,棧底的點一定是$Key$值最大的點了,也就是這顆樹的根啦!

(相信大家還是沒看懂,其實你可以手動模擬一遍,在加上以下的程式碼,相信一定可以懂)

inline void build()
{
    int stc[MAXN],sl = 0;//用棧來存當前未做的節點
    for(int i = 1;i<=n;i++)
    {
        int at = MakeNew(a[i]);//建一個新點
        while(sl&&t[stc[sl]].key < t[at].key) update(stc[sl]),sl--;//比該點小的彈出作為該點的孩子
        if(sl) t[at].l = t[stc[sl]].r,t[stc[sl]].r = at;//更改它父親和右孩子
        else t[at].l = stc[1];//原棧頂接在該節點的左邊
        stc[++sl] = at;//進棧
    }
    while(sl) update(stc[sl]),sl--;//未出棧的出棧並update
    rt = stc[1];//設定根節點
}

 $Insert$ 插入

先說插入一個點,首先,我們前面說過,有按權值的還有按大小的:

(1)按權值插入:

我們只要找到比它小的數,插到它們的後面。$Split$出比它小(小於等於也行)的即可。

inline void Insert(int u)
{
    int x,y;
    Split(rt,u,x,y);
    rt = Merge(Merge(x,MakeNew(u)),y);
}

(2)按大小插入

其實和上面的一樣,只是把$Split$改一下即可

 

當然呢,插入一堆點嘞,就把那些點建成一棵樹,然後當成一個點來插就好啦!

 $Makedown$建一個新點

突然忘了怎麼建點,但我覺得問題不大

inline int MakeNew(int val)
{
    t[++cnt].val = val,t[cnt].key = rand() * rand(),t[cnt].siz = 1;
    return cnt;
}

$Kth$找到第$K$大

inline int Kth(int now,int sum)
{
    while(1)
        if(sum <= t[t[now].l].siz) now = t[now].l;
        else if(sum == t[t[now].l].siz + 1) return now;
        else sum-=t[t[now].l].siz + 1,now = t[now].r;
}

 


以下是P3369 【模板】普通平衡樹的程式碼

#include<bits/stdc++.h>
using namespace std;
inline int read()
{
    int s = 0,w = 1;char ch = getchar();
    while(ch<'0'||ch>'9'){if(ch == '-')w=-1;ch = getchar();}
    while(ch>='0'&&ch<='9'){s = (s << 1) + (s << 3) + ch - '0';ch = getchar();}
    return s * w;
}
int rt,cnt;
struct Tree
{
    int l,r,val,key,siz;
}t[110000];
inline int MakeNew(int val)
{
    t[++cnt].val = val,t[cnt].key = rand() * rand(),t[cnt].siz = 1;
    return cnt;
}
inline void Split(int now,int w,int &u,int &v) // now�����ѵĸ� ��valΪ���Ϊ��uΪ��������vΪ������
{
    if(!now) u = v = 0;
    else
    {
        if(t[now].val <= w) u = now,Split(t[now].r,w,t[now].r,v);
        else v = now,Split(t[now].l,w,u,t[now].l);
        t[now].siz = t[t[now].r].siz + t[t[now].l].siz + 1;
    }
}
inline int Merge(int u,int v)
{
    if(!u||!v) return u + v;
    if(t[u].key < t[v].key)
    {
        t[u].r = Merge(t[u].r,v);
        t[u].siz = t[t[u].r].siz + t[t[u].l].siz + 1;
        return u;
    }
    else 
    {
        t[v].l = Merge(u,t[v].l);
        t[v].siz = t[t[v].r].siz + t[t[v].l].siz + 1;
        return v;
    }
}
inline int Kth(int now,int sum)
{
    while(1)
        if(sum <= t[t[now].l].siz) now = t[now].l;
        else if(sum == t[t[now].l].siz + 1) return now;
        else sum-=t[t[now].l].siz + 1,now = t[now].r;
}
int main()
{
    int n = read(),x,y,z,opt,a;
    srand(20040328);
    while(n--)
    {
        opt = read();a = read();
        if(opt == 1)
        {
            Split(rt,a,x,y);
            rt = Merge(Merge(x,MakeNew(a)),y);
        }
        else if(opt == 2)
        {
            Split(rt,a,x,z);
            Split(x,a-1,x,y);
            y = Merge(t[y].l,t[y].r);
            rt = Merge(Merge(x,y),z);
        }
        else if(opt == 3)
        {
            Split(rt,a-1,x,y);
            printf("%d\n",t[x].siz + 1);
            rt = Merge(x,y);
        }
        else if(opt == 4) printf("%d\n",t[Kth(rt,a)].val);
        else if(opt == 5)
        {
            Split(rt,a-1,x,y);
            printf("%d\n",t[Kth(x,t[x].siz)].val);
            rt = Merge(x,y);
        }
        else if(opt == 6)
        {
            Split(rt,a,x,y);
            printf("%d\n",t[Kth(y,1)].val);
            rt = Merge(x,y);
        }
    }
    return 0;
}

有鍋會繼續補……