(一)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 0
if 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 # 方案数为0
target //= 2
n = len(nums)
# 记忆化搜索
@cache
def dfs(i,c):
if i < 0:
return 1 if c==0 else 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)
(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 # 方案数为0
target //= 2
n = len(nums)
# 二维dp
f = [[0]*(target+1)for _ in range(n+1)]
f[0][0] = 1
for 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:
# 二维dp
f = [[0]*(target+1)for _ in range(2)]
f[0][0] = 1
for 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 # 方案数为0
target //= 2
n = len(nums)
# 一维dp
f = [0]*(target+1)
f[0] = 1
for 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:
...
...
...
# 记忆化搜索
@cache
def 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:
...
...
...
# 记忆化搜索
@cache
def 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:
...
...
...
# 一维dp
f = [0]*(target+1)
f[0] = 1
for 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)
# 一维dp
f = [1]*(target+1) # 把这里全都初始化成1
for 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:
...
...
...
# 记忆化搜索
@cache
def 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:
...
...
...
# 记忆化搜索
@cache
def dfs(i,c):
if i < 0:
return 1 if c<=0 else 0 // 至少情况:c<=0
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:
...
...
...
# 一维dp
f = [0]*(target+1)
f[0] = 1
for 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)
# 一维dp
f = [0]*(target+1)
f[0] = 1
for 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)
@cache
def dfs(i,c):
if i < 0:
return 0
if 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)
@cache
def dfs(i,c):
if i < 0:
return 0 if c==0 else inf
if 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] = 0
for 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] = 0
for 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] = 0
for 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