01. 背包问题知识(一)
01. 背包问题知识(一)
1. 背包问题简介
1.1 背包问题的定义
背包问题:背包问题是线性 DP 问题中一类经典而又特殊的模型。背包问题可以描述为:给定一组物品,每种物品都有自己的重量、价格以及数量。再给定一个最多能装重量为
的背包。现在选择将一些物品放入背包中,请问在总重量不超过背包载重上限的情况下,能装入背包的最大价值总和是多少?
根据物品限制条件的不同,背包问题可分为:0-1 背包问题、完全背包问题、多重背包问题、分组背包问题,以及混合背包问题等。
1.2 背包问题的暴力解题思路
背包问题的暴力解题思路比较简单。假设有
背包问题暴力解法的时间复杂度是指数级别的,我们可以利用动态规划算法减少一下时间复杂度。
下面我们来讲解一下如何使用动态规划方法解决各种类型的背包问题。
2. 0-1 背包问题
0-1 背包问题:有
件物品和有一个最多能装重量为 的背包。第 件物品的重量为 ,价值为 ,每件物品有且只有 件。请问在总重量不超过背包载重上限的情况下,能装入背包的最大价值是多少?
2.1 0-1 背包问题基本思路
0-1 背包问题的特点:每种物品有且仅有
件,可以选择不放入背包,也可以选择放入背包。
思路 1:动态规划 + 二维基本思路
1. 划分阶段
按照物品的序号、当前背包的载重上限进行阶段划分。
2. 定义状态
定义状态
状态
3. 状态转移方程
对于「将前
- 第
件物品不放入背包:问题转换为「前 件物品放入一个最多能装重量为 的背包中 ,可以获得的最大价值」为 。 - 第
件物品放入背包:问题转换为「前 件物品放入一个最多能装重量为 的背包中,可以获得的最大价值」为 ,再加上「放入的第 件物品的价值」为 ,则此时可以获得的最大价值为 。
接下来我们再来考虑一下第
- 如果当前背包的载重不足时(即
):第 件物品一定不能放入背包,此时背包的价值 仍为 时的价值,即 。 - 如果当前背包的载重足够时(即
):第 件物品可以考虑放入背包,或者不放入背包,此时背包的价值取两种情况下的最大值,即 。
则状态转移方程为:
4. 初始条件
- 如果背包载重上限为
,则无论选取什么物品,可以获得的最大价值一定是 ,即 。 - 无论背包载重上限是多少,前
件物品所能获得的最大价值一定为 ,即 。
5. 最终结果
根据我们之前定义的状态,
思路 1:代码
class Solution:
# 思路 1:动态规划 + 二维基本思路
def zeroOnePackMethod1(self, weight: [int], value: [int], W: int):
size = len(weight)
dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)]
# 枚举前 i 种物品
for i in range(1, size + 1):
# 枚举背包装载重量
for w in range(W + 1):
# 第 i - 1 件物品装不下
if w < weight[i - 1]:
# dp[i][w] 取「前 i - 1 件物品装入载重为 w 的背包中的最大价值」
dp[i][w] = dp[i - 1][w]
else:
# dp[i][w] 取「前 i - 1 件物品装入载重为 w 的背包中的最大价值」与「前 i - 1 件物品装入载重为 w - weight[i - 1] 的背包中,再装入第 i - 1 物品所得的最大价值」两者中的最大值
dp[i][w] = max(dp[i - 1][w], dp[i - 1][w - weight[i - 1]] + value[i - 1])
return dp[size][W]
思路 1:复杂度分析
- 时间复杂度:
,其中 为物品数量, 为背包的载重上限。 - 空间复杂度:
。
2.2 0-1 背包问题滚动数组优化
根据之前的求解过程可以看出:当我们依次处理前
也就是说在状态转移的过程中,我们只用到了当前行(第
所以我们没必要保存所有阶段的状态,只需要保存上一阶段的所有状态和当前阶段的所有状态就可以了,这样使用两个一维数组分别保存相邻两个阶段的所有状态就可以实现了。即:用
其实我们还可以进一步进行优化,我们只需要使用一个一维数组
思路 2:动态规划 + 滚动数组优化
1. 划分阶段
按照当前背包的载重上限进行阶段划分。
2. 定义状态
定义状态
3. 状态转移方程
在第
为了保证第
这是因为如果我们采用「从
而如果按照「从
因为
4. 初始条件
- 无论背包载重上限为多少,只要不选择物品,可以获得的最大价值一定是
,即 。
5. 最终结果
根据我们之前定义的状态,
思路 2:代码
class Solution:
# 思路 2:动态规划 + 滚动数组优化
def zeroOnePackMethod2(self, weight: [int], value: [int], W: int):
size = len(weight)
dp = [0 for _ in range(W + 1)]
# 枚举前 i 种物品
for i in range(1, size + 1):
# 逆序枚举背包装载重量(避免状态值错误)
for w in range(W, weight[i - 1] - 1, -1):
# dp[w] 取「前 i - 1 件物品装入载重为 w 的背包中的最大价值」与「前 i - 1 件物品装入载重为 w - weight[i - 1] 的背包中,再装入第 i - 1 物品所得的最大价值」两者中的最大值
dp[w] = max(dp[w], dp[w - weight[i - 1]] + value[i - 1])
return dp[W]
思路 2:复杂度分析
- 时间复杂度:
,其中 为物品数量, 为背包的载重上限。 - 空间复杂度:
。
2.3 0-1 背包问题的应用
2.3.1 题目链接
2.3.2 题目大意
描述:给定一个只包含正整数的非空数组
要求:判断是否可以将这个数组分成两个子集,使得两个子集的元素和相等。
说明:
。 。
示例:
- 示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11]。
- 示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
2.3.3 解题思路
思路 1:动态规划
这道题换一种说法就是:从数组中选择一些元素组成一个子集,使子集的元素和恰好等于整个数组元素和的一半。
这样的话,这道题就可以转变为「0-1 背包问题」。
- 把整个数组中的元素和记为
,把元素和的一半 看做是「0-1 背包问题」中的背包容量。 - 把数组中的元素
看做是「0-1 背包问题」中的物品。 - 第
件物品的重量为 ,价值也为 。 - 因为物品的重量和价值相等,如果能装满载重上限为
的背包,那么得到的最大价值也应该是 。
这样问题就转变为:给定一个数组
1. 划分阶段
按照当前背包的载重上限进行阶段划分。
2. 定义状态
定义状态
3. 状态转移方程
4. 初始条件
- 无论背包载重上限为多少,只要不选择物品,可以获得的最大价值一定是
,即 。
5. 最终结果
根据我们之前定义的状态,
所以最后判断一下 True
;否则返回 False
。
思路 1:代码
class Solution:
# 思路 2:动态规划 + 滚动数组优化
def zeroOnePackMethod2(self, weight: [int], value: [int], W: int):
size = len(weight)
dp = [0 for _ in range(W + 1)]
# 枚举前 i 种物品
for i in range(1, size + 1):
# 逆序枚举背包装载重量(避免状态值错误)
for w in range(W, weight[i - 1] - 1, -1):
# dp[w] 取「前 i - 1 件物品装入载重为 w 的背包中的最大价值」与「前 i - 1 件物品装入载重为 w - weight[i - 1] 的背包中,再装入第 i - 1 物品所得的最大价值」两者中的最大值
dp[w] = max(dp[w], dp[w - weight[i - 1]] + value[i - 1])
return dp[W]
def canPartition(self, nums: List[int]) -> bool:
sum_nums = sum(nums)
if sum_nums & 1:
return False
target = sum_nums // 2
return self.zeroOnePackMethod2(nums, nums, target) == target
思路 1:复杂度分析
- 时间复杂度:
,其中 为数组 的元素个数, 是整个数组元素和的一半。 - 空间复杂度:
。
参考资料
来源:https://github.com/itcharge/LeetCode-Py