[非旋平衡树]fhq_treap概念及模板,例题:普通平衡树,文艺线段树
文章目錄
- 概念
- 全套模板
- push_up模板
- split拆樹模板(按權值拆)
- split拆樹模板(按個數拆)
- merge合并模板(地址版)
- merge合并模板(帶返回根)
- 區間模板
- insert插入模板
- delete刪除模板
- find_kth找第k大模板
- get_rank找排名模板
- pre找前驅模板
- suf找后驅模板
- 例題1:普通平衡樹
- 題目
- 代碼實現
- 例題2:文藝線段樹
- 題目
- 代碼實現
建議在看這篇博客之間要了解一下帶旋Treap
我會在模板前面寫上一部分的思路講解,幫助各位理解
概念
根據它的名字我們也可以得知,這種數據結構就是treaptreaptreap的后代,只不過不帶旋轉,其余都是一致的
所以在運用和代碼上會有所異同。它比treaptreaptreap多了splitsplitsplit(拆樹)和mergemergemerge(合并)操作,所以得到的結果是可以多處理數據結構的區間問題。以一換一
接下來我們就重點介紹splitsplitsplit和mergemergemerge還有區間操作到底是個什么玩意兒???
全套模板
因為是自己修改后的模板,可能會有不嚴謹處,歡迎大家指出并更正!
先照樣介紹各個數組變量的含義:
SizeSizeSize:表示節點數量也可作最后一個點編號
cnt[p]cnt[p]cnt[p]:表示編號為ppp,值為xxx在treaptreaptreap中插入的次數
key[p]key[p]key[p]:表示該點ppp的值為xxx
rd[p]rd[p]rd[p]:就是我們自己搞的修正值,用rand()rand()rand()函數隨機生成
siz[p]siz[p]siz[p]:編號為ppp的子樹包括本身在內的節點數量即大小
son[p][2]son[p][2]son[p][2]:son[p][0]son[p][0]son[p][0]表示p的左兒子,son[p][1]son[p][1]son[p][1]表示ppp的右兒子
push_up模板
先蓄蓄力,放松放松
void push_up ( int x ) {siz[x] = siz[son[x][0]] + siz[son[x][1]] + cnt[x]; }split拆樹模板(按權值拆)
splitsplitsplit拆樹的結果就是把樹根據要求值kkk拆成兩半
左邊全是值≤k≤k≤k的點,右邊全是值>k>k>k的點
上圖講解:充分運用畫過的圖,我帶領大家走一遍,再不懂就不管本蒟蒻了
假設我們的kkk為35,那么首先從根節點1開始,發現1的權值25小于35
這個時候我們就能確定根節點以及根節點的左子樹的權值全都是小于35的
那么這個時候它們是屬于拆分后左邊的子樹的
但是我們會發現根節點的右子樹也存在可能值大于35的節點
我們就需要繼續往下拆分
接下來走到節點3,發現權值大于35,可以得出的結論是3節點以及它的右子樹的權值都是大于35的,應該是屬于拆分后的右子樹,
但是同樣的我們不能肯定它的左兒子是否也是歸屬于右邊,繼續往左拆分
最后走到了葉子節點,發現節點4的權值小于等于35也應該歸于左邊
這個時候就把節點4接到根節點1的右邊,成功把1和3的邊給斷掉
最后一層一層回溯,最頂層的兩個根節點就分別為1,3
節點1統領了所有權值小于等于kkk的子樹,節點3統領了所有權值大于kkk的子樹
我的寫法是傳地址,這樣就直接更改了
void split ( int p, int &l, int &r, int x ) {if ( ! p ) {l = r = 0;return;}if ( key[p] <= x ) {l = p;split ( son[p][1], son[p][1], r, x );push_up ( l );}else {r = p;split ( son[p][0], l, son[p][0], x );push_up ( r );} }
split拆樹模板(按個數拆)
此代碼有適用范圍!!!在某些題中會出錯
按下標拆的思路與按權值拆是一樣的,只不過往右子樹找的時候記得把左子樹和根占得位置給減掉即可
拆出來的左子樹的個數恰好是給定的kkk,右子樹就是剩下來的所有點
void split_id ( int p, int &l, int &r, int x ) {if ( ! p ) {l = r = 0;return;}if ( siz[son[p][0]] + 1 <= x ) {l = p;split_id ( son[p][1], son[p][1], r, x - siz[son[p][0]] - 1 );push_up ( l );}else {r = p;split_id ( son[p][0], l, son[p][0], x );push_up ( r );} }merge合并模板(地址版)
我們可以發現拆分子樹的時候,改變了樹的形態,這也是無法進行treaptreaptreap的旋轉操作的一個原因,
百因必有果,你的報應就是我
既然方便了splitsplitsplit拆分,改變了樹的形態,我們就必須再寫一個補丁函數,把樹進行還原修復
但是我們不再是使用權值kkk進行,我們思考treaptreaptreap用旋轉的目的是為了維護樹的鍵值不是從大到小就是從小到大
反正就是要有一定的順序
那么mergemergemerge的目的也是維護樹的鍵值有順序
本來splitsplitsplit拆的樹也是我們維護好了順序的
所以mergemergemerge合并的時候根據鍵值順序來合并,也能還原splitsplitsplit所拆的樹
在這里我仍然選擇的傳地址直接改在原來的地方,如果把上邊的splitsplitsplit理解了,那么我相信這個也就很好理解了
merge合并模板(帶返回根)
int merge ( int x, int y ) {if ( ! x || ! y )return x + y;if ( rd[x] < rd[y] ) {son[x][1] = merge ( son[x][1], y );push_up ( x );return x;}else {son[y][0] = merge ( x, son[y][0] );push_up ( y );return y;} }區間模板
其實就是先把這個區間[l,r][l,r][l,r]拆出來然后搞一波,再把它合并回去
可以理解為先把部隊里某一個方陣的士兵扯出來再捅幾刀最后再讓他們歸隊,好殘忍
void XXX ( int x, int y ) {int l, r, L, R;spilt ( root, l, r, y );split ( l, L, R, x - 1 );//區間里面進行的操作merge ( l, L, R );merge ( root, l, r ); }我們以翻轉reversereversereverse為例,小聲bb:是為了讓你們做文藝平衡樹更簡單
void reverse ( int x, int y ) {int l, r, L, R;spilt ( root, l, r, y );split ( l, L, R, x - 1 );lazy[R] = !lazy[R];//對[x,y]區間進行打標,1表示翻轉,0表示沒有翻轉 merge ( l, L, R );merge ( root, l, r ); }簡單過渡一下:其實多做幾道題多用用模板會對代碼更加理解,為了方便各位理解下面更改的函數,在這里簡單總結一下splitsplitsplit和mergemergemerge的思路
split(root,l,r,x)split(root,l,r,x)split(root,l,r,x)表示把以rootrootroot為根的子樹按照權值xxx拆分,lll存儲著小于等于xxx的子樹的根,rrr存儲著大于xxx的子樹的根
merge(root,l,r)merge(root,l,r)merge(root,l,r)表示把一棵子樹的根為lll和另一棵子樹的根為rrr合并為一棵根為rootrootroot的新根
那么其余的操作都可以用splitsplitsplit和mergemergemerge改變我們以前的寫法,新朋友就要多用用嘛!
insert插入模板
insertinsertinsert之前我們是用的遞歸方式,在這里就要充分運用splitsplitsplit和mergemergemerge
我聲明一下,很多很多篇博客都是直接新建一個節點,本蒟蒻就不理解了,對于一個點它可能已經出現在樹上了,這個時候就直接cnt++cnt++cnt++,為什么要選擇新建點呢?
所以我就費了九牛二虎之力寫出了自己想要的模板
當然對于某部分的題各個點之間是互不相同的,或其它特殊的要求,我的代碼就與大佬們成為一流的了,這個時候就可以刪掉我代碼中if的判斷即可,不刪也不影響,最多代碼長了一丟丟而已啦~
- 首先我們把樹先拆成權值都≤x≤x≤x的子樹和權值都>x>x>x的子樹
- 再把權值≤x≤x≤x的子樹拆分成權值≤x?1≤x-1≤x?1的樹和權值>x?1>x-1>x?1也就是權值等于xxx的樹
- 接著我們就判斷儲存權值等于xxx的樹的節點是否為空,
- 如果為空就意味著樹上并沒有該點,就新建一個點;
- 否則就直接cnt++cnt++cnt++再updateupdateupdate一下
- 拆了就要合并,我們怎么拆的就怎么倒著并回去,很簡單的,本蒟蒻都能自己打出來
delete刪除模板
仿照insertinsertinsert的思路
- 先把值為xxx的這個點拆出來
- 接下來判斷如果這個點插入的次數是否大于1
- 如果大于可以直接cnt??cnt--cnt??,該點不會消失,倒著合并回去;
- 否則該點就應該消失在樹上,我們可以通過不讓它參與合并,排擠它 ,那么它就不會出現在樹上了,直接把值小于等于x?1x-1x?1的樹和值大于xxx的樹合并即可
剩下的查找其實是可以照搬的,但是我還是給大家分享一些其它的寫法吧!!
find_kth找第k大模板
這個我還是很喜歡這種寫法的,所以就不更改了
Upd:
下面求排名為 xxx 的數的方法不一定是對的。
因為按照個數大小分裂代碼的正確性當且僅當數據中每個數互不相等。
顯然,設想某個數有若干個,占據了排名為一段的區間,如果按照 x/x?1x/x-1x/x?1 的個數分,全都劃在該數身上,則 RRR 就是個空子樹了。
如果直接判 RRR 是否為空也是錯誤的。
但是我也不知道為什么??!!所以還是麻煩大家寫上面的方法。
也有可能是因為博主的其它模板某些限制把。。。
數據結構真是一個比一個玄學!!凸(艸皿艸 )
get_rank找排名模板
我們就充分運用新學函數,思考一下如果把≤x?1≤x-1≤x?1的樹拆出來
那么它的大小+1+1+1是不是就是xxx的rankrankrank排名呢!!!實在是
pre找前驅模板
找前驅,這里是嚴格小于的情況,先拆分一下看有木有權值小于xxx的點
有的話我們就調用findfindfind_kthkthkth在拆分出來的那棵子樹中去找最后一個也就是xxx的前一個
suf找后驅模板
找后驅,與找前驅相似,這里是嚴格大于的情況,先拆分一下看有木有權值大于xxx的點
有的話我們就調用findfindfind_kthkthkth在拆分出來的那棵子樹中去找第一個也就是xxx的后一個
Upd:當然你可以直接暴力的裂開。以找前驅為例,把 ≤x?1\le x-1≤x?1 的子樹列出來,從子樹的根開始瘋狂走右兒子(如果有)。
void find_pre( int x ) {int l, r;split_val( rt, x - 1, l, r );int now = l;while( t[now].rson ) now = t[now].rson;printf( "%d\n", t[now].val );rt = merge( l, r ); }void find_suf( int x ) {int l, r;split_val( rt, x, l, r );int now = r;while( t[now].lson ) now = t[now].lson;printf( "%d\n", t[now].val );rt = merge( l, r ); }老套路來些題目練習練習,實在是太模板了,直接器官移植都能過,哎╮(╯▽╰)╭
例題1:普通平衡樹
題目
點擊查看
代碼實現
一樣一樣的,進行器官移植即可
#include <cstdio> #include <algorithm> using namespace std; #define MAXN 100005 #define INF 0x7f7f7f7f int root, n, Size; int son[MAXN][2], cnt[MAXN], siz[MAXN], rd[MAXN], key[MAXN];void push_up ( int x ) {siz[x] = siz[son[x][0]] + siz[son[x][1]] + cnt[x]; }void split ( int p, int &l, int &r, int x ) {if ( ! p ) {l = r = 0;return;}if ( key[p] <= x ) {l = p;split ( son[p][1], son[p][1], r, x );push_up ( l );}else {r = p;split ( son[p][0], l, son[p][0], x );push_up ( r );} }void merge ( int &p, int x, int y ) {if ( ! x || ! y ) {p = x + y;return;}if ( rd[x] < rd[y] ) {p = x;merge ( son[p][1], son[p][1], y );}else {p = y;merge ( son[p][0], x, son[p][0] );}push_up ( p ); }void insert ( int x ) {int l, r, L, R;split ( root, l, r, x );split ( l, L, R, x - 1 );if ( R ) {cnt[R] ++;push_up ( R );merge ( l, L, R );merge ( root, l, r );}else {++ Size;cnt[Size] = siz[Size] = 1;rd[Size] = rand ();key[Size] = x;merge ( l, L, Size );merge ( root, l, r );} }void delet ( int x ) {int l, r, L, R;split ( root, l, r, x );split ( l, L, R, x - 1 );if ( R && cnt[R] > 1 ) {cnt[R] --;push_up ( R );merge ( l, L, R );merge ( root, l, r );}elsemerge ( root, L, r ); }int find_kth ( int rt, int x ) {if ( siz[son[rt][0]] >= x )return find_kth ( son[rt][0], x );else if ( siz[son[rt][0]] + cnt[rt] < x )return find_kth ( son[rt][1], x - siz[son[rt][0]] - cnt[rt] );elsereturn key[rt]; }int pre ( int x ) {int l, r, result;split ( root, l, r, x - 1 );if ( siz[l] )result = find_kth ( l, siz[l] );elseresult = INF;merge ( root, l, r );return result; }int suf ( int x ) {int l, r, result;split ( root, l, r, x );if ( siz[r] )result = find_kth ( r, 1 );elseresult = INF;merge ( root, l, r );return result; }void get_rank ( int x ) {int l, r;split ( root, l, r, x - 1 );printf ( "%d\n", siz[l] + 1 );merge ( root, l, r ); }int main() {scanf ( "%d", &n );while ( n -- ) {int opt, x;scanf ( "%d %d", &opt, &x );switch ( opt ) {case 1 : insert ( x ); break;case 2 : delet ( x ); break;case 3 : get_rank ( x ); break;case 4 : printf ( "%d\n", find_kth ( root, x ) ); break;case 5 : printf ( "%d\n", pre ( x ) ); break;case 6 : printf ( "%d\n", suf ( x ) ); break;}}return 0; }例題2:文藝線段樹
題目
點擊查看題目
代碼實現
在這里因為涉及到一個區間翻轉問題,我們就可以類比線段樹打lazylazylazy標記,也對treaptreaptreap樹打一個標記
那么在我們進行split,mergesplit,mergesplit,merge操作時,要保證對于一個點,它的左兒子和右兒子是對的,所以這里要寫一個標記下放的pushdownpushdownpushdown
最后輸出數列的時候也采用遞歸的方式,左中右的中序遍歷,在這之間順便進行標記下放
因此這道題啟示我們,隨著我們的操作要求的不一樣,在splitsplitsplit和mergemergemerge中一些語句可能會發生順序變換,不能盲目地去背模板,一定要理解
可能會有部分代碼細節錯誤,因為這些題實在是水,導致有些寫錯的代碼還是能跑過數據
總結
以上是生活随笔為你收集整理的[非旋平衡树]fhq_treap概念及模板,例题:普通平衡树,文艺线段树的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 这个病毒有多可怕病毒到底有多可怕
- 下一篇: 【2019CSP-J 普及组题解】数字游