ps:笔记和代码按本人理解整理,重思路
【如果笔记对你有帮助,欢迎关注&点赞&收藏,收到正反馈会加快更新!谢谢支持!】
上期笔记:
动态规划背包问题学习笔记:目标和(0-1背包),零钱兑换(完全背包)-CSDN博客
背包问题:
- 问题:一个最大容量为
的背包,能装多少价值的物体 (物体A、B、C…)
- 物体A重量为
, 价值为
;物体B重量为
, 价值为
;物体C重量为
, 价值为
- 0-1背包问题:每个物体只有一个 → 不选 or 选
- 完全背包问题:每个物体有无数个 → 选 n 个(n = 0,1,2,3……)
题目1:和为目标值的最长子序列的长度
2915. 和为目标值的最长子序列的长度 - 力扣(LeetCode)
- 题意:
- 条件:子序列的和为目标和
- 求:子序列 长度的最大值
- 类型:0-1背包问题(每个数只有选 or 不选)
- 拆解:
- 0-1背包问题,先遍历物体,再遍历总额
- dp 储存当前子序列长度,初始化为 -inf (因为要求最大值),dp[0] = 0(满足和为0的子序列长度为0)
- 一维dp更新需要从右往左(此时左边的值还是上轮num对应的长度):
- dp[c] = max(dp[c], dp[c-num]+1) = max({不选num且和为c的子序列长度}, {和为c-num的子序列长度 + 1(指num)})
- 代码:
class Solution: def lengthOfLongestSubsequence(self, nums: List[int], target: int) -> int: n = len(nums) dp = [float('-inf')]*(target+1) dp[0] = 0 for num in nums: for c in range(target, num-1, -1): # 注意 c >= num dp[c] = max(dp[c], dp[c-num]+1) return dp[-1] if dp[-1] != float('-inf') else -1
题目2:分割等和子集
- 题意:
- 条件:子集和 = sum(nums) / 2
- 求:是否存在满足条件的子集
- 类型:0-1背包问题(每个数只有选 or 不选)
- 拆解:
- target = sum(nums) / 2 ,如果sum(nums)为奇数,target有小数,肯定不存在能平分的子集
- dp 储存是否找到已找到子集(True or False,用1 or 0表示)
- 初始化为 0 (指False),dp[0] = 1 (存在和为0的子集,即空集)
- 从右往左更新新的num:
- 如果满足 dp[c-num] == 1 (存在和为c-num的子集),则 dp[c] = 1 (和为c的子集也存在)
- 否则,就保持原来的dp[c],即 {不选num是否存在和为c的子集}
- 代码:
class Solution: def canPartition(self, nums: List[int]) -> bool: sum_ = sum(nums) if sum_ % 2 == 1: return False target = sum_ / 2 dp = [0 for _ in range(target+1)] dp[0] = 1 for i, num in enumerate(nums): for c in range(target, num-1, -1): if dp[c-num] == 1: # 看是否存在,dp[c-num]为True,dp[c]也为True dp[c] = 1 return dp[-1] == 1
题目3:将一个数字表示成幂的和的方案数
2787. 将一个数字表示成幂的和的方案数 - 力扣(LeetCode)
- 题意:
- 条件: 互不相同 正整数的
x
次幂之和 = n - 求:方案数
- 条件: 互不相同 正整数的
- 类型:0-1背包问题(互不相同:每个数只有选 or 不选)
- 拆解:
- 处理nums = [ i**x for i in range(n+1)] 并且 i**x < n
- 方案数 = {不选 num 的方案数} + {选 num 的方案数} = dp[c] + dp[c-num]
- 代码:
class Solution: def numberOfWays(self, n: int, x: int) -> int: nums = [] for i in range(1, n+1): if i**x > n: break nums.append(i**x) dp = [0]*(n+1) dp[0] = 1 for num in nums: for c in range(n, num-1, -1): dp[c] += dp[c-num] return dp[-1] % (10**9 + 7)
题目4:零钱兑换 II
- 题意:
- 条件:硬币(无限多)凑成总金额
- 求:方案数
- 类型:完全背包问题(每个硬币选 n 个, n=0,1,2,…)
- 拆解:
- 完全背包但是要:先遍历硬币,再遍历金额 → 防止重复计算(比如 amount=3, "1, 2"和"2, 1" 是同一个方案,不能重复计数)
- 方案数 = {不选 num 的方案数} + {选 num 的方案数} = dp[c] + dp[c-num]
- 本题需要正序遍历金额,因为硬币可以多次用:
- 如 amount = 5, coin = 1
0 1 2 3 4 5 [idx = 0] 初始 1 0 0 0 0 0 [idx = 1] coin = 1 =dp[0] =1 =dp[1] + dp[1-1] = 0+1= 1 =dp[2] + dp[2-1] = 0+1= 1 =dp[3] + dp[3-1] = 0+1= 1 =dp[4] + dp[4-1] = 0+1= 1 =dp[5] + dp[5-1] = 0+1= 1
- 如 amount = 5, coin = 1
-
代码:
class Solution: def change(self, amount: int, coins: List[int]) -> int: dp = [0 for _ in range(amount+1)] dp[0] = 1 for coin in coins: for c in range(coin, amount+1): # 正序 dp[c] += dp[c-coin] return dp[-1]
题目5:完全平方数
- 题意:
- 条件:完全平方数(可重复)和为n
- 求:最少数量
- 类型:完全背包问题
- 拆解:
- “先遍历平方数,再遍历总额” / “先遍历总额,再遍历平方数” 都可以,因为是求最小值,所以同一组合重复计算不影响结果
- 以“先遍历平方数,再遍历总额”为例
- 初始化为 inf(因为要求最小值),dp[0] = 0(总额为0需要的平方数的数量为0)
- 正序计算(因为平方数可以被重复使用)dp[c] = min(dp[c], dp[c-sq]+1) = {不选 sq 的平方和数量} + {选 sq 的平方和数量}
- 代码:
class Solution: def numSquares(self, n: int) -> int: # 计算平方和 s = 1 squres = [] while s**2 <= n: squres.append(s**2) s += 1 # 开始动态规划 dp = [float('inf')]*(n+1) dp[0] = 0 for sq in squres: for c in range(sq, n+1): # 正序(可重复取sq) dp[c] = min(dp[c], dp[c-sq]+1) return dp[-1] if dp[-1]!=float('inf') else -1