问题描述:
有n
件物品和一个最多能背重量为w
的背包。第i件物品的重量是weight[i]
,得到的价值是value[i]
。每件物品只能用一次,求解将哪些)物品装入背包里物品价值总和最大。
二维dp数组01背包
1. 确定dp数组以及下标的含义
dp[i][j]
表示从下标为[0-i]
任意组合放入背包后,背包容纳重量为j
时的最大价值
2. 确定递推公式
有两个方向推出来dp[i][j]
:
-
不放物品i:由
dp[i - 1][j]
推出,即背包容量为j
,里面不放物品i
的最大价值,此时dp[i][j]
就是dp[i - 1][j]
。(其实就是当物品
i
的重量大于背包j
的重量时,物品i
无法放进背包中,所以背包内的价值依然和前面相同。) -
放物品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得到的最大价值j - weight[i]
: 不装物品i时的容量dp[i - 1][j - weight[i]]
:装i
时前j - weight[i]
容量的最大价值。
当i
放进去时,那么这时候整个物品集就被分成两部分,1到i-1
和第i个,而这时i
是确定要放进去的,那么就把j
空间里的weight[i]
给占据了,只剩下j-weight[i]
的空间给前面i-1
,那么只要这时候前面i-1
在j-weight[i]
空间里构造出最大价值,即dp[i-1][j-weight[i]]
,再加上此时放入的i的价值value[i]
,就是dp[i][j]
了
递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
dp[i][j] = max(不放物品,放物品)
不装
i
即需要到前i-1
个里面选,也就是前**i-1
行j
背包容量下的最大价值**,由于前面都已经是最优解,直接取dp[i - 1][j]
就是不装i
条件下的最大价值
for (int i = 1; i < weight.size(); i++){ // 遍历物品
for (int j = 1; j <= bagweight; 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]);
}
}
}
为什么需要if (j < weight[i])
?
因为:当j-weight[i] < 0
时,数组会越界,即 dp[i - 1][j - weight[i]]
会报错
3. dp数组如何初始化
从dp[i][j]
的定义出发:
如果背包容量j
为0
的话,即dp[i][0]
,无论是选取哪些物品,背包价值总和一定为0。
如果i=0
的情况就是只有一个物品0,即dp[0][j]
,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。
- 当
j < weight[0]
,dp[0][j]
应该是 0,因为背包容量比编号0的物品重量还小。- 当
j >= weight[0]
时,dp[0][j]
应该是value[0]
,因为背包容量放足够放编号0物品。
这里只列举初始化dp[0][j]
(因为构造vector
的时候,基本上都已经初始化元素为0了)
// 初始化
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
4. 确定遍历顺序
先遍历物品,后遍历重量
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品(已经初始化dp[0][j]的情况了)
for(int j = 1; j <= bagweight; j++) { // 遍历背包容量(已经初始化dp[i][0]的情况了)
// ...
}
}
5. 举例推导dp数组
完整代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int bag(vector<int> weight, vector<int> value, int bagweight) {
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
for (int j = weight[0]; j <= bagweight; j++){
dp[0][j] = value[0];
}
for (int i = 1; i < weight.size(); i++){
for (int j = 1; j <= bagweight; 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]);
}
}
}
for (auto x : dp){
for (auto xx : x){
cout << xx << " ";
}
cout << endl;
}
return dp[weight.size() - 1][bagweight];
}
int main(){
vector<int> weight = { 1, 3, 4 };
vector<int> value = { 15, 20, 30 };
int bagweight = 4;
bag(weight, value, bagweight);
system("PAUSE");
return 0;
}
一维滚动dp数组01背包
为什么可以将二维数组dp[i][j]
转化为dp[i]
呢?
二维数组的递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
从上面公式可以看出,当前层i
的数据都是由上一层i-1
推导出来的。
那是否可以直接将上一层i-1
的数据直接拷贝到当前层i
上,直接进行计算呢?
答案是可以的,通过观察发现,dp[i][j]
的数据只与正上方的值dp[i-1][j]
和左上角的值dp[i-1][j-weight[i]]
这两个数据有关,而与右边的数据无关,那么从右向左遍历,遍历时左边的数据还是上一行的数据没有更新, 这样子用一行数组很好的实现了我们的最终目的。
如dp[2][4] = max(dp[1][4], dp[1][0] + value[2]);
- 正上方的值:35
- 左上角的值:0 + 30
- 最终值:
max(35,30)
dp[1][4] = max(dp[0][4], dp[0][1] + value[1]);
- 正上方的值:15
- 左上角的值:15 + 20
- 最终值:
max(15,35)
dp[2][3] = dp[1][3]
,由于 j < weight[2]
1. 确定dp数组以及下标的含义
dp[j]
表示从容量为j
的背包所背的最大价值
2. 确定递推公式
有两个方向推出来dp[j]
:
- 不放物品i:取上一层的数值(即二维数组中正上方的数值),滚动数组只有不更新,就一直是上一层的数值,所以取
dp[j]
即可 - 放物品i:
dp[j - weight[i]] + value[i]
表示 容量为【j
- 物品i
重量 】的背包 加上 【物品i
】的价值。此时就是容量为j
的背包所背的最大价值
递归公式: dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
**可以看出相对于二维dp数组的写法,就是把dp[i][j]
中i
的维度去掉了。**ps:实在忘记公式了,可以直接先写出二维的,再把递归公式中的i
去掉
3. dp数组如何初始化
dp[0] = 0;
背包容量为0所背的物品的最大价值就是0。
4. 确定遍历顺序
一维dp数组遍历顺序:
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量(从大到小)
// ..
}
}
为什么遍历背包容量的时候是从大到小呢?
为了避免前面的值会对后面的更新造成影响;
因为在二维数组写法中,每行值的更新依赖于上一行的值(正上方,左上角);
而一维dp数组中每个值的更新依赖于还没被更新的前面元素的值。
换个说法,一维dp数组中每个值的更新依赖于上次循环更新过且本次循环还未更新过的前面元素的值。
一维数组写法相当于二维数组写法每行数据拷贝给下一行用以下一行的数据更新。
所以在一维dp数组中如果正序遍历,我们如果遍历到dp[j]
,dp[j]
左边或者说二维dp数组左上方的值就已经被修改了。那么算出来的max值
肯定是不对的
5. 举例推导dp数组
一维dp,分别用物品0,物品1,物品2 来遍历背包,最终得到结果如下:
完整代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int bag(vector<int> weight, vector<int> value, int bagweight) {
vector<int> dp[bagweight + 1]; // 一维滚动数组
for (int i = 0; i < weight.size(); i++){ // 遍历物品
for (int j = bagweight; j >= weight[i]; j--){ // 从大到小遍历背包重量
dp[j] = max(dp[j], dp[j - weight[i] + value[i]);
}
}
return dp[bagweight];
}
int main(){
vector<int> weight = { 1, 3, 4 }; // 已经排序好了
vector<int> value = { 15, 20, 30 };
int bagweight = 4;
bag(weight, value, bagweight);
cout << ret << endl;
system("PAUSE");
return 0;
}
参考文章:
01背包理论基础
01背包理论基础(滚动数组)