1. 程式人生 > >史上最強圖解Treap總結, 不是淺談!

史上最強圖解Treap總結, 不是淺談!

大家都很強, 可與之共勉。

Treap = Tree + Heap.
樹堆,在資料結構中也稱Treap,是指有一個隨機附加域滿足堆的性質的二叉搜尋樹,其結構相當於以隨機資料插入的二叉搜尋樹。其基本操作的期望時間複雜度為O(logn)。相對於其他的平衡二叉搜尋樹,Treap的特點是實現簡單,且能基本實現隨機平衡的結構。

Treap 維護堆的性質的方法只用到了左旋和右旋, 程式設計複雜度比Splay小一點, 並且在兩者可完成的操作速度有明顯優勢

開始

每一個節點需要儲存至少四個資訊,當前節點的數值 ( val ), 優先順序 ( key ), 左右兒子 ( ls, rs )。 除此之外, 可能還會儲存以該節點為根的樹的大小 ( siz ), 以及該節點相同的數個數 ( same )。

typedef class TreapNode  {
public:
    int val;
    int siz, key, same;
    TreapNode *ls, *rs;

    inline TreapNode ( )  {     }
    inline TreapNode ( int val, TreapNode* & node ) : val ( val ), key ( rand ( ) ), siz ( 1 ), same ( 1 ) {    ls = rs = node; }

    inline void update ( )  {
        siz = ls -> siz + rs -> siz + same;
    }
} Node;

當然, 博主是用的指標實現, 指標相比陣列有更多的細節需要注意。 在BZOJ-3224中大概比RBT慢2ms。

關於隨機函式

< cstdlib > 中的rand ( ) 速度比較慢, 而在資料結構中對於素的如果要求過高, 可以使用手寫 rand ( )。

inline int rand ( )  {
    static int seed = 233;
    return seed = ( int ) seed * 482711LL % 2147483647; 
}

其中seed可以隨便取一個非零的數。 具體原理是什麼, 當然我也不會證明了。

首先是旋轉
操作

分為左旋和右旋, 我習慣用Zag, Zig來叫。

圖解一下

具體的程式碼如下

一定要記住的是當前節點的要旋轉節點為null時, 不轉。 否則root很有可能變成null, 將會影響一系列的操作。

inline void Zig ( Node* &nd )  {
    tmp = nd -> ls;
    if ( tmp == null ) return;
    nd -> ls = tmp -> rs;
    tmp -> rs = nd;
    tmp -> siz = nd -> siz;
    nd -> update ( );
    nd = tmp;
}

inline void Zag ( Node* &nd )  {
    tmp = nd -> rs;
    if ( tmp == null ) return;
    nd -> rs = tmp -> ls;
    tmp -> ls = nd;
    tmp -> siz = nd -> siz;
    nd -> update ( );
    nd = tmp;
}

接下來是插入操作

插入的過程中, 一定要滿足Treap的特點。
左兒子的值比當前節點小, 右兒子的值比當前節點大。
維護關於key值堆的性質

看完, 你一定會理解原理, 並且熟練掌握。

插入值為18,優先順序為20的結點後,違反了最小堆的定義,因此要進行調整,把優先順序小的往上提,也就是小的優先順序插入的是右子樹,那麼需要進行一次左旋轉( Zag ),這裡進行一次旋轉過後就OK了。

同樣,這種情況左旋轉,旋轉後發現還是不滿足最小堆的定義,並且小優先順序的結點在左子樹,所以還需要進行右旋轉 ( Zig ),如下圖所示:

右旋後,還是不滿足性質, 還需要左旋 ( Zag ):

當然這是在遞迴呼叫之中實現的。

如果該節點為null, 就新開一個。 用建構函式會很方便。
如果遇到插入兩個相同的值, 那麼該節點的same直接+1就好。
完成每一個節點的插入, 都要update
這個update()操作的優秀之處就在於null空節點, 若使用NULL系統自帶的空指標, 那麼直接指向左右兒子就會指出去, 報錯。。。

程式碼如下:

inline void Insert ( Node* &nd, int& val )  {
    if ( nd == null )  {
        nd = ++tail;
            *nd = Node ( val, null );
        return;
    }

    if ( nd -> val == val )  ++nd -> same;
    else  {
        if ( val > nd -> val )  {
            Insert ( nd -> rs, val );
            if ( nd -> rs -> key < nd -> key )
            Zag ( nd );
        }   else  {
            Insert ( nd -> ls, val );
            if ( nd -> ls -> key < nd -> key )
                Zig ( nd );
        }
    }

    nd -> update ( );
}

之後是刪除操作

注意:

這是二叉樹刪除法
對比插入, 相對於刪除要複雜一些, 可以直接看程式碼明白。

不要忘了update()!!!

inline void Delete ( Node* &nd, int x )  {
    if ( nd == null )  return;
    if ( nd -> val == x )  {
        if ( nd -> same > 1 )  {
            --nd -> same; nd -> update ( ); return;
        }
        if ( nd -> ls == null && nd -> rs == null ) { nd = null; return;  }
        else if ( nd -> ls == null && nd -> rs )  nd = nd -> rs;
        else if ( nd -> ls && nd -> rs == null )  nd = nd -> ls;
        if ( nd -> ls -> key < nd -> rs -> key ) { Zig ( nd ); Delete ( nd -> rs, x );  }
        else  { Zag ( nd ); Delete ( nd -> ls, x );  }
    }  else if ( x > nd -> val )  Delete ( nd -> rs, x );
            else  Delete ( nd -> ls, x );
    nd -> update ( );
}

主要的就是這兩個那麼還有前驅,後驅,排名,第K大。

那麼唯一注意的是前後驅在題目中的定義。

如BZOJ-1588

前後驅可以等於節點數值本身
那麼就應該這麼寫

inline int query_pre ( Treap* &nd, int val )  {
    if ( nd == null )  return 0xefefefef;
    if ( val < nd -> val )
        return query_pre ( nd -> ls, val );
    return max ( nd -> val, query_pre ( nd -> rs, val ) );
}

inline int query_post ( Treap* &nd, int val )  {
    if ( nd == null )  return 0x7fffffff;
    if ( val > nd -> val )
        return query_post ( nd -> rs, val );
    return min ( nd -> val, query_post ( nd -> ls, val ) );
}

如果是BZOJ-3224, 讀錯題了就Wa爽了。。。

inline void query_pre ( Node* &nd, int &ans, int val )  {
    if ( nd == null )  return;
    if ( val > nd -> val )
        ans = nd -> val, query_pre ( nd -> rs, ans, val );
    else query_pre ( nd -> ls, ans, val );
}

inline void query_post ( Node* &nd, int &ans, int val )  {
    if ( nd == null )  return;
    if ( val < nd -> val )
        ans = nd -> val, query_post ( nd -> ls, ans, val );
    else query_post ( nd -> rs, ans, val );
}

當然根據定義也可以用kth 與 rank 兩者一起求出前後驅。

具體程式碼及細節請讀者自己推敲

給出完整版參考程式碼
注意的是 Init( ) 中的 null 維護的節點資訊。

#include <bits/stdc++.h>

unsigned int seed, n, opt;

int ans, x;

inline int rand ( )  {
    return seed = ( int ) seed * 482711LL % 2147483647; 
}

typedef class TreapNode  {
public:
    int val;
    int siz, key, same;
    TreapNode *ls, *rs;

    inline TreapNode ( )  {     }
    inline TreapNode ( int val, TreapNode* & node ) : val ( val ), key ( rand ( ) ), siz ( 1 ), same ( 1 ) {    ls = rs = node; }

    inline void update ( )  {
        siz = ls -> siz + rs -> siz + same;
    }
} Node;

Node *pool, *root, *tail, *null, *tmp;

inline void Init ( )  {
    seed = 233;
    pool = new Node [ n + 5 ];
    root = null = tail = pool;
    null -> siz = 0, null -> same = 0, null -> val = 0, null -> key = rand( ), null -> ls = null -> rs = null;
}

inline void Zig ( Node* &nd )  {
    tmp = nd -> ls;
    if ( tmp == null ) return;
    nd -> ls = tmp -> rs;
    tmp -> rs = nd;
    tmp -> siz = nd -> siz;
    nd -> update ( );
    nd = tmp;
}

inline void Zag ( Node* &nd )  {
    tmp = nd -> rs;
    if ( tmp == null ) return;
    nd -> rs = tmp -> ls;
    tmp -> ls = nd;
    tmp -> siz = nd -> siz;
    nd -> update ( );
    nd = tmp;
}

inline void Insert ( Node* &nd, int& val )  {
    if ( nd == null )  {
        nd = ++tail;
            *nd = Node ( val, null );
        return;
    }

    if ( nd -> val == val )  ++nd -> same;
    else  {
        if ( val > nd -> val )  {
            Insert ( nd -> rs, val );
            if ( nd -> rs -> key < nd -> key )
            Zag ( nd );
        }   else  {
            Insert ( nd -> ls, val );
            if ( nd -> ls -> key < nd -> key )
                Zig ( nd );
        }
    }

    nd -> update ( );
}

inline int kth ( Node* &nd, int k )  {
    int tmp = nd -> ls -> siz + nd -> same;
    if ( nd -> ls -> siz < k && k <= tmp )  return nd -> val;
    return nd -> ls -> siz >= k ? kth ( nd -> ls, k ) : kth ( nd -> rs, k - tmp );
}

inline void Delete ( Node* &nd, int x )  {
    if ( nd == null )  return;
    if ( nd -> val == x )  {
        if ( nd -> same > 1 )  {
            --nd -> same; nd -> update ( ); return;
        }
        if ( nd -> ls == null && nd -> rs == null ) { nd = null; return;  }
        else if ( nd -> ls == null && nd -> rs )  nd = nd -> rs;
        else if ( nd -> ls && nd -> rs == null )  nd = nd -> ls;
        if ( nd -> ls -> key < nd -> rs -> key ) { Zig ( nd ); Delete ( nd -> rs, x );  }
        else  { Zag ( nd ); Delete ( nd -> ls, x );  }
    }  else if ( x > nd -> val )  Delete ( nd -> rs, x );
            else  Delete ( nd -> ls, x );
    nd -> update ( );
}

inline int query_rank ( Node* &nd, int x )  {
    if ( nd == null )  return 0;
    if ( nd -> val == x )  return nd -> ls -> siz + 1;
    else  return  ( x > nd -> val ) ? nd -> ls -> siz + nd -> same + query_rank ( nd -> rs, x )
                                    : query_rank ( nd -> ls, x );
}

inline void query_pre ( Node* &nd, int &ans, int val )  {
    if ( nd == null )  return;
    if ( val > nd -> val )
        ans = nd -> val, query_pre ( nd -> rs, ans, val );
    else query_pre ( nd -> ls, ans, val );
}

inline void query_post ( Node* &nd, int &ans, int val )  {
    if ( nd == null )  return;
    if ( val < nd -> val )
        ans = nd -> val, query_post ( nd -> ls, ans, val );
    else query_post ( nd -> rs, ans, val );
}

using std :: cin;
using std :: cout;
using std :: endl;

int main ( )  {
    cin >> n;
    Init ( );
    register int i = 1;
    loop :;  {
        cin >> opt >> x;
        switch ( opt )  {
            case 1:
                Insert ( root, x );  break;
            case 2:
                Delete ( root, x );  break;
            case 3:
                cout << query_rank( root, x ) << endl; break;
            case 4:
                cout << kth ( root, x ) << endl; break;
            case 5:
                query_pre ( root, ans, x ); cout << ans << endl; break;
            case 6:
                query_post ( root, ans, x ); cout << ans << endl; break;
        }
    }  if ( ++i <= n )  goto loop;
}