动态规划:01背包(滚动数组)

滚动数组就是把二维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数组定义、递推公式、初始化、遍历顺序从二维数组到一维数组统统深度剖析了一遍,没有放过任何难点。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值