背包问题-01背包扫盲【动规五步法】
0 - 前言
本文针对01背包问题,主要参考【代码随想录】大佬的背包九讲,上一个链接。本文中示范用例与代码取自大佬链接中。
1 - 什么是01背包
物品数量为N
,每个物品的重量为weight[i],i∈[1, N]
,对应的物品价值为value[i], i∈[1, N]
,每件物品只有一件,问题是:将哪些物品放进背包后,背包中的价值总和最大?
2 - 动规五步法解决背包
1、确定dp数组与下标含义:
dp[i] [j]
表示从下标[0-i]
内的物品任意取,存放进容量为j
的背包后,最大价值总和是多少。也就是,行索引i
用户来遍历物品,列索引j
用来遍历背包容量。遍历物品能理解,那么为什么要遍历背包容量呢?这是因为需要一点点增加背包容量,直到容量为j
,表明是有状态转移存在的,因此才可以用动态规划来解决。
dp[i] [j]
数组形式如下:
dp[i][j]
0 1 2 3 4 5
物品0:
物品1:
物品2:
2、确定递推公式:
一般二维dp数组会从行索引与列索引两个方向上求出状态递推。
- 先来看行索引:
i
的上一状态应该是i-1
。在不放物品i
的时候的dp[i][j]
=dp[i-1][j]
,表示j
背包直接从[0~ i-1]
内选取物品后的最大价值。dp[i][j]
=dp[i-1][j]
的真正含义应该是:在j
背包中,不管有没有空间放weight[i]
,都不放i
。 - 再来看列索引:这里
j
的上一状态并不是j-1
,这是由背包问题的特殊场景造成的,因为行索引的情况已经将**不放i
的情况解决完了,那么列索引只能解决放i
**的情况,换言之,j
的上一状态一定要预留出weight[i]
的空间保证j
背包一定能放下i
。那么j
的上一状态应该是什么呢?应该是j-weight[i]
。所以从列索引角度,dp[i-1][j-weight[i]] + value[i]
就是背包中放物品i
的最大价值。其中,行索引之所以不是i
而是i-1
,是要保证j
的上一状态没有放过i
。
那么递推公式显而易见了,dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]])
其实不推导,直接解读递推公式的话,也是比较容易理解的。dp[i-1][j]
代表:不将物品i
放入背包的最大价值。dp[i-1][j-weight[i]] + value[i]
表示:将物品i
放进背包之后的最大价值。两者之中的最大值就是dp[i][j]
。
3、dp数组的初始化
二维dp数组初始化就是看两条边。
dp[i][0]
:即背包容量为0的时候,此时放不进去物品,dp[i][0] = 0
dp[0][j]
:遍历背包容量来存放物品0,只有两种情况,放得下物品0(dp[0][j]=value[0]
)和放不下(dp[0][j]=0
)。
4 、确定遍历顺序
可以先遍历物品i
(dp数组行索引),再遍历背包容量j
(dp数组列索引)
5、举例推导dp数组
假设物品信息如下
重量 价值
物品0 1 15
物品1 3 20
物品2 4 30
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
完整代码如下:
void test_2_wei_bag_problem1() {
vector<int> weight = {
1, 3, 4};
vector<int> value = {
15, 20, 30};
int bagWeight = 4;
// 二维数组
vector<vector<int>> dp(weight.size() + 1, vector<int>(bagWeight + 1, 0));
// 初始化
for (int j = 0; j <= bagWeight; ++j) {
if (weight[0] <= j)
dp[0][j] = value[0];
}
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) {
// 遍历物品
for(int j = 0; 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]);
}
}
cout << dp[weight.size() - 1][bagWeight] << endl;
}
int main() {
test_2_wei_bag_problem1();
}