Treap旋平衡树

平衡树(Treap==tree+heap)

二叉搜索树固然好用,但是容易退化成一条链,复杂度会被卡,那么怎样去掉这个缺点呢,聪明的人类有办法了。
其实不难发现,二叉搜索树在保证有序的前提下有很多形状,譬如:

和这玩意

很明显是等价的嘛,而且后者复杂度明显是低的,那我们就要想办法尽可能构造出下面这棵树而不是上面那棵。
那咋办嘞?
回想一下搜索树的定义

struct tree{
    int l,r,v,sum,size;
}t[MAXN];

现在我们的插入操作是按照 \(v\) 大小一个个往里放的,给你个有序序列你就完蛋。。。
那么就有个人想出了一个好办法,我改变一下搜索树的定义,我在原来的基础上加点东西:

struct tree{
    int l,r,v,sum,size;
    int ran;
}t[MAXN];

\(ran\) 存一个随机值,要知道,我们这是一棵二叉树啊!!所以是可以实现堆操作的。
我们现在不光维护树中的各个 \(v\) 有序,我们还存了一个堆在里面,这样让树同时维护一个有序序列和一个堆,这样形状就不会是链了。
刚才讲了 \(ran\) 是随机值,我们在对 \(n\) 个数排序的同时,又生成了 \(n\) 个随机值并对其排序,那你说树还退化成链的可能吗?

没有!!

那么这就是平衡树的原理了,\(Treap=Tree+heap\)
那么接下来就是如何维护的问题了,我们知道维护一个堆,其实本质就是让父亲和儿子互相交换。
该上来的上来,该下去的下去。
那么如何交换父子节点还能让交换前后的平衡树等价呢?
上图:

有问题吗?没有问题。美其名曰:左右旋。。。
如果左儿子上来就右旋,右儿子上来就左旋。
用字母表示一下:

发现其实只有 \(A、B\) 点改变了。

那么假设我们对节点\(g\)进行旋转,也就是:

右旋:\(g\)的左儿子变成\(g\)的左儿子的右儿子。\(g\)的左儿子的右儿子变成\(g\)
左旋:\(g\)的右儿子变成\(g\)的右儿子的左儿子。\(g\)的右儿子的左儿子变成\(g\)

void R_rotate(int &g){
	int y=t[g].l;
	t[g].l=t[y].r;
	t[y].r=g;
	push_up(g);push_up(y);
	g=y;
}//右旋
void L_rotate(int &g){
	int y=t[g].r;
	t[g].r=t[y].l;
	t[y].l=g;
	push_up(g);push_up(y);
	g=y;
}//左旋

一定要传实参!!(不要问为啥,写多了就悟出来了)

那么剩下的就和搜索树基本是一回事儿了,只不过多了个删除:

板子题面自己看

准备操作:

int n,opt,v,rt,head_size;
struct tree{
	int l,r,v,sum,size,ran;
}t[100005];
void push_up(int &g){t[g].size=t[t[g].l].size+t[t[g].r].size+t[g].sum;}//维护子树大小
void R_rotate(int &g){
	int y=t[g].l;
	t[g].l=t[y].r;t[y].r=g;
	push_up(g);push_up(y);
	g=y;
}//右旋
void L_rotate(int &g){
	int y=t[g].l;
	t[g].l=t[y].r;t[y].r=g;
	push_up(g);push_up(y);
	g=y;
}//左旋

\(1.\)插入:

void insert(int &g,int &v){
	if(!g){
		g=++head_size;
		t[g].v=v;++t[g].sum;++t[g].size;
		t[g].ran=rand();//随机产生一个数
		return ;
	}//如果当前节点为空,新增节点。
	else { ++t[g].size;//来了一个点,子树大小+1;
		if(t[g].v==v) ++t[g].sum;//如果相等直接sum+1
		else if(v<t[g].v){//小于往左走
			insert(t[g].l,v);
			if(t[g].ran>t[t[g].l].ran)rxuan(g);//维护堆
		} 
		else {//大于往右走
			insert(t[g].r,v);
			if(t[g].ran>t[t[g].r].ran)lxuan(g);//维护堆
		}
	}
	push_up(g);//子树们一直在转,有必要维护当前节点的size;
}

\(2.\)删除\(v\)

如果是叶子,直接删除,没有问题。
如果在链上,直接让唯一的孩子代替自己,没有问题。
左右孩子都有,那就让它往下旋,直到变成以上两个情况为止。

void delet(int &g,int &v){
	if(t[g].v==v){//找到点了
		if(t[g].sum>1){--t[g].sum;--t[g].size;}//如果有重复的,那就直接--sum;
		else if(!t[g].l||!t[g].r)g=t[g].l+t[g].r;//满足前两种情况,
		else if(t[t[g].l].ran<t[t[g].r].ran){rxuan(g);delet(g,v);}//维护堆并对其进行相应左右旋
		else {lxuan(g);delet(g,v);}
		return ;
	}
	--t[g].size;//还没找到点的时候,说明要删的点肯定在在子树里,所以维护一下各个点的子树大小。
	if(t[g].v<v)delet(t[g].r,v);//大了往右
	else delet(t[g].l,v);//小了往左
}

\(3.\)查询\(v\)的排名

int ranking(int &v){
	int g=rt,k=0;
	while(g){
		if(t[g].v==v)return k+t[t[g].l].size+1;
		if(v<t[g].v)g=t[g].l;
		else {k+=t[t[g].l].size+t[g].sum;g=t[g].r;}
	}
	return k;
}

自己悟……

\(4.\)查询排名为\(v\)的数

int find(int &v){
	int g=rt,k=0;
	while(g){
		if(k+t[t[g].l].size>=v)g=t[g].l;
		else {
			k+=t[t[g].l].size;
			if(k+t[g].sum>=v)return t[g].v;
			else {k+=t[g].sum;g=t[g].r;}
		}
	}
}

自己悟……

\(5.\)\(v\)的前驱

int front(int &v){
	int g=rt,re=-200000000;
	while(g){
		if(t[g].v<v){re=t[g].v;g=t[g].r;}
		else g=t[g].l;
	}
	return re;
}

自己悟……

\(6.\)\(v\)的后继

int back(int &v){
	int g=rt,re=200000000;
	while(g){
		if(t[g].v>v){re=t[g].v;g=t[g].l;}
		else g=t[g].r;
	}
	return re;
}

自己悟……

还是说一定要传实参!!!(\(自己悟\))

然后平衡树你就会了。
事实上,会了搜索树就会平衡树。
无非就是加了个旋转,删除,挺简单的,嗯(挺简单还调两天)

不过还挺好用!!!(虽然板子就一百行)

更好用的还得看FHQ

posted @ 2021-12-19 14:43  Konnya_ku  阅读(67)  评论(1编辑  收藏  举报