• [学习笔记] 平衡树——Treap


    前置技能:平衡树前传:BST

    终于学到我们喜闻乐见的平衡树啦!

    所以我们这次讲的是平衡树中比较好写的(Treap).

    (以后会写splay的先埋个坑在这)

    好了,进入正题.

    step 1

    我们知道,BST虽然很方便,

    但是,它很容易被卡成一条链.

    因此,我们需要一个能够保持平衡的BST.

    于是就有了我们众所周知的平衡树.

    而平衡树保持平衡的方法,据本蒟蒻所知就是旋转节点.

    通过旋转BST的节点,既保持BST的性质,又使它变得平衡.

    而旋转其实也很好理解,

    先看这张丑陋的图:

    (其中(x),(y)为节点,(A),(B),(C)为子树)

    然后我们假设(A),(B)中有很多点,而(C)中只有很少的点.

    于是,我们要通过旋转来使平衡树变平衡.

    我们可以知道,(B)中的点的权值都是大于(x)而小于(y)的(等于全看个人爱好qwq),

    于是,我们可以这么一转:

    这样,既保持了BST的性质(自己仔细想一下就能明白了),又变得更加平衡了.

    而旋转的过程也很简单,

    上面我们是将(y)的左儿子(x)转到它的位置,

    根据图片,我们可以看到,

    (y)就变成了(x)的右儿子,而(x)的右儿子就变成了它的左儿子.

    所以这就非常简单了,看代码吧:

    inline void l_rotate(int &p){//将p的左儿子转到p的位置
    	int q=t[p].l;
    	t[p].l=t[q].r;t[q].r=p;p=q;
    	update(t[p].r);update(p);//update根据题目来定
    }
    

    而将右儿子转上来就刚好相反:

    inline void r_rotate(int &p){
    	int q=t[p].r;
    	t[p].r=t[q].l;t[q].l=p;p=q;
    	update(t[p].l);update(p);
    }
    

    另外,其实我们可以发现,

    如果是将(y)的左儿子转上来,

    那么(y)就变成了(x)的相反方向的儿子(即右儿子),

    (x)的右儿子就补上了(y)的左儿子.

    所以,旋转可以直接合并成一个函数(这一点会在splay里面讲的,所以就先不具体说了).

    那么,旋转讲完了,

    然而到底怎样才能让树平衡,要什么时候旋转呢?

    我们发现,在随机的数据下,普通的BST就是接近平衡的,

    所以,Treap的平衡也就是这样——听天由命,

    给每个节点另外给一个随机的权值(dat),

    然后保证(dat)满足堆性质(这里我们以大根堆为例),

    也就是说,如果一个节点的(dat)小于它儿子节点,它就该转了.

    那么这样,我们就能实现平衡啦.(并且Treap其实也就是Tree和Heap的合成词哦)

    接下来,就该讲Treap的应用了:

    step 2

    其实,Treap的几个应用和BST并没有多大区别,

    所以在BST中讲过的几个操作就直接看代码吧(主要是看看旋转的操作):

    inline void insert(int &p,int val){
    	if(!p){p=New(val);return ;}
    	if(t[p].val==val) t[p].cnt++;
    	else insert(val<t[p].val? t[p].l:t[p].r,val);
    	if(t[t[p].l].dat>t[p].dat) l_rotate(p);//检查一下是否满足堆性质再旋转
    	if(t[t[p].r].dat>t[p].dat) r_rotate(p);
    	update(p);
    }
    
    //接下来的删除似乎改的比较多哈(一开始忘记了qwq)
    //在这里,我们就不用寻找代替的节点了,
    //直接把它转到叶子节点再删就行啦
    
    inline void remove(int &p,int val){
    	if(!p) return ;
    	if(t[p].val==val){
    		if(t[p].cnt>1){t[p].cnt--;update(p);return ;}
    		if(!t[p].l&&!t[p].r){p=0;return ;}//叶子节点直接删
    		if(!t[p].r||t[t[p].l].dat>t[t[p].r].dat) l_rotate(p),remove(t[p].r,val);//如果没有右儿子或左儿子的dat更大就把左儿子转上来再删
    		else r_rotate(p),remove(t[p].l,val);
    		update(p);return ;
    	}
    	remove(val<t[p].val? t[p].l:t[p].r,val);update(p);
    }
    
    //接下来的就和BST一样了
    inline int getnext(int val){
    	int p=root,ans=2;
    	while(p){
    		if(t[p].val==val){
    			if(!t[p].r) break;
    			p=t[p].r;while(t[p].l) p=t[p].l;
    			ans=p;break;
    		}
    		if(t[p].val>val&&t[p].val<t[ans].val) ans=p;
    		p=val<t[p].val? t[p].l:t[p].r;
    	}
    	return t[ans].val;
    }
    
    inline int getpre(int val){
    	int p=root,ans=1;
    	while(p){
    		if(t[p].val==val){
    			if(!t[p].l) break;
    			p=t[p].l;while(t[p].r) p=t[p].r;
    			ans=p;break;
    		}
    		if(t[p].val<val&&t[p].val>t[ans].val) ans=p;
    		p=val<t[p].val? t[p].l:t[p].r;
    	}
    	return t[ans].val;
    }
    

    然而,如果只有这些操作,那set岂不是可以替代手写平衡树?

    所以,我们还有两个操作其实在BST里面就有的只是忘记讲了qwq

    寻找一个值val的排名(只需要找已经插入的节点)

    首先,我们可以知道,排名就是比它小的值的个数(+1),

    而我们在寻找这个值(val)所在的节点时,有三种情况:

    1.当前节点的权值大于(val).

    此时,我们只需要向左走就行了.

    2.当前节点的权值等于(val).

    那么这时候,就直接返回它左子树的节点数量(size).

    3.当前节点的权值小于(val).

    那么显然,当前节点及它的左子树的权值都小于(val),

    于是我们加上左子树的(size)以及当前节点的元素个数(cnt)(针对于重复元素),

    再往右找即可.

    来看代码吧:

    inline int getrank(int p,int val){
    	if(t[p].val==val) return t[t[p].l].size;
    	if(val<t[p].val) return getrank(t[p].l,val);
    	return t[t[p].l].size+t[p].cnt+getrank(t[p].r,val);	
    } 
    

    但是我们要注意的一点是,

    我们最后返回的,是小于(val)的元素个数,因此要再加一,

    但由于插入了一个(-INF)(避免越界),又要再减掉一(就等于没变,但原理必须要清楚).

    让我们进入到第二个操作:

    寻找排名为rank的元素

    其实这个的思路和前面的也差不多...(并且和权值线段树很像)

    还是三种情况:

    1.当前节点左子树的元素个数大于等于(rank).

    那么显然,答案在左子树中,因此往左边走就行了.

    2.左子树元素个数(size)加上当前节点的重复元素个数(cnt)大于等于(rank).

    那么当前节点的权值就是答案了.

    3.左子树元素个数(size)加上当前节点的重复元素个数(cnt)小于(rank).

    那么答案就在右子树中啦,但是要将(rank)减掉左子树元素个数(size)加上当前节点的重复元素个数(cnt)

    那么看代码吧:

    inline int getval(int p,int rank){
    	if(t[t[p].l].size>=rank) return getval(t[p].l,rank);
    	if(t[t[p].l].size+t[p].cnt>=rank) return t[p].val;
    	return getval(t[p].r,rank-t[t[p].l].size-t[p].cnt);
    }
    

    好吧,几个操作讲完啦!!!

    来看道例题吧:洛谷P3369 【模板】普通平衡树

    这题就是个板子了.

    直接上代码吧:

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <stdlib.h>
    #define INF 0x7fffffff
    using namespace std;
    
    inline int read(){
    	int sum=0,f=1;char c=getchar();
    	while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
    	while(c<='9'&&c>='0'){sum=sum*10+c-'0';c=getchar();}
    	return sum*f;
    }
    
    struct tree{int l,r,size,cnt,val,dat;}t[100001];
    int n,tot,root;
    
    inline int New(int val){
    	t[++tot].val=val;t[tot].dat=rand();
    	t[tot].size=t[tot].cnt=1;
    	return tot;
    }
    
    inline void update(int p){
    	t[p].size=t[t[p].l].size+t[t[p].r].size+t[p].cnt;
    }
    
    inline void l_rotate(int &p){
    	int q=t[p].l;
    	t[p].l=t[q].r;t[q].r=p;p=q;
    	update(t[p].r);update(p);
    }
    
    inline void r_rotate(int &p){
    	int q=t[p].r;
    	t[p].r=t[q].l;t[q].l=p;p=q;
    	update(t[p].l);update(p);
    }
    
    inline void build(){
    	New(-INF);New(INF);
    	t[1].r=2;root=1;
    	update(1);
    }
    
    inline void insert(int &p,int val){
    	if(!p){p=New(val);return ;}
    	if(t[p].val==val) t[p].cnt++;
    	else insert(val<t[p].val? t[p].l:t[p].r,val);
    	if(t[t[p].l].dat>t[p].dat) l_rotate(p);
    	if(t[t[p].r].dat>t[p].dat) r_rotate(p);
    	update(p);
    }
    
    inline void remove(int &p,int val){
    	if(!p) return ;
    	if(t[p].val==val){
    		if(t[p].cnt>1){t[p].cnt--;update(p);return ;}
    		if(!t[p].l&&!t[p].r){p=0;return ;}
    		if(!t[p].r||t[t[p].l].dat>t[t[p].r].dat) l_rotate(p),remove(t[p].r,val);
    		else r_rotate(p),remove(t[p].l,val);
    		update(p);return ;
    	}
    	remove(val<t[p].val? t[p].l:t[p].r,val);update(p);
    }
    
    inline int getnext(int val){
    	int p=root,ans=2;
    	while(p){
    		if(t[p].val==val){
    			if(!t[p].r) break;
    			p=t[p].r;while(t[p].l) p=t[p].l;
    			ans=p;break;
    		}
    		if(t[p].val>val&&t[p].val<t[ans].val) ans=p;
    		p=val<t[p].val? t[p].l:t[p].r;
    	}
    	return t[ans].val;
    }
    
    inline int getpre(int val){
    	int p=root,ans=1;
    	while(p){
    		if(t[p].val==val){
    			if(!t[p].l) break;
    			p=t[p].l;while(t[p].r) p=t[p].r;
    			ans=p;break;
    		}
    		if(t[p].val<val&&t[p].val>t[ans].val) ans=p;
    		p=val<t[p].val? t[p].l:t[p].r;
    	}
    	return t[ans].val;
    }
    
    inline int getrank(int p,int val){
    	if(t[p].val==val) return t[t[p].l].size;
    	if(val<t[p].val) return getrank(t[p].l,val);
    	return t[t[p].l].size+t[p].cnt+getrank(t[p].r,val);	
    } 
    
    inline int getval(int p,int rank){
    	if(t[t[p].l].size>=rank) return getval(t[p].l,rank);
    	if(t[t[p].l].size+t[p].cnt>=rank) return t[p].val;
    	return getval(t[p].r,rank-t[t[p].l].size-t[p].cnt);
    }
    
    int main(){
    	n=read();build();
    	for(int i=1;i<=n;i++){
    		int opt=read(),x=read();
    		if(opt==1){insert(root,x);}
    		else if(opt==2){remove(root,x);}
    		else if(opt==3){printf("%d
    ",getrank(root,x));}
    		else if(opt==4){printf("%d
    ",getval(root,x+1));}//因为有一个-INF所以要加一
    		else if(opt==5){printf("%d
    ",getpre(x));}
    		else if(opt==6){printf("%d
    ",getnext(x));}
    	}
    	return 0;
    }
    

    Treap终于讲完啦.

    等着更splay吧...(或许坑填不上了)

  • 相关阅读:
    前端面试
    react 【npx createreactapp myapp】执行错误
    npm yarn安装完成后,查不到版本号
    I love cnblogs
    万万没想到VFP也可以这样硬,调用微信的硬能力,扫码、上报位置、支付都可以
    VFP为公众号添加一个报名功能,代码不多,但谁能得扬名立万
    公众号回复消息不能超过5秒,VFP大数据处理来不及怎么办?
    爆肝怒赞,不会也会了,VFPBS用Form调用webapi和文件上传
    狐友们,万万不可掉队,VFP开发企业微信第一关回调该怎么配
    十行代码完成公众号对话,VFP的能力就是这么强悍,你学会了吗?
  • 原文地址:https://www.cnblogs.com/zsq259/p/10983432.html
Copyright © 2020-2023  润新知