完全背包
完全背包问题是动态规划中的一个典型问题,与0-1背包问题相似,但有一个关键的区别:在完全背包问题中,每种类型的物品可以选取无限次,而在0-1背包问题中,每种物品只能选取一次。
问题定义: 给定一个固定大小的背包和一系列物品,每种物品都有自己的重量和价值,求在不超过背包容量的前提下,背包中物品的最大价值总和。
解题步骤:
-
确定状态:
- 确定动态规划状态。通常,状态表示为
dp[i][w]
,表示从前i
件物品中选取若干件放入容量为w
的背包中可以得到的最大价值。
- 确定动态规划状态。通常,状态表示为
-
初始化状态:
dp[0][w]
初始化为0,因为没有物品时,无论背包容量为多少,能够装入的最大价值都是0。
-
状态转移方程:
- 对于每个物品,我们可以选择放入或者不放入背包。如果不放入背包,那么
dp[i][w] = dp[i-1][w]
。如果放入背包,因为是完全背包问题,我们可以选择放入多个相同的物品,所以dp[i][w] = max(dp[i][w], dp[i][w-weight[i]] + value[i])
。这里的dp[i][w-weight[i]] + value[i]
表示在当前背包容量下,放入一个当前物品后的最大价值。
- 对于每个物品,我们可以选择放入或者不放入背包。如果不放入背包,那么
-
计算顺序:
- 在0-1背包问题中,为了保证每个物品只被计算一次,我们需要逆序更新状态。但在完全背包问题中,由于每种物品可以选取多次,我们应该正序更新状态。
-
优化空间复杂度:
- 可以将二维dp数组优化为一维数组。在更新时,因为每个物品可以选取多次,所以对于每个物品,我们正序遍历背包容量,更新
dp[w] = max(dp[w], dp[w-weight[i]] + value[i])
。
- 可以将二维dp数组优化为一维数组。在更新时,因为每个物品可以选取多次,所以对于每个物品,我们正序遍历背包容量,更新
518. 零钱兑换 II
零钱兑换 II 问题是一个经典的动态规划问题,也被认为是完全背包问题的变体。在这个问题中,你被给定一些不同面额的硬币和一个总金额,你需要计算出用这些硬币组成该总金额的方法数量。
问题描述如下:
给定不同面额的硬币 coins
和一个总金额 amount
,编写一个函数来计算可以凑成总金额所需的硬币组合数。假设每一种面额的硬币有无限个。
示例:
输入: coins = [1, 2, 5], amount = 5
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
在这个问题中,我们考虑的是组合数而不是最大价值。
解题步骤
-
定义状态:
- 创建一个数组
dp
,其中dp[i]
表示组成金额i
的硬币组合数。
- 创建一个数组
-
初始化状态:
- 初始化
dp[0] = 1
,因为组成金额0的方式只有一种,就是选择0个硬币。 - 其他
dp[i]
初始化为0。
- 初始化
-
状态转移方程:
- 遍历每一种硬币,对于每个硬币的面额
coin
,更新金额i
从coin
到amount
的dp[i]
值。 dp[i] += dp[i - coin]
。这表示当前金额i
的组合数等于不使用当前硬币时的组合数加上使用了当前硬币之后剩余金额i - coin
的组合数。
- 遍历每一种硬币,对于每个硬币的面额
-
迭代更新
dp
数组:- 对于
coins
中的每个硬币,我们正序遍历dp
数组从coin
到amount
,这与完全背包问题的处理方式一致,因为我们可以使用无限个相同的硬币。
- 对于
-
优化空间复杂度:
- 与完全背包问题一样,我们可以仅使用一维数组来解决问题。
代码实现
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
# 初始化dp数组,dp[0]为1,因为组成金额0只有一种方式
dp = [0] * (amount + 1)
dp[0] = 1
# 遍历每种硬币,更新dp数组
for coin in coins:
for i in range(coin, amount + 1):
dp[i] += dp[i - coin]
# 返回组成总金额的组合数
return dp[amount]
在这个实现中,我们通过不断迭代更新 dp
数组来计算出所有可能的组合数。最终 dp[amount]
就是我们的答案,它表示用 coins
中的硬币组成 amount
总金额的组合数。
377. 组合总和 Ⅳ
组合总和 IV 问题是关于计算给定一系列数字和一个目标总和,有多少种不同的组合方式可以使得所选数字之和等于目标总和。这个问题可以通过动态规划解决,但是与前面的完全背包问题和零钱兑换问题有所不同,因为它关注的是排列的数量而不是组合的数量。这意味着元素的顺序会影响到最终的计数。
问题描述如下:
给定一个由正整数组成的数组 nums
和一个目标整数 target
,找出 nums
中数字的所有唯一组合,这些组合的和为 target
。每个数字在每个组合中可以使用无限次。
与组合数问题不同,数字的顺序不同的序列被视为不同的组合。
示例:
输入: nums = [1, 2, 3], target = 4
输出: 7
解释: 所有可能的组合是:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
注意顺序的不同,组合也被视为不同的组合。
解题步骤
-
定义状态:
- 创建一个数组
dp
,其中dp[i]
表示达到总和为i
的唯一组合数。
- 创建一个数组
-
初始化状态:
- 初始化
dp[0] = 1
,因为达到总和为0的方式只有一种,即不选择任何数字。
- 初始化
-
状态转移方程:
- 对于每个可能的总和
i
从1
到target
,遍历nums
中的每个数字num
: - 如果
num
小于或等于i
,那么dp[i] += dp[i - num]
。这代表了当前总和i
的组合数是之前总和为i - num
的组合数的总和。
- 对于每个可能的总和
-
迭代计算:
- 对于每个
i
,我们都需要计算所有小于或等于i
的num
的dp[i]
值。
- 对于每个
-
优化空间复杂度:
- 类似于之前的问题,我们可以使用一维
dp
数组来减少空间复杂度。
- 类似于之前的问题,我们可以使用一维
代码实现
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 i >= num:
dp[i] += dp[i - num]
return dp[target]
在这个问题中,由于顺序会影响组合的结果,我们在计算 dp[i]
时会考虑所有可能到达 i
的路径。例如,3
可以由 1+2
或 2+1
得到,在这里它们被视为两种不同的组合。这是计算排列数的关键所在。最终 dp[target]
就是我们的答案,表示用 nums
中的数字组成 target
总和的不同排列数。