零基础学01背包 口诀是物品一件一件增加,背包一点一点变大

01背包问题的资料看下来,我总结了一句话,物品一件一件增加,背包一点一点变大

01背包问题描述

  • 最基本的01背包问题描述是,有一个限重W的背包,有好几件重量为weight,价值为value的物品供你挑选,要在不超过背包限重的前提下,巧妙地选择物品,使得背包里面的物品价值最大。
  • 面试笔试遇到的是01背包问题的应用题,重点在于把原始问题转化为背包问题,就跟做数学题套公式有点像,把题目中的已知条件跟背包限重W,以及物品的重量weight和价值value对应起来,问题就会迎刃而解。

动归三部曲

由于01背包问题过于常见,递推公式很容易就能背下来,有人写dp时对初始化也比较佛系,思考深度不够。然而深刻理解01背包问题的推导和初始化,对理解动态规划的本质意义重大。

题目举例:W = 4 weight=[1,3,4] value=[15,20,30]

1.定义dp数组

这时需要把物品一件一件增加,背包一点一点变大这句话放在心里。

  • 一共有三件物品可以选,所谓物品一件一件增加就是这样,第一次没物品可以挑,第二次1件物品可以挑,第三次2件物品可以挑… 每步都多出了一件物品供自己挑选,那自己就可以决定,放进去还是不放了。
  • 限重为4,所谓背包一点一点变大就是这样,4的限重太大,先从限重为0开始算起,直到达到4为止。

最终dp是一个3*5的数组,dp[i][j]的含义就是有0-i件物品供你挑选,放入容量为j的背包能获得的最大价值

(对于动态规划问题的分析,要始终牢记dp的含义)

那最后的结果就是有0-2件物品供你挑选,放入容量为4的背包能获得的最大价值,自然是数组右下角那个格子dp[2][4]了。

img

2.递推公式

递推公式怎么去想呢?核心就是每次多了件物品可以挑,要区分这个物品到底要不要放进背包。

  • 这个物品要

dp[i][j] = dp[i-1][j] 比如说0-2件物品供你挑,限重j,那你决定不要第2件物品,那好,背包限重没变,这个时候背包的价值就是0-1件物品供你挑,限重j时计算的最大价值dp[i-1][j](也可以理解成,这个物品要是不拿,就跟没他没什么区别,所以还是之前没他时候,即i-1时的最大价值)。

  • 这个物品不要

dp[i][j] = dp[i-1][j-weight[i]]+value[i] 比如说0-2件物品供你挑,限重j,那你决定要第2件物品,那好,这个时候背包装的价值肯定先把这件物品的价值给算上,那剩下的价值呢,就给0-1件物品去完成吧,只不过因为我放了第2件物品,背包的限重变少了weight[i]。

  • 取最大价值 可挑可不挑,那就看看哪个价值大就选哪个作为最大价值。

递推的顺序就是下图了

img

3. 初始化

要初始化的地方就是递推不到的位置,然后算别的dp[i][j]的时候需要用到的值。图中就是第0列和第0行。

  • 第0列 限重0,无论多少物品那都没的放,价值肯定为0,所以第0列全0。
  • 第0行 有第0件物品可以挑,那如果限重>=这个物品,就代表物品可以放进去,价值就是这个物品的值。

img

C++代码实现 含详细注释

#include <iostream>
#include <vector>
using namespace std;
int main(){
    int W = 4;//限重
    int weight[3] = {1,3,4};//物品重量
    int value[3] = {15,20,30};//物品价值
    vector<vector<int>> dp(3,vector<int>(W+1,0)); //3*5的dp
    //初始化
    for(int i = 0;i < 3;i++){
        dp[i][0] = 0;
    }
    for(int j = 1;j <= W;j++){
        if(j >= weight[0]) dp[0][j] = value[0];
    }
    //递推
    for(int i = 1;i < 3;i++){
        for(int j = 1;j <= 4;j++){
            dp[i][j] = dp[i-1][j];//不拿这件物品
            if(j >= weight[i])//下标不越界 或者说之后限重>=物品才能拿物品
                //拿这件物品和不拿这件物品 找最大
                dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
        }
    }
    //打印下dp
    for(int i = 0; i < 3;i++){
        for(int j = 0;j <=4;j++){
            cout << dp[i][j] << " ";
        }
        cout << endl;
    }
    cout << dp[2][4];//最终结果
    return 0;
}

手推一次dp数组加深理解 为优化打基础

手推时,发现可以先把上一行的数据整体移动下来,当限重>=该物品重量时再思考如果挑选这个物品价值是多少,选更大价值即可(要是限重小了那这个物品肯定不能放,只能从上一行移下来了)。

手推二维dp数组可以为滚动数组优化做铺垫。

img

滚动数组优化

滚动数组要着重理解的是初始化和遍历顺序的问题。

动归三部曲

1. 定义dp数组

根据手推dp数组中的经验我们可以知道,算下一行是先把上一行的数据给搬下来了。所以优化时可以只用一行格子,而不用全部的格子。

==dp[j] 含义就是限重j时的最大价值。==一件物品一件物品去迭代,返回的是最后一个格子的值。

2. 递推

!用一排格子就得思考一个问题,值覆盖了怎么办

在二维dp中,显然我们用到了上方和左上方的值,那一维dp就会用到原地和左方的值。那要是从左往右算,值变了咋整,不就不是上一轮结束的dp了吗。所以只能倒着往前遍历,就不会有这种事了,反正dp[i]算的时候又跟后面的值没关系。

下图所示的是迭代到第1件物品时要用到的其他值。(weight[2] = 3,所以只需要算限重3和4。绿色是橙色格子在递推时需要用到的值)

img

3. 初始化

当然可以像为二维dp初始化第一行一样初始化一维dp,然后从第1件物品开始迭代。但其实可以初始化为全0,从第0件物品开始迭代,效果一样。

检验全0的初始化对不对,主要看第0件物品的迭代是否正确。此时发现,仍然是当限重>=weight[0]时,dp[i]=max(dp[i],dp[i-weight[i]]+value[i])=max(0,value[i])=value[i]

C++代码加注释

#include <iostream>
#include <vector>
using namespace std;
int main(){
    int W = 4;//限重
    int weight[3] = {1,3,4};//物品重量
    int value[3] = {15,20,30};//物品价值
    vector<int> dp(W+1,0); //5的dp
    //初始化 全0
    //递推
    for(int i = 0;i < 3;i++){
        for(int j = 4;j >= 0;j--){
        	if(j >= weight[i]) 
        	    dp[j] = max(dp[j],dp[j-weight[i]]+value[i]); 
        }
    }
    cout << dp[4];//最终结果
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值