关于堆和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的定义是小的放左边,大于等于放右边,旋转的时候等于的可能转到左边了,你删除的时候就没删掉哇。。。