01背包详解

01背包


【说明】本文主要参考来源为崔添翼大佬著名的《背包九讲》,大佬讲的非常透彻,而我所作的工作就是将这篇讲义入门化、详细化,你也可以理解为《背包九讲》的笔记版本。

你也可以直接浏览大佬的文章:https://github.com/tianyicui/pack。

1.1 问题引入

​有N件物品和一个容量为V的背包。放入第 i 件物品耗费的费用是 Ci,得到的价值是 Wi。求解将哪些物品装入背包可使价值总和最大。

1.2 基本思路

​01背包是属于动态规划下的子类背包问题,01背包也是所有背包问题中较为简单的一类背包问题。’

​这是最基础的背包问题,特点是:每种物品只有一件,可以选择放或者不放。

​我们首先会想到用子问题来定义状态:即dp[i][v]表示前i件物品放入一个容量为v的背包中可以获得的最大价值。其状态转移方程为:

dp[i][v] = max(dp[i - 1][v], dp[i - 1][v - c[i]] + w[i]);
// 前i件物品放在容量为v的背包中可以获得最大价值 = max(前i - 1件物品放在容量为v的背包中可以获得最大价值, 前i - 1件物品放在容量为v - c[i]的背包中可以获得最大价值 + w[i])
// 也就是这一件物品到底要不要放进背包的问题

​这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:“将前 i 件物品放入容量为v的背包中”这个子问题,若只考虑第 i 件物品的策略(放或不放),那么就可以转化为一个只和前 i - 1 件物品相关的问题。如果不放第 i 件物品,那么问题就转化为“前 i - 1 件物品放入容量为 v 的背包中”,价值为 dp[i- 1][v];如果放第 i 件物品,那么问题就转化为“前 i − 1 件物品放入剩下的容量为 v − Ci 的背包中”,此时能获得的最大价值就是 dp[i − 1][v − Ci] 再加上 通过放入第 i件物品获得的价值 Wi

参考代码如下:

for (int i = 1; i <= n; i++) // 有n个物品 
    for (int j = c[i]; c[i] <= V; j++) // 从i这个物品大小开始
        dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - c[i]] + w[i]);//解释看上面的

(PS:如果看不懂可以找一道题目,然后改一下上面的代码,输出一下 dp 数组里面存的是什么东西,好好理解一下。)

1.3 优化空间复杂度

​上面的时间复杂度和空间复杂度均为 O(VN) ,其中时间复杂度已经不能再进行优化了(毕竟一定要递推每一个状态嘛),但是空间复杂度却可以优化到 O(V)

​先考虑上面的基本思路是如何实现的,肯定是有一个主循环i <-- 1...N (for (int i = 1; i <= n; i++)),每次算出来二维数组dp[i][v]的所有值。那么,如果只用一个数组dp[i...n],能不能保证第i次循环结束后dp[i]中所表示的就是我们定义的状态dp[i][v]呢?

​我们先来观察一下基础的状态转移方程:

dp[i][v] = max(dp[i - 1][v], dp[i - 1][v - c[i]] + w[i]);

​可以观察得出dp[i][v]是由dp[i - 1][v]dp[i - 1][v - c[i]] + w[i] 两个子问题递推出来的,而在i - 1之前的任何dp数组其实是再也用不上的,举个例子:我们要求dp[4][6],那么现在dp[4][6]仅和dp[3][6]以及dp[3][1 到 5]有关,和其他数组无关。而且只和这一列之前的数组有关(dp[4][6]只和6列之前的数组有关)。(这其实也是轮廓dp的基本思想)

请添加图片描述

​那么我们可以在第二层循环中倒序计算dp数组的值就好。

    for (int i = 1; i <= n; i++) // 有n个物品 
        for (int j = V; j >= c[i]; j--) // 从最大容量背包开始计算
            dp[j] = max(dp[j], dp[j - c[i]] + w[i]);

​顺序计算dp数组会改变当前dp数组的值,从而使得之后的dp数组不是由上一个状态转移过来的(甚至不知道是什么个状态),倒序计算就不会造成影响。

1.4 初始化的细节问题

​我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。

​一种区别这两种问法的实现方法是在初始化的时候有所不同。 如果是第一种问法,要求恰好装满背包,那么在初始化时除了 dp[0]为 0,其它dp[1..V ]均设为 −∞,这样就可以保证最终得到的 dp[V ] 是一种恰好装满背包的最优解。 如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将 dp[0..V ] 全部设为 0。

​可以这样理解:初始化的 dp数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为 0 的背包可以在什么也不装且价值为 0 的情况下被“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,应该被赋值为 -∞ 了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为 0,所以初始时状态的值也就全部为 0 了。

​ 总结:

  1. 恰好装满背包,需要将dp[0]初始化为 0, 其他初始化为−∞。
  2. 只要求价值最大,只需要将dp数组全部初始化为0。

1.5 小结

​01 背包问题是最基本的背包问题,它包含了背包问题中设计状态、方程的最基本思想。另外,别的类型的背包问题往往也可以转换成 01 背包问题求解。故一定要仔细体会上面基本思路的得出方法,状态转移方程的意义,以及空间复杂度怎样被优化。

参考例题:

序号题目(来源洛谷)难度
1P1048 采药1
2P1049 装箱问题1
3P1164 小A点菜1
4P1060 开心的金明1
5P3985 不开心的金明3

参考资料:

  1. 崔添翼大佬的《背包九讲》,https://github.com/tianyicui/pack。
  • 10
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TUStarry

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值