代码随想录算法训练营Day31 | Leetcode1049. 最后一块石头的重量 II、494. 目标和、474.一和零

代码随想录算法训练营Day31 | Leetcode1049. 最后一块石头的重量 II、494. 目标和、474.一和零

一、最后一块石头的重量 II

相关题目:Leetcode1049
文档讲解:Leetcode1049
视频讲解:Leetcode1049

1. Leetcode1049. 最后一块石头的重量 II

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

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。

最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
提示:

  • 1 <= stones.length <= 30
  • 1 <= stones[i] <= 1000
  • 思路:
    • 本题其实是尽量让石头分成重量相同的两堆,相撞之后剩下的石头就是最小的。全部石头重量是 sum,则需尽可能拼成重量为 sum / 2 的石头堆, 这样剩下的石头堆也是尽可能接近 sum/2 的重量。 所以问题可以转化为有一堆石头,每个石头都有自己的重量,是否可以装满最大重量为 sum / 2 的背包,求背包最多能装多少。
    • 动规五部曲
      • 确定 dp 数组以及下标的含义:dp[j] 表示容量为 j 的背包,最多可以背最大重量为 dp[j]。石头的重量是 stones[i],石头的价值也是 stones[i] 。
      • 确定递推公式:dp[j] = max(dp[j], dp[j - stones[i]] + stones[i])。
      • dp 数组如何初始化:因为重量都不会是负数,所以 dp[j] 都初始化为 0 即可,这样在递归公式 dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]) 中 dp[j] 才不会初始值所覆盖。
      • 确定遍历顺序:因为使用一维 dp 数组,物品遍历的 for 循环放在外层,遍历背包的 for 循环放在内层,且内层 for 循环倒序遍历!
      • 举例推导 dp 数组:输入:[2,4,1,1],此时target = (2 + 4 + 1 + 1)/2 = 4 ,dp 数组状态图如下:
        请添加图片描述
  • 卡哥
class Solution:
    def lastStoneWeightII(self, stones: List[int]) -> int:
        dp = [0] * 15001
        total_sum = sum(stones)
        target = total_sum // 2

        for stone in stones:  # 遍历物品
            for j in range(target, stone - 1, -1):  # 遍历背包
                dp[j] = max(dp[j], dp[j - stone] + stone)

        return total_sum - dp[target] - dp[target]

###卡哥版(简化版)
class Solution:
    def lastStoneWeightII(self, stones):
        total_sum = sum(stones)
        target = total_sum // 2
        dp = [0] * (target + 1)
        for stone in stones:
            for j in range(target, stone - 1, -1):
                dp[j] = max(dp[j], dp[j - stone] + stone)
        return total_sum - 2* dp[-1]
  • 二维 DP 版
class Solution:
    def lastStoneWeightII(self, stones: List[int]) -> int:
        total_sum = sum(stones)
        target = total_sum // 2
        
        # 创建二维dp数组,行数为石头的数量加1,列数为target加1
        # dp[i][j]表示前i个石头能否组成总重量为j
        dp = [[False] * (target + 1) for _ in range(len(stones) + 1)]
        
        # 初始化第一列,表示总重量为0时,前i个石头都能组成
        for i in range(len(stones) + 1):
            dp[i][0] = True
        
        for i in range(1, len(stones) + 1):
            for j in range(1, target + 1):
                # 如果当前石头重量大于当前目标重量j,则无法选择该石头
                if stones[i - 1] > j:
                    dp[i][j] = dp[i - 1][j]
                else:
                    # 可选择该石头或不选择该石头
                    dp[i][j] = dp[i - 1][j] or dp[i - 1][j - stones[i - 1]]
        
        # 找到最大的重量i,使得dp[len(stones)][i]为True
        # 返回总重量减去两倍的最接近总重量一半的重量
        for i in range(target, -1, -1):
            if dp[len(stones)][i]:
                return total_sum - 2 * i
        
        return 0
  • 一维 DP 版
class Solution:
    def lastStoneWeightII(self, stones):
        total_sum = sum(stones)
        target = total_sum // 2
        dp = [False] * (target + 1)
        dp[0] = True

        for stone in stones:
            for j in range(target, stone - 1, -1):
                # 判断当前重量是否可以通过选择之前的石头得到或选择当前石头和之前的石头得到
                dp[j] = dp[j] or dp[j - stone]

        for i in range(target, -1, -1):
            if dp[i]:
                # 返回剩余石头的重量,即总重量减去两倍的最接近总重量一半的重量
                return total_sum - 2 * i

        return 0

二、目标和

相关题目:Leetcode494
文档讲解:Leetcode494
视频讲解:Leetcode494

1. Leetcode494. 目标和

给你一个非负整数数组 nums 和一个整数 target 。向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式

  • 例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
提示:

  • 数组非空,且长度不会超过 20 。
  • 初始的数组的和不会超过 1000 。
  • 保证返回的最终结果能被 32 位整数存下。
  • 思路:
    • 本题要构建表达式使其结果为 target,则可以将表达式中正负数分开两部分,即 left - right = target。而注意到 left + right = sum,sum 是固定的。right 可以由 left 表示: right = sum - left,由 left - (sum - left) = target 推导出 left = (target + sum) / 2 。此时问题转化为在集合 nums 中找出和为 left 的组合
    • 动态规划 (二维 dp 数组)
      • 确定 dp 数组以及下标的含义:dp[i][j] 表示使用下标为 [0, i] 的 nums[i] 能够凑满 j 这么大容量的包,有 dp[i][j] 种方法。

      • 确定递推公式:对于物品 i 有两种情况:

        • 不放物品 i:即背包容量为 j,里面不放物品 i,装满有 dp[i - 1][j] 种方法。
        • 放物品 i: 即先空出物品i的容量,背包容量为(j - 物品 i 容量),放满背包有 dp[i - 1][j - 物品 i 容量] 种方法。

        当 j - nums[i] 小于零说明背包容量装不下物品 i,所以此时装满背包的方法值 等于不放物品 i 的装满背包的方法,即:dp[i][j] = dp[i - 1][j],所以递推公式为:

        • nums[i] > j:dp[i][j] = dp[i - 1][j]
        • nums[i] <= j:dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]
      • dp 数组如何初始化:由递推公式可知求解 dp[i][j] 是由其上方和左上方推出,那么二维数组的最上行和最左列一定要初始化。

        • dp[0][0] 的值,也就是装满背包容量为 0 的方法数量是 1,即放 0 件物品。
        • dp[0][j] 初始化:dp[0][j] 表示只放物品 0, 把容量为 j 的背包填满有几种方法。只有背包容量为物品 0 的容量的时候方法为 1,此时正好装满。其他情况下为装不满或装不下。所以初始化:dp[0][nums[0]] = 1 ,其他均为 0 。
        • dp[i][0] 初始化:dp[i][0] 表示背包容量为 0, 放物品 0 到物品 i,装满有几种方法。其为物品 0 到物品 1 中 0 的组合数,记 0 个数为 numZero,则有 dp[i][0] = 2^(numZero)。
      • 确定遍历顺序:当前值是由上方和左上方推出,所以遍历顺序一定是从上到下,从左到右。

      • 举例推导 dp 数组输入:nums: [1, 1, 1, 1, 1],target: 3,bagSize = (target + sum) / 2 = (3 + 5) / 2 = 4,dp 数组状态变化如下:
        请添加图片描述

    • 动态规划 (一维 dp 数组)
      • 确定 dp 数组以及下标的含义:dp[i][j] 去掉行的维度即 dp[j],其表示填满 j 这么大容积的包,有 dp[j] 种方法。
      • 确定递推公式:二维 dp 数组递推公式: dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]],去掉维度i 之后,递推公式:dp[j] = dp[j] + dp[j - nums[i]] ,即:dp[j] += dp[j - nums[i]]。
      • dp 数组如何初始化:dp[0] 初始为1 ,即装满背包为 0 的方法有一种,放 0 件物品。
      • 确定遍历顺序:遍历物品放在外循环,遍历背包在内循环,且内循环倒序(为了保证物品只使用一次)。
      • 举例推导 dp 数组:输入:nums: [1, 1, 1, 1, 1],,target: 3,bagSize = (target + sum) / 2 = (3 + 5) / 2 = 4,dp 数组状态变化如下:
        请添加图片描述
  • 回溯法
class Solution:
    def backtracking(self, candidates, target, total, startIndex, path, result):
        if total == target:
            result.append(path[:])  # 将当前路径的副本添加到结果中
        # 如果 sum + candidates[i] > target,则停止遍历
        for i in range(startIndex, len(candidates)):
            if total + candidates[i] > target:
                break
            total += candidates[i]
            path.append(candidates[i])
            self.backtracking(candidates, target, total, i + 1, path, result)
            total -= candidates[i]
            path.pop()

    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        total = sum(nums)
        if target > total:
            return 0  # 此时没有方案
        if (target + total) % 2 != 0:
            return 0  # 此时没有方案,两个整数相加时要注意数值溢出的问题
        bagSize = (target + total) // 2  # 转化为组合总和问题,bagSize就是目标和

        # 以下是回溯法代码
        result = []
        nums.sort()  # 需要对nums进行排序
        self.backtracking(nums, bagSize, 0, 0, [], result)
        return len(result)
  • 二维 DP
class Solution:
    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        total_sum = sum(nums)  # 计算nums的总和
        if abs(target) > total_sum:
            return 0  # 此时没有方案
        if (target + total_sum) % 2 == 1:
            return 0  # 此时没有方案
        target_sum = (target + total_sum) // 2  # 目标和

        # 创建二维动态规划数组,行表示选取的元素数量,列表示累加和
        dp = [[0] * (target_sum + 1) for _ in range(len(nums) + 1)]
        dp = [[0] * (target_sum + 1) for _ in range(len(nums))]

        # 初始化状态
        dp[0][0] = 1
        if nums[0] <= target_sum:
            dp[0][nums[0]] = 1
        numZero = 0
        for i in range(len(nums)):
            if nums[i] == 0:
                numZero += 1
            dp[i][0] = int(math.pow(2, numZero))

        # 动态规划过程
        for i in range(1, len(nums)):
            for j in range(target_sum + 1):
                dp[i][j] = dp[i - 1][j]  # 不选取当前元素
                if j >= nums[i - 1]:
                    dp[i][j] += dp[i - 1][j - nums[i]]  # 选取当前元素

        return dp[len(nums)-1][target_sum]  # 返回达到目标和的方案数
  • 一维 DP
class Solution:
    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        total_sum = sum(nums)  # 计算nums的总和
        if abs(target) > total_sum:
            return 0  # 此时没有方案
        if (target + total_sum) % 2 == 1:
            return 0  # 此时没有方案
        target_sum = (target + total_sum) // 2  # 目标和
        dp = [0] * (target_sum + 1)  # 创建动态规划数组,初始化为0
        dp[0] = 1  # 当目标和为0时,只有一种方案,即什么都不选
        for num in nums:
            for j in range(target_sum, num - 1, -1):
                dp[j] += dp[j - num]  # 状态转移方程,累加不同选择方式的数量
        return dp[target_sum]  # 返回达到目标和的方案数

三、一和零

相关题目:Leetcode474
文档讲解:Leetcode474
视频讲解:Leetcode474

1. Leetcode474.一和零

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
提示:

  • 1 <= strs.length <= 600
  • 1 <= strs[i].length <= 100
  • strs[i] 仅由 ‘0’ 和 ‘1’ 组成
  • 1 <= m, n <= 100
  • 思路:

    • 本题其实是 01背包问题,只不过这个背包有两个维度,一个是 m 一个是 n,而不同长度的字符串就是不同大小的待装物品。
    • 动规五部曲
      • 确定 dp 数组以及下标的含义:dp[i][j] 表示最多有 i 个 0 和 j 个 1 的 strs 的最大子集的大小为 dp[i][j]。
      • 确定递推公式:dp[i][j] 可以由前一个 strs 里的字符串推导出来,strs 里的字符串有 zeroNum 个 0,oneNum 个 1。dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1。然后在遍历的过程中,取 dp[i][j] 的最大值,所以递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1)。
      • dp 数组如何初始化:因为物品价值不会是负数,初始为 0,保证递推的时候 dp[i][j] 不会被初始值覆盖。
      • 确定遍历顺序:外层 for 循环遍历物品,内层 for 循环遍历背包容量且从后向前遍历。
      • 举例推导 dp 数组:以输入:[“10”,“0001”,“111001”,“1”,“0”],m = 3,n = 3为例,最后 dp 数组的状态如下所示:
        请添加图片描述
  • 动态规划

###DP(版本一)
class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        dp = [[0] * (n + 1) for _ in range(m + 1)]  # 创建二维动态规划数组,初始化为0
        for s in strs:  # 遍历物品
            zeroNum = s.count('0')  # 统计0的个数
            oneNum = len(s) - zeroNum  # 统计1的个数
            for i in range(m, zeroNum - 1, -1):  # 遍历背包容量且从后向前遍历
                for j in range(n, oneNum - 1, -1):
                    dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1)  # 状态转移方程
        return dp[m][n]

###DP(版本二)
class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        dp = [[0] * (n + 1) for _ in range(m + 1)]  # 创建二维动态规划数组,初始化为0
        # 遍历物品
        for s in strs:
            ones = s.count('1')  # 统计字符串中1的个数
            zeros = s.count('0')  # 统计字符串中0的个数
            # 遍历背包容量且从后向前遍历
            for i in range(m, zeros - 1, -1):
                for j in range(n, ones - 1, -1):
                    dp[i][j] = max(dp[i][j], dp[i - zeros][j - ones] + 1)  # 状态转移方程
        return dp[m][n]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值