【算法】动态规划:01背包问题(详解)

题目

一共有 N 件物品,第 i 件物品的体积是 v[i],价值是 c[i] 。
有一个容量为 M 的背包。
在每件物品只能拿一次的前提下,求背包能装入物品的总价值最大为多少(并不要求背包装满,只要价值最大就行)?

基本思路

  1. 01背包是动态规划类问题。一般需要将主问题分解为多个子问题,然后记录下不同子问题的解,最终才能推得最终问题的答案。
  2. 定义子问题:前i件物品恰放入一个容量为v的背包可以获得的最大价值是多少?
  3. 我们用 f[i][v] 表示 前i件物 恰放入一个 容量为v 的背包可以获得的 最大价值
  4. 要求主问题f[i][v] 就得先知道子问题F[i-1][v]f[i-1][ v-v[i] ]是多少 。
    然后通过状态转移方程:f[i][v]=max{ f[i-1][v] , f[i-1][ v-v[i] ]+c[i]] }
  5. 状态转移方程是整个背包问题的核心。解释:“前i个物品放入容量为v的背包中”可以转化为“前i-1个物品已经放入了容量为v的背包中(即为f[i-1][v])” 和 “第i个物品放还是不放入背包中?”
    如果第i个物品不放入背包,那么f[i][v]=f[i-1][v]
    如果第i个物品放入背包,那么f[i][v]=f[i-1][ v-v[i] ] + c[i],此时这个式子里面只有f[i-1][ v-v[i] ]未知,问题转化为“前i-1个物品放入了剩余容量为v-v[i]的背包中能获得最大价值是多少?”

二维数组表示代码

思路用二位数组储存子问题的答案,我们要求f[N][M],就得先求出所有的f[i][v].
这里的i是由0到N,v是由0到M。所以需要用双重for循环来求,i的变化为主循环,v的变化为次循环。这样就会一行一行地得到一个矩阵。

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxN=1e3+10;

int f[maxN][maxN];///f[i][j]体积为j的背包在前i件物品中能装的最大价值
int v[maxN];///物品体积
int c[maxN];///物品价值

int main()
{
    int t;
    scanf("%d",&t);
    while(t--){
        int n,m;
        memset(f,0,sizeof(f));
        scanf("%d%d",&n,&m);

        for(int i=1;i<=n;i++){
            scanf("%d",&c[i]);///格物品价值
        }

        for(int i=1;i<=n;i++){
            scanf("%d",&v[i]);///各物品体积
        }

        ///计算所有子问题的答案。
        for(int i = 1;i <= n; i++){
            for(int j = 0;j <= m; j++){
                f[i][j]=f[i-1][j];///体积为 i-1 可以拿f[i-1][j], 那体积比它大的 i 的至少也可以拿它那么多吧
                if(j >= v[i])///防止f[j-v[i]]越界
                    f[i][j] = max(f[i-1][j] , f[i-1][j - v[i]] + c[i]);
            }
        }
        printf("%d\n",f[n][m]);
    }
   return 0;
}

二维数组的缺点

时间复杂度已经不能优化了,为O(N*M)。但是二维数组的空间复杂度太高,我们可以将其改成 一维数组进行储存

优化空间复杂度

  • 二维数组方法中,由代码可知道,矩阵的每一行的其中每个元素f[i][j],都是由它的上一行的元素f[i-1][j]或者f[i][j-v[i]]推得。我们用f[v]表示上面定义的f[i][v],相应的f[ v-v[i] ]表示上面提到的f[i][ v-v[i] ]
  • 状态转移方程就变成了:
    f[v]=max{ f[v] , f[ v-v[i] ] + c[i] }特别注意: 等号后面的 f[v] 和前面那个f[v]不一样。 后面的f[v]是上一次循环得到的结果(相当于二维数组的前一行中的元素),而这一次循环所求的f[v]需要由上一次循环得到的f[v]推得(类比二维数组方法: 由上一行推出下一行 )。这里的f[ v-v[i] ]也是上一次循环得到的结果(相当于二维数组方法里面的前一行中的元素)。

!!!!视频图片演示!!!!!!:

戳我看b站大佬演示视频
注意看,计算每一个新的子问题时,都用到了哪些位置上的已经推得的数据。
这样你就会明白:

为什么可以用一维数组替代二维数组保存?(因为每一行更新完,上一行的数据不再需要了,所以根本不需要将它一直储存起来。)

为什么一维数组的第二次循环到从大到小?!(接着往下看*_*

一维数组表示代码

#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxN=1e3+10;

int c[maxN],v[maxN];
int f[maxN];///前i个物品中体积为v所能获取的最大价值。

int main()
{
    int t;
    scanf("%d",&t);
    while(t--){
        int n,m;
        scanf("%d%d",&n,&m);
        ///一定要初始化f数组,并且是全部初始化,不能因为想节约时间而只初始化一部分。
        memset(f,0,sizeof(f));
        ///输入已知数据
        for(int i=0;i<n;i++)
            scanf("%d",&v[i]);///物品体积
        for(int i=0;i<n;i++)
            scanf("%d",&c[i]);///物品价值
		///计算子问题答案。
        for(int i=0;i<n;i++){///前i个物品
            for(int j = m; j >= v[i]; j--){ ///体积从m->0(实际上要从m->V[i],防止f[ j-v[i] ]越界)
                f[j]=max(f[j] , f[ j-v[i] ] + c[i] );
            }
        }
        printf("%d\n",f[m]);///体积为m的背包能装下的最大价值
    }
    return 0;
}

代码中关键问题

上面代码的第二重循环条件为什么是从大的 M,到小的 v[i]? 为何不是由小到大呢?还是因为状态转移方程右边的f[ v-v[i] ]表示 上一次主循环的结果只有当它未被这一次主循环更新的时候,才能拿来用。如果你把逆序改成顺序的话,那么 f[ v-v[i] ]这个表达式代表的上一次循环得到的值就 会被当前主循环所覆盖,变成了二维数组方法中的f[i][ v-v[i] ],而不是f[i-1][ v-v[i] ].

其他背包问题

其他背包问题就很简单啦,基本上都和01背包分不开,理解了01背包,学其他背包势如破竹。
上CSDN搜索"背包九讲完整版",下载个文档自己看看,哈哈哈。
没有积分下载的话找我,我发给你。菜鸟扣扣 : 2215782031

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值