平衡树初探

Splay

注意事项

 0.rotate()左旋右旋。找点规律可以写一起;

  splay()利用rotate()把一个节点转到目标点的儿子处;

  find()找到一个节点并把它转到根,没有这个节点那就可能是把他的前驱或者后继转上来;

  nxt()利用find()找前驱、后继,把x转上来之后,暴力跳子树就可以了;

  insert()向下查找时要同时记录父节点;

  pop()利用nxt()找到前驱后继,然后把前驱旋到根,后继旋到前驱儿子处,这样x就在后继的右儿子处且x没有儿子;
 
  kth()注意是<=不是<;

  rnk()直接find()然后取\(sum[ch[rt][0]]+1\)即可;

  区间修改问题可以把l-1旋到根,r+1旋到根的儿子,那么区间\([l,r]\)就在节点r+1的左子树,打个reverse标记即可。等询问到这个节点时,先下传标记。修改后update,进入子树前pushdown,能splay就splay.

 1.更新节点可以直接splay(),不需要做两次update(),因为rotate()过程中会update()。(除了删除一个节点时,即cnt=0时,因为这个时候这个节点已经从树上删去,不可能从他开始splay)。
 2.操作完能splay()就尽量splay()。
 3.开局insert()inf和-inf两个哨兵节点,以防要查找的值比树上值都小/大。同时要注意rnk()和kth()要相应做微调。
 4.find()的while做完后p不能为0(因为最后要splay(),如果是0做splay()会把rt也置为0),而insert里的可以为0(当p为0时会新增一个节点,p就不为0了)。

Code

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
inline int read(){//be careful for long long!
    register int x=0,f=1;register char ch=getchar();
    while(!isdigit(ch)){if(ch=='-')f=0;ch=getchar();}
    while(isdigit(ch)){x=x*10+(ch^'0');ch=getchar();}
    if(f)return x;return -x;
}

const int N=1e5+10,inf=0x7fffffff;
namespace Splay{
    int tot,rt,val[N],cnt[N],sum[N],fa[N],ch[N][2];
    inline int getdir(int nw){return nw==ch[fa[nw]][1];}
    inline void update(int nw){sum[nw]=cnt[nw]+sum[ch[nw][0]]+sum[ch[nw][1]];}
    inline void rotate(int nw){
	int f=fa[nw],as=fa[f],k=getdir(nw);
	ch[f][k]=ch[nw][k^1],fa[ch[nw][k^1]]=f;
	ch[nw][k^1]=f,fa[f]=nw;
	fa[nw]=as;if(as)ch[as][ch[as][1]==f]=nw;
	update(f),update(nw);
    }
    inline void splay(int nw,int goal=0){
	for(int f;(f=fa[nw])^goal;rotate(nw))
	    if(fa[f]^goal)rotate(getdir(f)==getdir(nw)?f:nw);
	if(!goal)rt=nw;
    }
    inline void find(int x){
	int p=rt;if(!p)return;
	while(val[p]^x&&ch[p][val[p]<x])p=ch[p][val[p]<x];
	splay(p);
    }
    inline int nxt(int x,int type){
	find(x);int p=rt;
	if((val[p]<x&&!type)||(val[p]>x&&type))return p;
	p=ch[p][type];type^=1;
	while(ch[p][type])p=ch[p][type];
	return p;
    }
    inline void insert(int x){
	int p=rt,f=0;
	while(val[p]^x&&p)p=ch[f=p][val[p]<x];
	if(p){++cnt[p],splay(p);return;}
	p=++tot;val[p]=x,cnt[p]=sum[p]=1,fa[p]=f,ch[p][0]=ch[p][1]=0;
	if(f)ch[f][val[f]<x]=p;
	splay(p);
    }
    inline void pop(int x){
	int l=nxt(x,0),r=nxt(x,1);
	splay(l),splay(r,l);
	int p=ch[r][0];
	if(cnt[p]>1){--cnt[p],splay(p);return;}
	ch[r][0]=0;update(r),update(l);
    }
    inline int rnk(int x){
	find(x);return sum[ch[rt][0]]+1;
    }
    inline int kth(int k){
	int p=rt;
	while(1){
	    if(k<=sum[ch[p][0]])p=ch[p][0];
	    else{
		k-=sum[ch[p][0]];
		if(k<=cnt[p])return val[p];
		else k-=cnt[p],p=ch[p][1];
	    }
	}
    }
}
using namespace Splay;

int main(){
    insert(inf),insert(-inf);
    int n=read();
    while(n--){
	switch(read()){
	case 1:{insert(read());break;}
	case 2:{pop(read());break;}
	case 3:{printf("%d\n",rnk(read())-1);break;}
	case 4:{printf("%d\n",kth(read()+1));break;}
	case 5:{printf("%d\n",val[nxt(read(),0)]);break;}
	case 6:{printf("%d\n",val[nxt(read(),1)]);break;}
	}
    }
    return 0;
}

fhq_treap

注意事项

 0.Treap是随机附加域满足(例如小根堆)性质的二叉搜索树

  split有按权值分裂(权值小于等于k的在左树)和按排名分裂(就是前k大在左树),按权值用的比较多。讨论一下当前节点的情况看分到左树还是右树然后递归就可以了;

  merge假设x的权值是小于y的,那么其左右的相对位置已经符合BST性质,只需考虑满足堆性质,讨论一下x和y的key值大小决定谁做根(小根堆就key小的做根),然后递归处理;

  insert因为treap中元素是可重的,所以直接按权值x把treap分裂,然后加入一个新点再merge起来;

  pop先按权值x分裂,再把左树按权值x-1分裂,这样得到的新的右树的点权值都是x。把新的右树根的两个儿子merge起来,就相当于把根pop掉了,然后再把三堆merge成一个treap;

  rnk按权值x-1分裂,答案就是左树大小+1;

  kth和splay一样实现就好了。刚开始想着用按排名分裂搞搞,但常数反而还不如暴力跳优秀;

  pre按权值x-1分裂,再kth查询左树最大的就可以了(排名为左树大小的那个);

  suf按权值x分裂,再kth查询右树最小的就可以了(排名为1的)。

 1.任何操作做完都要merge回来,保证时刻只有一个treap。

 2.不需要建哨兵节点。splay要建的原因是pop的时候要访问前驱和后继。

 3.还是一样的,修改后update,进入子树前pushdown,修改完千万别忘update!!!

Code

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
inline int read(){//be careful for long long!
    register int x=0,f=1;register char ch=getchar();
    while(!isdigit(ch)){if(ch=='-')f=0;ch=getchar();}
    while(isdigit(ch)){x=x*10+(ch^'0');ch=getchar();}
    if(f)return x;return -x;
}

const int N=1e5+10;
int n;

namespace fhq_treap{
    int val[N],key[N],ch[N][2],siz[N],cnt,rt,x,y,z;
    inline int New(int k){val[++cnt]=k,siz[cnt]=1,key[cnt]=rand();return cnt;}
    inline void update(int p){siz[p]=siz[ch[p][0]]+siz[ch[p][1]]+1;}
    inline void split(int nw,int v,int &x,int &y){
	if(!nw){x=y=0;return;}
	if(val[nw]<=v)x=nw,split(ch[nw][1],v,ch[x][1],y),update(x);
	else y=nw,split(ch[nw][0],v,x,ch[y][0]),update(y);
    }
    inline int merge(int x,int y){
	if(!x||!y)return x+y;
	if(key[x]<=key[y]){ch[x][1]=merge(ch[x][1],y),update(x);return x;}
	else{ch[y][0]=merge(x,ch[y][0]),update(y);return y;}
    }
    inline void insert(int k){
	split(rt,k,x,y);
	rt=merge(merge(x,New(k)),y);
    }
    inline void pop(int k){
	split(rt,k,x,y),split(x,k-1,x,z);
	z=merge(ch[z][0],ch[z][1]);rt=merge(merge(x,z),y);
    }
    inline int rnk(int k){
	split(rt,k-1,x,y);int ans=siz[x]+1;
	merge(x,y);return ans;
    }
    inline int kth(int k,int p=rt){
	while(1){
	    if(k<=siz[ch[p][0]])p=ch[p][0];
	    else if(k<=siz[p]-siz[ch[p][1]])return val[p];
	    else k-=siz[p]-siz[ch[p][1]],p=ch[p][1];
	}
    }
    inline int pre(int k){
	split(rt,k-1,x,y);int ans=kth(siz[x],x);
	merge(x,y);
	return ans;
    }
    inline int suf(int k){
	split(rt,k,x,y);int ans=kth(1,y);
	merge(x,y);
	return ans;
    }
}
using namespace fhq_treap;

int main(){
    srand(time(0));
    int n=read();
    for(int t=1;t<=n;++t){
	switch(read()){
	case 1:{insert(read());break;}
	case 2:{pop(read());break;}
	case 3:{printf("%d\n",rnk(read()));break;}
	case 4:{printf("%d\n",kth(read()));break;}
	case 5:{printf("%d\n",pre(read()));break;}
	case 6:{printf("%d\n",suf(read()));break;}
	}
    }
    return 0;
}

例题

Luogu3369 【模板】普通平衡树

Link
板子题。

Luogu3391 【模板】文艺平衡树

Link
一旦进入子树就pushdown。
注意这两道题splay都需要建立哨兵节点。

posted @ 2019-12-10 19:27  Fruitea  阅读(176)  评论(0编辑  收藏  举报