1. 程式人生 > >「學習筆記」 FHQ Treap

「學習筆記」 FHQ Treap

FHQ Treap

FHQ Treap (%%%發明者範浩強年年NOI金牌)是一種神奇的資料結構,也叫非旋Treap,它不像Treap zig zag搞不清楚(所以叫非旋嘛),也不像Splay完全看不懂,而且它能完成Treap與Splay能完成的所有事,程式碼短,理解也容易。


基本操作

FHQ Treap和Treap很像,都是給每個節點一個隨機的權值,使它滿足堆的性質。建議先了解Treap(沒必要實現,懂得原理即可)。不過,如果有兩個節點值相同,FHQ Treap不會用一個數組cnt記錄個數,而是直接再開一個節點。

FHQ的基本操作只有兩個:Split與Merge。

Split表示把一棵樹分成兩棵,Merge表示把一棵樹合併成一棵。


變數&函式約定

int L[MAXN], R[MAXN], sz[MAXN], rk[MAXN], val[MAXN], tot;
int root;

int New( int v ){ return val[++tot] = v, rk[tot] = rand(), L[tot] = R[tot] = 0, sz[tot] = 1, tot; }

#define Updata(x) sz[x] = sz[L[x]] + sz[R[x]] + 1

沒寫成結構體,沒寫成指標。

\(L[i]\)表示\(i\)的左兒子,\(R[i]\)表示\(i\)的右兒子,\(sz[i]\)

表示以\(i\)為根的子樹包含的節點數,\(rk[i]\)表示為了保持平衡隨機賦予的權值,\(val[i]\)表示該節點儲存的值,\(tot\)表示節點數,\(root\)表示當前的根節點。

\(New(v)\)表示新建一個值為\(v\)的節點(可以看成一棵只有一個節點平衡樹)

\(Updata(x)\)表示更新節點\(x\)\(sz\)

提醒:這裡“值”與“權值”是不一樣的,“值”表示節點儲存的值,“權值“僅僅用於維持平衡,注意區分


Split

怎麼分割呢?

常見的分割方法有兩種,一種是按值分,一種是按排名分(實現差不多,這裡只講按值分)。

先來看看定義。

void Split( int c, int k, int &x, int &y );

c表示當前要分割的樹的根節點,並且把值\(\le k\)的節點分割出來,構成一棵樹,把\(x\)賦為根節點,其他節點另外構成一棵樹,把\(y\)賦為其根節點。\(x\)\(y\)用引用(&)更方便處理。

對於當前的樹,如果根節點\(c\)的值\(\le k\)\(c\)的左子樹也全部\(\le k\),所以我們可以把\(x\)賦為\(c\),保留左子樹,將右子樹\(\le k\)的部分分割出來作為\(x\)的右子樹。剩下的部分自然也就是在\(> k\)的部分。\(>k\)的情況同理。具體我們用遞迴實現。

void Split( int c, int k, int &x, int &y ){
    if ( c == 0 ){ x = y = 0; return; }//如果當前處理的樹為空,分出的兩個子樹當然也為空,所以直接賦值返回。
    if ( val[c] <= k ) x = c, Split( R[c], k, R[x], y );//如果根節點值小於等於k,把x賦為c,繼續處理右子樹,並把小於等於k的部分分到x的右子樹,其他分到y
    else y = c, Split( L[c], k, x, L[y] );
    Updata(c);//別忘了更新sz
}

Merge

上面分割的操作不會改變堆的性質與二叉查詢樹的性質,但是在合併的時候要注意保持堆的性質。

void Merge( int &c, int x, int y );

表示把以\(x\)\(y\)為根節點的樹合併,將\(c\)賦為根節點。

注意:上面分割時x的所有節點的值都小於y的,合併時也要注意x的所有節點小於等於y,否則會出錯

由於\(x\)\(y\)的權值在兩顆樹中是最大的,所以合併後的樹根節點不是\(x\)就是\(y\)。所以比較\(x\)\(y\)的權值就可以判斷誰為根節點。

假設以\(x\)為根。因為保證\(x\)的所有節點的值都小於等於\(y\)的,所以\(y\)肯定會合並在\(x\)的右子樹。所以,我們不用動\(x\)的左子樹,合併\(x\)的右子樹與\(y\)作為\(x\)的右子樹。\(y\)為根時同理。這樣,就巧妙完成了同時維護堆的性質與二叉查詢樹的性質。

我們還是用遞迴。

void Merge( int &c, int x, int y ){
    if ( !x || !y ){ c = x | y; return; }
    if ( rk[x] >= rk[y] ) c = x, Merge( R[x], R[c], y );
    else c = y, Merge( L[y], x, L[c] );
    Updata(c);
}

我剛開始也理解不了這兩種操作。主要瓶頸在難以想象。其實可以看做只處理當前的,未處理的留到下一步,反正操作方法都一樣。


剩下的都可以用這兩種操作實現。


插入操作

直接把它分成\(\le v\)的樹和\(> v\)的樹,將新建的節點與\(\le v\)的樹合併,再與\(>y\)樹合併即可。

//opt 1
void Ins( int v ){
    int x, y, z(New(v));
    Split( root, v, x, y );
    Merge( x, x, z );
    Merge( root, x, y );
}

刪除操作

分成\(\le k\)\(> k\)兩顆樹,再分成\(<k\)\(=k\)\(> k\)三棵樹,將\(=k\)左右子樹合併,相當於刪去\(=k\)的一個節點,然後將三棵樹重新合併即可。

// opt 2
void Del( int v ){
    int x, y, z;
    Split( root, v, x, y );
    Split( x, v - 1, x, z );
    Merge( z, L[z], R[z] );
    Merge( x, x, z );
    Merge( root, x, y );
}

查詢排名

其實可以用while迴圈,,,但是,,,我,,,懶,,,所,,,以,,,直,,,接,,,,,,,

//opt 3
int GetRankByVal( int v ){
    int x, y, t;
    Split( root, v - 1, x, y );
    t = sz[x];
    Merge( root, x, y );
    return t + 1;
}

查詢值

這真的不能用Split和Merge偷懶了,,,所以乖乖寫個while吧~

技術含量不高,自行理解。

//opt 4
int GetValByRank( int rk ){
    int c(root);
    while( c ){
        if ( sz[L[c]] + 1 == rk ) return val[c];
        else if ( sz[L[c]] >= rk ) c = L[c];
        else rk -= 1 + sz[L[c]], c = R[c];
    }
    return -1;//題目沒要求。。。只是為了自己查錯
}

查詢字首

分成兩顆樹\(<v\)\(\ge v\),在\(<v\)樹中找最大值即可。

//opt 5
int GetPre( int v ){
    int x, y, z;
    Split( root, v - 1, x, y );
    z = x;
    while( R[z] ) z = R[z];
    Merge( root, x, y );
    return val[z];
}

查詢字尾

與查詢字首同理。

//opt 6
int GetNxt( int v ){
    int x, y, z;
    Split( root, v, x, y );
    z = y;
    while( L[z] ) z = L[z];
    Merge( root, x, y );
    return val[z];
}

完整程式碼

洛谷P3369 【模板】普通平衡樹

#include<bits/stdc++.h>
using namespace std;
#define MAXN 100005

int L[MAXN], R[MAXN], sz[MAXN], rk[MAXN], val[MAXN], tot;
int root;

int New( int v ){ return val[++tot] = v, rk[tot] = rand(), L[tot] = R[tot] = 0, sz[tot] = 1, tot; }
#define Updata(x) sz[x] = sz[L[x]] + sz[R[x]] + 1

void Split( int c, int k, int &x, int &y ){
    if ( c == 0 ){ x = y = 0; return; }
    if ( val[c] <= k ) x = c, Split( R[c], k, R[x], y );
    else y = c, Split( L[c], k, x, L[y] );
    Updata(c);
}

void Merge( int &c, int x, int y ){
    if ( !x || !y ){ c = x | y; return; }
    if ( rk[x] >= rk[y] ) c = x, Merge( R[x], R[c], y );
    else c = y, Merge( L[y], x, L[c] );
    Updata(c);
}
//opt 1
void Ins( int v ){
    int x, y, z(New(v));
    Split( root, v, x, y );
    Merge( x, x, z );
    Merge( root, x, y );
}
// opt 2
void Del( int v ){
    int x, y, z;
    Split( root, v, x, y );
    Split( x, v - 1, x, z );
    Merge( z, L[z], R[z] );
    Merge( x, x, z );
    Merge( root, x, y );
}
//opt 3
int GetRankByVal( int v ){
    int x, y, t;
    Split( root, v - 1, x, y );
    t = sz[x];
    Merge( root, x, y );
    return t + 1;
}
//opt 4
int GetValByRank( int rk ){
    int c(root);
    while( c ){
        if ( sz[L[c]] + 1 == rk ) return val[c];
        else if ( sz[L[c]] >= rk ) c = L[c];
        else rk -= 1 + sz[L[c]], c = R[c];
    }
    return -1;
}
//opt 5
int GetPre( int v ){
    int x, y, z;
    Split( root, v - 1, x, y );
    z = x;
    while( R[z] ) z = R[z];
    Merge( root, x, y );
    return val[z];
}
//opt 6
int GetNxt( int v ){
    int x, y, z;
    Split( root, v, x, y );
    z = y;
    while( L[z] ) z = L[z];
    Merge( root, x, y );
    return val[z];
}

int T;

int main(){
    srand(time(0));//隨機數種子別忘了
    root = New(INT_MAX);//虛節點,避免一個節點都沒有不方便合併。注意要用一個很大的數,查詢排名時就不用-1
    scanf( "%d", &T );
    while( T-- ){
        int opt, x;
        scanf( "%d%d", &opt, &x );
        switch( opt ){
            case 1: Ins(x); break;
            case 2: Del(x); break;
            case 3: printf( "%d\n", GetRankByVal(x) ); break;
            case 4: printf( "%d\n", GetValByRank(x) ); break;
            case 5: printf( "%d\n", GetPre(x) ); break;
            case 6: printf( "%d\n", GetNxt(x) ); break;
        }
    }
    return 0;
}

FHQ Treap還可以資瓷可持久化~比Treap、Splay好用多啦