Leetcode刷题笔记43:动态规划5(494. 目标和-474. 一和零)

213 阅读3分钟

导语

leetcode刷题笔记记录,本篇博客是贪心部分的第二期,主要记录题目包括:

Leetcode 494. 目标和

题目描述

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

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

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

示例 1:

输入: nums = [1,1,1,1,1], target = 3
输出: 5
解释: 一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

示例 2:

输入: nums = [1], target = 1
输出: 1

提示:

  • 1 <= nums.length <= 20
  • 0 <= nums[i] <= 1000
  • 0 <= sum(nums[i]) <= 1000
  • -1000 <= target <= 1000

解法

这个问题实际上转化为了一个背包问题。问题的核心是,给定的nums中有多少种方式,使得其中的数加起来等于(target + sum(nums)) // 2。为什么要这样转化呢?因为每个数要么被加,要么被减。如果我们让它加,那就相当于把这个数放进“背包”里。所以,问题就转化为了:有多少种方法可以从nums中选择一些数,使其和为(target + sum(nums)) // 2

动规五部曲:

  1. dp数组含义:dp[i]表示有多少种方法从nums中选出一些数,使其和为i
  2. 递推公式:dp[j] += dp[j-nums[i]]
  3. 初始化:dp[0]=1,其他为0
  4. 遍历顺序,与0-1背包的一维滚动数组数组的顺序一致;
  5. 打印数组:略
class Solution:
    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        # 计算nums的总和
        total = sum(nums)
        
        # 首先检查是否可能达到目标
        # 如果 (total + target) 是奇数,那么不可能达到目标
        # 同样,如果目标的绝对值大于总和,那么也不可能达到目标
        if (total + target) % 2 != 0 or abs(target) > total:
            return 0
        
        # 计算新的目标:这是我们要从nums中选出的数的总和
        positive = (total + target) // 2
        
        # 初始化dp数组,dp[i]表示有多少种方法从nums中选出一些数,使其和为i
        dp = [0] * (positive + 1)
        dp[0] = 1  # 选择0个数,使其和为0的方法只有1种
        
        # 遍历nums中的每个数
        for i in range(len(nums)):
            # 从positive开始,逆向到nums[i]
            # 为每个j,考虑是否使用nums[i]来达到目标
            for j in range(positive, nums[i]-1, -1):
                # 更新dp[j]:不使用nums[i]的方法数 + 使用nums[i]的方法数
                dp[j] += dp[j-nums[i]]
        
        # dp[positive]就是我们的答案:从nums中选择一些数,使其和为positive的方法数
        return dp[positive]

Leetcode 474. 一和零

题目描述

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。

请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

 

示例 1:

输入: strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出: 4
解释: 最多有 5031 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 41 ,大于 n 的值 3

示例 2:

输入: strs = ["10", "0", "1"], m = 1, n = 1
输出: 2
解释: 最大的子集是 {"0", "1"} ,所以答案是 2

 

提示:

  • 1 <= strs.length <= 600
  • 1 <= strs[i].length <= 100
  • strs[i] 仅由 '0' 和 '1' 组成
  • 1 <= m, n <= 100

解法

这个问题实际上是一个多维的0-1背包问题,其中两个维度分别是m(0的最大数量)和n(1的最大数量)。使用动规五部曲:

  1. dp数组含义:这里由于背包容量是二维的,所以定义一个二维dp数组,其表示装满i个0,j个1的背包,最多背了dp[i][j]个物品;
  2. 递推公式:回顾0-1背包的递推公式为: dp[j]=max(dp[j],dp[jweight[i]]+value[i])dp[j]=max(dp[j], dp[j-weight[i]]+value[i]),这里的二维dp数组递推公式为:
dp[i][j]=max(dp[ix][jy]+1,dp[i][j])dp[i][j]=max(dp[i-x][j-y]+1, dp[i][j])
  1. 初始化:考虑其代表含义,dp[0][0]应表示背包容量为0,0时的物品个数,那么肯定为0,由于递推公式中的max操作,所以非零下标也初识为0;
  2. 遍历顺序:先物品,再背包,且背包倒序;
  3. 打印dp数组

这里需要注意的一点是x,y的初始化。应该在选择一个字符串后进行初始化。完整代码如下:

class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        # 初始化二维dp数组,其中dp[i][j]表示有i个0和j个1时最多可以组成的子集数
        dp = [[0] * (n+1) for _ in range(m+1)]
        
        # 遍历输入字符串的列表
        for string in strs:
            x, y = 0, 0  # x和y用于存储当前字符串中0和1的数量
            
            # 统计当前字符串中0和1的数量
            for char in string:
                if char == "0":
                    x += 1
                else:
                    y += 1
            
            # 更新dp数组
            # 从右下角开始更新,这样可以确保同一个字符串不会被多次使用
            for i in range(m, x - 1, -1):
                for j in range(n, y - 1, -1):
                    # dp[i][j]要么是自己(不使用当前字符串),要么是dp[i-x][j-y] + 1(使用当前字符串)
                    dp[i][j] = max(dp[i][j], dp[i - x][j - y] + 1)
        
        # 最后的dp[m][n]就是问题的答案
        return dp[m][n]

总结

image.png