leetcode(4)背包系列题目
背包递推公式
-
问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); ,对应题目如下:
如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!
class Solution: def canPartition(self, nums: List[int]) -> bool: target = sum(nums) if target % 2 == 1:return False target //= 2 # 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200 # 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了 dp = [0] * 10001 for i in range(len(nums)): for j in range(target, nums[i] - 1, -1): dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]) return dp[target] == target
dp[target]里是容量为target的背包所能背的最大重量。
那么分成两堆石头,一堆石头的总重量是dp[target],另一堆就是sum - dp[target]。
在计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的。
那么相撞之后剩下的最小石头重量就是 (sum - dp[target]) - dp[target]。class Solution: def lastStoneWeightII(self, stones: List[int]) -> int: all_weight = sum(stones) target = all_weight // 2 dp = [0] * 1501 for i in range(len(stones)): for j in range(target, stones[i] - 1, -1): dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]) return all_weight - dp[target] * 2
-
问装满背包有几种方法:dp[j] += dp[j - nums[i]] ,对应题目如下:
假设加法的总和为x,那么减法对应的总和就是sum - x。
所以我们要求的是 x - (sum - x) = target
x = (target + sum) / 2
此时问题就转化为,装满容量为x背包,有几种方法。
对于01背包问题一维dp的遍历,nums放在外循环,target在内循环,且内循环倒序。class Solution: def findTargetSumWays(self, nums: List[int], target: int) -> int: sumValue = sum(nums) if abs(target) > sumValue or (sumValue + target) % 2 == 1: return 0 bagSize = (sumValue + target) // 2 dp = [0] * (bagSize + 1) dp[0] = 1 for i in range(len(nums)): for j in range(bagSize, nums[i] - 1, -1): dp[j] += dp[j - nums[i]] return dp[bagSize]
求组合数:外层for循环遍历物品,内层for遍历背包。
class Solution: def change(self, amount: int, coins: List[int]) -> int: dp = [0] * (amount + 1) dp[0] = 1 for c in coins: for j in range(c, amount + 1): dp[j] += dp[j - c] return dp[-1]
求排列数:外层for遍历背包,内层for循环遍历物品。
class Solution: def combinationSum4(self, nums: List[int], target: int) -> int: dp = [0] * (target + 1) dp[0] = 1 for j in range(1, target + 1): for c in nums: if j - c >= 0: dp[j] += dp[j - c] return dp[-1]
看成完全背包的排列问题
class Solution: def climbStairs(self, n: int) -> int: dp = [0] * (n + 1) dp[0] = dp[1] = 1 for j in range(2, n + 1): for i in range(1, 3): # 物品只在【1,2】里面取 dp[j] += dp[j - i] return dp[-1]
-
问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); 对应题目如下:
其实是01背包问题!
不过这个背包有两个维度,一个是m 一个是n,而不同长度的字符串就是不同大小的待装物品。class Solution: def findMaxForm(self, strs: List[str], m: int, n: int) -> int: dp = [[0] * (n + 1) for _ in range(m + 1)] # 默认初始化0 for cur_str in strs: # 遍历物品 zeros = cur_str.count('0') ones = cur_str.count('1') # 遍历背包容量,且从后向前遍历 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]
-
问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); 对应题目如下:
考过没做出来
初始化凑成每个金额都使用面额为1的硬币,则最多需要amount个硬币,因此初始化为amount + 1
与完全平方数的区别是coins里面可能没有1,所以dp要初始化为amount + 1class Solution: def coinChange(self, coins: List[int], amount: int) -> int: dp = [amount + 1] * (amount + 1) # 都是amount + 1 dp[0] = 0 for c in coins: for j in range(c, amount + 1): dp[j] = min(dp[j], dp[j - c] + 1) return dp[-1] if dp[-1] < amount + 1 else -1 # 如果<就说明是由coins里面组成的
考过没做出来
就是物品(可以无限件使用),凑个正整数n就是背包,问凑满这个背包最少有多少物品?class Solution: def numSquares(self, n: int) -> int: nums = [i**2 for i in range(1, n + 1) if i**2 <= n] dp = [n + 1] * (n + 1) dp[0] = 0 for num in nums: for j in range(num, n + 1): dp[j] = min(dp[j], dp[j - num] + 1) return dp[-1]
外层遍历背包内层遍历物品时要注意判断if j >= len(word):
能不能凑到第i个位置就看:
①不使用当前的word,dp[j]已经用其他方式凑好没有
②使用当前单词,j - len(word)的位置已经凑好没有and 当前的word 跟s在这个位置所需要的单词是不是一样的 s[j - len(word) : j]class Solution: def wordBreak(self, s: str, wordDict: List[str]) -> bool: n = len(s) dp = [False] * (n + 1) dp[0] = True for j in range(1, n + 1): for word in wordDict: if j >= len(word): dp[j] = dp[j] or dp[j - len(word)] and word == s[j - len(word) : j] return dp[-1]
总结:动态规划-我的一生之敌...(不是)转移方程一定要想清楚!!!
一、01背包问题
如果使用二维dp数组,先遍历物品还是先遍历背包重量都可以,且两层循环都是正序遍历!
如果使用一维dp数组,只能是物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!
a.内层循环遍历顺序
二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。
因为倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!
举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15
如果正序遍历
dp[1] = dp[1 - weight[0]] + value[0] = 15
dp[2] = dp[2 - weight[0]] + value[0] = 30
此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。
为什么倒序遍历,就可以保证物品只放入一次呢?
倒序就是先算dp[2]
dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)
dp[1] = dp[1 - weight[0]] + value[0] = 15
所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
那么问题又来了,为什么二维dp数组历的时候不用倒序呢?
因为对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖!
b.两个嵌套for循环的顺序
只能是物品遍历的for循环放在外层,遍历背包的for循环放在内层。
因为一维dp的写法,背包容量一定是要倒序遍历,如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。
二、完全背包问题
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
a.内层循环遍历顺序
01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。
而完全背包的物品是可以添加多次的,所以要从小到大正序遍历
b.两个嵌套for循环的顺序
在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!
因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。
三、求装满背包有几种方法
- 如果求组合数就是外层for循环遍历物品,内层for遍历背包。(518.零钱兑换II)
- 如果求排列数就是外层for遍历背包,内层for循环遍历物品。(377. 组合总和 Ⅳ 、70. 爬楼梯进阶版(完全背包)
初始化为:
dp = [0] * (amount + 1)
dp[0] = 1
递推公式:dp[j] += dp[j - nums[i]]
- 如果求最小数,那么两层for循环的先后顺序就无所谓了。(322. 零钱兑换 、279.完全平方数 )
初始化为:
dp = [amount + 1] * (amount + 1)
dp[0] = 0
递推公式为:dp[j] = min(dp[j], dp[j - nums[i]] + 1)
四、多重背包问题
多重背包介于完全背包和01背包问题之间,即每种物品的个数是指定的。
参考资料:
背包问题总结篇
一篇文章吃透背包问题!
一文搞懂完全背包问题