P1090 合并果子

原题链接 https://www.luogu.org/problemnew/show/P1090

看了题面,应该就会想到用贪心+二叉堆吧。

先带大家走一遍思路:

题目要求消耗的最小体力值,很轻易就想到每次找两个最小的堆进行合并,重复n-1次,那么这样消耗的体力一定是最少的,贪心思想!

对于排序,我们更轻易得想到用sort排序,每次取出最小的两个元素就好啦;

然后就像我一样用sort快排交了上去,发现TLE了一堆!!!

为什么呢???

sort排序固然很快!但是对于n-1排序的话,就远远不如堆排序了。堆排序运用了二分的思想,大大缩短了排序时间!

二分思想?如果不了解堆排序的你现在可能有点懵,下面解释一下堆排序的原理及代码实现吧:   详细请看大佬的博客https://www.cnblogs.com/chengxiao/p/6129630.html  qaq~

预备知识

堆排序

  堆排序是利用这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。首先简单了解下堆结构。

  堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:

同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子

该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:

大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]  

小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]  

ok,了解了这些定义。接下来,我们来看看堆排序的基本思想及基本步骤:

堆排序基本思想及步骤

  堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了

步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。

  a.假设给定无序序列结构如下

2.此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整。

4.找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。

这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。

此时,我们就将一个无需序列构造成了一个大顶堆。

步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。

a.将堆顶元素9和末尾元素4进行交换

b.重新调整结构,使其继续满足堆定义

c.再将堆顶元素8与末尾元素5进行交换,得到第二大元素8.

后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序

再简单总结下堆排序的基本思路:

  a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;

  b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;

  c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int n,m,a[10005],sum=0,ans=0;
void put(int k)
{
    sum++;                            //加入新元素,sum加一 
    int now,next;                     //now记录新元素的编号,next记录该元素的父亲的编号 
    a[sum]=k;
    now=sum;
    while(now>1)                      //如果now不是根结点 
    {
        next=now/2; 
        if(a[now]>=a[next]) return ;  //如果孩子大于等于父亲,说明符合小根堆的特征,直接返回 
        swap(a[now],a[next]);          
        now=next;
    }
}
int get()                             //从最小堆中取出一个元素                             
{
    int now=1,next,small;             //now是记录取出的元素在堆中的编号,根据题意我们要取得是最小值,根结点符合,所以now赋值为1 
    small=a[1];                       //small记录堆中的最小元素 
    a[1]=a[sum];                      //用堆中最后一个元素将第一个元素覆盖 
    sum--;                            //堆的长度减一 
    while(now*2<=sum)                 //now的左孩子要在堆内 
    {
        next=now*2;                   //next记录now的左孩子的编号 
        if(a[next]>a[next+1]&&next+1<=sum) next++;     //如果now的右孩子next+1在堆内且右孩子小于左孩子,next加一改成右孩子的编号,保证next记录的元素是now孩子中最小的 
        if(a[now]<=a[next]) return small;              //如果now比最小孩子还小,说明符合小根堆的特征,直接返回small 
        swap(a[now],a[next]);         //如果到了这一步,说明now比next大,那么就要交换它们两个的值 
        now=next;                     //now换成next的值继续往下搜 
    }
    return small;                     //这里别忘了再返回一次 
}
int main()
{
    int x,y;
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>m;
        put(m);                       //将输入的m入堆,并进行调整,维持它的最小堆 
    }
    for(int i=1;i<n;i++)
    {
        x=get();                      //取出堆中的最小元素 
        y=get();                      //取出堆中的次小元素 
        ans+=x+y;                     //记录消耗的体力值 
        put(x+y);                      //将新合成的果堆入堆 
    }    
    cout<<ans<<endl;
    return 0;
}

 

posted @ 2019-04-26 20:32  暗い之殇  阅读(399)  评论(2编辑  收藏  举报