文艺平衡树 Splay 学习笔记(1)
(這里是Splay基礎操作,reserve什么的會在下一篇里面講)
好久之前就說要學Splay了,結果茍到現在才學習。
可能是最近良心發現自己實在太弱了,聽數學又聽不懂只好多學點不要腦子的數據結構。
感覺Splay比Treap良心多了——代碼真的好寫。
對于Splay顯然可以維護Treap的所有操作,并且本質是BST。
先看看Splay是怎么維護普通平衡樹操作的吧。
首先先定義一些基礎的變量(若不作特殊說明這些變量的意義不變)
int t[N][2] // t[x][0]表示節點x的左子樹,t[x][1]表示節點x的右子樹
int cnt[N] // cnt[x]表示節點x存儲多少個重復的數
int val[N] // val[x]表示節點x存儲數的大小
int par[N] // par[x]表示節點x的直接父親,特別的,根節點的直接父親為0
int size[N] // size[x]表示在BST中x子樹中存儲數的個數
Check(x) 函數 :
詢問節點x是其父親的左兒子(return 0)還是右孩子(return 1)
int check (int x) { return rs(par[x])==x; }由上述代碼可知,根節點的check(root)值為0,即根節點是0節點的左兒子(Nothing Special)
Up(x)函數:
對于節點x維護其size值為兩個孩子的size值+自身cnt值。
void up(int x){size[x]=size[ls(x)]+size[rs(x)]+cnt[x]; }Rotate(x)函數:
對于節點x旋轉到其父親節點,且不改變樹BST性質。(使得樹形態較為隨機)
這里需要解釋一下Rotate的解釋和記憶方法(和Treap中Rotate類似)
對于一棵有根樹(且父親指向兒子的邊有向),我們現在以把左兒子旋到父節點為例。
第1步,考慮4的右子樹已經有元素了,考慮把右子樹連接到父節點左兒子處。不改變BST性質。
在此基礎上考慮第二步,就是吧2(4的直接父親接到4的右邊)不改變BST性質。
這個時候我們會發現,只進行第三部就可以完成一次rotate操作。
即連一條1指向4的邊即可。
對于左邊節點轉到父親節點,一般稱之為右旋
對于右旋函數的代碼,不難得到。
void rotate(int x){int y=par[x];t[y][0]=t[x][1]; par[t[x][1]]=y;t[x][1]=y; t[par[y]][check(y)]=x;par[x]=par[y]; par[y]=x;up(y); up(x); } 右旋那左旋呢???
所有0和1的地方去個反不就好了!!!(至少我是那么記的)
對于 真正的旋轉代碼:
void rotate(int x){int y=par[x],k=check(x);t[y][k]=t[x][k^1]; par[t[x][k^1]]=y;t[x][k^1]=y; t[par[y]][check(y)]=x;par[x]=par[y]; par[y]=x;up(y); up(x); }Splay(x,goal) 操作
把節點x通過若干次rotate操作使其到達目標節點goal或者為根,而goal卻成為節點x的兒子。
我們可以分三種情況討論:
1. goal 是 x的直接父親(邊界),那么直接將x旋到父親位置即可
2. x和x的直接父親y和x的爺爺z在同一條直線上(不會打結嗎?我們需要給定一種順序)
那么先旋轉父親y,再旋轉x,這個時候想就被旋轉到y和z節點上方了。
3.?x和x的直接父親y和x的爺爺z在不在同一條直線上(直接把x轉兩次不就行了么)
我們可以參考下圖,模擬一條鏈上的Splay操作。
?簡單的代碼實現如下(自然語言是多么的無力....)
void splay(int x,int goal=0) {while (par[x]!=goal) {int y=par[x],z=par[y];if (z!=goal) {if (check(x)==check(y)) rotate(y);else rotate(x);}rotate(x);}if (!goal) root=x;}
?Insert(x)操作
插入一個值為x的元素。
顯然從根節點開始按照BST性質訪問Splay,直到找到一個節點v,其值val[v]恰好為x,那么直接增加個數
如果找不到節點的的val[v]恰好為x,那么新建一個節點即可。
最后Splay一下防止出現長鏈的情況。
void insert(int x) {int cur=root,p=0;while (cur && val[cur]!=x)p=cur,cur=t[cur][x>val[cur]];if (cur) cnt[cur]++;else {cur=++tot;if (p) t[p][x>val[p]]=cur;ls(cur)=rs(cur)=0;par[cur]=p; val[cur]=x;size[cur]=cnt[cur]=1;}splay(cur); }Find(x)函數
將val值小于等于x的val值最大一個節點,旋轉到根。
題目解決在于如何找到val值小于等于x的val最大的一個節點,注意不能找到空節點,所以要判斷。
找到節點v直接利用Splay操作,旋轉到根即可。
void find(int x){if (!root) return;int cur=root;while (t[cur][x>val[cur]] && val[cur]!=x)cur=t[cur][x>val[cur]];splay(cur); }Rank(x)函數
求值x的排名(x可能曾經沒有出現過),排名的定義是比x小的元素個數+1。
利用find操作后,如果x之前出現過,即val[root]=x,那么直接輸出左子樹的size
否則那么根節點一定比x小那么還需要加上根節點的cnt
int rank(int x){find(x);if (val[root]>=x) return size[ls(root)];else return size[ls(root)]+cnt[root]-1; }pre(x)和suc(x)函數
求值x的前驅(比x小的最大數,若沒有是-無窮),求值x的后繼(比x大的最小數,若沒有是+無窮),x可能沒有出現過。
求前驅,考慮find操作以后小于等于x的元素都在根及根的左側,那么如果根直接小于x(x之前沒出現過),那么直接打印根就行
否則在左子樹中找一直往右子樹走找最大的即可。
求后繼,考慮find操作以后大于等于x的元素都在根及根的左側,那么如果根直接大于x(x之前沒出現過),那么直接打印根就行
否則在右子樹中找一直往左子樹走找最小的即可。
int pre_id(int x) {find(x);if (val[root]<x) return root;int cur=ls(root);while (rs(cur)) cur=rs(cur);return cur; } int pre_val(int x){return val[pre_id(x)]; } int suc_id(int x) {find(x);if (val[root]>x) return root;int cur=rs(root);while (ls(cur)) cur=ls(cur);return cur; } int suc_val(int x){return val[suc_id(x)]; }erase(x)操作
刪除權值為x的一個數。
考慮數x的前驅和后繼是唯一的,那么求出x的前驅和x的后繼,均用Splay操作轉到根節點和根節點的右兒子處,
那么根節點右兒子的左兒子一定就可知道是x的了。直接刪除它即可,特別的是,剩余個數大于1和等于1的時候需要不同處理
其中大于1的時候,直接吧cnt減去1即可,等于1的時候,則需要刪除節點所有的信息。
void erase(int x){int last=pre_id(x),next=suc_id(x);splay(last),splay(next,last);int d=ls(rs(root));if (cnt[d]>1) cnt[d]--,splay(d);else t[next][0]=0; }Treap模板題目:https://www.luogu.org/problemnew/show/P3369
# include <bits/stdc++.h> using namespace std; const int N=2e5+10; struct Splay{# define ls(x) (t[x][0])# define rs(x) (t[x][1])# define inf (0x3f3f3f3f)int t[N][2],cnt[N],val[N],size[N],par[N];int root,tot;Splay() { tot=root=0; insert(-inf); insert(inf);}int check(int x) { return rs(par[x])==x; }void up(int x){size[x]=size[ls(x)]+size[rs(x)]+cnt[x];}void rotate(int x){int y=par[x],k=check(x);t[y][k]=t[x][k^1]; par[t[x][k^1]]=y;t[x][k^1]=y; t[par[y]][check(y)]=x;par[x]=par[y]; par[y]=x;up(y); up(x);}void splay(int x,int goal=0) {while (par[x]!=goal) {int y=par[x],z=par[y];if (z!=goal) {if (check(x)==check(y)) rotate(y);else rotate(x);}rotate(x);}if (!goal) root=x;}void insert(int x) {int cur=root,p=0;while (cur && val[cur]!=x)p=cur,cur=t[cur][x>val[cur]];if (cur) cnt[cur]++;else {cur=++tot;if (p) t[p][x>val[p]]=cur;ls(cur)=rs(cur)=0;par[cur]=p; val[cur]=x;size[cur]=cnt[cur]=1;}splay(cur);}void find(int x){if (!root) return;int cur=root;while (t[cur][x>val[cur]] && val[cur]!=x)cur=t[cur][x>val[cur]];splay(cur);}int pre_id(int x) {find(x);if (val[root]<x) return root;int cur=ls(root);while (rs(cur)) cur=rs(cur);return cur;}int pre_val(int x){return val[pre_id(x)];}int suc_id(int x) {find(x);if (val[root]>x) return root;int cur=rs(root);while (ls(cur)) cur=ls(cur);return cur;}int suc_val(int x){return val[suc_id(x)];}int rank(int x){find(x);if (val[root]>=x) return size[ls(root)];else return size[ls(root)]+cnt[root]-1;}int kth_id(int k) {if (k>tot||k<0) return -1;int cur=root;while (true) {if (t[cur][0]&&k<=size[ls(cur)]) cur=ls(cur);else if (k>size[ls(cur)]+cnt[cur]) {k-=size[ls(cur)]+cnt[cur];cur=rs(cur);} else return cur;}}int kth_val(int k){return val[kth_id(k+1)];}void erase(int x){int last=pre_id(x);int next=suc_id(x);splay(last);splay(next,last);int d=ls(rs(root));if (cnt[d]>1) cnt[d]--,splay(d);else t[next][0]=0;} }tr; int main() {int T; scanf("%d",&T);while (T--) {int op,x;scanf("%d%d",&op,&x);switch(op) {case 1:tr.insert(x);break;case 2:tr.erase(x);break;case 3:printf("%d\n",tr.rank(x));break;case 4:printf("%d\n",tr.kth_val(x));break;case 5:printf("%d\n",tr.pre_val(x));break;case 6:printf("%d\n",tr.suc_val(x));break;}}return 0; } P3369-Splay做法?
轉載于:https://www.cnblogs.com/ljc20020730/p/10597659.html
總結
以上是生活随笔為你收集整理的文艺平衡树 Splay 学习笔记(1)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 建设银行信用卡怎么查卡号?查询方式有这几
- 下一篇: 跨域(Cross-Domain) AJA