TREAP

TREAP

Treap = Tree + Heap.
樹堆,在數據結構中也稱Treap,是指有一個隨機附加域知足堆的性質的二叉搜索樹,其結構至關於以隨機數據插入的二叉搜索樹。其基本操做的指望時間複雜度爲O(logn)。相對於其餘的平衡二叉搜索樹,Treap的特色是實現簡單,且能基本實現隨機平衡的結構。html

Treap 維護堆的性質的方法只用到了左旋和右旋, 編程複雜度比Splay小一點(??), 而且在二者可完成的操做速度有明顯優點node

然而一個裸的treap是不能支持區間操做的,因此可能功能沒有splay那麼強大.編程

treap的操做方式和splay差很少,但由於treap的結構是不能改變的,而splay的形態能夠隨意改變,因此在實現上會有一點小區別.數據結構

treap具備如下性質:

  1. Treap是關於key的二叉排序樹(也就是規定的排序方式)。
  2. Treap是關於priority的堆(按照隨機出的優先級做爲小根/大根堆)。(非二叉堆,由於不是徹底二叉樹)
  3. key和priority肯定時,treap惟一。

爲何除了權值還要在用一個隨機值做爲排序方法呢?隨機分配的優先級,使數據插入後更不容易退化爲鏈。就像是將其打亂再插入。因此用於平衡二叉樹。函數

首先是treap的定義:ui

struct treap{
	int ch[2], cnt, size, val, rd;
	//treap不須要記錄父指針,rd表示節點的隨機值
}t[N];

更新

直接統計子樹大小.url

void up(int x){
	t[x].size = t[t[x].ch[0]].size+t[t[x].ch[1]].size+t[x].cnt;
}

旋轉

treap須要支持平衡樹的性質,因此是須要用到旋轉的.這裏旋轉的方法是和splay的旋轉方法是同樣的,就不貼圖了.由於treap中並無記錄父節點,因此須要傳一個參數表示旋轉方向.spa

void rotate(int &x, int d){//x表明的是旋轉時做爲父節點的節點,d表明的是旋轉的方向
//d==0時是左兒子旋上來, d==1是右兒子旋上來.
    int son = t[x].ch[d];
    t[x].ch[d] = t[son].ch[d^1];
    t[son].ch[d^1] = x; up(x), up(x=son);//至關於up(son)
}

插入/刪除

由於treap在其餘操做過程當中是並不改變樹的形態的,因此在插入或是刪除時要先找到要插入/刪除的節點的位置,而後再建立一個新的節點/刪除這個節點..net

而後考慮到插入/刪除後樹的形態有可能會改變,因此要考慮要經過旋轉維護treap的形態.咱們這裏分類討論一下: 插入指針

  • 能夠直接按照中序遍歷結果找到最終的對應位置,而後再經過隨機值維護它堆的性質.
void insert(int &x, int val){
    if(!x){//找到對應位置就新建節點
        x = ++cnt;
        t[x].cnt = t[x].size = 1;
        t[x].val = val, t[x].rd = rand();
        return;
    }
    t[x].size++;//由於插入了數,因此在路徑上每一個節點的size都會加1
    if(t[x].val == val){t[x].cnt++; return;}//找到了直接返回
    int d = t[x].val < val; insert(t[x].ch[d], val);//不然遞歸查找插入位置
    if(t[x].rd > t[t[x].ch[d]].rd) rotate(x, d);
}

刪除

  • 先找到要刪除的節點的位置.
  • 若是這個節點位置上有多個相同的數,則直接cnt--.
  • 若是隻有一個兒子或者沒有兒子,直接將那個兒子接到這個節點下面(或將兒子賦值爲0).
  • 若是有兩個兒子,現將隨機值小的那個旋到這個位置,將根旋下去,而後將旋以後的狀況轉化爲前幾種狀況遞歸判斷.
void delet(int &x, int val){
    if(!x) return;//防止越界
    if(t[x].val == val){
        if(t[x].cnt > 1){t[x].cnt--, t[x].size--;return;}//有相同的就直接cnt--
        bool d = t[ls].rd > t[rs].rd;
        if(ls == 0 || rs == 0) x = ls+rs;//只有一個兒子就直接把那個兒子放到這個位置
        else rotate(x, d), delet(x, val);//不然將x旋下去,找一個隨機值小的替代,直到回到1,2種狀況
    }
    else t[x].size--, delet(t[x].ch[t[x].val<val], val);//遞歸找到要刪除的節點.
}

查排名

仍是由於treap不能改變形態,因此不能像splay同樣直接找到這個點旋轉到根,因此咱們用遞歸的方式求解,咱們用到的目前這個點的權值做爲判斷的依據,並在找到節點的路上不斷累加小於該權值的個數.

看代碼理解一下吧.

int rank(int x, int val){
    if(!x) return 0;
    if(t[x].val == val) return t[ls].size+1;//找到了就返回最小的那個
    if(t[x].val > val) return rank(ls, val);//若是查找的數在x的左邊,則直接往左邊查
    return rank(rs, val)+t[ls].size+t[x].cnt;//不然往右邊查,左邊的全部數累加進答案
}

查找第k小的數

由於只須要找到中序遍歷中的第k個,因此在找第k小的時候能夠直接用splay同樣的方法,也是遞歸求解.

int kth(int root, int k){
    int x = root;
    while(1){
        if(k <= t[ls].size) x = ls;
        else if(k > t[ls].size+t[x].cnt)
            k -= t[ls].size+t[x].cnt, x = rs;
		else return t[x].val;
    }
}

查找前驅/後繼

仍然是由於不能改變樹的形態,須要遞歸求解.一樣的找一個節點的前驅就直接在它左半邊中找一個最大值就能夠了.若是是在這個節點的右邊的話就一直向下遞歸,若是遞歸有一個分支直到葉子節點如下都一直沒找到一個比該權值要小的值,那麼最後要返回一個-inf/inf來防止答案錯誤(同時找到葉子節點下面也是要及時return防止越界).

int pre(int x, int val){
    if(!x) return -inf;//防止越界,同時-inf沒法更新答案,
    if(t[x].val >= val) return pre(ls, val);//若是該節點的權值大於等於要找的權值
    //則不能成爲前驅,遞歸查找左子樹(有可能找到前驅)
    return max(pre(rs, val), t[x].val);//找右子樹中是否存在前驅
}

int nex(int x, int val){//同上
    if(!x) return inf;
    if(t[x].val <= val) return nex(rs, val);
    return min(nex(ls, val), t[x].val);
}

既然treap有這麼多不能實現的操做,那這個treap有什麼用呢?

顯然是有的,咱們由於支持旋轉的treap不能改變樹的形態來完成操做,因此這裏介紹一中更增強大的數據結構:

無旋treap

簡介

無旋treap具備treap的一些性質,好比二叉樹和堆的性質,同時也是一顆平衡樹.

無旋treap是怎麼個無旋的方法的呢?其實相比於帶旋轉的treap,無旋treap只是多了兩個特殊的操做:splitmerge .

那麼這兩個操做究竟是什麼這麼厲害呢?說簡單點,就是一個分離子樹和一個合併子樹的過程.

咱們能夠用split操做分離出1~前k個節點,這樣就能夠經過兩次split操做就能夠提取出任意一段區間了.

而merge操做能夠將兩個子樹合併,並同時維護好新合併的樹的treap全部的性質.

下面終點講一下這兩個操做:

split

首先看一張split的動圖: 操做過程如上,節點中間的值表明權值,右邊的數字表明隨機值. 咱們在操做的過程當中須要將一顆樹剖成兩顆,而後爲了還能進行以後的操做,分離出的兩顆字數必須也是知足性質的,爲了找到這兩顆子樹,咱們在分離的過程當中須要記錄下這兩顆子樹的根.

從圖中能夠看出,其實這個分離的操做也能夠理解爲將一棵樹先剖開,而後再按照必定的順序鏈接起來,也就是將從x節點一直到最坐下或是最右下剖出來,而後再繼續處理剖出來鏈的剩餘部分.

看代碼理解一下吧.

void split(int x, int k, int &a, int &b){//開始時a,b傳的是用來記錄兩顆子樹根的變量
//x表明目前操做到了哪個節點,k是要分離出前k個節點
	if(!x){a = b = 0; return;}//若是沒有節點則須要返回
	if(k <= t[ls].size)	b = x, split(ls, k, a, ls);//若是第k個在左子樹中
	//則往左走,同時左子樹的根就能夠肯定了,那麼就把ls賦值爲根.
	//同時爲了以後要把接下來處理出的節點再連上去,要再傳ls做爲參數,將以後改變爲根的接到如今的x的左兒子
	else a = x, split(rs, k-t[ls].size-1, rs, b);//同理
}

固然,要實現查找前驅後繼的話能夠不用分離前k個節點的方法來分離,能夠直接按照權值來分離節點,方法相似.

void split(int x, int val, int &a, int &b){
    if(!x){a = b = 0; return;}
    if(t[x].val <= val) a = x, split(rs, val, rs, b);
    //若是帶等於就是把>val的節點分到第二顆子樹中.
    //不然就是將<=val的節點分到第一顆子樹中.
    else b = x, split(ls, val, a, ls); up(x);
}

merge

注:圖中最後插入的9位置錯了,應該是在8的右下. 首先merge操做是有前提條件的,要求是必須第一顆樹權值最大的節點要大於第二棵樹權值最小的節點.

由於有了上面那個限制條件,因此右邊的子樹只要是插在左邊的這顆樹的右兒子上就能夠維護它的中序遍歷,那麼咱們就只須要考慮如何維護它平衡樹的性質.

這裏咱們就須要經過玄學的隨機值來維護這個樹的性質了.咱們在合併兩顆樹時,由於左邊的權值是必定小於右邊的,因此左邊的那棵樹必定是以一個根和它的左子樹的形式合併的,而右邊的那棵樹就是以根和右子樹的形式合併的,那麼若是此次選擇的是將左邊的樹合併上來的話,那麼下一次合併過來的位置必定是在這個節點位置的右兒子位置(能夠看圖理解一下).

你能夠把這個過程理解爲在第一個Treap的左子樹上插入第二個樹,也能夠理解爲在第二個樹的左子樹上插入第一棵樹。由於第一棵樹都知足小於第二個樹,因此就變成了比較隨機值來肯定樹的形態。

int merge(int x, int y){
    if(x == 0 || y == 0) return x+y;
    if(t[x].rd < t[y].rd){//最好merge函數不要用宏定義的變量
//由於這個比較的兩顆樹的根的隨機值,用宏定義容易寫錯
        t[x].ch[1] = merge(t[x].ch[1], y);
        up(x); return x;
    }
    else {
        t[y].ch[0] = merge(x, t[y].ch[0]);
        up(y); return y;
    }
}

到這裏兩個核心操做就完成了,那麼那些insert,delete,get_rank,get_pre,get_nex的操做該怎麼作呢?其實很簡單,就考慮一下將這顆樹如何分離,而後從新合併好就能夠了.

插入

插入仍是老套路,先找到位置而後插入.這裏咱們能夠先分離出val和它以前的節點,而後把val權值的節點加到第一顆樹的後面,而後合併.

void insert(int val){
    split(root, val, r1, r2);
    root = merge(r1, merge(newnode(val), r2));
}

至於newnode的話就直接新建一個節點就能夠了.

int newnode(int val){
    t[++cnt].val = val; t[cnt].rd = rand(), t[cnt].size = 1;
    return cnt;
}

刪除

先將val和它前面的權值分離出來,用r1記錄這個根,再在r1樹中分離出val-1的權值的樹,用r2記錄這顆樹,那麼val這個權值必定是已經被分離到以r2爲根的樹中,刪掉這個數(能夠直接把這個位置的節點用它左右兒子合併後的根代替),最後將分離的這幾顆樹按順序合併回去就能夠了.

void delet(int val){
    r1 = r2 = r3 = 0; split(root, val, r1, r3);
    split(r1, val-1, r1, r2);
    r2 = merge(t[r2].ch[0], t[r2].ch[1]);
    root = merge(r1, merge(r2, r3));
}

查找某數的排名

能夠直接將全部比它小的權值分離到一顆樹中,那麼此時排名就是這顆樹的大小+1了.

int rank(int val){
    r1 = r2 = 0; split(root, val-1, r1, r2);
    ans = t[r1].size+1;
    root = merge(r1, r2);
    return ans;
}

查找第k小的數

能夠直接在整顆樹中直接找,操做方法相似splay.

int kth(int rt, int k){
    int x = rt;
    while(1){
        if(k <= t[ls].size) x = ls;
        else if(k > t[ls].size+t[x].cnt)
            k -= t[ls].size+t[x].cnt, x = rs;
        else return x;
    }
}

查找前驅/後繼

以val-1爲分界線將整棵樹分離開,那麼前驅就是第一顆樹中的最大的數字(嚴格比val小). 以val爲分界線將整顆樹分離開,後繼就是第二顆樹中的最小的數字(嚴格比val大).

int pre(int val){
    r1 = r2 = 0; split(root, val-1, r1, r2);
    ans = t[kth(r1, t[r1].size)].val;
    root = merge(r1, r2);
    return ans;
}

int nex(int val){
    r1 = r2 = 0; split(root, val, r1, r2);
    ans = t[kth(r2, 1)].val;
    root = merge(r1, r2);
    return ans;
}

經過節點編號找節點在中序遍歷中的位置

由於treap自己是不能經過編號來進行操做的,它只能經過$split$來分離子樹,因此在只知道節點編號的時候不能很快找到節點的位置.

因此爲了方便尋找節點.咱們在treap中多記錄一個$father$,而後就能夠經過不斷的向上跳來記錄中序遍歷比它小的節點的個數.

而中序遍歷比它小的節點也就是在它左子樹中的節點或是在它祖先的左子樹中的節點.咱們只須要在向根跳的時候將全部左子樹記錄下來就能夠了.可是由於有可能某個節點是根的左兒子,這樣在向上跳的時候就不用重複統計答案了.

int find(int cnt){
    int node = cnt, res = t[t[cnt].ch[0]].size+1;
    while(node != root && cnt){
		if(get(cnt)) res += t[t[t[cnt].fa].ch[0]].size+1;
		cnt = t[cnt].fa;
    }
    return res;
}

而後咱們須要考慮$father$的修改.由於會改變樹的形態的只有$split$和$merge$操做,因此只須要在這兩個函數進行修改就能夠了.其實很簡單,只要在修改兒子的同時修改父親就能夠了.

$split$

void split(int x, int k, int &a, int &b, int faa = 0, int fab = 0){
    if(x == 0){ a = b = 0; return; }
    if(k <= t[t[x].ch[0]].size) t[x].fa = fab, b = x, split(t[x].ch[0], k, a, t[x].ch[0], faa, x);
    else t[x].fa = faa, a = x, split(t[x].ch[1], k-t[t[x].ch[0]].size-1, t[x].ch[1], b, x, fab); up(x);
}

$merge$

int merge(int x, int y){
    if(x == 0 || y == 0) return x+y;
	    if(t[x].rd < t[y].rd){
	    t[x].ch[1] = merge(t[x].ch[1], y);
	    t[t[x].ch[1]].fa = x; up(x); return x;
    }
    else {
	    t[y].ch[0] = merge(x, t[y].ch[0]);
	    t[t[y].ch[0]].fa = y; up(y); return y;
    }
}

如何在$O(n)$時間內構造一顆平衡treap

能夠考慮用棧實現. 每次將一個節點壓入棧中,爲了知足構造的這顆平衡樹的中序遍歷是按照咱們的輸入順序的,因此每次插入的節點必定是在以前構造的部分的右邊的.

因此這裏用了一個棧,每次加入一個節點就把新節點編號加入棧.而且棧中的元素都在棧的右邊.

簡單點說,也就是若是不考慮這個隨機值的話,構造的平衡樹就是一條鏈.從棧頂到棧底是一條從下到上的鏈,像這樣:

可是咱們顯然是不能讓它構造一條鏈的,因此咱們經過隨機值來判斷何時要將這條鏈縮短,並把這條連接到新的根的右兒子上.咱們再假設如今已經插入了4個數,而且隨機值正好是知足它成爲一條鏈的狀況的,那麼該如何插入5呢?(節點邊上的括號內的值是隨機值).

顯然按照隨機值維護堆的性質的原理,5節點應該是要到1節點的下面的,那麼那一重$while$循環就會進行判斷,並保存$last$最後到哪一個位置.(以下圖)

顯然咱們這樣能夠將一條鏈分開來,而且不會改變它原有的鏈.最後將節點按順序接上,這樣新插入一個節點就完成了.

以後每次插入節點都是這個方法,這樣造出的平衡樹也是相對平衡的(隨機值保證了樹平衡的性質).

複雜度的話雖然裏面套了個$while$,可是每一個點都是隻會入棧出棧一次的,因此均攤下來是$O(n)$的.

int build(int len){
    for(int i=1;i<=len;i++){
        int temp = newnode(s[i]), last = 0;
        while(top && t[stk[top]].rd > t[temp].rd)
            up(stk[top]), last = stk[top], stk[top--] = 0;
        if(top) t[stk[top]].ch[1] = temp;
        t[temp].ch[0] = last, stk[++top] = temp;
    }
    while(top) up(stk[top--]); return stk[1];
}

總結

無旋treap到這裏就差很少講完了,感受這個確實是很好用的,起碼這個調試難度比其餘的平衡樹要簡單一些.

分離區間什麼的操做也是這個道理,能夠本身想一下.

例題固然仍是跳到這裏看呀.

相關文章
相關標籤/搜索