背包问题-01背包之滚动数组【动规五步法】
0 - 前言
本文是参考【代码随想录】大佬的【背包专题】,受益匪浅,上一个链接
1 - 滚动数组的由来
滚动数组的提出,简化了背包问题中dp数组的维度(由2维变为1维)
在 背包问题【01背包扫盲】【动规五步法】 中说到解决01背包问题的递推公式为:dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]])
。
仔细观察,如果将dp数组的第i-1
行复制到第i
行,那么递推公式就可以写成dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i])
。因为将dp数组i-1
行复制到i
行的操作并不影响weight数组和value数组,所以改写后的递推公式只影响了dp数组的行索引。
再进一步,既然递推式中的dp数组行索引i
此时都完全相同,完全可以用一维dp数组表示,递推式变为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
将二维dp数组上一行直接复制到下一行(即用i-1
状态的一维dp数组作为i
状态的一维dp数组的初值),以这种逻辑更新一维dp数组,就像拉杆的老虎机,每次都会滚动更新,这就是滚动数组字面意思的由来。
2 - 动规五步法
使用动规五步法来分析滚动数组
1、确定dp数组以及下标含义:
dp[j]
表示容量为j
的背包能装的最大价值
2、dp数组递推公式:
首先一维数组dp[j]
只有一个更新方向,那就是从j
的上一状态j-weight[i]
中更新出来,更新值为dp[j-weight[i]] + value[i]
。需要注意的是,dp[j]虽然只有一个更新方向,但是他是有初值的(dp[i-1][j]
复制过来的初值),因此需要在初值与更新值二者之中取最大。递推公式为:dp[j] = max(dp[j], dp[j-weight[i]] + value[i])
3、dp数组初始化:
不同于一般的动态规划问题,滚动数组的一维dp数组不仅需要初始化刚开始的几个值,需要对所有位置都进行初始化。这是由滚动的原因造成的。因为题目中value都是正数,而递推公式每次要取最大值,所以dp[j] = 0
。【代码随想录】大佬还提到,如果value存在负数,那么dp[j]
就要全部初始化为负无穷。
4、dp数组遍历顺序:
因为滚动的特性,我们必须在i-1
状态将所有j
全部遍历完,计算完所有的dp[j]
才能进入到i
状态进行复制,所以我们必须先遍历背包容量j
,再来遍历物品i
。【代码随想录】大佬友情提醒,这里在遍历背包容量时,与二维dp数组不同,一定要进行倒序遍历,即背包容量从大到小,这样是为了保证物品i
只被放入一次。
5、举例演示dp数组:
假设物品信息如下,背包容量为4
重量 价值
物品0 1 15
物品1 3 20
物品2 4 30
dp[j]
j 0 1 2 3 4
i
物品0: 0 15 15 15 15
物品1: 0 15 15 20 35
物品2: 0 15 15 20 35
大佬代码如下:
void test_1_wei_bag_problem() {
vector<int> weight = {
1, 3, 4};
vector<int> value = {
15, 20, 30};
int bagWeight = 4;
// 初始化
vector<int> dp(bagWeight + 1, 0);
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]);
}
}
cout << dp[bagWeight] << endl;
}
int main() {
test_1_wei_bag_problem();
}