[note]fhq_treap


fhq_treap

這東西據說是某個叫范浩強的神仙搞出來的,
他的這種treap可以不用旋轉並且資磁很多平衡樹操作,
復雜度通過隨機的鍵值來保證(樹大致平衡,期望一次操作復雜度\(logn\))
依靠核心函數split和merge實現絕大多數操作
首先建樹的話可以笛卡爾樹優化到\(O(n)\),暴力merge\(O(nlogn)\)
通過以下幾個操作進行說明(以下默認權值與v相同split到左邊)

  • 插入數v:將原樹從v的位置分裂成x,y,合並x,v,再合並x,y.
  • 刪除數v:將原樹從v-1分裂成x,y,將y從v分成y,z,那么將y樹的根刪去(\(merge(ls_y,rs_y)\))
    (ps:如果相同權值的全刪掉,那么整個y樹都不要了),接着把剩下x,y,z的merge回來.
  • 查詢v的rank(rank定義為比v小的數的個數+1):那么從v-1處split成x,y,返回x樹的sz+1.
  • 查詢rank為k的數:同普通treap,不詳述.
  • 查詢v的前驅:從v-1處split成x,y,返回x樹的最后一個,為空則無.(ps:查詢第sz個用求k大的方法)
  • 查詢v的前驅:從v處split成x,y,返回y樹的第一個,為空則無.

split

上述操作的split均按權值分裂,我們先來看看split函數
[按權值split]

void split(int x,int&l,int&r,int k){
	if(!x){l=r=0;return;}//到空節點返回0
	if(val[x]<=k){l=x;split(rs[l],rs[l],r,k);pu(l);}//x分給左樹,接着分x的右兒子
	else{r=x;split(ls[r],l,ls[r],k);pu(r);}//x分給右樹,接着分x的左兒子
}

[按下標split]

void split(int x,int&l,int&r,int k){
	if(!x){l=r=0;return;}
	if(sz[ls[x]]+1<=k){l=x;split(rs[l],rs[l],r,k-sz[ls[x]]-1);pu(l);}//注意修改k
	else{r=x;split(ls[r],l,ls[r],k);pu(r);}
}

對於我們遍歷到每一個點,假如它的權值小於等於k,那么它的所有左子樹,都要分到左邊的樹里,然后遍歷它的右兒子.
假如大於k,把它的所有右子樹分到右邊的樹里,遍歷左兒子.

merge

再看merge函數(默認滿足大根堆性質)

void merge(int&x,int l,int r){
	if(!l||!r){x=l|r;return;}//l或r為空則返回另一個
	if(fix[l]>fix[r]){x=l;merge(rs[x],rs[x],r);}//按鍵值確定父子關系
	else{x=r;merge(ls[x],l,ls[x]);}pu(x);
}

由於第一棵樹的權值都小於第二棵樹,那么就只需要比較鍵值確定父子關系
如果fix[l]>fix[r],那么比較l的右兒子和r
否則比較r的左兒子和l,遞歸merge

以上是一些基本操作,有了這些可以完成[模板]普通平衡樹
放幾個函數的code

void insert(int v){
    int x,y;split(rt,x,y,v-1);
    merge(x,x,newnode(v));merge(rt,x,y);
}
void del(int v){
    int x,y,z;split(rt,x,y,v);split(x,x,z,v-1);
    merge(z,ls[z],rs[z]);merge(x,x,z);merge(rt,x,y);
}
void rk(int v){
    int x,y;split(rt,x,y,v-1);
    printf("%d\n",sz[x]+1);merge(rt,x,y);
}
int kth(int k){
    int x=rt;
    while(1){
        if(k==sz[ls[x]]+1)return val[x];
        if(k<=sz[ls[x]])x=ls[x];
        else k-=sz[ls[x]]+1,x=rs[x];//注意先修改k!!!
    }
}
void pre(int v){
    int x,y;split(rt,x,y,v-1);
    printf("%d\n",sz[x]?kth(x,sz[x]):-inf);
    merge(rt,x,y);
}
void suf(int v){
    int x,y;split(rt,x,y,v);
    printf("%d\n",sz[y]?kth(y,1):inf);
    merge(rt,x,y);
}

有的時候我們需要對一個區間進行操作,這時只要

void XXX(int l,int r){
    int x,y,z;split(rt,x,y,r);split(x,x,z,l-1);
    //do sth such as put reverse tag,put add tag
    merge(x,x,z);merge(rt,x,y);
}

可持久化

說起來目前為止這都是很多平衡樹都能維護的東西,
而且我們發現每次操作都需要split和merge多次,這會是一個較大的常數
那么它牛逼在哪里?
它容易寫
它可以持久化.
我們只需要將原先的根rt開成rt[],每次訪問v版本就在rt[v]上查,
我們知道可持久化對修改有一個要求就是修改不能影響之前的版本,也就是不能改變先前版本的樹的形態
我們發現涉及形態修改的函數只有merge和split,於是我們稍作修改
[可持久化split]

void split(int x,int&l,int&r,int k){
    if(!x){l=r=0;return;}
    if(k>=val[x]){l=++tot;cp(l,x);split(rs[l],rs[l],r,k);pu(l);}//cp即copy,把節點復制過來
    else{r=++tot;cp(r,x);split(ls[r],l,ls[r],k);pu(r);}
}

[可持久化merge]

void merge(int&x,int l,int r){
    if(!l||!r){x=l|r;return;}x=++tot;//新建節點
    if(fix[l]>fix[r]){cp(x,l);merge(rs[x],rs[x],r);}
    else{cp(x,r);merge(ls[x],l,ls[x]);}pu(x);
}

討論中有提到merge不用再開點,因為點已經在split中建好了
對此博主並不太清楚,歡迎大佬指教
我們發現空間是\(nlogn\)(n是修改操作數)的,由於一次操作需要多次split,merge所以空間還要乘個常數
有的帶刪除操作的題我們可以考慮垃圾車回收節點節省空間
有了這些我們可以完成[模板]可持久化平衡樹
其他平衡樹的題應該都可以寫了嗯

end


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM