目录
图片来源:动态规划:01背包理论基础
01背包问题
物品 | 重量 | 价值 |
0 | 1 | 15 |
1 | 3 | 20 |
2 | 4 | 30 |
背包最大重量为4,问背包能背的物品最大价值是多少?
这是一个典型的01背包问题,其他很多题目其实都需要转化为01背包问题,所以我们先来搞明白01背包问题的解法
解法一:暴力解法
其实这个问题对于物品来说就是选于不选的问题,那么我们可以用回溯法,来遍历所有的可能情况,这时时间复杂度就是O(2^n),n表示物品数量。
解法二:动态规划
上述暴力解法的时间复杂度是指数增长的,所以需要用动态规划来优化时间复杂度。下面,将按照代码随想录当中介绍的动态规划五部曲来分解01背包问题。
二维dp
一、dp数组的含义
对于二维dp数组dp[i][j]的含义为:任取小标为0-i的物品放进容量为j的背包,价值总和最大为多少。
二、dp递推公式
dp[i][j]可以由两个部分推导出
- 对于物品i,我们选择不取,那么dp[i][j] = dp[i-1][j],即因为背包容量为j,里面不放物品i的最大价值(也就是物品i的重量大于背包j的重量时),背包里的价值依然和前面相同
- 放物品i,那么由dp[i-1][j-weight[j]]推导出,此时的含义为背包容量为j-weight[i]时,不放物品i的最大价值,那么dp[i-1][j-weight[i]]+value[i]就表示放了物品i的价值
所以递推公式为:dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i])
三、dp数组初始化
由递推公式我们可知,i由[i-1][j]和[i-1][j-weight[i]]推导出,那么我们需要将dp数组的第一列和第一行都进行初始化。
- 第一列初始化都可以为0,因为此时背包容量为0无法放进任何物品
- 第一行的初始化需要在j >= weight[0]的位置开始,将dp[0][j] = value[i]
0 | 1 | 2 | 3 | 4 | |
物品0 | 0 | 15 | 15 | 15 | 15 |
物品1 | 0 | ||||
物品2 | 0 |
四、dp数组遍历顺序
对于二维dp,数组的遍历的顺序和最后的结果没有太大的关系,因为dp[i][j]的值有其上方和左上方推导而来,只要这两个方向有值,那么不管是哪种遍历顺序,其实都能够得到相对于的结果。
0 | 1 | 2 | 3 | 4 | |
物品0 | 0 | 15 | 15 | 15 | 15 |
物品1 | 0 | 15 | 15 | 20 | 35 |
物品2 | 0 | 15 | 15 | 20 | 35 |
五、打印dp数组
这部分是在程序调试的过程中来实现,下面是基于Go语言的实现
func main() {
// 如果是参数版,那么需要传递的参数为:(weight, value []int, bagweight)
weight := []int{1, 3, 4}
value := []int{15, 20, 30}
bagweight := 4
dp := make([][]int, len(weight))
for i := 0; i < len(weight); i++ {
dp[i] = make([]int, bagweight+1)
}
// dp数组初始化
for i := weight[0]; i < bagweight+1; i++ {
dp[0][i] = value[0]
}
for i := 1; i < len(weight); i++ { // 遍历物品
for j := 1; j < bagweight+1; j++ { // 遍历背包
if j < weight[i] {
dp[i][j] = dp[i-1][j]
} else {
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i])
}
}
}
fmt.Println(dp[len(weight)-1][bagweight])
}
func max(a, b int) int {
if a > b {
return a
} else {
return b
}
}
一维dp(滚动数组)
还是基于上述例子来理解。
对于背包问题其实状态是可以压缩的,在使用二维数组时,递推公式:dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i]),其实这里第i层可以直接复用第i-1层的信息,那么递推公式就变成dp[i][j] = max(dp[i][j], dp[i][j-weight[i]]+value[i]),这样就相当于是前i-1个物品已经放进了当前背包容量j中,那么当前物品i放入背包容量j中是否能使得价值总和最大。
因此经过上述分析,我们不难发现,与其使用二维数组增大时间和空间开销,不如使用一维数组来实现上述描述。此时dp数组的含义如下所示:
一、dp数组的含义
dp[j]:表示当前容量j的背包中,所背物品的价值最大可为dp[j]
二、dp递推公式
由于这里是一维数组,那么dp[j]只能由一个公式推导而来:dp[j-weight[i]]+value[i]
这个式子表示在容量为j-weight[i]的背包(该容量下已达最大价值)中加上物品i的价值,就表示此时容量为j的背包选择了物品i时的背包内价值是多少。这里其实也涉及到物品i的选择与不选择问题,上述式子表示选择了物品i,而不选择物品i时,dp[j]的值维持不变。这里为什么是维持不变,在下面讲述dp数组的遍历顺序时就能够明白。
- 完整的递推公式:dp[j] = max(dp[j], dp[j-weight[i]]+value[i])
三、dp数组初始化
dp[j]表示容量为j的背包,所背的物品的价值可以最大为单dp[j],那么dp[0] = 0,其他非0下标的初始化则看给的物品价值是否为正值,因为递推公式的含义为物品i选与不选的最大值,在最初始阶段没有物品来,只要不影响后续物品的选则,那么我们给dp数组非0下标的初始化为0就可以了。
四、dp数组遍历顺序
一维dp的遍历顺序和二维dp的遍历顺序区别很大。
在二维dp数组的遍历中,我们说优先遍历物品还是优先遍历背包其实都没有太大的区别,只要保证遍历时上方和左上方有值存在就可以。
但是在一维数组遍历中,这个顺序就不能随意。这里直接给出结论:我们遍历是先遍历物品,再遍历背包,切背包遍历应该倒序遍历
我们从下面几个方面来分析为什么:
1、如果我们在先遍历物品,再遍历背包的时候,选择正序遍历
使用正序遍历,会导致同一个物品被多次使用,在01背包问题中,每个物品只能使用一次
我们使用图片样例来展示这个错误:
dp数组初始化:
遍历到物品0,当遍历背包时,从容量为1开始,发现可以放下,则dp[1] = 15:
遍历到物品0,背包遍历到容量2,此时依然可以放下物品0,那么这里dp[2] = dp[2-weight[0]]+value[0] = dp[1] + 15 = 15 + 15 =30,这时意味着物品0被放入了两次。
因此,我们需要倒序遍历,下面给出倒叙遍历的图示样例:
1)物品0
2)物品1
3)物品2
在这里会对dp[4] = 35 和dp[4-4] + value[2] = 0 + 30 = 30进行对比,选择大的那个
2、如果我们先遍历背包,再遍历物品
解释完为什么要倒叙遍历,接下来解释为什么不可以先遍历背包再遍历物品
如果我们先遍历背包那就是在当前容量j下,物品i是否能够放进去。其实这样无法保存背包之前存入物品的信息,后续的物品会覆盖之前存入背包的物品,相当于只放入了一个物品。为什么会这么说呢?我们看递推公式就能够明白,当物品i已经放入背包j时,下一个物品来临,max操作会在dp[j]和dp[j-weight[i]]+value[i]中选择一个,而这个时候背包j中只有物品i,前置背包中还没有物品,那相当于就是物品i和物品i+1比较谁的价值,谁就放入背包j中。
我们以一个图例来直观的展示这个错误:
1) 容量为4的背包
物品0
物品1,此时还是在容量为4的背包下,那么按照递推公式来推导:dp[4] = max(dp[4], dp[4-weight[1]+value[1]] = max(15, dp[1] + 20) = max(15, 0 + 20) = 20。到这里可以很直观的看到,本来容量为4的背包可以放入物品0和物品1两个,但是这样遍历物品1直接覆盖了物品0,无法得到最大的所背物品价值。
五、打印dp数组
这部分是在程序调试的过程中来实现,下面是基于Go语言的实现
func main() {
// 如果是参数版,那么需要传递的参数为:(weight, value []int, bagweight)
weight := []int{1, 3, 4}
value := []int{15, 20, 30}
bagweight := 4
dp := make([]int, bagweight+1) // 初始化为0
for i := 0; i < len(weight); i++ { // 遍历物品
for j := bagweight; j >= weight[i]; j-- { // 倒序遍历背包
dp[j] = max1(dp[j], dp[j-weight[i]]+value[i])
}
fmt.Println(dp)
}
fmt.Println(dp[bagweight])
}
func max1(a, b int) int {
if a > b {
return a
} else {
return b
}
}
结果:
完全背包问题
待续.....