Heap:贪心算法助手

Greedy Gas Fill

Alice和Bob要去roadtrip,问题来了:因为路途有点远,我们估计要在路上停下来加油。那么去哪里加油能让我们停下次数最少呢?不同的加油站离出发距离不一样,能加油的量也不一样。就有了这样的对话:

Bob提议:“别一边开一边计划了,我们就一脚油门开到底,不行了再“后悔“去错过的那些能加最多油的地方加。“

Alice说:“那你怎么知道哪些地方油最多呢?“

Bob回答道:“我们在路上拿个小本本记下来就是了“。

这个“小本本“,把油最多的地方记下来的数据结构,就可以是堆(Heap)。

最低加油次数

输入:target = 100, startFuel = 10, stations = [[10,60],[20,30],[30,30],[60,40]]
输出:2
解释:
我们出发时有 10 升燃料。
我们开车来到距起点 10 英里处的加油站,消耗 10 升燃料。将汽油从 0 升加到 60 升。
然后,我们从 10 英里处的加油站开到 60 英里处的加油站(消耗 50 升燃料),
并将汽油从 10 升加到 50 升。然后我们开车抵达目的地。
我们沿途在1两个加油站停靠,所以返回 2

因为python默认最小对,所以要反转一下符号

def minRefuelStops(target, startFuel, stations):
    far = stop = 0
    queue = []
    stations.append((target, float('inf')))
    for dist, fuel in stations:
        startFuel -= (dist-far)       # 一脚油门开到这儿
        while startFuel < 0 and queue:# 不行,得去小本本上油最多的地方加油
            startFuel -= heapq.heappop(queue)
            stop += 1
        if startFuel < 0: return -1
        heapq.heappush(queue, -fuel)  # 拿小本本记下最多可以加多少油
        far = dist
    return stop

二叉堆

堆Heap,又叫优先队列Priority Queue。本质是一棵满二叉树。它把数组中dd[0]空出来,然后每一个数据就很方便的能找到它的左右儿子d[left]=d[i*2]d[right]=d[i*2+1]。一个最小堆的重要性质是每个节点都小于等于它的子节点

二叉堆:来源https://oi-wiki.org/ds/binary-heap/

为啥说堆是贪心算法的助手呢?因为它在堆顶总是存着最大或者最小的那个数字。比如在区间调度中,我们先处理最先来、腾出最先结束的。又比如贪心优化啊的问题中,我们先考虑优化空间最大的(见练习:最大平均分)。类似的情景中,我们都可以用堆来保留这些“最值“。

注意Python中默认的heapq永远是最小堆,如果要把它当作最大堆来用,就在heappush的时候加入val的负值-val。别在heappop的时候忘记把负号反过来就好了:)

heapq库中除了常规的heappushheappop的操作,还几个方便的函数,可以让你少写几行代码:

  • heappushpop:和先push再pop等价

  • heapreplace:和先pop再push等价,不过更高效一些

时间复杂度上,heappushheappop都是O(logn)的复杂度,然而原地建成一个堆的heapifyO(n)的操作(粗略证明)。

还有一些其他的堆类型,在其他操作时有更快的时间效率(WikipediaOI Wiki配对堆等介绍)。

数组简单实现

下面是手动实现的最小堆。最大堆只需把less函数变成more就好了。

主要功能:

  • insert(val)O(logn)插入一个元素 实现:在满二叉树的最后插入一个元素,然后用swim(k)让它游上来

  • pop()O(logn)得到并删除最小元素 实现:得到满二叉树的根值,交换根与尾元素交换然后删除新的尾元素,然后让新的根用sink(k)

    沉下去

  • swim(k):不断地把第k个元素val和父亲比较,如果val小就游上去

  • sink(k):不断地把第k个元素val和小儿子比较,如果val大就沉下去

经典的例题有:

  • 进程安排:最小堆保存了最早结束时间

  • 最大的K个数:大小为K的最小堆作为缓存,来筛选是否加入新元素

  • 合并K个升序链表[LC 23]:大小为K的最小堆作为缓存排序。延伸:设计推特[LC 355]。

例题:进程安排

下面几题都用优先队列来帮助找到最先结束的进程。

例题:车队 II

Last updated

Was this helpful?