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

首先说一下堆的性质:分为大根堆和小根堆,根节点都是最值,小根堆的根节点是最小的,每个堆都比它的两个子堆要小,大根堆的根节点是最大的,每个堆都比它的两个子堆要大。

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

合并果子,是指有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;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值