Leetcode Link: 1049. 最后一块石头的重量 II - 力扣(LeetCode)

题目

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。 每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。 最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0

解法一

自己的思路 1:从暴力递归到动态规划

思路: 仔细理解题目,题目可以认为等价于:

将输入的 stone 的分成两组,输出两组和的差的最小值。

由于 stone 是固定的,因此一组大另一组一定小,所以实际上可以通过计算其中一组距离 target = sum(stone)/2 距离再乘2来求得两组的最小差

那么我们的问题进一步转化成:

从输入的 stone 挑出一些石头,让这些石头的和距离target 最小(即01背包问题,stone就是物品,target就差不多是我们的背包容量,但是我们实际上是可以超过背包容量的,我们要的是最小差)

那么,我们的子问题是给定当前位置 idx, 当前总和 ssum, 我们需要构造一个函数,该函数返回的是从 idx 到最后位置上的石头里,距离target的最小值

class Solution:
    def lastStoneWeightII(self, stones: List[int]) -> int:
        target = sum(stones) / 2
        res = self.process(stones, target, 0, 0)
        return int(2*res)
 
    # [idx .. n] 中距离 target 的最小值
    # 前面的不考虑了,就知道前面的和为ssum
    def process (self, stones, target, idx, ssum):
	    # base case: 石头都已经挑没了
	    # 注意此时 idx 已经越界了
        if idx == len(stones):
            return abs(ssum-target)
        ## 分成两种情况,pick当前位置的石头,不pick当前位置的石头
        p1 = self.process(stones, target, idx+1, ssum)            
        p2 = self.process(stones, target, idx+1, ssum+stones[idx])
        return min(p1, p2)

根据上面的暴力递归,改写成动态规划

class Solution:
    def lastStoneWeightII(self, stones: List[int]) -> int:
        total_sum = sum(stones)
        target = total_sum / 2
        # [行 idx, 列 total sum+1]
        dp = [[0 for _ in range (total_sum)] for _ in range (len(stones)+1)]  
        # 根据 base case 设置初始值
        for ss in range(total_sum):
            dp[len(stones)][ss] = abs (ss - target)
        # 根据依赖分别填写其他的值
        # 这里可以看看下面的笔记,有一点小弯
        for i in range(len(stones)-1, -1, -1):
            for j in range(total_sum):
                if not 0 <= (j+stones[i]) < total_sum:
                    dp[i][j] = dp[i+1][j]  # 这一步实际上暴力递归中没有体现,是debug出来的
                else:
                    dp[i][j] = min(dp[i+1][j], dp[i+1][j+stones[i]])
        return int(2*dp[0][0])

由于上述思路存在诸多的弊端,我尝试向 01 背包的解题模式靠近, 即子问题返回的是从[idx.. n]中,剩余的值的最小值

class Solution:
    def lastStoneWeightII(self, stones: List[int]) -> int:
        target = sum(stones) / 2
        res = self.process(stones, target, 0, target)
        return int(2*res)
 
    # [idx .. n] abs(rest) 的最小值
    # 等价于距离 target 的最小
    def process (self, stones, target, idx, rest):
	    # 3个base case
        if rest == 0:  # 1. ==0 肯定最小,直接返回
            return 0
        if rest < 0:   # 小于零肯定不用再继续了,因为都是正数,只能更小,所以直接返回 abs(rest)
            return abs(rest)
        if idx == len(stones): # 当越界的时候,也直接返回了
            return abs(rest)
        
        p1 = self.process(stones, target, idx+1, rest)             # 不要 idx
        p2 = self.process(stones, target, idx+1, rest-stones[idx]) # 要 idx
        return min(p1, p2)

然而,该思路在变成动态规划时,是有更严重的问题的。下面我贴出转化后的代码,来分析为什么这个转成动态规划的代码是错误的

class Solution:
    def lastStoneWeightII(self, stones: List[int]) -> int:
        total_sum = sum(stones)
        target = total_sum // 2
        dp = [[0 for _ in range(target)] for _ in range(len(stones)+1)]  # [idx, rest]
        for i in range(len(stones)+1):
            dp[i][0] == 0
        for i in range(target):
            dp[len(stones)][i] = abs(i)
 
        for i in range(len(stones)-1, -1, -1):
            for j in range(target):
                if not 0 <= (j-stones[i]) < target:
                    dp[i][j] = min(dp[i+1][j], abs(j-stones[i]))
                else:
                    dp[i][j] = min(dp[i+1][j], dp[i+1][j-stones[i]])
        return int(2*dp[0][target-1])

问题

  1. 实际上我们依然没能解决行的长度为 len(stones)+1 的逻辑不一致性
  2. 列数变成了 target,然而 target 有可能是 0.5 的小数,这样是不能直接 in range(target) 的,这就导致了我们的算法一定会在这个思路上出错,除非进一步处理一下
  3. 然而稍微好点的是我们在两个for里的越界判断是可以体现在暴力递归中的,但是由于本方法不能直接转dp,故也没辙~

代码随想录中似乎是采用的本思路,然而是直接套的01背包模板,后面可以熟悉看看。

启发和联系