滚动数组就是把二维dp降为一维dp
例子:背包最大重量为4。
一维dp数组(滚动数组)
对于背包问题其实状态都是可以压缩的。
例如,在使用二维背包问题时,递推公式dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])
;
其实可以把dp[i-1]那一层拷贝到dp[i]中,表达式就变为dp[i][j]=max(dp[i][j],dp[i][j-weight[i]]+value[i])
;
与其把dp[i-1]拷贝到dp[i]上,不如直接使用一个一维数组,只用dp[i](一维数组其实就是一个滚动数组)
这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当期层。
动态规划5部曲:
- 1.确定dp数组与下标含义
在一维dp数组中,dp[j]表示容量为j的背包,所背物品价值最大为dp[j]。 - 2.一维数组的递归公式
如何推导dp[j]呢?
dp[j]
可以通过dp[j-weight[i]]
推导出来,dp[j-weight[i]]表示容量为j-weight[i]的背包所背的最大价值。
dp[j-weight[i]]+value[i]
表示容易为j-weight[i]的背包加上物品i的价值(也就是容量为j的背包,放入了物品i之后的价值,即dp[j])
此时dp[j]有两个选择,放还是不放物品i:
①不放物品i:即取自己dp[j]相当于二维数组的dp[i-1][j];
②放物品i:取dp[j]为dp[j-weight[i]]+value[i];
所以递归公式为:
dp[j]=max(dp[j],dp[j-weight[i]]+value[i])
;
可以看出相对于二维dp数组,就是把dp[i][j]中i的维度去掉了。 - 3.一维dp数组的初始化
dp[j]表示容量为j的背包所背物品的最大价值为dp[j],那么dp[0]就是容量为0的背包所背的物品的最大价值,即dp[0]=0
;
那么除了下标为0外,其他下标应该初始化为多少呢?
dp数组在推导时一定是取价值最大的数,如果题目给的价值都是正整数,那么非0下标都初始化为0就可以了。 - 4.一维数组的遍历顺序
代码如下:
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]);
}
}
发现遍历背包的顺序和二维数组不一样,二维是从j=0开始加,而一维是从j=bagweight开始减。
这是为什么?
倒序遍历是为了保证物品i只被放入一次!,如果一旦正序遍历,那么物品0就会被重复加入多次。
举例:物品0的重量weight[0]=1,价值value[0]=15
如果正序遍历:
dp[1]=dp[1-weight[0]]+value[0]=15;
dp[2]=dp[2-weight[0]]+value[0]=30;
此时dp[2]就是30了,意味着物品0被放入了两次,所以不能正向遍历。
为什么倒序遍历,就可以保证物品i只被放入一次?
倒序先算dp[2]
dp[2]=dp[2-weight[0]]+value[0]=15;(dp数组都初始化为0)
dp[1]=dp[1-weight[0]]+value[0]=15;
所以倒序遍历,每次取得的状态不会与之前取得状态重合,这样每种物品就自取一次了。
那么为什么二维dp数组不用倒序遍历呢?
因为dp[i][j]都是通过dp[i-1][j]计算而来,本层的dp[i][j]并不会被覆盖。
再来看两层for循环嵌套的顺序,代码中是先遍历物品再遍历背包容量,那么是否可以先遍历背包容量再遍历物品呢?
不可以!
因为一维dp的写法,背包容量一定要倒序遍历,如果遍历背包容量放在上一层,那么每个dp[j]中只会放入一个物品,即背包里只放入了一个物品。
倒序遍历的原因是,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。
- 5.举例推导一维dp数组
一维dp数组,分别用物品1、2、3遍历背包,最终结果如下:
一维背包完整代码
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();
}
可以看出,一维背包比二维背包简洁,而且空间复杂度降了一个数量级,倾向于用一维dp数组进行推导。
总结
以上的讲解可以开发一道面试题目(毕竟力扣上没原题)。
就是本文中的题目,要求先实现一个纯二维的01背包,如果写出来了,然后再问为什么两个for循环的嵌套顺序这么写?反过来写行不行?再讲一讲初始化的逻辑。
然后要求实现一个一维数组的01背包,最后再问,一维数组的01背包,两个for循环的顺序反过来写行不行?为什么?
注意以上问题都是在候选人把代码写出来的情况下才问的。
就是纯01背包的题目,都不用考01背包应用类的题目就可以看出候选人对算法的理解程度了。
此时01背包理论基础就讲完了,用了两篇文章把01背包的dp数组定义、递推公式、初始化、遍历顺序从二维数组到一维数组统统深度剖析了一遍,没有放过任何难点。