【动态规划】背包问题的应用
0-1背包问题的应用
应用1:Leetcode.416
题目
分析
设
如果
如果
边界条件
边界条件就是凑成和为零的方案,即对于任何数字都不选,它也对应一种方法,所以
状态转移
对于数组中的第
-
如果不选择当前元素,则当前状态与
相同; -
如果选择当前元素,则当前状态与
相同。
也就是说,我们只需要从上述两种方案中,选择一个结果为
代码实现
class Solution: def canPartition(self, nums: List[int]) -> bool: n = len(nums) _sum = sum(nums) if _sum % 2 != 0: return False _sum = _sum // 2 dp = [[False for _ in range(_sum + 1)] for _ in range(n + 1)] for i in range(n + 1): dp[i][0] = True for i in range(1, n + 1): for j in range(1, _sum + 1): # 背包容量不足,不能装入nums[i-1] if j < nums[i-1]: dp[i][j] = dp[i - 1][j] else: dp[i][j] = dp[i - 1][j] | dp[i - 1][j - nums[i - 1]] return dp[n][_sum]
对其压缩状态,优化后的实现:
class Solution: def canPartition(self, nums: List[int]) -> bool: n = len(nums) _sum = sum(nums) if _sum % 2 != 0: return False _sum = _sum // 2 dp = [False] * (_sum + 1) dp[0] = True for i in range(1, n + 1): for j in range(_sum, 0, -1): if j >= nums[i - 1]: dp[j] = dp[j] | dp[j - nums[i - 1]] return dp[_sum]
完全背包问题的应用
应用1:Leetcode.518
题目
分析
设
边界条件
使用任何面额的金币,凑出金额
状态转移
对于数组
-
当前金额
小于当前的金币的面额,凑法数量就是不选择当前面额的金币的种数,即: -
当前金额
大于等于当前的金币面额,此时,可以选择也可以不选择当前金币:-
如果不选择,那么,凑法数量就是与前一个状态相同,即:
-
如果选择,那么,凑法数量就是在当前状态下,多次选择当前面额的金币种数,即:
因此,当面额总数
大于当前金币面额时,凑法数量就是上述两种选择之和: -
综上,结合上述两种场景,状态转移方程为:
代码实现
from typing import List class Solution: def change(self, amount: int, coins: List[int]) -> int: n = len(coins) dp = [[0 for _ in range(amount + 1)] for _ in range(n + 1)] for i in range(0, n + 1): dp[i][0] = 1 for i in range(1, n + 1): for j in range(1, amount + 1): if j - coins[i - 1] >= 0: dp[i][j] = dp[i][j - coins[i - 1]] + dp[i - 1][j] else: dp[i][j] = dp[i - 1][j] return dp[n][amount] if __name__ == "__main__": s = Solution() print(s.change(amount=5, coins=[1, 2, 5]))
对其压缩状态,优化后的实现:
from typing import List class Solution: def change(self, amount: int, coins: List[int]) -> int: dp = [0 for _ in range(amount + 1)] dp[0] = 1 for i in range(1, len(coins) + 1): for j in range(1, amount + 1): if j - coins[i - 1] >= 0: dp[j] = dp[j - coins[i - 1]] + dp[j] return dp[amount]
应用2:Leetcode.322
题目
方法一:自底向上动态规划
分析
简而言之,题目等价于:
有
种物品和一个容量为 的背包。第 种物品的重量是 ,每种物品的数量都有无限个。求解将哪些物品装入背包可使这些物品的重量刚好等于背包容量,且物品数量最少。
假设
边界条件
如果金额为零,显然最少的硬币数量为零,即:
状态转移
这里,我们直根据完全背包问题的模板接给出状态转移方程:
代码实现
from typing import List class Solution: def coinChange(self, coins: List[int], amount: int) -> int: n = len(coins) dp = [float("INF")] * (amount + 1) dp[0] = 0 for i in range(1, n + 1): for j in range(1, amount + 1): if j >= coins[i - 1]: dp[j] = min(dp[j], dp[j - coins[i - 1]] + 1) result = dp[amount] if dp[amount] == float("INF"): result = -1 return result
方法二:自顶向下动态规划
分析
这里我们利用分治的思想:通过定义一个带返回值的递归函数,将问题分解为子问题(子树),通过递归推导出答案。
我们定义一个函数 traverse(coins: List[int], amount: int, memory: List[int])
表示:当硬币为
代码实现
from typing import List class Solution: def coinChange(self, coins: List[int], amount: int) -> int: memory = [0] * amount return self.traverse(coins, amount, memory) def traverse(self, coins: List[int], amount: int, memory: List[int]) -> int: if amount == 0: return 0 if amount < 0: return -1 # 避免重复搜索 if memory[amount - 1] != 0: return memory[amount - 1] answer = float("INF") for coin in coins: result = self.traverse(coins, amount - coin, memory) if result == -1: continue answer = min(answer, result + 1) if answer == float("INF"): answer = -1 # 记录已经计算过的结果 memory[amount - 1] = answer return answer
应用3:Leetcode.337
题目
分析
这道题如果直接使用回溯暴力搜索组合数,会导致内存超出限制。
这里,我们考虑使用动态规划求解,即在给定数组 nums 中,选择若干元素的组合,使它们的和等于 target,其中,元素可以多次重复选择。
这道题与完全背包问题的模型类似,但是,这道题的差异是:选择的不同物品的顺序代表不同的方案,因此,我们需要求出满足条件的所有物品的排列数。
假设数组的长度为
即把所有的个数组合都选择一遍,然后将每个数字对应的所有方案数求和,就是最后的答案。
边界条件
当不选择任何元素时,只有一种方案,因此,有
状态转移
对于任意的
-
如果最后一个数选择
,则方案数为: ; -
如果最后一个数选择
,则方案数为: ; -
如果最后一个数选择
,则方案数为: ; -
因此,当选择
那么,我们只需要遍历所有的组合,并累加组合,即可得到答案。
这里需要注意,物品种类和物品个数的区别:
-
完全背包是从背包中选择
类物品,每类物品可以无限制重复地选择,因此,状态转移可以从当前状态 继续转移; -
这道题是从背包中选择
个物品,它们可以是同一类物品,也可以是多类物品,选择的物品总数固定(能选择的物品总数不会超过 ),因此,状态转移需要从上一个状态 转移。
代码实现
class Solution: def combinationSum4(self, nums: List[int], target: int) -> int: result = 0 dp = [[0 for _ in range(target + 1)] for _ in range(target + 1)] dp[0][0] = 1 length = target # 遍历结果集中的物品数量,所有凑成和为target时,结果集中最多可以选择target个物品 for i in range(1, length + 1): # 遍历可以凑成的和 for j in range(target + 1): # 尝试选择每一个物品 for num in nums: if num <= j: # 累加可以选的物品的总数之和 dp[i][j] += dp[i - 1][j - num] result += dp[i][j] return result
优化后
class Solution: def combinationSum4(self, nums: List[int], target: int) -> int: dp = [0] * (target + 1) dp[0] = 1 for i in range(1, target + 1): for num in nums: if num <= i: dp[i] += dp[i - num] return dp[target]
参考:
本文作者:LARRY1024
本文链接:https://www.cnblogs.com/larry1024/p/17038583.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步