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 了。
总结:
- 恰好装满背包,需要将
dp[0]
初始化为 0, 其他初始化为−∞。 - 只要求价值最大,只需要将
dp
数组全部初始化为0。
1.5 小结
01 背包问题是最基本的背包问题,它包含了背包问题中设计状态、方程的最基本思想。另外,别的类型的背包问题往往也可以转换成 01 背包问题求解。故一定要仔细体会上面基本思路的得出方法,状态转移方程的意义,以及空间复杂度怎样被优化。
参考例题:
序号 | 题目(来源洛谷) | 难度 |
---|---|---|
1 | P1048 采药 | 1 |
2 | P1049 装箱问题 | 1 |
3 | P1164 小A点菜 | 1 |
4 | P1060 开心的金明 | 1 |
5 | P3985 不开心的金明 | 3 |
参考资料:
- 崔添翼大佬的《背包九讲》,https://github.com/tianyicui/pack。