「學習筆記」 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]\)
\(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];
}
完整程式碼
#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好用多啦