从数量最多的堆取走礼物

标签: 数组 模拟 堆(优先队列)

难度: Easy

给你一个整数数组 gifts ,表示各堆礼物的数量。每一秒,你需要执行以下操作:

  • 选择礼物数量最多的那一堆。
  • 如果不止一堆都符合礼物数量最多,从中选择任一堆即可。
  • 选中的那一堆留下平方根数量的礼物(向下取整),取走其他的礼物。

返回在 k 秒后剩下的礼物数量

示例 1:

输入:gifts = [25,64,9,4,100], k = 4
输出:29
解释: 
按下述方式取走礼物:
- 在第一秒,选中最后一堆,剩下 10 个礼物。
- 接着第二秒选中第二堆礼物,剩下 8 个礼物。
- 然后选中第一堆礼物,剩下 5 个礼物。
- 最后,再次选中最后一堆礼物,剩下 3 个礼物。
最后剩下的礼物数量分别是 [5,8,9,4,3] ,所以,剩下礼物的总数量是 29 。

示例 2:

输入:gifts = [1,1,1,1], k = 4
输出:4
解释:
在本例中,不管选中哪一堆礼物,都必须剩下 1 个礼物。 
也就是说,你无法获取任一堆中的礼物。 
所以,剩下礼物的总数量是 4 。

提示:

  • 1 <= gifts.length <= 103
  • 1 <= gifts[i] <= 109
  • 1 <= k <= 103

Submission

运行时间: 28 ms

内存: 16.2 MB

class Solution:
    def pickGifts(self, gifts: List[int], k: int) -> int:
        for i in range(len(gifts)):
            gifts[i] *= -1  # 变成相反数,这样堆化后就是最大堆了
        heapify(gifts)  # 原地堆化
        while k and -gifts[0] > 1:
            heapreplace(gifts, -isqrt(-gifts[0]))  # 直接修改堆顶
            k -= 1
        return -sum(gifts)

Explain

这个题解通过将所有礼物堆的数量变为负数并构造最大堆来快速地访问最多礼物的堆。在每个周期中,从堆中取出堆顶元素(最多礼物的堆),留下其平方根个礼物,即修改堆顶的值并保持堆的性质。操作持续 k 秒或直到堆顶的礼物数量小于等于 1。最后,返回堆中所有元素的负数之和,即剩余礼物的总数。

时间复杂度: O(k log n)

空间复杂度: O(1)

class Solution:
    def pickGifts(self, gifts: List[int], k: int) -> int:
        # 将所有的礼物数量取反,用来构建最大堆
        for i in range(len(gifts)):
            gifts[i] *= -1
        # 将数组堆化,现在数组顶部是最小的数,即最大的原始数的相反数
        heapify(gifts)
        # 如果还有剩余的秒数并且堆顶元素绝对值大于1(即原始元素大于1)
        while k and -gifts[0] > 1:
            # 将堆顶元素替换为它的平方根的相反数,并重新堆化
            heapreplace(gifts, -isqrt(-gifts[0]))
            k -= 1
        # 返回所有剩余礼物的总数,即负数总和的相反数
        return -sum(gifts)

Explore

选择平方根作为每次操作后留下的礼物数量的函数,主要是因为平方根可以有效地减少堆顶的值,而又不会过快地减少到接近零,从而保持了游戏的持续性和竞争性。立方根和对数函数在减少大数时可能过于保守或过于激进。平方根提供了一个中等速度的递减,使得大量礼物的堆在多次操作后仍然具有相对较多的礼物,维持游戏的平衡。

在Python中,标准库的堆(heapq)是最小堆,它总是让最小的元素位于堆顶。通过将所有礼物数量取反,我们实际上是在使用最小堆的性质来模拟一个最大堆的行为。这样做的优势是可以直接使用Python标准库而不需要实现一个最大堆,同时能够快速地访问和处理最多礼物的堆,这对于解题是非常有效和直接的方法。

`heapify`函数将一个普通列表转换成一个堆结构,这个过程的时间复杂度是O(n),这是初始化堆的一个高效操作。而`heapreplace`则是一个组合操作,它先弹出堆顶元素,然后将新元素加入堆中,并重新调整堆以维护堆的性质,这个操作的时间复杂度是O(log n)。这两种操作使得我们能够以对数时间复杂度处理堆顶元素并快速更新堆,这对于需要频繁访问和修改堆顶元素的问题非常关键。

算法通过循环处理,不断地将堆顶元素(即最多的礼物堆)的礼物数量取其平方根,然后继续进行堆操作。即使`k`大于数组长度,算法仍然会持续处理直到`k`次操作完成或所有的礼物堆的数量都不再允许按照题目要求进行操作(即堆顶的礼物数量小于等于1)。这确保了无论`k`的值或数组的长度如何,算法都能正确地反复处理并更新堆,最终能准确计算出剩余的礼物总数。