LeetCodeCampsDay36动态规划part04
LeetCodeCampsDay36动态规划part04
01背包,分割等和子集的变式题目
416.分割等和子集是问:能否将背包装满到target值
1049.石头:尽量将背包装满到target值,如果装不到,则最小差值是多少
494.目标和:装满一个到target值,有多少种方法
474.一和零:装满一个背包,最多有多少物品
1049. 最后一块石头的重量 II
https://leetcode.cn/problems/last-stone-weight-ii/
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
- 如果 
x == y,那么两块石头都会被完全粉碎; - 如果 
x != y,那么重量为x的石头将会完全粉碎,而重量为y的石头新重量为y-x。 
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
示例 1:
1  | 输入:stones = [2,7,4,1,8,1]  | 
示例 2:
1  | 输入:stones = [31,26,33,21,40]  | 
提示:
1 <= stones.length <= 301 <= stones[i] <= 100
动态规划思路
如何让这一堆石头相撞后,剩下的石头最小?
- 极端情况下:当把这一堆石头分成两堆并且两堆体积相等时,剩下的石头为零
 - 次优解为:两堆体积尽量相似!剩下的石头最小
 
这题目之前416.分割等和子集有些像
可以用一个背包装石头,并找一个最小差值:背包的重量 与 剩下石头的重量 差值最小
- 使用一维背包,dp背包的下标:石头重量,背包dp[i]含义:背包的重量
 - 递推公式:dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]),因为重量就是价值,所以weight[i]=val[i]=stones[i]
 - dp初始化,一维背包初始化全为零,背包最大容量设置为sumOfStones也是可以的,但将在极端情况下,只要让背包装满sumOfStones的一半就可以了,即令dp的长度为sumOfStones//2
 - 遍历顺序:行遍历对每个石头遍历,列遍历对背包重量(从大到小遍历,范围[sumOfStones // 2, stones[i]],保证每个物品只添加一次,这样才是01背包;如果从小到大遍历,就会变成完全背包,指每个物品可以放多次)
 - 举例
 
动态规划代码
- 时间复杂度:O(m × n) , m是石头总重量(准确的说是总重量的一半),n为石头块数
 - 空间复杂度:O(m)
 
1  | class Solution:  | 
494. 目标和
https://leetcode.cn/problems/target-sum/
给你一个非负整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
- 例如,
nums = [2, 1],可以在2之前添加'+',在1之前添加'-',然后串联起来得到表达式"+2-1"。 
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
示例 1:
1  | 输入:nums = [1,1,1,1,1], target = 3  | 
示例 2:
1  | 输入:nums = [1], target = 1  | 
提示:
1 <= nums.length <= 200 <= nums[i] <= 10000 <= sum(nums[i]) <= 1000-1000 <= target <= 1000
本题思路
本题目需要根据nums求target,看似既要求正数又要求负数情况,但实际上,可以将题目转换成只求正数集,一旦得到了正确的正数集,那sum-正数集就是对应的负数集
令作为正数集的和为targetSum,作为负数集的和(每个数字仍然为正数)为remainSum
则
- 
targetSum + remainSum = sum
 - 
targetSum - remainSum = target
 
targetSum + targetSum - target = sum
targetSum = (sum + target) // 2
现在问题变成了:求有多少种方法装满left,即背包最大容量为left有多少种装法(或有多少种方法装right)
回溯思路
回溯的思路和之前“组合总和”代码相似,但前提也是要把题目转换一个思考方式,不需要真的考虑每次加一个数或减一个数,而是只考虑:
如何获得满足条件的正数集
回溯代码
1  | class Solution:  | 
动态规划思路
在确定了动态规划里的targetSum值(对应下面代码里的left),则把问题转成了把背包装满到targetSum的问题
- dp数组下标与含义:dp[j]下标j表示当前背包容量,有dp[j]个方法把背包装满到target
 - 递推公式:dp[j] = dp[j] + dp[j - nums[i]]
 
先只考虑物品0,如图:
(这里的所有物品,都是题目中的数字1)。
装满背包容量为0 的方法个数是1,即 放0件物品。
装满背包容量为1 的方法个数是1,即 放物品0。
装满背包容量为2 的方法个数是0,目前没有办法能装满容量为2的背包。
接下来 考虑 物品0 和 物品1,如图:
装满背包容量为0 的方法个数是1,即 放0件物品。
装满背包容量为1 的方法个数是2,即 放物品0 或者 放物品1。
装满背包容量为2 的方法个数是1,即 放物品0 和 放物品1。
其他容量都不能装满,所以方法是0。
接下来 考虑 物品0 、物品1 和 物品2 ,如图:
装满背包容量为0 的方法个数是1,即 放0件物品。
装满背包容量为1 的方法个数是3,即 放物品0 或者 放物品1 或者 放物品2。
装满背包容量为2 的方法个数是3,即 放物品0 和 放物品1、放物品0 和 物品2、放物品1 和 物品2。
装满背包容量为3的方法个数是1,即 放物品0 和 物品1 和 物品2。
dp[2][2] = 3,即 放物品0 和 放物品1、放物品0 和 物品 2、放物品1 和 物品2, 如图所示,三种方法:
容量为2 的背包,如果不放 物品2 有几种方法呢?
有 dp[1][2] 种方法,即 背包容量为2,只考虑物品0 和 物品1 ,有 dp[1][2] 种方法,如图:
容量为2 的背包, 如果放 物品2 有几种方法呢?
首先 要在背包里 先把物品2的容量空出来, 装满 刨除物品2容量 的背包 有几种方法呢?
刨除物品2容量后的背包容量为 1。
此时装满背包容量为1 有 dp[1][1] 种方法,即: 不放物品2,背包容量为1,只考虑物品 0 和 物品 1,有 dp[1][1] 种方法。
如图:
有录友可能疑惑,这里计算的是放满 容量为2的背包 有几种方法,那物品2去哪了?
在上面图中,你把物品2补上就好,同样是两种方法。
dp[2][2] = 容量为2的背包不放物品2有几种方法 + 容量为2的背包放物品2有几种方法
所以 dp[2][2] = dp[1][2] + dp[1][1] ,如图:
以上过程,抽象化如下:
- 不放物品i:即背包容量为j,里面不放物品i,装满有dp[i - 1][j]中方法。
 - 放物品i: 即:先空出物品i的容量,背包容量为(j - 物品i容量),放满背包有 dp[i - 1][j - 物品i容量] 种方法。
 
本题中,物品i的容量是nums[i],价值也是nums[i]。
递推公式:dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];
考到这个递推公式,我们应该注意到,j - nums[i] 作为数组下标,如果 j - nums[i] 小于零呢?
说明背包容量装不下 物品i,所以此时装满背包的方法值 等于 不放物品i的装满背包的方法,即:dp[i][j] = dp[i - 1][j];
所以递推公式:
1  | if (nums[i] > j) dp[i][j] = dp[i - 1][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[j] = max(dp[j] , dp[j - nums[i]] + xxx),现在要注意dp的含义是有多少种装包的方法而不是求它的价值
动态规划代码
- 时间复杂度:O(n × m),n为正数个数,m为背包容量
 - 空间复杂度:O(m),m为背包容量
 
1  | def findTargetSumWays(self, nums: List[int], target: int) -> int:  | 
474. 一和零
https://leetcode.cn/problems/ones-and-zeroes/
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例 1:
1  | 输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3  | 
示例 2:
1  | 输入:strs = ["10", "0", "1"], m = 1, n = 1  | 
提示:
1 <= strs.length <= 6001 <= strs[i].length <= 100strs[i]仅由'0'和'1'组成1 <= m, n <= 100
动态规划思路
本题里的物品只有子串,但背包需要使用二维,其中dp[j][k]表示最多有j个0,有k个1的最大子集的大小为dp[j][k]
并且,本题的问题是:装满这个背包,最多可以装多少个物品,最终返回值dp[m][n]
- dp数组下标与含义:dp[j][k]表示最多有j个0,有k个1的最大子集的大小为dp[j][k]
 - 递推公式:与前面的公式不同,对于每个物品strs[i],比如’11001’,需要先判断它有x个0,有y个1;
- 然后判断dp[j - x][k - y] + 1 与 dp[j][k]的大小,表示如果,如果装得下,则把这个子串’11001’添加进来则会多一个物品
 
 - 初始化,将dp全部初始化为0
 - 遍历顺序:先遍历物品,但需要对物品计算每个子串的0&1个数;再遍历背包,按0和1进行两层循环的遍历(因为先遍历0或1都可以,不影响)
- 注意,遍历背包里,仍然需要像回滚dp数组一样,倒序遍历,因为虽然这里的dp是二维数组,但它只是
两个维度的容量 
 - 注意,遍历背包里,仍然需要像回滚dp数组一样,倒序遍历,因为虽然这里的dp是二维数组,但它只是
 
动态规划代码
- 时间复杂度: O(kmn),k 为strs的长度
 - 空间复杂度: O(mn)
 
1  | class Solution:  | 















