3n 块披萨

标签: 贪心 数组 动态规划 堆(优先队列)

难度: Hard

给你一个披萨,它由 3n 块不同大小的部分组成,现在你和你的朋友们需要按照如下规则来分披萨:

  • 你挑选 任意 一块披萨。
  • Alice 将会挑选你所选择的披萨逆时针方向的下一块披萨。
  • Bob 将会挑选你所选择的披萨顺时针方向的下一块披萨。
  • 重复上述过程直到没有披萨剩下。

每一块披萨的大小按顺时针方向由循环数组 slices 表示。

请你返回你可以获得的披萨大小总和的最大值。

示例 1:

输入:slices = [1,2,3,4,5,6]
输出:10
解释:选择大小为 4 的披萨,Alice 和 Bob 分别挑选大小为 3 和 5 的披萨。然后你选择大小为 6 的披萨,Alice 和 Bob 分别挑选大小为 2 和 1 的披萨。你获得的披萨总大小为 4 + 6 = 10 。

示例 2:

输入:slices = [8,9,8,6,1,1]
输出:16
解释:两轮都选大小为 8 的披萨。如果你选择大小为 9 的披萨,你的朋友们就会选择大小为 8 的披萨,这种情况下你的总和不是最大的。

提示:

  • 1 <= slices.length <= 500
  • slices.length % 3 == 0
  • 1 <= slices[i] <= 1000

Submission

运行时间: 28 ms

内存: 16.3 MB

class Solution:
    def maxSizeSlices(self, arr: List[int]) -> int:
        n=len(arr)
        k=n//3
        arr=[0]+arr+[0]
        h=[]
        pre=[0]*(n+2)#前驱
        nxt=[0]*(n+2)#后继
        vis=[False]*(n+2)#是否删除了
        for i in range(1,n+1):
            pre[i]=i-1
            nxt[i]=i+1
            heappush(h,(-arr[i],i))
        pre[1]=n
        nxt[n]=1
        def remove(x):
            nxt[pre[x]]=nxt[x]
            pre[nxt[x]]=pre[x]
            vis[x]=True
        tot=0
        for _ in range(k):
            while h and vis[h[0][1]]:
                heappop(h)
            if len(h)==0:
                break
            val,idx=heappop(h)
            val*=-1
            tot+=val
            arr[idx]=arr[pre[idx]]+arr[nxt[idx]]-val
            heappush(h,(-arr[idx],idx))
            remove(pre[idx])
            remove(nxt[idx])
            
        return tot

Explain

这道题的思路是使用贪心算法加上双向链表和最小堆。首先,为了方便处理环形数组,我们在数组两端各添加一个0,并建立双向链表来表示这个环。我们的目标是在每一轮中选择一个披萨片,使得当前披萨片加上相邻两个披萨片的总和最大,然后删除这三个披萨片,并更新相邻披萨片的值。为了快速找到这样的披萨片,我们使用最小堆来维护每个披萨片及其相邻披萨片的总和。每次从堆中取出最小的元素,检查其是否已经被删除(由于堆的延迟删除特性),如果没有被删除,则更新总和并进行相应的删除和更新操作。这个过程重复进行k次,其中k是披萨片总数除以3。

时间复杂度: O(nlogn)

空间复杂度: O(n)

class Solution:
    def maxSizeSlices(self, arr: List[int]) -> int:
        n=len(arr)
        k=n//3
        arr=[0]+arr+[0]
        h=[]
        pre=[0]*(n+2)  # 前驱
        nxt=[0]*(n+2)  # 后继
        vis=[False]*(n+2)  # 是否删除了
        for i in range(1,n+1):
            pre[i]=i-1
            nxt[i]=i+1
            heappush(h,(-arr[i],i))
        pre[1]=n
        nxt[n]=1
        def remove(x):
            nxt[pre[x]]=nxt[x]
            pre[nxt[x]]=pre[x]
            vis[x]=True
        tot=0
        for _ in range(k):
            while h and vis[h[0][1]]:
                heappop(h)
            if len(h)==0:
                break
            val,idx=heappop(h)
            val*=-1
            tot+=val
            arr[idx]=arr[pre[idx]]+arr[nxt[idx]]-val
            heappush(h,(-arr[idx],idx))
            remove(pre[idx])
            remove(nxt[idx])
            
        return tot

Explore

在数组两端各添加一个0主要是为了处理环形数组的边界情况。在环形结构中,数组的第一个元素和最后一个元素是相邻的,通过添加0可以简化编程逻辑,使得数组的起始和结束位置能够统一处理。这样,在使用双向链表表示环时,可以无需编写特殊的边界条件代码,从而降低错误率并简化代码实现。

双向链表在此解法中用于动态地删除和更新元素。双向链表允许在O(1)时间复杂度内删除任何一个节点,并快速更新其前驱和后继节点,这对于该题中需要频繁删除和调整元素位置的需求来说非常重要。尽管理论上可以使用数组或其他数据结构,但双向链表在此场景下提供了无与伦比的操作效率和简便性,尤其是在需要频繁地插入和删除操作时。

题解中使用最小堆可能是一个错误,因为题目要求是在每一轮中选择一个披萨片,使得当前披萨片加上相邻两个披萨片的总和最大。因此,应该使用最大堆来维护每个披萨片及其相邻披萨片的总和,以便每次都能从堆中取出当前总和最大的元素。如果确实使用了最小堆,这可能是题解中的一个逻辑错误或者是描述错误。

堆中的延迟删除策略是通过在堆操作中结合一个标记数组来实现的。在这种策略中,当一个元素从逻辑上被删除(即不再是堆中的有效元素)时,并不立即从物理上从堆中移除,而是简单地标记这个元素为已删除。当这个元素浮到堆顶部,堆的操作(如pop)会检查该元素是否被标记为删除,如果是,则丢弃它并继续从堆中移除下一个元素。这种策略的必要性在于它避免了复杂和耗时的堆重构操作,尤其是在连续删除多个堆元素的情况下,可以显著提高效率。