申明:仅个人小记
前言
只讨论0-1背包和完全背包问题,以游戏闯关为背景辅助理解背包问题。
一、基本交代
0-1背包问题
定义为体积为j的背包,从编号为 1,2,…i 的i个物品中挑选并放入背包,背包能盛放的最大价值。
状态转移方程为
其中,
分别是编号为i的物品的重量(weight)和价值(value)。
完全背包问题
定义为体积为j的背包,从编号为 1,2,…,i 的i种物品中挑选并放入背包,背包能盛放的最大价值。
状态转移方程为
其中,
分别是编号为i的物品的重量和价值。
注意到: 0-1背包中每种物品只有一个,而完全背包每种物品的数量为无限个。
二、逻辑部分
0-1背包问题
游戏闯关,每一关的编号为(i,j)。第(i,j)关中有编号为 1,2,…,i 的物品各一个,背包的体积为 j 。找出一个物品组合,使得物品能够放入背包并且背包中物品总价值达到最大,这个价值记为
, 找到这个最大价值即可通关。
看的出来,这是一个组合问题,而且显然组合的情况很多(属于O(n!)级别)即计算复杂度很高。可以借助动态规划的思想,每次只考虑第(i, j)关中的第i个物品,而不关心其他的,进而大大降低复杂度。
开始分析
在第 (i, j) 关中,我们只考虑第 i 个物品,对于第 i 个物品在最优解中只有会被放入背包和不被放入背包两种情况。
(1) 若第 i 个物品在最优解中
从空背包开始,第 i 个物品最终必然会在背包里面,我们可以直接最先将其放入空背包中。此时背包剩余体积为 j-w,物品还有1,2,…,i-1,我们需要从其中继续找出最好的组合使得在背包体积为j-w且物品为1,2,…,i-1的情况下达到价值最高(继续找的一定得是最好的组合吗?此处可以反证,即如果继续找的组合不是价值最高,那么加上已经放入背包的编号为 i 的物品,总体的价值并不是最高的,与最初假设最优矛盾)。
可以发现,这种局面等价于游戏的第 (i-1, j-w) 关,即体积为 j-w 的背包和1,2,…,i-1这些物品,求
。
故而,此时可以确定出一个状态关系
(2) 若第i个物品不在最优解中
显然,第 i 个物品最终不会出现在背包里面,故而,在第 (i,j )关游戏中,第 i 个物品存在或者不存都是一样的,并不会在最终结果中发挥作用,故而我们直接扔掉第 i 个物品。此时局面为背包体积为 j ,物品分别是1,2,…i-1这 i-1 个物品,我们要做的就是在这种局面下寻找最优解。
可以发现 ,这种局面等价于 游戏的第(i-1,j)关,即体积为 j 的背包和1,2,…,i-1 这些物品,求
。
故而,此时确定出一个状态关系为
得出结论
到底第 i 个物品在不在最优解中,这得由我们的目标——最大价值化,来决定,所以,哪种情况下它的价值会更大,就选哪种。即
完全背包问题
第 (i,j) 关中,背包体积为 j, 物品种类编号为1,2,…,i,每种物品数量没有上限。此时,分使用 i 类物品和不使用 i 类物品两种情况。
开始分析
(1) 使用第 i 类物品
此时,因为第 i 类物品数量没有上限,而你只会使用有限量的第 i 类物品,所以,不管你用几个 i 类物品,当使用完后,物品的情况没有发生变化,即编号仍为1,2,…,i ,每种物品数量为无限。
此时至少使用1个第 i 类物品,这个数量是确定的,故而我们分析先用掉 1个第 i 类物品的情况: 此时背包剩余体积为 j-w, 物品情况没变,则此时的局面等价于游戏的第 (i, j-w) 关。
故而,确定出一个状态关系式为
注意:前后都是 i,而不是出现 i-1。
(2) 不使用第 i 类物品
显然,此时对于游戏的第(i,j)关来讲,第 i 类物品的存在是没有意义的,进而我们可以直接扔掉第 i 类物品,此时局面为: 背包体积为 j ,物品为编号1,2,…,i-1都是无限量,这个局面等价于游戏的第(i-1,j)关。此时,确定出一个状态关系为
得出结论
根据目标——背包盛放最大价值,我们来确定到底是否使用第 i 类物品,即
三、代码优化
代码优化主要就是在空间复杂度上的优化,初步看上述状态关系式,dp 应该是用二维数组来实现,二维数组的规模为背包的体积值X物品的种类数目。仔细分析,发现
这两个中,每次计算一行时只会涉及对上一行或者本行前面部分的读取,基于这个现象,我们可以把二维数组优化为一维数组。优化后结果都是
但要注意计算顺序是存在区别的。对于0-1背包问题,在计算
的时候,应保证
没有被更新(没有更新意味着是上一行的数据),故而计算顺序应该 j 从大到小;对于完全背包问题,在计算
时候,dp[j]要么直接来自上一行对应的一格的值
,要么是来自本行的前面的值
// list[i]为结构体对象,其中存放着第i类物品的单个物品的体积和价值
// 0-1背包计算顺序
for (int i = 1; i <= n; i++) // n种物品
for (int j = s; j >= list[i].w; j --) // s 为指定的背包体积
dp[j] = max(dp[j],dp[j-list[i].w]+list[i].v)
// 完全背包计算顺序
for (int i = 0; i <= n; i ++)
for (int j = list[i].w; j <= s; j ++)
dp[j] = max(dp[j],dp[j-list[i].w]+list[i].v)