(一)01背包
1.回溯三问
# capacity:背包容量
# w[i]: 第 i 个物品的体积
# v[i]: 第 i 个物品的价值
# 返回:所选物品体积和不超过 capacity 的前提下,所能得到的最大价值和
def zero_one_knapsack(capacity:int,w:List[int],v:List[int]) -> int:n = len(w)@cache #记忆化搜索 def dfs(i,c):if i < 0:return 0if c < w[i]:return dfs(i-1,c)return max(dfs(i-1,c),dfs(i-1,c-w[i])+v[i])return dfs(n-1,capacity)
2.常见变形
3.实战力扣题
494. 目标和 - 力扣(LeetCode)
给你一个非负整数数组 nums
和一个整数 target
。向数组中的每个整数前添加 '+'
或 '-'
,然后串联起所有整数,可以构造一个 表达式 :
- 例如,
nums = [2, 1]
,可以在2
之前添加'+'
,在1
之前添加'-'
,然后串联起来得到表达式"+2-1"
。
返回可以通过上述方法构造的、运算结果等于 target
的不同 表达式 的数目。
>>思考和分析:
- 正数和:p
- 负数和:s-p
- p-(s-p) = t
- 2p=s+t
- 化简可得: p=(s+t)/2
(1)记忆化搜索
class Solution:def findTargetSumWays(self, nums: List[int], target: int) -> int:target += sum(nums)if target < 0 or target%2: # 负数 or 奇数return 0 # 方案数为0target //= 2n = len(nums)# 记忆化搜索@cachedef dfs(i,c):if i < 0:return 1 if c==0 else 0if c < nums[i]:return dfs(i-1,c)return dfs(i-1,c)+dfs(i-1,c-nums[i])return dfs(n-1,target)
(2)1:1 翻译成递推
class Solution:def findTargetSumWays(self, nums: List[int], target: int) -> int:target += sum(nums)if target < 0 or target%2: # 负数 or 奇数return 0 # 方案数为0target //= 2n = len(nums)# 二维dpf = [[0]*(target+1)for _ in range(n+1)]f[0][0] = 1for i,x in enumerate(nums):for c in range(target+1):if c<x:f[i+1][c] = f[i][c]else:f[i+1][c] = f[i][c] + f[i][c-x]return f[n][target]
- 优化空间
方式一:二维数组优化
- f[(i+1)%2][c] = f[i%2][c] + f[i%2][c-w[i]]
class Solution:def findTargetSumWays(self, nums: List[int], target: int) -> int:# 二维dpf = [[0]*(target+1)for _ in range(2)]f[0][0] = 1for i,x in enumerate(nums):for c in range(target+1):if c<x:f[(i+1)%2][c] = f[i%2][c]else:f[(i+1)%2][c] = f[i%2][c] + f[i%2][c-x]return f[n%2][target]
方式二:一维数组优化
- f[i+1][c] = f[i][c] + f[i][c-w[i]]
- f[c]=f[c]+f[c-w[i]]
class Solution:def findTargetSumWays(self, nums: List[int], target: int) -> int:target += sum(nums)if target < 0 or target%2: # 负数 or 奇数return 0 # 方案数为0target //= 2n = len(nums)# 一维dpf = [0]*(target+1)f[0] = 1for x in nums:for c in range(target,x-1,-1):f[c] = f[c] + f[c-x]return f[target]
拓展:问题思考(O_O)?如果题目改成至多为 target,该怎么修改呢?
- 「恰好」改成「至多」
# 恰好
class Solution:def findTargetSumWays(self, nums: List[int], target: int) -> int:.........# 记忆化搜索@cachedef dfs(i,c):if i < 0:return 1 if c==0 else 0 # 边界条件:由于要求是恰好组成。恰好情况:当c=0的时候,才能返回1,表示这是一个合法的方案;if c < nums[i]:return dfs(i-1,c)return dfs(i-1,c)+dfs(i-1,c-nums[i])return dfs(n-1,target)改成如下:# 至多
class Solution:def findTargetSumWays(self, nums: List[int], target: int) -> int:.........# 记忆化搜索@cachedef dfs(i,c):if i < 0:return 1 // 至多情况:不用判断c == 0 的情况if c < nums[i]:return dfs(i-1,c)return dfs(i-1,c)+dfs(i-1,c-nums[i])return dfs(n-1,target)
# 恰好
class Solution:def findTargetSumWays(self, nums: List[int], target: int) -> int:.........# 一维dpf = [0]*(target+1)f[0] = 1for x in nums:for c in range(target,x-1,-1):f[c] = f[c] + f[c-x]return f[target]改成如下:# 至多
class Solution:def findTargetSumWays(self, nums: List[int], target: int) -> int:.........n = len(nums)# 一维dpf = [1]*(target+1) # 把这里全都初始化成1for x in nums:for c in range(target,x-1,-1):f[c] = f[c] + f[c-x]return f[target]
- 「恰好」改成「至少」
# 恰好
class Solution:def findTargetSumWays(self, nums: List[int], target: int) -> int:.........# 记忆化搜索@cachedef dfs(i,c):if i < 0:return 1 if c==0 else 0 # 边界条件:由于要求是恰好组成。恰好情况:当c=0的时候,才能返回1,表示这是一个合法的方案;if c < nums[i]:return dfs(i-1,c)return dfs(i-1,c)+dfs(i-1,c-nums[i])return dfs(n-1,target)改成如下:# 至少
class Solution:def findTargetSumWays(self, nums: List[int], target: int) -> int:.........# 记忆化搜索@cachedef dfs(i,c):if i < 0:return 1 if c<=0 else 0 // 至少情况:c<=0return dfs(i-1,c)+dfs(i-1,c-nums[i])return dfs(n-1,target)
# 恰好
class Solution:def findTargetSumWays(self, nums: List[int], target: int) -> int:.........# 一维dpf = [0]*(target+1)f[0] = 1for x in nums:for c in range(target,x-1,-1):f[c] = f[c] + f[c-x]return f[target]改成如下:# 至少
class Solution:def findTargetSumWays(self, nums: List[int], target: int) -> int:.........n = len(nums)# 一维dpf = [0]*(target+1)f[0] = 1for x in nums:for c in range(target,-1,-1):f[c] = f[c] + f[max(c-x,0)] # 表示把所有 c<=0 的状态 都记录到 f[0] 里面return f[target]
target=0 的情况下,每个物品都可以 「选」 或者 「不选」,那么一共有 种方案,刚好这种写法可以从 1 开始,算出来 (这句话我听得迷迷糊糊的,有小伙伴懂得指导我一下_(:з」∠)_)
(二)完全背包
1.回溯三问
# capacity:背包容量
# w[i]: 第 i 个物品的体积
# v[i]: 第 i 个物品的价值
# 每种物品可以无限次重复选
# 返回:所选物品体积和不超过 capacity 的前提下,所能得到的最大价值和
def unbounded_knapsack(capacity:int,w:List[int],v:List[int]) -> int:n = len(w)@cachedef dfs(i,c):if i < 0:return 0if c < w[i]:return dfs(i-1,c)return max(dfs(i-1,c),dfs(i,c-w[i])+v[i])return dfs(n-1,capacity)
(1)01背包和完全背包的递归式对比:
【重点】在选了一个物品之后,i 是不变的,表示你可以继续选第 i 种物品,所以这里不是递归到 i-1,而是 i
2.常见变形
3.实战力扣题
322. 零钱兑换 - 力扣(LeetCode)
给你一个整数数组 coins
,表示不同面额的硬币;以及一个整数 amount
,表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1
。你可以认为每种硬币的数量是无限的。
(1)记忆化搜索
class Solution:# 边界条件:由于要求是恰好组成 amount,所以和上一题一样# 当c=0的时候,才能返回0,表示这是一个合法的方案;否则就应该返回无穷大,表示这是一个不合法的方案# 这样后面去min的时候就自然的取到了不是无穷大的那个方案(硬币个数)# 如果ans小于inf,表示这是一个合法的方案,否则一个合法的方案都没有def coinChange(self, coins: List[int], amount: int) -> int:n = len(coins)@cachedef dfs(i,c):if i < 0:return 0 if c==0 else infif c < coins[i]:return dfs(i-1,c)return min(dfs(i-1,c),dfs(i,c-coins[i])+1)ans = dfs(n-1,amount)return ans if ans<inf else -1
(2)1:1 翻译成递推
class Solution:def coinChange(self, coins: List[int], amount: int) -> int:n = len(coins)# 数组的初始值:根据递归的边界条件,可知当i=0,c=0,才是0;其它的都是无穷大# 步骤:1:直接把数组初始化成无穷大 2.把f[0][0]改成0就好了f=[[inf] * (amount+1) for _ in range(n+1)]f[0][0] = 0for i,x in enumerate(coins):for c in range(amount+1):# 强调一下,c 表示剩余容量if c<x:f[i+1][c] = f[i][c]else:f[i+1][c] = min(f[i][c],f[i+1][c-x]+1)ans = f[n][amount]return ans if ans < inf else -1
- 优化空间
方式一:二维数组优化
- f[(i+1)%2][c] = min(f[i%2][c],f[(i+1)%2][c-w[i]]+v[i])
class Solution:def coinChange(self, coins: List[int], amount: int) -> int:n = len(coins)# 数组的初始值:根据递归的边界条件,可知当i=0,c=0,才是0;其它的都是无穷大# 步骤:1:直接把数组初始化成无穷大 2.把f[0][0]改成0就好了f=[[inf] * (amount+1) for _ in range(2)]f[0][0] = 0for i,x in enumerate(coins):for c in range(amount+1):# 强调一下,c 表示剩余容量if c<x:f[(i+1)%2][c] = f[i%2][c]else:f[(i+1)%2][c] = min(f[i%2][c],f[(i+1)%2][c-x]+1)ans = f[n%2][amount]return ans if ans < inf else -1
方式二:一维数组优化
- f[i+1][c] = min(f[i][c],f[i+1][c-w[i]]+v[i])
- f[c] = min(f[c],f[c-w[i]]+v[i])
- 需要注意:在恰好装满的情况下,这个数组它不一定是个有序数组
class Solution:def coinChange(self, coins: List[int], amount: int) -> int:n = len(coins)f=[inf] * (amount+1) f[0] = 0for x in coins:for c in range(x,amount+1):f[c] = min(f[c],f[c-x]+1)ans = f[amount]return ans if ans < inf else -1
总结一下循环顺序的问题(O_O)?
- 如果你在写一个不是背包的DP问题,但是得到了类似的状态转移方程,那么要怎么思考循环的顺序呢?
实战篇:这篇文章就是得到了类似的状态转移方程,用上图来思考循环顺序
LCR 166.珠宝的最高价值 + 动态规划 + 记忆化搜索 + 递推 + 空间优化-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/134207574?spm=1001.2014.3001.5501
参考和推荐视频:
0-1背包 完全背包【基础算法精讲 18】_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV16Y411v7Y6/?spm_id_from=333.788&vd_source=a934d7fc6f47698a29dac90a922ba5a3