Leetcode刷题笔记——动态规划(背包问题)篇
一、0-1 背包问题
0-1背包问题简介
有 n
件物品和一个最多能背重量为 w
的背包。第 i
件物品的重量是 weight[i]
,得到的价值是value[i]
。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大
1. 确定 dp
数组(dp table
)以及下标的含义
dp[i][j]:
从下标为 [0-i]
的物品里任意取,放进容量为 j
的背包,价值总和最大是多少
2. 确定递推公式
case1:
不放物品 i:由dp[i - 1][j]
推出,即背包容量为j
,里面不放物品i
的最大价值
case2:
放物品i:由dp[i - 1][j - weight[i]]
推出,dp[i - 1][j - weight[i]]
为背包容量为j - weight[i]
的时候不放物品i
的最大价值,那么dp[i - 1][j - weight[i]] + value[i]
(物品i的价值),就是背包放物品i得到的最大价值
故状态转移方程:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
3. dp
数组如何初始化
dp[0][j]
即:使用各个容量的背包存放编号 0
的物品所能获得的最大价值,即第一行的初始化
dp[i][0]
即:背包容量为 0
的时候,存放各个物品编号所能获得的最大价值,即第一列的初始化
其实从递推公式可以看出 dp[i][j]
是由左上方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖,这里统一把dp数组统一初始为0,更方便一些
4. 确定遍历顺序
可以看出,有两个遍历的维度:物品与背包重量
5. 打印 dp
数组
python代码解法
class Solution:
def BagProblem(self, weight: List[int], value: List[int], bag_capacity: int) -> int:
"""
:param weight: 物品重量
:param value: 物品价值
:param bag_capacity: 背包容量
:return: 背包所能装的最大价值
"""
nums = len(weight)
dp = [[0] * (bag_capacity + 1) for _ in range(nums)]
for i in range(0, bag_capacity + 1):
if i >= weight[0]:
dp[0][i] = value[0]
for i in range(1, nums):
for capacity in range(1, bag_capacity + 1):
# 不放物品i: dp[i - 1][capacity]:背包容量为 capacity 不放物品i的最大价值
# 放物品i: dp[i - 1][capacity - weight[i]] + value[i] 的最大价值
if capacity >= weight[i]: # 当背包容量大于等于物品重量的时候通过状态转移方程计算最大价值
dp[i][capacity] = max(dp[i - 1][capacity],
dp[i - 1][capacity - weight[i]] + value[i])
else:
dp[i][capacity] = dp[i - 1][capacity]
print(dp)
return dp[nums - 1][bag_capacity]
if __name__ == '__main__':
s = Solution()
weight = [1, 3, 4] # 物品重量
weight.sort()
value = [15, 20, 30] # 物品价值
value.sort()
bag_capacity = 4
print(s.BagProblem(weight, value, bag_capacity))
0-1背包问题的优化
对于背包问题其实状态都是可以压缩的
1. 确定 dp
数组(dp table
)以及下标的含义
在一维 dp
数组中,dp[j]
表示:容量为 j
的背包,所背的物品价值可以最大为 dp[j]
2. 确定递推公式
dp[j]
为容量为 j
的背包所背的最大价值,那么如何推导 dp[j]
呢?
dp[j]
有两个选择:
case1
:不放物品 i
,取自己 dp[j]
相当于 二维 dp
数组中的 dp[i-1][j]
case2
:放物品 i
,取 dp[j - weight[i]] + value[i]
,放物品 i
,指定是取最大的,毕竟是求最大价值
3. dp
数组如何初始化
dp[j]
表示:容量为 j
的背包,所背的物品价值可以最大为 dp[j]
,那么 dp[0]
就应该是 0
,因为背包容量为 0
所背的物品的最大价值就是 0
物品0
的重量 weight[0] = 1
,价值 value[0] = 15
正序遍历
dp[1] = dp[1 - weight[0]] + value[0] = 15
dp[2] = dp[2 - weight[0]] + value[0] = 30
此时 dp[2]
就已经是 30
了,意味着物品 0
,被放入了两次
倒序就是先算dp[2]
dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)
dp[1] = dp[1 - weight[0]] + value[0] = 15
从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了
4.一维dp
数组遍历顺序
二维 dp
遍历的时候,背包容量是从小到大,而一维 dp
遍历的时候,背包是从大到小,
倒序遍历是为了保证物品 i
只被放入一次!,一旦正序遍历了,那么物品0就会被重复加入多次!
weight = [1, 3, 4] # 物品重量
value = [15, 20, 30] # 物品价值
bag_capacity = 4 # 背包容量
dp = [0] * (bag_capacity + 1) # 表示容量为j的的背包所能装的最大价值为dp[j]
for i in range(0, len(weight)): # 遍历物品
for capacity in range(bag_capacity, weight[i] - 1, -1): # 倒序遍历背包容量
dp[capacity] = max(dp[capacity], dp[capacity - weight[i]] + value[i])
print(f"倒叙遍历背包容量至物品{i}的重量,在每个背包容量下所能装载的最大价值为:{dp}(每个物品只能装一次)")
# 倒叙遍历背包容量至物品 0 的重量,在每个背包容量下所能装载的最大价值为:[0, 15, 15, 15, 15](每个物品只能装一次)
# 倒叙遍历背包容量至物品 1 的重量,在每个背包容量下所能装载的最大价值为:[0, 15, 15, 20, 35](每个物品只能装一次)
# 倒叙遍历背包容量至物品 2 的重量,在每个背包容量下所能装载的最大价值为:[0, 15, 15, 20, 35](每个物品只能装一次)
5. 打印 dp
数组
0-1背包问题力扣实战练习
第一题
Leetcode416:分割等和子集:中等题 (详情点击链接见原题)
给你一个 只包含正整数 的非空数组
nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等
这道题目是要找是否可以将这个数组分割成两个子集使得两个子集的元素和相等,那么只要找到集合里能够出现 sum / 2
的子集总和就算是可以分割
- 背包的体积为
sum / 2
- 背包要放入的物品(集合里的元素)集合里的元素代表物品的重量和价值
- 背包如果正好装满说明找到了总和为
sum / 2
的子集 - 背包中的每一个元素是不可重复放入的
python代码解法(使用二维数组)
class Solution:
def canPartition(self, nums: List[int]) -> bool:
if sum(nums) % 2 == 1:
return False
target = sum(nums) // 2
dp = [[False for _ in range(target + 1)] for _ in range(len(nums))]
if nums[0] <= target:
dp[0][nums[0]] = True
for i in range(1, len(nums)): # 遍历物品
for j in range(0, target + 1): # 遍历背包容量
dp[i][j] = dp[i - 1][j]
if nums[i] == j:
dp[i][j] = True
continue
if nums[i] < j:
# 1.不选择 nums[i]:说明在[0, i - 1]这个子区间内已经有一部分元素使得它们的和为 j
# 2.选择 nums[i],如果在[0, i - 1]这个子区间内找到一部分元素使得它们的和为 j - nums[i]
dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]]
return dp[len(nums) - 1][target]
1. 确定 dp
数组(dp table
)以及下标的含义
dp[i]
表示背包总容量为i
,放进物品后所背的最大重量为dp[i]
那么如果背包容量为 target
, dp[target]
就是装满 背包之后的重量,所以 当 dp[target] == target
的时候,背包就装满了
2.确定递推公式
本题相当于往背包里放入数值
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
3. dp
数组如何初始化
dp[0] = 0
,如果题目给的元素都是正整数那么非 0
下标都初始化为 0
就可以了,如果题目所给的元素有负数,那么非 0
下标应初始化为 -∞
,这样才能让 dp
数组在递推过程中取得最大的价值,而不是被初始值覆盖了
# 题目说每个数组中的元素不会超过100,数组的大小不会超过200,总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了
dp = [0] * 10001
4.确定遍历顺序:如果使用一维 dp
数组,物品遍历的 for
循环放在外层,遍历背包的 for
循环放在内层,且内层 for
循环倒序遍历
python代码解法
class Solution:
def canPartition(self, nums: List[int]) -> bool:
if sum(nums) % 2 != 0:
return False
nums.sort()
target = sum(nums) // 2
# 题目说每个数组中的元素不会超过100,数组的大小不会超过200
# 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了
dp = [0] * 10001
for i in range(0, len(nums)): # 遍历物品(nums中的元素)
# 遍历背包容量, 每一个元素一定是不可重复放入,所以从大到小遍历
for j in range(target, nums[i] - 1, -1):
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
# 集合中的元素正好可以装满容量为target的背包
if dp[target] == target:
return True
return False
二、完全背包问题
1.完全背包问题简介
有 N
件物品和一个最多能背重量为 W
的背包。第i件物品的重量是 weight[i]
,得到的价值是 value[i]
。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件
weight = [1, 3, 4] # 物品重量
value = [15, 20, 30] # 物品价值
bag_capacity = 4 # 背包容量
dp = [0] * (bag_capacity + 1) # 表示容量为j的的背包所能装的最大价值为dp[j]
for i in range(0, len(weight)): # 遍历物品
for capacity in range(weight[i], bag_capacity + 1): # 倒序遍历背包容量
dp[capacity] = max(dp[capacity], dp[capacity - weight[i]] + value[i])
print(f"遍历物品{i}的重量至背包容量,在每个背包容量下所能装载的最大价值为:{dp}(每个物品可以装无限次)")
# 遍历物品 0 的重量至背包容量,在每个背包容量下所能装载的最大价值为:[0, 15, 30, 45, 60](每个物品可以装无限次)
# 遍历物品 1 的重量至背包容量,在每个背包容量下所能装载的最大价值为:[0, 15, 30, 45, 60](每个物品可以装无限次)
# 遍历物品 2 的重量至背包容量,在每个背包容量下所能装载的最大价值为:[0, 15, 30, 45, 60](每个物品可以装无限次)
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次
2.完全背包问题力扣实战练习
第一题
Leetcode322:零钱兑换:中等题 (详情点击链接见原题)
给你一个整数数组
coins
,表示不同面额的硬币;以及一个整数amount
,表示总金额
计算并返回可以凑成总金额所需的 最少的硬币个数
1. 确定 dp
数组(dp table
)以及下标的含义
dp[j]
:凑足总额为j所需钱币的最少个数为 dp[j]
2.确定递推公式
凑足总额为 j - coins[i]
的最少个数为 dp[j - coins[i]]
,那么只需要加上一个钱币 coins[i]
即 dp[j - coins[i]] + 1
就是 dp[j]
(考虑 coins[i]
)
递推公式:dp[j] = min(dp[j - coins[i]] + 1, dp[j])
;
3. dp
数组如何初始化
4. 确定遍历顺序
python代码解法
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [float('inf')] * (amount + 1) # dp[j]:表示凑成总金额为j的最少硬币个数,初始化为最大值
dp[0] = 0
for coin in coins: # 遍历硬币(遍历物品)
for j in range(coin, amount + 1): # 遍历金额(遍历背包容量)
dp[j] = min(dp[j], dp[j - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1