动态规划-背包问题
动态规划中最为经典的题目必然就是背包问题, 而背包问题的基础就是01背包.
引用一个大佬梳理的背包图解
由上图可以看到,01背包其实就是背包选物品,一种物品只有一个,要么选,要么不选.
先通过一个简单的01背包问题来理解01背包问题.
01背包
有N件物品和一个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
物品名称 | 价值 | 重量 |
---|---|---|
物品0 | 1 | 10 |
物品1 | 2 | 15 |
物品2 | 5 | 20 |
根据我们上一张总结的动态规划步骤,
第一步:确定dp数组
dp[i][j]:从0到i件物品中选择放入背包容量为j的背包中的最大价值
第二步:递推公式
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])
按照题目的描述,一个物品要么选,要么不选,选的话只能选一次 物品i不选的情况为dp[i-1][j],物品i选择的情况是dp[i-1][j-weight[i]] + value[i];
第三步:dp数组初始化
物品0放入容量小于它重量的背包的value应该都是0,容量为0的背包放不下任何一件物品.
dp[0][i] = 0;
dp[j][0] = 0;
第四步:确定遍历顺序
遍历的顺序:从前到后遍历
第五步:举例推导dp数组
滚动数组
前面一篇中我们有提到滚动数组,作为优化空间复杂度.
01背包是否可以用滚动数组呢?
当然是可以的,我们可以看到01背包的遍历也是一层一层的往下的
所以公式 dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]) 好像可以简化成 dp[j] = max(dp[j], dp[j-weight[i]] + value[i]).
有没有感觉有什么不对?
是的, 如果按照二维数组的遍历顺序来执行这个一位数组,会导致被重复的抽取,即从小到大的遍历顺序时,
dp[j] = max(dp[j], dp[j-weight[i]] + value[i]) 等价于二维数组的 dp[i][j] = max(dp[i-1][j], dp[i][j-weight[i]] + value[i])
所以在一维数组的公式下,我们需要以背包容量倒序的顺序进行遍历.
代码实现
func backpack01(arr [][]int, cap int) int {
if len(arr) == 0 {
return 0
}
x := len(arr)
dp := make([][]int, x)
for k := range dp {
dp[k] = make([]int, cap + 1)
}
for i := 0; i < x; i++ {
dp[i][0] = 0
}
for j := 0; j <= cap; j++ {
if arr[0][0] <= j {
dp[0][j] = arr[0][1]
} else {
dp[0][j] = 0
}
}
var tmp float64
for i := 1; i < x; i++ {
for j := 1; j <= cap; j++ {
if j - arr[i][0] >= 0 {
tmp = math.Max(float64(dp[i - 1][j]), float64(dp[i - 1][j - arr[i][0]] + arr[i][1]))
} else {
tmp = float64(dp[i - 1][j])
}
dp[i][j] = int(tmp)
}
}
return dp[x - 1][cap]
}
扩展
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.
完全背包
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
物品名称 | 价值 | 重量 |
---|---|---|
物品0 | 1 | 10 |
物品1 | 2 | 15 |
物品2 | 5 | 20 |
根据动态规划步骤,
第一步:确定dp数组
dp[i][j]:从0到i件物品中选择放入背包容量为j的背包中的最大价值,这个和01背包一样
第二步:递推公式
dp[i][j] = max(dp[i-1][j], dp[i][j-weight[i]] + value[i])
按照题目的描述,一个物品要么选,要么不选,选的话可以选无数次 物品i不选的情况为dp[i-1][j],物品i选择的情况是dp[i][j-weight[i]] + value[i];也就是即使选过了第i个物品,也还能继续选i物品
第三步:dp数组初始化
物品0放入容量小于它重量的背包的value应该都是0,由于一个物品可以多次放置,所以完全背包初始化的时候与01背包有些许的不通,容量为0的背包放不下任何一件物品.
dp[0][i] = 0;
dp[j][0] = 0;
第四步:确定遍历顺序
遍历的顺序:从前到后遍历
第五步:举例推导dp数组
func backpack(arr [][]int, cap int) int {
if len(arr) == 0 {
return 0
}
x := len(arr)
dp := make([][]int, x)
for k := range dp {
dp[k] = make([]int, cap + 1)
}
for i := 0; i < x; i++ {
dp[i][0] = 0
}
for j := 0; j <= cap; j++ {
if (j / arr[0][0]) > 0 {
dp[0][j] = arr[0][1] * (j / arr[0][0])
} else {
dp[0][j] = 0
}
}
var tmp float64
for i := 1; i < x; i++ {
for j := 1; j <= cap; j++ {
if j - arr[i][0] >= 0 {
tmp = math.Max(float64(dp[i - 1][j]), float64(dp[i][j - arr[i][0]] + arr[i][1]))
} else {
tmp = float64(dp[i - 1][j])
}
dp[i][j] = int(tmp)
}
}
return dp[x - 1][cap]
}
滚动数组
完全背包是否可以用滚动数组来做空间复杂度的优化呢.从我们上面推导的二维数组的递推公式再结合01背包中我们推导一维数组的思路,可以看到完全背包也是可以做滚动数组的.只是在遍历顺序上,背包容量的遍历顺序与01背包的遍历顺序恰好相反,是从小到大的遍历.
func backpackTwo(arr [][]int, cap int) int {
if len(arr) == 0 {
return 0
}
x := len(arr)
dp := make([]int, cap + 1)
var tmp float64
for i := 0; i < x; i++ {
for j := 0; j <= cap; j++ {
if j - arr[i][0] >= 0 {
tmp = math.Max(float64(dp[j]), float64(dp[j - arr[i][0]] + arr[i][1]))
} else {
tmp = float64(dp[j])
}
dp[j] = int(tmp)
}
}
return dp[cap]
}
扩展
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 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。
学习渠道: 微信公众号 代码随想录