模板:二叉搜索树平衡树
文章目錄
- 前言
- 二叉搜索樹
- 代碼
- treap
- 代碼
- splay
- 開點
- 旋轉
- splay
- 插入
- 查找第k大元素
- 查找給定元素的排名
- 前驅&后繼
- 刪除
- 完整代碼
- 練習總結
前言
終于開始學這個東西了
看了好幾篇博客才找到一篇可讀的qwq
我曾經還以為線段樹碼量大…我真傻,真的
所謂平衡樹,就是把二叉搜索樹加了一個隨機權值
并通過旋轉使這個權值始終符合堆的性質
(treap=tree+heap)
我覺得平衡樹主要的功能就是維護排名相關的東西
(update:更正觀點!平衡樹最好用的地方還是區間問題,排名問題在序列上可以主席樹,動態的可以樹狀數組,為啥要寫splay這種東西…)
前驅后繼這些其實都可以直接拿set偷懶
(當然本剛學treap1h的蒟蒻的理解完全沒有參考價值)
一開始WA成了60分qwq
千萬注意一定不要落掉無處不在的pushup!
二叉搜索樹
不學BST,何以treap? ——魯迅
二叉搜索樹是一種二叉樹的樹形數據結構,其定義如下:
空樹是二叉搜索樹。
若二叉搜索樹的左子樹不為空,則其左子樹上所有點的附加權值均小于其根節點的值。
若二叉搜索樹的右子樹不為空,則其右子樹上所有點的附加權值均大于其根節點的值。
二叉搜索樹的左右子樹均為二叉搜索樹。
二叉搜索樹上的基本操作所花費的時間與這棵樹的高度成正比。對于一個有 n個結點的二叉搜索樹中,這些操作的最優時間復雜度為 Ologn,最壞為On。隨機構造這樣一棵二叉搜索樹的期望高度為logn。
代碼
#include<bits/stdc++.h> using namespace std; const int N=1e5+100; #define ll long long int n,m,k; int x,y; int cnt[N],ls[N],rs[N],val[N],siz[N],tot=1; void insert(int &o,int v){//插入元素if(!o){o=++tot;val[o]=v;ls[o]=rs[o]=0;siz[o]=cnt[o]=1;}siz[o]++;if(val[o]==v) {cnt[o]++;return;}if(v<val[o]) insert(ls[o],v);else insert(rs[o],v); } int delmin(int &o){if(!ls[o]){int u=o;o=rs[o];return u;}else{int u=delmin(ls[o]);siz[o]-=cnt[u];return u;} } void del(int &o,int v){//刪除元素siz[o]--;if(val[o]==v){if(cnt[o]>1) cnt[o]--;else if(ls[o]&&rs[o]) o=delmin(rs[o]);else o=ls[o]+rs[o];return;}if(v<val[o]) del(ls[o],v);else del(rs[o],v); } int askrank(int o,int v){//查詢x的排名if(val[o]==v) return siz[ls[o]]+1;else if(val[o]>v) return askrank(ls[o],v);else return siz[ls[o]]+cnt[o]+askrank(rs[o],v); } int asknth(int o,int k){//查詢第k大的元素if(siz[ls[o]]>=k) return asknth(ls[o],k);else if(siz[ls[o]]+cnt[o]>=k) return val[o];else return asknth(rs[o],k-(siz[ls[o]]+cnt[o])); } int main(){scanf("%d",&n);int flag;val[1]=-2e9;int r=1;for(int i=1;i<=n;i++){scanf("%d%d",&flag,&x);if(flag==1) insert(r,x);else if(flag==2) del(r,x);else if(flag==3) printf("%d\n",askrank(1,x));else if(flag==4) printf("%d\n",asknth(1,x));}return 0; } /**/treap
旋轉是平衡樹的靈魂
一個很重要的技巧是利用0/1存儲左右兒子
這樣在旋轉的時候寫起來會容易很多
代碼
#include<bits/stdc++.h> using namespace std; const int N=1e6+2e5+100; #define ll long long int n,m,k; int x,y; int cnt[N],ch[N][2],val[N],siz[N],tot,r,dat[N]; int New(int v){val[++tot]=v;dat[tot]=rand();ch[tot][0]=ch[tot][1]=0;siz[tot]=cnt[tot]=1;return tot; } void pushup(int o){siz[o]=siz[ch[o][0]]+siz[ch[o][1]]+cnt[o]; } void build(){r=New(-2e9);ch[1][1]=New(2e9);pushup(r); } void rotate(int &o,int d){int temp=ch[o][!d];ch[o][!d]=ch[temp][d];ch[temp][d]=o;o=temp;pushup(o);pushup(ch[o][d]); } void insert(int &o,int v){if(!o){o=New(v);return;}if(v==val[o]){cnt[o]++;pushup(o);return;}int d= v>val[o];insert(ch[o][d],v);if(dat[ch[o][d]]>dat[o]) rotate(o,!d);pushup(o); } void del(int &o,int v){if(!o) return;if(v==val[o]){if(cnt[o]>1){cnt[o]--;pushup(o);return;}if(ch[o][0]||ch[o][1]){int d=!ch[o][1]||dat[ch[o][1]]<dat[ch[o][0]];rotate(o,d);del(ch[o][d],v);pushup(o);}else o=0;return;}if(v<val[o]) del(ch[o][0],v);else del(ch[o][1],v);pushup(o); } int getrank(int o,int v){if(!o) return 1;if(val[o]==v) return siz[ch[o][0]]+1;else if(v<val[o]) return getrank(ch[o][0],v);else return getrank(ch[o][1],v)+siz[ch[o][0]]+cnt[o]; } int getnth(int o,int k){if(!o) return 2e9;if(siz[ch[o][0]]>=k) return getnth(ch[o][0],k);else if(siz[ch[o][0]]+cnt[o]>=k) return val[o];else return getnth(ch[o][1],k-(siz[ch[o][0]]+cnt[o])); } int getpre(int v){int res=-2e9,p=r;while(p){if(val[p]<v){res=val[p];p=ch[p][1];}else p=ch[p][0];}return res; } int getnxt(int v){int res=2e9,p=r;while(p){if(val[p]>v){res=val[p];p=ch[p][0];}else p=ch[p][1];}return res; } int main(){scanf("%d%d",&n,&m);build();int flag;for(int i=1;i<=n;i++){scanf("%d",&x);insert(r,x);}int ans=0,lst=0;for(int i=1;i<=m;i++){scanf("%d%d",&flag,&x);x^=lst;if(flag==1) insert(r,x);else if(flag==2) del(r,x);else if(flag==3){int res=getrank(r,x)-1;lst=res;ans^=res;}else if(flag==4){int res=getnth(r,x+1);lst=res;ans^=res;}else if(flag==5){int res=getpre(x);lst=res;ans^=res;}else{int res=getnxt(x);lst=res;ans^=res;}}printf("%d\n",ans);return 0; } /**/splay
看好幾篇博客說splay在區間問題的功能更強大,所以也學習了splay
最后實在de不出來bug還是動用了減法原理
累死窩了qwq
這個東西真的好難debug
但是決定以后就用它了awa
當然要用強的啦
這個東西好好講講
開點
所有的點都是開出來的
開點還是比較正常
int New(int v,int fa){val[++tot]=v;f[tot]=fa;ch[tot][0]=ch[tot][1]=0;siz[tot]=cnt[tot]=1;return tot; }旋轉
它也是旋轉完成的,不能沒有它
和treap一樣啦
防止寫錯,總體的改變可以分三對
splay
splay怎么能不splay呢
所以我們現在講講splay的splay部分(停止扯淡)
splay總的來說就是把一個結點不停轉轉轉
一直轉到根的地方
沿途長鏈死光光
從而保證復雜度的正確性
這也是splay的精髓所在
而且跳到根也便利了我們其他的操作
有一個很關鍵的細節
就是當父親和自己相對于各自父節點的方向同向時
必須要先轉父親
不然就無法達到消鏈的目的
這個可以自己畫畫圖理解
(我看別人題解畫的天花亂墜,最后還是自己畫圖才明白的)
代碼極為簡潔
插入
開始干正事了
找到應該加點的位置開點
然后splay一下
注意pushup!
查找第k大元素
這個很好寫
理解起來應該也不難
查找給定元素的排名
這個也沒有太大的難度
(盡管我de了一年多之后發現就是這里寫掛的)
為了后面刪除元素的遍歷我們找到這個元素后splay一下
前驅&后繼
這里是找的根的前驅(后繼)
找給定值的前驅(后繼)的話就先insert進去,它自動splay到根,然后再求就行了
刪除
這個是重點
首先把刪除的元素利用前面現成的findrank提到根上
有副本就直接刪
否則看它的兒子情況
啥都沒有就直接變空樹了
只有一個就把那個兒子當成根
如果兩個兒子都有就考慮把根的前驅提上來
因為是前驅,所以它在到x之前一定沒有右兒子
也就是這樣:
再轉一下:
注意到待刪元素一定沒有左兒子
因此我們可以把B直接接到pre上達到刪除的目的
也就是:
這樣就ok啦
void del(int v){findrank(v);if(cnt[r]>1) {cnt[r]--;return;}else if(!ch[r][0]&&!ch[r][1]){r=0;return;}else if(!ch[r][0]){int temp=r;r=ch[r][1];f[r]=0;return;}else if(!ch[r][1]){int temp=r;r=ch[r][0];f[r]=0;return;}int temp=ch[r][1],pre=findpre(),oldr=r;splay(pre);ch[r][1]=temp;f[temp]=r;pushup(r); }完整代碼
#include<bits/stdc++.h> using namespace std; const int N=1e5+100; #define ll long long int n,m,k; int x,y; int cnt[N],ch[N][2],val[N],siz[N],tot,r; int f[N]; int New(int v,int fa){val[++tot]=v;f[tot]=fa;ch[tot][0]=ch[tot][1]=0;siz[tot]=cnt[tot]=1;return tot; } void pushup(int o){if(o) siz[o]=siz[ch[o][0]]+siz[ch[o][1]]+cnt[o]; } void build(){r=New(-2e9,0);ch[1][1]=New(2e9,r);pushup(r); } int getwhich(int x){return ch[f[x]][1]==x; } void rotate(int x){int fa=f[x],gfa=f[fa];int k=getwhich(x);int temp=ch[x][k^1];f[temp]=fa;ch[fa][k]=temp;f[x]=gfa;if(gfa) ch[gfa][ch[gfa][1]==fa]=x;f[fa]=x;ch[x][k^1]=fa;pushup(x);pushup(fa); } void splay(int x){for(int fa;fa=f[x];rotate(x)){if(f[fa]) rotate((getwhich(fa)==getwhich(x))?fa:x);}r=x; } void insert(int v){if(!r){r=New(v,0);return;}int now=r,fa=0;while(1){if(val[now]==v){cnt[now]++;pushup(now);pushup(fa);splay(now);break;}fa=now;now=ch[now][v>val[now]];if(!now){ch[fa][v>val[fa]]=New(v,fa);pushup(fa); // printf("\ninsert:(pre)\n"); // print();splay(tot); // printf("\ninsert:(after)\n"); // print();break;}} } int findnth(int k){int now=r;while(1){if(siz[ch[now][0]]>=k) now=ch[now][0];else if(siz[ch[now][0]]+cnt[now]>=k) return val[now];else{k-=siz[ch[now][0]]+cnt[now];now=ch[now][1];}} } int findrank(int x){int now=r,ans=0;while(1){if(!now) return ans+1;if(val[now]>x) now=ch[now][0];else if(val[now]==x){ans+=siz[ch[now][0]];splay(now);return ans+1;}else{ans+=siz[ch[now][0]]+cnt[now];now=ch[now][1];}} } int findpre(){int now=ch[r][0];while(ch[now][1]) now=ch[now][1];return now; } int findnxt(){int now=ch[r][1];while(ch[now][0]) now=ch[now][0];return now; } void del(int v){findrank(v);if(cnt[r]>1) {cnt[r]--;return;}else if(!ch[r][0]&&!ch[r][1]){r=0;return;}else if(!ch[r][0]){int temp=r;r=ch[r][1];f[r]=0;return;}else if(!ch[r][1]){int temp=r;r=ch[r][0];f[r]=0;return;}int temp=ch[r][1],pre=findpre(),oldr=r;splay(pre);ch[r][1]=temp;f[temp]=r;pushup(r); } int main(){scanf("%d",&n);int flag;for(int i=1;i<=n;i++){scanf("%d%d",&flag,&x);if(flag==1) insert(x);else if(flag==2) del(x);else if(flag==3) printf("%d\n",findrank(x));else if(flag==4) printf("%d\n",findnth(x));else if(flag==5){insert(x);printf("%d\n",val[findpre()]);del(x);}else{insert(x);printf("%d\n",val[findnxt()]);del(x);} // print();}return 0; } /**/練習總結
傳送門
總結
以上是生活随笔為你收集整理的模板:二叉搜索树平衡树的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 恍然大悟的悟的意思 恍然大悟的悟的意思简
- 下一篇: 阶段总结:8.09-8.18 十日模拟