【做题经验】ybt:堆

本文通过四道题目详细介绍了堆和优先队列的应用,包括堆排序、结构体与优先队列的结合、越界处理以及处理动态更新的账单问题。强调了在实际编程中需要注意的细节和优化技巧,旨在帮助读者熟练掌握堆和优先队列的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这篇博客里,我想介绍一下堆,再通过几道题介绍一下一些细节(优化)。(所有例题均来自openjudge)
1. 堆及优先队列
2. “合并果子”:基本堆
3. “最小函数值”:优先队列
4. “看病”:用优先队列来谈结构体
5. “小明的账单”:结构体的巧用

堆与优先队列

首先,堆是一种特殊的数据结构,它是通过递归来进行定义的,类似于冒泡排序的过程,它从下往上,将最小值(小根堆)或最大值(大根堆)移到最上方。而且,在插入或取出数时,它能以较快的速度(**nlogn**)再次维护最值。
当然,这只是堆的最基本应用。它的作用远远不止于此,不过由于知识所限,我只能这么理解,若用这种粗浅的理解方式,它,就是一个劣化版的优先队列,而优先队列的速度又快(logn)又好写,为什么不用优先队列呢?

“合并果子”

显然,我们应当排序,并按从小到大合并两堆果子。那么,这就是堆的第一个用法——堆排序,它的思路是这样的:有这么一棵完全二叉树,我们利用交换,使得每一个父结点都严格小于它的两个子结点,那么最小的结点自然就在最上面(注意:下面的结点不一定是从小到大排列)。
很容易想到,这是递归调用的结构:我们从最下面一层求起,然后依次往上,便构建了一个堆。

void heapsort(int v)
{
    int k=v+v;
    if(k>n) return;
    if(k+1>n) {
        if(x[v]>x[k]) {int t=x[v];x[v]=x[k];x[k]=t; }
        return ;
    }
    int j=k;
    if(x[k+1]<x[k]) j++;
    if(x[v]>x[j]) {int t=x[v];x[v]=x[j];x[j]=t;}
    heapsort(j);
}

这是递归调用的部分。在这里,我想请大家注意两个部分:
第一是一个很重要的结论——对于一颗二叉树,它任意一个父节点v的子节点是2v和2v+1(如果有的话)。
第二点,是定义了一个j,利用j++的方式来找出子节点中最大的那一个,代码清晰,一目了然,也很好写。

for(int i=n/2;i>0;i--)
    heapsort(i);

这是主程序里面的构建堆部分。
这里也可以得出一个同样重要的结论。一棵n结点二叉树最小的父节点是n/2,值得思考这是为什么。

“最小函数值”

这个时候,我们就要开始使用优先队列了。
首先看这道题的数据规模:n,m<=10000;显然,若将每一个函数从一到m的值全部算出来,然后进行排序,光是计算都要超时。很自然,我们发现函数的对称轴小于零,所有的函数都是单调递增的。
那么,这有一种普遍,浅显,也用途广泛的优化,请各位读者记住:(不过我想你们都应该会这个)**我们求出所有函数在x=1时的最小值,之后选择最小的那一个,求出它在x+1时的最值,重复此步骤,直到选出了m个数。**
这是一种贪心思想,**每次选择最小的一个扩展,将其入队并重复此操作**。如果学过Kruskal算法的人一定不会陌生,正确性在这里显而易见。
思路上的东西讲完了,但在实践部分可能会遇到一些问题:
显然,这题该使用优先队列,可每次队首的那个元素是哪一个函数求出来的?此时自变量的值又是多少?我们该考虑结构体了,它是所有**离散结构的基础**。
struct sd{
    int sum,pos,han;
    bool operator < (const sd& x) const{
      return sum>x.sum; 
    }
    sd(int u,int v,int y):sum(u),pos(v),han(y){}
};

这是结构体里面的东西,sum是值,pos是指第pos个函数,han是此时自变量的取值。
第一点是里面的运算符重载,这是定义优先队列的基础,相信大家都会。
之后是最后面一行,那是一个构造函数,具体来讲,它是让我在赋值时更方便的工具,让我可以用Q.push((sd){s,p1,p2});这样的方式来加入元素,不过不用这个也无伤大雅。你大可以像这样:;

   sd p;p.pos=xxx;p.xx=xxx;p.xxx=xxxxxx;
   Q.push(p);

效果一样,不过写得要多一些。

while(k++<m)
    {
        sd temp=Q.top();Q.pop();printf("%d ",temp.sum);
        int p1=temp.pos,p2=temp.han+1;
        int s=fun[p1][0]*p2*p2+fun[p1][1]*p2+fun[p1][2];
        Q.push((sd){s,p1,p2});
    }

这是主函数的部分,没有什么好说的,自行操作熟练不在话下。

“看病”

既然上一道题的解法会了,这一道题也是没有什么问题,注意一点:一切数据结构都要判断是否越界,在这里,题目允许错误时输出错误报告,但有些题不会告诉你,这个时候一定要谨慎判断是否可能出现越界的情况,及时退出。
好在有一个函数xx.empty(),当空的时候返回true,否则返回假。这样,程序也不难写出了,此题较水,可以跳过。

“小明的账单”

相比前面几道题,这道题显得“不那么水”,因为每次都要输出最大值和最小值,而在中间还有向其中加入数据的过程,所以显然不能通过单纯的排序解决(每一次都排序的话肯定超时)。我们想到的是优先队列,两个优先队列分别输出最大,最小值。
这就有一个问题,stl的优先队列并不支持删除一个“位于中间”的数,所以很可能造成一张账单在第一天最小,第二天最大的情况,从两个优先队列中被输出两次。解决这个问题的方法也很简单但重要:开一个bool型数组vis[],并用结构体,将账单的大小与位置记录下来,每次top()时,判断这张账单是否被用过。
请大家注意两个细节:第一,保存的是账单的位置而不是大小,因为账单可能会有大小重复的情况;第二,空间开了一个(m),时间上,由于每张账单最多在两个队列里分别判断一次,所以新增的时间复杂度还是(n)。所以没有什么问题。

 sd m=Q1.top();Q1.pop();
while(!x[m.b])
     { m=Q1.top();Q1.pop(); }
x[m.b]=false;
printf("%d ",m.a);
 sd t=Q2.top();Q2.pop();
while(!x[t.b])
     { t=Q2.top();Q2.pop(); }
printf("%d\n",t.a);

这是每一天支付账单的过程。

结尾

那么到这里,最基本的堆和优先队列的介绍就到此完毕了,这一节大多数是一些基础的模板与小技巧,请大家多多熟练(背板),并能独自打出这些程序,并在实践中尝试使用这些小技巧,为更高阶的内容优化基础。
感谢您的阅读!笔者为一个新手,难免有阙漏之处,届时请指正!如果你觉得这篇文章有什么值得借鉴参考的地方,请点一个赞或者评论让我知道,这样我的写作会更有动力!
下一章,可能是从最大(m)子段和到最大子矩阵(省选)一系列dp过程的推导作为复习;也有可能是来点“拆点构图”的最短路径的“高级题目(联赛难度)”,敬请期待!
2017.10.24

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值