2023-01-09 21:26阅读: 67评论: 0推荐: 0

【动态规划】背包问题的应用

0-1背包问题的应用

应用1:Leetcode.416

题目

416.分割等和子集

分析

dp[i][j] 表示取数组 nums 中前 i 个元素,可以凑成和为 j 的种类是否大于 0。同时假设,设数组 nums 中所有元素的和为 S ,长度为 n

如果 S 不能被 2 整除,那么,所有的元素肯定不能分割为等和子集。

如果 S 可以被 2 整除,那么,我们就可以将数组中的元素看作物品,背包容量为 S/2,题目就可以转换为0-1背包问题,最后答案就是 dp[i][S/2]

边界条件

边界条件就是凑成和为零的方案,即对于任何数字都不选,它也对应一种方法,所以

dp[i][0]=true,0i<n

状态转移

对于数组中的第 i 个元素 nums[i1] ,它有两种选择:

  • 如果不选择当前元素,则当前状态与 dp[i1][j] 相同;

  • 如果选择当前元素,则当前状态与 dp[i1][jnums[i1]] 相同。

也就是说,我们只需要从上述两种方案中,选择一个结果为 true 的方案即可,所以,状态转移方程:

dp[i][j]=dp[i1][j] | dp[i1][jnums[i1]],nums[i1]jS/2

代码实现

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

题目

518. 零钱兑换 II

分析

dp[i][j] 表示使用前 i 枚金币能凑出金额 j 的凑法数量。

边界条件

使用任何面额的金币,凑出金额 0 的数量都是 1,也就是说不使用金币,它对应的凑法数量为 1,即:

dp[i][0]=1,0iamount

状态转移

对于数组 coins 中的第 i 枚金币 coins[i1] ,都有两种选择:

  • 当前金额 j 小于当前的金币的面额,凑法数量就是不选择当前面额的金币的种数,即:

    dp[i][j]=dp[i1][j],j<coins[i1]

  • 当前金额 j 大于等于当前的金币面额,此时,可以选择也可以不选择当前金币:

    • 如果不选择,那么,凑法数量就是与前一个状态相同,即:

      dp[i][j]=dp[i1][j],jcoins[i1]

    • 如果选择,那么,凑法数量就是在当前状态下,多次选择当前面额的金币种数,即:

      dp[i][j]=dp[i][jcoins[i1]],jcoins[i1]

    因此,当面额总数 j 大于当前金币面额时,凑法数量就是上述两种选择之和:

    dp[i][j]=dp[i1][j]+dp[i][jcoins[i1]],jcoins[i1]

综上,结合上述两种场景,状态转移方程为:

dp[i][j]={dp[i1][j],j<coins[i1]dp[i1][j] + dp[i][jcoins[i1]],jcoins[i1]

代码实现

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

题目

322. 零钱兑换

方法一:自底向上动态规划

分析

简而言之,题目等价于:

N 种物品和一个容量为 W 的背包。第 i 种物品的重量是 wi,每种物品的数量都有无限个。求解将哪些物品装入背包可使这些物品的重量刚好等于背包容量,且物品数量最少。

假设 dp[i] 表示金额 i 最少可以兑换的硬币数量。

边界条件

如果金额为零,显然最少的硬币数量为零,即:

dp[0]=0

状态转移

这里,我们直根据完全背包问题的模板接给出状态转移方程:

dp[i]=minj=0n1{dp[icoins[j]]+1},icoins[j]

代码实现

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]) 表示:当硬币为 coins 时,凑齐金额 amount 的最小数量,其中,memory 记录已经搜索过的结果,避免重复计算。

代码实现

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

题目

377. 组合总和 Ⅳ

分析

这道题如果直接使用回溯暴力搜索组合数,会导致内存超出限制。

这里,我们考虑使用动态规划求解,即在给定数组 nums 中,选择若干元素的组合,使它们的和等于 target,其中,元素可以多次重复选择。

这道题与完全背包问题的模型类似,但是,这道题的差异是:选择的不同物品的顺序代表不同的方案,因此,我们需要求出满足条件的所有物品的排列数。

假设数组的长度为 n,我们定义 dp[i][j] 表示选择 i 个元素,凑成和为 j 的方案数,那么,最后要求的答案就是

answer=i=0targetdp[i][target]

把所有的个数组合都选择一遍,然后将每个数字对应的所有方案数求和,就是最后的答案。

边界条件

当不选择任何元素时,只有一种方案,因此,有

dp[0][0]=1

状态转移

对于任意的 dp[i][j] 而言,组合中的最后一个数可以选择 nums 中的任意一个值,即:

  • 如果最后一个数选择 nums[0],则方案数为:dp[i1][jnums[0]];

  • 如果最后一个数选择 nums[1],则方案数为:dp[i1][jnums[1]];

  • 如果最后一个数选择 nums[2],则方案数为:dp[i1][jnums[2]];

因此,当选择 i 个元素时,可以凑成和为 j 的组合数应该为上述所有方案的总和,即

dp[i][j]=k=0n1f[i1][jnums[k]], jnums[n], and 0itarget

那么,我们只需要遍历所有的组合,并累加组合,即可得到答案。

这里需要注意,物品种类物品个数的区别:

  • 完全背包是从背包中选择 i 类物品,每类物品可以无限制重复地选择,因此,状态转移可以从当前状态 i 继续转移;

  • 这道题是从背包中选择 i 个物品,它们可以是同一类物品,也可以是多类物品,选择的物品总数固定(能选择的物品总数不会超过 target),因此,状态转移需要从上一个状态 i1 转移。

代码实现

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 中国大陆许可协议进行许可。

posted @   LARRY1024  阅读(67)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
  1. 1 404 not found REOL
404 not found - REOL
00:00 / 00:00
An audio error has occurred.