您的位置:首页 > 其它

堆排序(优先队列)——合并果子

2016-08-29 22:01 726 查看
首先说一下堆的性质:分为大根堆和小根堆,根节点都是最值,小根堆的根节点是最小的,每个堆都比它的两个子堆要小,大根堆的根节点是最大的,每个堆都比它的两个子堆要大。

顺便说一下二叉树的性质,左子树小于根节点小于右子树,所以都要先序遍历。

合并果子,是指有n堆果子,每次合并两堆,每次花费的力气为两堆之和,求合并为一堆后,花费的最小力气。

显然,每次合并最小的两堆即可满足题意。

又显然,要用堆排序,这里用小根堆。

(PS:堆跟二叉树一样,都是一个一维数组,每个点的左子树(左堆)的编号等于它*2,右子树(右堆)的编号等于它*2+1)

堆和树的优势:不需要像队列一样一个个枚举 而是每次判断将剩余情况二分,能将O(N)变成O(logN)。(二分的思路有很多 堆和树还有快排)

显然二分的前提是队列要有某种规律,比如需要先排好序。

比如一个数列1,2,3,4,5,6,7,8,9,10;(a[1]~a[10])

比如要查找8;

倘若枚举,显然要8次;

如果二分,一开始的范围显然是全部(1~10)。

取中间a[(1+10)/2]=a[5]=5<8;

那么范围变为6~10。(一次查找)

继续查找,在剩余范围二分。

a[(6+10)/2]=a[8]=8。找到目标(两次查找)

这就是二分的优点。

当然,倘若要寻找1,枚举会比二分快,但二分的优势是整体优化,也就是说,在刚刚的样例里,显然所有的数在4次以内都会被搜到,而枚举,最多需要10次。

下面上代码:

#include<iostream>
#include<cstdio>
using namespace std;
long long ans;
int a[110000],len,s;
int main(){
int i,j,k;
int n,u;
cin>>n;
for(i=1;i<=n;i++){
cin>>a[i];//每次读入一个堆,存在队尾,因为直接在最低层,可能会比之前的数小,不满足堆的性质,就开始排序(往上浮)
u=i;
while(a[u]<a[u/2] && u>1){//如果它比它的根堆还小 并且它不是根堆(若它是根堆即a[1]那么它的父堆为a[0]=0 所以需要特判)
k=a[u];
a[u]=a[u/2];
a[u/2]=k;
u/=2;//交换位置 u指新插入的数在数组中的位置
}
}
len=n;
while(len>1){
s=a[1];//每次取前两堆求和,先取出第一堆
a[1]=a[len];//将第一堆直接赋值成最后一堆
len--;//取出了一堆 所以长度-1
u=1;//将最后一堆变成了第一堆 很可能不满足堆的性质 此时堆的编号为1 可能需要往下沉
while((a[u*2]<a[u] && u*2<=len) || (a[u*2+1]<a[u] && u*2+1<=len)){//若它比它的子堆大 并且它的子堆未超出长度(之前删除的堆没有覆盖 只是将长度-1了所以数组后面的值仍然在那)
if(a[u*2]>a[u*2+1] && len>=u*2+1)u=u*2+1;//优先往小的那边沉
else u*=2;
k=a[u];
a[u]=a[u/2];
a[u/2]=k;//交换
}
s+=a[1];//之前已经取出过一堆了 现在重新使堆满足性质后 取出第二堆相加
a[1]=s;//将取出的两堆合起来放回去,显然此时可能不满足堆的性质了,需要往下沉
ans+=s;//将花费的力气加入ans
u=1;//此时可能需要下沉的堆的编号为1
while((a[u*2]<a[u] && u*2<=len) || (a[u*2+1]<a[u] && u*2+1<=len)){
if(a[u*2]>a[u*2+1] && len>=u*2+1)u=u*2+1;
else u*=2;
k=a[u];
a[u]=a[u/2];
a[u/2]=k;//交换
}
}
cout<<ans;//输出
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  堆排序 优先队列