关于堆和treap

                                               关于堆和treap

      其实堆对我不太友好,错了几次,就不用了,我宁愿敲线段树。。我以为我这辈子都不会用堆了(当然,有优先队列,还要手写干嘛,作为肥宅的。。。)但是为了教师弟,我还是写了一下,然后顺便写个总结,堆太“捞”(low)了,所以我加了treap进去。emmm,没学过BST(二叉搜索树)的出门左转百度。。。

          堆分大根堆,小根堆。。。大根堆就是根的权值比左右儿子都大(注意:这里是二叉堆。。。),那小根堆不就是左右儿子权值都比自己大。。。emmmm。。。对于堆这个名字还是很有趣的,我以前以为他是从下到上维护,,,因为堆嘛,肯定要先堆下面,然后再堆上面。。。再给你们讲个笑话,听说树的根在最上面。。。(噗哈哈哈哈哈哈。。。。听得懂的才真娱乐)言归正传,那堆我们知道是什么了,基本操作,询问最大值,询问最小值,插入,删除。。。。询问都很简单辣,就是整棵树的根就是答案。插入,我们就要引入一个神奇的概念:完全二叉树,就是分整齐的和不整齐两种(个人通俗的理解)是不是非常通俗易懂呢。。。(笑哭.jpg)整齐就是节点数刚好是2^0+2^1+2^2+……2^x,不整齐就是:节点数不是那个鬼公式。。。但是不整齐里面也有完全二叉树和不完全二叉树,不整齐的树里面最底层的节点,也要从左到右依次排列,不能有空隙。。这种树就不是完全二叉树了,作者用着粗俗的理解,把你们带入了粗俗的坑。。。专业术语你们还是去BFS普及吧(BFS=Baidu First Search)

          然后我们可以给每个节点标号,就从上到下,然后从左到右依次标号吧,你们开心就好。。。但是我个人觉得从0开始更好一些左节点就是root*2+1,右节点就是root*2+2,是不是很机智啊。。。笑哭.jpg。。这样不会浪费一个数字,而且又好记板子。。。  

     我们先讲插入吧。。。我们当前的最后一个点的编号是SZ(size缩写)SZ++为插入的这个点创造一个空间,然后把这个点往上更新。。。如果是大根堆就判断父亲是否比他小,小的话就交换位置。。。二话不说,上代码。。

void heap_push(int x)
{
	int i=sz++;
	while(i>0)//防止越界,0是根,p最上面是0所以这里是i>0
	{
		int p=(i-1)/2;//找到父亲的编号
		if(heap[p]<=x) break;//我这里的是小根堆
		heap[i]=heap[p];//把父亲移动到现在的位置
		i=p;//变成他父亲的编号
	}
	heap[i]=x; //替换最终的位置
}

很容易理解吧,,,

      然后我们来讲讲删除吧。。删除最大的或者最小的。。。也就是把根给删除掉。。。我们就找编号最大的那个,也就是最后一个的点,emmmm,把它移动到根的位置,然后往下更新。。。我们假设是小根堆,我们判断一下当前处理的这个点是否比他的两个儿子都小。如果都小的话就可以停了,否则,选两个儿子中最小的那个来继承他的位置,然后他和那个儿子交换一下位置,不断地往下更新(更新就是为了维护他的堆,因为对堆的定义是每一个根都小于/大于他的儿子)上代码

int heap_pop()
{
	int x=heap[--sz],i=0;//删除了一个数,整个树的size就小了
	while(i*2+1<sz)//有没有越界
	{
		int a=i*2+1,b=i*2+2;//a是左儿子,b是右儿子
		if(b<sz&&heap[b]<heap[a]) a=b;//如果右儿子没有越界,并且更小就替换,这里统一用a表示小的那个儿子
		if(heap[a]>=x) break;//如果两个儿子中最小的那个都不比当前的根小,就可以停了
		heap[i]=heap[a];//否者就替换
		i=a;
	}
	heap[i]=x;
}

差不多就这两个操作吧。。。挺好用的堆,但是我不是很喜欢,它对我不是很友好。。。全部代码如下

#include<iostream>
#include<cstdio>
// lson=root*2+1   rson=root*2+2 result: 根编号为0 
using namespace std;
int heap[1000001],sz=0,n;

void heap_push(int x)
{
	int i=sz++;
	while(i>0)
	{
		int p=(i-1)/2;
		if(heap[p]<=x) break;
		heap[i]=heap[p];
		i=p;
	}
	heap[i]=x; 
}

int heap_pop()
{
	int x=heap[--sz],i=0;
	while(i*2+1<sz)
	{
		int a=i*2+1,b=i*2+2;
		if(b<sz&&heap[b]<heap[a]) a=b;
		if(heap[a]>=x) break;
		heap[i]=heap[a];
		i=a;
	}
	heap[i]=x;
}

int main()
{
	cin>>n;
	while(n--)
	{
		int a,b;
		scanf("%d",&a);
		if(a==1)
		{
			scanf("%d",&b);
			heap_push(b);
		}
		if(a==2) printf("%d\n",heap[0]);
		if(a==3) heap_pop();
	}
	return 0;
}

下面到高能的地方了,treap,,,treap其实就是tree+heap就是BST+堆。。。我们看普通的BST,二叉搜索树,比根大的放根的右儿子,比根小的放根的左儿子,一直往下找,找到一个空的点就可以放了。这里没有动图,自己模拟一下。。。所以查询就是近似logn,它近似平衡。。。如果本来插入的顺序就是一个顺序组怎么办,那么你的BST会退化成一条链。。。。每次查询就是n的了。。。。感觉要完对吧。。。所以我们给这个神奇的BST加入了一些神奇的操作。。。。用堆的性质去改它。。。我们本来是按照键值插入的了,我们现在给他一个权值,每个点都给一个权值(肯定是rand出来啊,越乱越好,就像Hash一样乱搞。。)。。然后让这棵树同时满足键值的顺序和大根堆的性质(根权值在儿子之间最大)。这个时候就要引入旋转操作,你怎么知道你一定会刚好满足这两个性质,所以要一些操作去维护。我们先按照键值插入,然后看如果左儿子的权值比根大了,就右旋转,右儿子大了就左旋转。。。先看看右旋转吧。。。仔细想一想,既然Lson大,那么就把Lson旋转到根,A还是Lson的左儿子,Rson还是根的右儿子,根变成的Lson的右儿子,Lson的右儿子变成了根的左儿子,他们之间键值关系是没变的,左边是的关系是A<Lson<B<根<Rson,现在旋转了之后,还是A<Lson<B<根<Rson同时权值关系也维护成了一个大根堆。。。其实你们一直漏了一个细节吧。。。就是根的爸爸对应的儿子要变成lson了吧。没错。。。这个就交给取地址返回值处理吧。。。上代码

void Lrotate(int &root)//返回值,把根给改了
{
	int x=Rson[root];
	Rson[root]=Lson[x];
	Lson[x]=root;
	Maintain(root);//这个维护信息先别管。。。后面再说。。。
	root=x;
	return;
}

void Rrotate(int &root)//两个其实差不多,画个图就行了。。。
{
	int x=Lson[root];
	Lson[root]=Rson[x];
	Rson[x]=root;
	Maintain(root);
	root=x;
	return;
}


void Insert_(int &root,int x)
{
	if(!root)
	{
		root=x;
		return ;
	}
	if(Val[x]<Val[root]) Insert_(Lson[root],x);
	else Insert_(Rson[root],x);
	if(w[Lson[root]]>w[root]) Rrotate(root);//左儿子大,右旋转
	if(w[Rson[root]]>w[root]) Lrotate(root);//右儿子大,左旋转
	Maintain(root);
	return;
}

这样之后,这棵树退化的几率很小很小辣。。。emmm他还可以维护一些信息方便你找这个序列中第K大的数。。。维护一个size,size[i]表示以i为根的树有多少个节点(包括i)emmm,很简答啦,就是size[i]=size[Lson[i]]+size[Rson[i]]+1。。。就是这样辣查询第K大就是看子树的大小,然后夹迫法,一步步逼近答案。我的代码。。。

#include<iostream>
#include<cstdio>
#include<ctime>
#include<cstdlib>
#include<cstring>
using namespace std;

int n,m,cnt=1,a[150000],b[150000],w[150000],Lson[1500000],Rson[150000],Val[150000],size[150000];
int num;

void in_()
{
	srand(20031231);
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	for(int i=1;i<=m;i++)
		scanf("%d",&b[i]);
	for(int i=1;i<=n;i++)
		w[i]=rand()%123456789+1;
	
	return;
}

void Maintain(int root)
{
	size[root]=size[Lson[root]]+size[Rson[root]]+1;
	return;	
}

void Lrotate(int &root)
{
	int x=Rson[root];
	Rson[root]=Lson[x];
	Lson[x]=root;
	Maintain(root);
	root=x;
	return;
}

void Rrotate(int &root)
{
	int x=Lson[root];
	Lson[root]=Rson[x];
	Rson[x]=root;
	Maintain(root);
	root=x;
	return;
}


void Insert_(int &root,int x)
{
	if(!root)
	{
		root=x;
		return ;
	}
	if(Val[x]<Val[root]) Insert_(Lson[root],x);
	else Insert_(Rson[root],x);
	if(w[Lson[root]]>w[root]) Rrotate(root);
	if(w[Rson[root]]>w[root]) Lrotate(root);
	Maintain(root);
	return;
}

int find(int root,int x)
{
	if(!root) return 0;
	if(x==size[Lson[root]]+1) return root;
	if(x<size[Lson[root]]+1) return find(Lson[root],x);
	if(x>size[Lson[root]]+1) return find(Rson[root],x-size[Lson[root]]-1);
}

void work()
{
	w[0]=-1;
	int cur=1,cnt=0;
	for(int i=1;i<=n;i++)
	{
		Val[i]=a[i];
		size[i]=1;
		Insert_(num,i);
		while(i==b[cur])
		{
			cnt++;
			int ans=find(num,cur);
			printf("%d\n",Val[ans]);
			cur++;
		}
	}
	return;
}

int main()
{
	in_();
	work();
	return 0;
}

差点忘了,还有一个删除操作。。。也很好理解的。。。先找到这个点。。。然后把它转到叶子节点,然后删除就行了。。。

void Remove(int &root,int v)
{
	if(!Val[root]) return;
	if(Val[root]==v)
	{
		if(!Lson[root]&&!Rson[root])
		{
			root=0;
			return;
		}
		if(w[Lson[root]]>w[Rson[root]])
		{
			Rrotate(root);
			Remove(Rson[root],v);
		}
		else 
		{
			Lrotate(root);
			Remove(Lson[root],v);
		}
	}
	else 
		if(v<Val[root]) Remove(Lson[root],v);
	else Remove(Rson[root],v);
	Maintain(root);
	return;
}

作者写了这么多,有点懒了,维护信息maintain的位置你们就自己探究吧。。。。treap事实上不是很实用,一般都是用splay或者线段树实现。。。对了,当有给他的值相同的时候。。。Val相同的时候可能会出一些小错误,所以就把Val相同的累积到一个点上,然后用一个数组记录这个点上累计了多少个数,就是有多少个相同的点了。。。。这样就不会错了。。。至于为什么会错,因为有些treap的定义是小的放左边,大于等于放右边,旋转的时候等于的可能转到左边了,你删除的时候就没删掉哇。。。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值