0-1 背包问题
0-1背包问题是经典的动态规划问题,它有两种衍生的问题形式,我们称之为完全背包问题和多重背包问题
基本的0-1背包问题:
热动分析.jpg
给一个capacity为n的背包,能装weight总共不超过n的物品,给定m个物品,每一个物品具有价值value和重量weight,我们希望能装的物品value的总和越大越好,那么应该怎么做呢?
#include
以上这个代码基本就是解决基本0-1背包问题的算法思路了,我们可以看到,问题解决的关键在于找到一条合理的状态转移方程,所以我们考虑用一个合适的“容器”,用来记录一些中间状态,比如dp_cache[i][j]用来表示:
考虑前i个物品(1,...,i)中,在容量为j的情况下能够装下的物品的最大价值。为了减少思维负担,我们信任这个最大价值,并且认为他是最优的,那么下一步的问题就是我们要不要在继续装下一个物品
那么问题就转化成dp_cache[i + 1][j]究竟是容量不够没有装下第 i+1 个物品,还是装下了却得不偿失所以不装,还是装下了确实地能够提高整体容纳物品的价值总和,所以我们会有下面的思考
dp_cache[i + 1][j] = k
- 当不能装下这个物品(第i + 1个物品),也就是j比第i + 1个物品的的重量要小,那么自然而然不装下第i + 1个物品,k就是dp_cache[i][j]
- 当能够装下这个物品,那么我们需要考虑要不要装下它,是否值得
- 如果值得,那么k就是dp_cache[i][j - weight[i + 1]] + value[i + 1],也就是考虑前i个物品,在容量为j - weight[i+1]扣除第i+1个物品的剩余容量的最佳值加上装入的新物品的价值之和
- 否则,保持不变,也就是k = dp_cache[i][j]比装入这个物品后的情况(dp_cache[i][j - weight[i + 1]] + value[i + 1])要大,那么我们不装入这个新的物品
为了方便下面我们使用dp数组代指dp_cache
这里注意一下
值得注意的是,我们在迭代的过程中,要求dp[i][j]需要考虑的“原材料”,也就是我们所说的迭代前项,是dp[i-1][0,1,2,...,j]与本行的数据都没有关系(指dp[i][0,1,2,3,...,j - 1]),仅仅只用到了上一行的数据,也就是说,将暂存数组从二维优化为一维是可能的
| xx1 | xx2 | xx3 |
| oo1 | oo2 | tag |
如上表所示,要求tag,那么xx是需要用到的,oo是用不到的,而要求第二行第二列的值,也只需要第一行第一列,第一行第二列的值作为推导的前项。
假如我们只用一个一维数组dp_cache来暂存价值信息,并且我们还只有第一行的数据
他现在长这样
| xx1 | xx2 | xx3 |
现在,我们要求下一行第一个数据oo1,没问题,直接使用这个一维数组的第一个数据递推
他现在长这样
| oo1 | xx2 | xx3 |
接下来,我们要求下一个数据oo2,但是问题出现了,求oo2需要xx1还有xx2,但是xx1已经被覆盖了,这样的话我们这个想法就宣告失败了。
那么,如果我们倒着来求dp[i+1][0...j]呢?
我们先求oo3(也就是之前的tag),oo3需要xx1, xx2, xx3来递推得到,也确实有,所以我们顺利得到oo3
这样,这个表现在长这样
| xx1 | xx2 | oo3 |
我们再求oo2,需要xx2和xx1,可以得到
| xx1 | oo2 | oo3 |
最后我们需要求oo1,可以由xx1推导得到
| oo1 | oo2 | oo3 |
这样,我们前i + 1个物品的dp数组已经求出来了
所以相对应的,我们可以把我们的算法代码优化一下,注意由于dp_cache结构改变init()函数也应当改变
// 新初始化函数
这样,第二个版本的算法执行函数就出来了
// 第二个版本
如果j小于weight[i]那么,显然是没有继续往下遍历的必要了,所以可以继续改为
// 最终版本
以上,就是0-1背包问题的解法了,假设物品有m个,背包容量为n,他的空间复杂度是O(n),时间复杂度是O(n*m);
时间上应该是可以再优化的(比如说最后一次迭代不去求dp[m][0....j-1],直接返回dp[m][n]也就是dp_cache[n]),但是O(n*m)的时间复杂度还是逃不掉的
完全背包问题
魔法分析.jpg
给一个capacity为n的背包,能装weight总共不超过n的物品,给定m种物品,每一种物品具有价值value和重量weight,我们希望能装的物品value的总和越大越好,那么应该怎么做呢?
注意这个问题和基本0-1背包问题的区别!!!
注意这个问题和基本0-1背包问题的区别!!!
注意这个问题和基本0-1背包问题的区别!!!
m种物品的数量是不限的!
m种物品的数量是不限的!
m种物品的数量是不限的!
有的小伙伴这个时候就有点懵逼了,那咋办啊,暴力遍历一下啊?
// 我们假设初始化函数没有问题
这种做法简单粗暴够直接,我喜欢,但是它没有利用到同一行的数据
我们记
k1 = dp[i][j] + value[i]
k2 = dp[i - 1][j + weight[i]]
dp[i][j]代表的是前 i 种物品在 j 这个容量下的最优值,我们足够地信任他,那么当我们考虑dp[i][j + weight[i]]的值的时候,我们要作出一个判断,是继续装第 i 种物品更加合适还是只考虑前 i - 1 种但是容量增大更合适
也就是说
记 x = j + weight[i]
dp[i][x] = max(dp[i][x - weight[j]] + value[i], dp[i - 1][x])
dp[i][x] = max(k1, k2)
这样的话,我们就可以得到相对不那么暴力的算法了
// 第二版
发现了没有,dp[i][j]的递推的“原材料”是dp[i][j-w]还有dp[i-1][j]
我们是否可以效仿之前提到的做法,优化这个算法的空间复杂度,将二维暂存数组转化为一维数组以节省空间呢
当然可以!!
由于之前详细讲过转化成滚动数组的验证方法,这里只是给出较为简略的说明
这是二维数组版本
| xx1 | xx2 | xx3 |
| oo1 | oo2 | oo1 |
好的,现在假设我们已经得到了前 i - 1 种的结果 xx1 xx2 xx3
| xx1 | xx2 | xx3 |
现在我们要求 oo1, oo1需要xx1还有边界的0(希望这个无需说明)作为递推的前项
现在这个表长这样
| oo1 | xx2 | xx3 |
现在我们要求 oo2,oo2要求同一行的前置位已经求出和同一列的前置位已经求出,也就是ooX(X < 2)还有xx2的数据作为递推的前项,这里刚好满足,所以我们填下oo2
现在这个表长这样
| oo1 | oo2 | xx3 |
现在我们要求 oo3,oo3要求同一行的前置位已经求出和同一列的前置位已经求出,也就是ooX(X < 3)还有xx3的数据作为递推的前项,这里刚好满足,所以我们填下oo3
现在这个表长这样
| oo1 | oo2 | oo3 |
大功告成
我们反过来思考一下,为什么基本的0-1背包问题的递推要避开同一行的数据而不得不倒着遍历?
恰恰是因为要避免重复装入第i个物品
而在完全背包问题中,我们要反其道行之,就是要考虑可能的重复装入情况,这样最终的代码反而是非常简洁的
// 最终版,初始化函数什么的自己去改,这里只展示proc函数
代码仅供参考
多重背包问题
不想分析.jpg
你直接把它转换成基本0-1背包问题不就完事了么,这题摸了
结束
最后祝您,身体健康,再见