一、完全背包问题与其解法
1.1 完全背包问题
假设有一个可装载重量为W的背包,以及一组物品,每种物品都有一个重量和一个价值。要求在不超过背包容量的前提下,选择一些物品放入背包中,使得背包中物品的总价值最大化。
与0-1背包问题不同的是,完全背包问题允许同一种物品可以选择多次放入背包中。也就是说,对于每一种物品,可以选择将它放入背包中0次、1次、2次......直到放满为止。
1.2 完全背包问题与0-1背包问题解法区别
完全背包问题与0-1背包问题的解法大致类似,但因其二者的特性差别,导致有部分差别。
1.2.1 遍历背包容量的顺序
使用滚动一维数组时:
0-1 背包:因需保证每个物品最多被添加一次,所以遍历背包容量时是倒序
完全背包:可以对每个物品添加多次,所以遍历背包容量时是正序(可以手写比较一下)
倒序遍历背包容量 正序遍历背包容量
1.2.2 内外的遍历顺序是否可交换
0-1 背包:因为每个物体至多只能使用一次,遍历背包容量时是倒序,也就是从右往左的方向,但总体的遍历是依据之前的情况去递推后续节点,也就是从左往后推导,此时需要外侧有一个正序遍历的循环,所以需先正序遍历物体,后逆序遍历背包容量。
完全背包:见排列与组合部分的辨析。
二、完全背包问题的应用例题
2.1 零钱兑换 II
2.1.1 题目
给你一个整数数组 coins
表示不同面额的硬币,另给一个整数 amount
表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0
。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
示例 1:
输入:amount = 5, coins = [1, 2, 5] 输出:4 解释:有四种方式可以凑成总金额: 5=5 5=2+2+1 5=2+1+1+1 5=1+1+1+1+1
示例 2:
输入:amount = 3, coins = [2] 输出:0 解释:只用面额 2 的硬币不能凑成总金额 3 。
示例 3:
输入:amount = 10, coins = [10] 输出:1
提示:
1 <= coins.length <= 300
1 <= coins[i] <= 5000
coins
中的所有值 互不相同0 <= amount <= 5000
2.1.2 题目链接
2.1.3 解题思路和过程想法
(1)解题思路
# 分析:无限的物体,求凑齐特定值的方法数----> 完全背包问题
# 数组:dp[j] 表示构成总金额 j 的组成方法数
# 递推关系:dp[j] += dp[j-coin[i]]
# 初始化:dp[0]若是初始化为0,则后续全是0,无法进行
dp[0] = 1
# 举例递推:纯完全背包内外循环顺序可调换,但遍历背包需正序,这样才能表示物品可以添加多次
for coin in coins:
for j in range(coin,amount+1):
dp[j] += dp[j-coin]
(2)过程想法
上述是“组合”问题,注意和“排列”对比一下
2.1.4 代码
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
# 分析:无限的物体,求凑齐特定值的方法数----> 完全背包问题
# 数组:dp[j] 表示构成总金额 j 的组成方法数
dp = [0] * (amount+1)
# 递推关系:dp[j] += dp[j-coin[i]]
# 初始化:dp[0]若是初始化为0,则后续全是0,无法进行
dp[0] = 1
# 举例递推:纯完全背包内外循环顺序可调换,但遍历背包需正序,这样才能表示物品可以添加多次
for coin in coins:
for j in range(coin,amount+1):
dp[j] += dp[j-coin]
return dp[amount]
2.2 组合总和 IV
2.2.1 题目
给你一个由 不同 整数组成的数组 nums
,和一个目标整数 target
。请你从 nums
中找出并返回总和为 target
的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
示例 1:
输入: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) 请注意,顺序不同的序列被视作不同的组合。
示例 2:
输入:nums = [9], target = 3 输出:0
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 1000
nums
中的所有元素 互不相同1 <= target <= 1000
进阶:如果给定的数组中含有负数会发生什么?问题会产生何种变化?如果允许负数出现,需要向题目中添加哪些限制条件?
2.2.2 题目链接
2.2.3 解题思路和过程想法
(1)解题思路
# 分析:由多种数量为无限的物体,组成一个固定值,求其排列方法数---> 完全背包问题
# 数组:dp[j] 固定值 j 的排列方法数
# 递推关系:dp[j] += dp[j-nums[i]]
# 初始化
dp[0] = 1
# 举例递推:完全背包的排列方法数需先遍历背包,后遍历物体
for j in range(1,target+1): # 遍历背包
for i in range(len(nums)): # 遍历物体
if j >= nums[i]:
dp[j] += dp[j-nums[i]]
(2)过程想法
上述是“排列”问题,注意和“组合”对比一下
2.2.4 代码
class Solution:
def combinationSum4(self, nums: List[int], target: int) -> int:
# 分析:由多种数量为无限的物体,组成一个固定值,求其排列方法数---> 完全背包问题
# 数组:dp[j] 固定值 j 的排列方法数
dp = [0] * (target+1)
# 递推关系:dp[j] += dp[j-nums[i]]
# 初始化
dp[0] = 1
# 举例递推:完全背包的排列方法数需先遍历背包,后遍历物体
for j in range(1,target+1): # 遍历背包
for i in range(len(nums)): # 遍历物体
if j >= nums[i]:
dp[j] += dp[j-nums[i]]
return dp[target]
三、完全背包问题的排列与组合问题
交换内外循环顺序 :先遍历物体可保证同数量的情况仅计算一次,
先遍历背包可保证同数量但有不同出现的顺序都能被计算一次。
组合:遍历物体的循环是在外层,遍历背包容量的循环是在内层。
排列:遍历背包容量的循环是在外层,遍历物品的循环是在内层。
四、常用套路总结
排列问题:1+2+1 和 2+1+1 是两种不同方法
递推关系:求组成的方法数——————dp[j] += dp[j-i],
求背包装载的最大“价值” ——dp[j] = max(dp[j], dp[j-weight[i]]+value[i])
求背包装载的最少“数量” ——dp[j] = min(dp[j], dp[j-weight[i]]+1)
初始化:求组成的方法数——dp[0] = 1
其他值初始化———初始化不影响覆盖的值,求最大-->初始化最小;
求最小-->初始化无穷大值