今天分享的内容是动态规划的经典问题--0-1 背包问题
0-1背包问题的描述如下:给定一组物品,每种物品都有自己的重量和价值,背包的总容量是固定的。我们需要从这些物品中挑选一部分,使得背包内物品的总价值最大,同时不超过背包的总容量。
举个例子:假设这组物品的质量分别是:
const weight = [2, 2, 6, 4, 3];
背包总容量为 9,请问将这些物品装入背包,最多能装多少,分别是哪些?
思路分析
之前用回溯算法解决过这个问题,回溯算法有其自身的局限性--指数级别的复杂度。那这篇文章就和上篇文章一样,用动态规划的思路把 0-1 背包问题重新解决一遍
回溯算法的思路是,每种可能都去尝试,即遍历所有的可能,找到最优解。而动态规划不会傻傻地遍历所有的可能性,而是会去掉重复的路径,在剩下的路径下继续探索
背包的总容量是 9,那么不看具体的物品重量,放入背包的重量的可能性一共有 9 种,动态规划将会使计算范围框定在这 9 种范围之内,并且相同重量的物品组合,只保留其中一种。这样下来,动态规划的复杂度就是 n*m 了,n 是物品的种类,m 是背包的总容量
看不明白?没关系,看看下面的过程分析就懂啦
过程分析
weight = [2, 2, 6, 4, 3];
代码的实现就是填满上面的表格。行表示,该行的物品放
与不放
的重量种类;列表示背包中物品重量之和
上面第一个行填了两个 true,第一个 true 表示不放 1 号物品时,背包的总重量是 0kg;第二个 true 表示放 1 号物品时,背包的总重量是 2kg
继续填第二行
weight = [2, 2, 6, 4, 3];
共有 3 个 true
, 第一个 true 和 第二个 true
表示不放入 2 号物品时,背包的重量可能性--有 0kg
和 2kg
两种可能性;第三个 true 表示,同时放 1 号物品和 2 号物品的情况。
其实,大家仔细想想,如果只放 2 号物品,不放 1 号物品时,背包的重量是多少?也是 2kg,所以第二个 true 也表示只放 2 号物品,不放 1 号物品的情况。这里动态规划将两个 2kg 的情况合并成一种情况。
以至于 1 号和 2 号物品的放与不放有 2^2 种情况,但是组合成的重量却只有 3 种情况. 这就是动态规划提升性能的关键
继续往下看
、
weight = [2, 2, 6, 4, 3];
第三行共有 5 个 true,前 3 个 true 直接看成是不放 3 号物品时,背包重量的可能性。后面两个 true,一个是 6kg,是只放 3 号物品的情况;另一个是 8kg,可以看成是放 1 号和 3 号物品的情况,也可以看成是 2 号和 3 号物品的情况。
3 种物品共有 2^3 情况,但是动态规划将其合并成 5 种情况。
true 的填写是有规律的,首先填写不放该行物品的情况,即直接照抄上一行的内容,然后从前往后依次对每个 true 的重量加上自身重量,比如第 3 行的第 4 个 true,就是第一个 true 的 0kg 加上 6kg 得到的。第 3 行的第 5 个 true ,是第二个 true 的 2kg 加上 6kg 得到的。再往后相加的话就超过 9kg 了,就停止相加。
大家可以试着推推第 4 行的内容
第 4 行:
weight = [2, 2, 6, 4, 3];
第 5 行:
weight = [2, 2, 6, 4, 3];
可以看到第 5 行的最后一格是 true,也就说明,物品的组合中,是可以将背包填满的。所以背包可以装入的最大重量是 9kg,问题解决!
代码实现
const weight = [1, 2, 2, 6, 5, 8];const findWeight = (weight) => {const store = Array(weight.length).fill(1).map(() => Array(packageWeight + 1).fill(false));store[0][0] = true;store[0][weight[0]] = true;for (let i = 1; i < store.length; i++) {// 不放第i个物品for (let j = 0; j <= packageWeight; j++) {if (store[i - 1][j]) store[i][j] = true;}// 放第i个物品for (let j = 0; j <= packageWeight - weight[i]; j++) {if (store[i - 1][j]) store[i][j + weight[i]] = true;}}for (let i = packageWeight; i >= 0; i--) {if (store[weight.length - 1][i]) {console.log("the max weight is ", i);break;}}
}
- 定义一个数组
weight
,表示物品的重量。变量packageWeight
,表示背包的容量。接着实现了一个名为findWeight
的函数,该函数接受一个参数weight,即物品重量的集合
在findWeight函数中,首先创建一个二维数组store,用来表示上面过程分析的表格,表格的内容初始化为 false。store 的行和列含义也和上面表格一致。在遍历 store 之前,先初始化第一行的内容,即放与不放 0 号物品的两种情况。
遍历 store 数组,从第二行开始,也就是 i==1。每一行先考虑不放第 i 个物品。然后再计算放入第 i 个物品的时候,这一行的内容有什么变化。
遍历完所有的行,store 数组中最后一行的最后一个 true 所表示的重量,就是我们要的答案啦
执行代码
findWeight(weight);
// the max weight is 9
这是 store 的内容:
换个数据试试:
const weight = [2, 2, 6, 4, 4];
findWeight(weight);
// the max weight is 8
这是 store 中的内容情况:
遗留的问题
按照上面的代码只是知道了最多能装多少,但不知道装了哪些东西,你可以根据最后生成的store内容判断放入了哪些东西吗?在之后的文章中,大家可以看到对这个问题的解答
总结
这篇文章讲了如何用动态规划来解决 0-1 背包问题,其中有解题的思路分析,还有具体的过程分析,每个过程都有图片,很清晰了。相信大家对着图片多看几遍,就知道动态规划是怎么解决问题的了。最后还有 JS 代码实现,大家可以 copy 到本地跑一跑,这样会更清楚