0-1背包问题
描述
0-1背包问题的基本逻辑就是,在n个重量不同,价值不同的物品中任选几个,使得可容纳重量位w的背包达到最大价值。
举例
有以下三个物品
物品 | 重量 | 价值 |
---|---|---|
物品0 | 10 | 20 |
物品1 | 4 | 10 |
物品2 | 6 | 15 |
背包的容量为10
明确dp数组的定义
dp[i][j]
表示容量为j
的背包,在物品i
之前任取物品能达到的最大价值
对于我们这个问题就是要确定dp[2][10]
的值,就是代表了容量为10的背包,在物品2之前任取物品所能找到的最大价值
初始化
i/j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
物品0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 20 |
物品1 | 0 | ||||||||||
物品2 | 0 |
如上表所示,第一列,也就是dp[i][0]
背包为0的前提下,肯定装不了物品,所以初始化为0
第一行,dp[0][j]
不同的背包容量下装物品0,那显然只有装得下物品0的时候价值为物品0的价值,否则,价值就是0
dp公式
对于每一个dp[i][j]
有两种情况
- 选择装物品
i
能达到的最大价值
选择物品i
换句话说就是一定要有物品i
,也就是说,在选择物品i
的前提下,在选择剩下的物品,看能达到的最大价值,那么递推公式就是
dp[i][j] = dp[i-1][j-weight[i]] + value[i]
- 不选择物品
i
能达到的最大价值
不选择,很容易写出如下递推公式
dp[i][j] = dp[i-1][j]
最终dp公式如下表示
dp[i][j] = max(dp[i-1][j-weight[i]] + value[i], dp[i-1][j])
遍历顺序
如何便利dp数组呢?
i/j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
物品0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 20 |
物品1 | 0 | ||||||||||
物品2 | 0 |
根据递推公式可以看出,每一个dp[i][j]
都是和他的前一行的数据有关,并且是对应位置前一行的前面的列有关,因为我们已经做了如上的初始化,所以先遍历物品和遍历容量都可以。
伪代码
dp[n][m+1] // n个物品,背包容量为m
weight[n] // n个物品的重量
value[n] // n个物品的价值
// 初始化
for(i : 0 -> n-1) {
dp[i][0] = 0 // 第一列初始化为0
}
for(j : 0 -> m) {
dp[0][j] = j >= weight[0] ? value[0] : 0 // 第一行初始化
}
// 遍历
for(i : 1 -> n-1) {
for(j : 1 -> m) {
dp[i][j] = max(j > weight[i] ? dp[i-1][j-weight[i]] + value[i] : 0
, dp[i-1][j])
}
}
// 返回结果
return dp[n-1][m]
从上述过程中可以看到,对于dp数组,我们在遍历当前行的元素时,所需要的是上一行并且是当前列的前面列的元素,所以,这里为了节省空间,可以将二位dp压缩成一维数组,节省空间复杂度
一维dp定义
dp[j]
代表容量为j
的背包所能装在的最大价值
dp数组的递推公式
跟二维dp对应的一个结果,应为将二维数组压缩,上一行的元素保存在当前行,所以不用i-1
- 选择装物品
i
能达到的最大价值
dp[j] = dp[j-weight[i]] + value[i]
- 不选择物品
i
能达到的最大价值
dp[j] = dp[j]
最终dp数组的递推公式如下所示
dp[j] = max(dp[j-weight[i]] + value[i], dp[j])
dp数组初始化
根据,dp数组的递推公式可以看出,将dp数组全部初始化为0即可
遍历顺序
对于二维dp来说,先遍历物品或者是先遍历重量都可以,但是对于一位dp来说一定要先便利物品在遍历重量,并且,在遍历重量的时候,还需要倒序遍历。原因如下
i/j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
物品0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 20 |
物品1 | 0 | ||||||||||
物品2 | 0 |
- 倒序遍历重量的原因就是,因为你需要的是前一行并且当前列前一列的元素,因为此时历史元素是存在当前行的,如果你是从前往后遍历,就会破坏之前的列
- 先遍历物品的原因就是,因为倒序遍历重量是一个前提,在这个前提下,你只能先遍历物品,否则先遍历重量的话,就没有所需要的历史元素的记录
伪代码
dp[m+1] // n个物品,背包容量为m
weight[n] // n个物品的重量
value[n] // n个物品的价值
// 初始化
for(i : 0 -> m) {
dp[i] = 0 // 全部初始化为0
}
// 遍历
for(i : 0 -> n-1) { // 先遍历物品
for(j : m -> 0) { // 倒序遍历背包容量
if(j <= weight[i])
break
dp[j] = max(dp[j-weight[i]] + value[i], dp[j])
}
}
// 返回结果
return dp[m]