今天还是来分享回溯算法系列--0-1 背包
0-1背包问题是一个经典的优化问题。它的问题描述如下:给定一组物品,每种物品都有自己的重量和价值,背包的总容量是固定的。我们需要从这些物品中挑选一部分,使得背包内物品的总价值最大,同时不超过背包的总容量。
举个例子:假设这组物品的质量分别是:
javascript
const weight = [4, 3.2, 2, 0.6, 6, 3.1, 0.8];
背包总容量为 9,请问将这些物品装入背包,最多能装多少,分别是哪些?
问题很简单,手动似乎也可以解决这个问题,但是当物品的种类多了起来,就很难凑起来的。
0-1 背包的应用场景
0-1背包问题在生活中的应用场景非常广泛,以下是一些常见的例子:
- 背包旅行 在背包旅行中,我们需要选择一些物品,例如衣物、食品、药品等,使得背包内物品的总价值最大,同时不超过背包的总容量。
- 装箱问题 装箱问题是指在一定数量的货物中,选择一些货物装入箱子,使得箱子中货物的总价值最大,同时不超过箱子的总容量。
- 库存管理 在库存管理中,我们需要选择一些库存商品,使得库存商品的总价值最大,同时不超过库存的限制条件。
- 优化仓库存储 在仓库存储中,我们需要选择一些商品,使得商品的总价值最大,同时不超过仓库的存储空间。
- 资源分配 在资源分配中,我们需要选择一些资源,使得资源的总价值最大,同时不超过资源的限制条件。
应用还是挺广泛的吧😄
问题分析
先从简单的问题入手,只分析最大重量,不管价值多少。
n 个物品,每个物品都判断放与不放,就会有 2^n
种状态。回溯算法正好是用来解决这种问题的,当问题有多个步骤,每一步都面临多种选择,并且每一步都会对后续的步骤产生影响,从而影响问题的解。不断地往前走,直到走不下去了,就会退,选择之前步骤的其他分支。如此遍历完左右的步骤,所有步骤的所有分支,就可以得到问题的解了。
回溯算法会遍历所有的分支,所以复杂度非常高,为
O(2^n)
, 指数级别的复杂度
回到背包问题,遍历物品的时候,将当前物品放进背包,然后判断下一个物品;当前物品也可以不放进背包,然后判断下一个物品。选择哪一个都没有关系,因为当从下一个物品的判断退回来的时候,就可以选择另外一个选择了。不太理解?没关系,看看下面代码就理解了
javascript
const weight = [4, 3.2, 2, 0.6, 6, 3.1, 0.8]; // 物品的重量数组
const packageWeight = 9; // 背包的容量
let maxWeight = 0; // 最大价值
let hasInput = []; // 已放入背包的物品的重量数组
const findWeight = (index, currentInput, currentWeight) => { // 定义递归函数
if (index == weight.length || currentWeight == packageWeight) { // 如果物品索引已经到达了物品数组的末尾或者当前背包的重量已经达到了背包的容量
if (currentWeight > maxWeight) { // 如果当前背包的重量大于之前的最大重量
maxWeight = currentWeight; // 更新最大重量和已放入背包的物品的重量数组
hasInput = currentInput;
}
return; // 返回函数,结束递归
}
findWeight(index + 1, currentInput, currentWeight); // 不将物品放入背包,物品索引加1,当前背包的重量保持不变,已放入背包的物品的重量数组不添加当前物品的重量
if (currentWeight + weight[index] <= packageWeight) { // 如果当前背包的重量加上当前物品的重量小于等于背包的容量
findWeight(index + 1, [...currentInput, weight[index]], currentWeight + weight[index]); // 将物品放入背包,物品索引加1,当前背包的重量加上当前物品的重量,已放入背包的物品的重量数组添加当前物品的重量
}
}; // 递归调用函数,直到找到一种物品组合使得背包内物品的总重量最大,且不超过背包的容量
代码中定义了物品的重量数组weight
, 背包的容量packageWeight
, 最大重量maxWeight
和已放入背包的物品的重量数组hasInput
之后定义了一个递归函数findWeight
,它接受三个参数:
- 物品索引
index
, 表示判断第几个物品 - 当前放入背包的物品重量数组
currentInput
, - 当前背包的重量
currentWeight
我们直接看代码的中间。两个地方分别调用了自身,表示不把物品放进背包,以及把物品放进背包。顺着第一个findWeight
调用,进去后index+1
,进而就面临判断下一个物品是否放入背包中。当第一个findWeight
调用结束之后,就接着调用findWeight
。这也可以看作不放当前物品的分支
上的所有可能性都走完了,再去走放入当前物品的分支
上的所有可能性
调用第二个findWeight
之前,会做一个判断,如果将当前物品放入背包之后,大于背包的容量,就不走了。这是提前阻断不必要的分支,节省性能。
如果物品索引已经到达了物品数组的末尾或者当前背包的重量已经达到了背包的容量, 就返回不继续判断了,并且如果当前背包的重量大于之前的maxWeight
, 则更新maxWeight
和hasInput
这样遍历了所有的分支,所有的可能性,maxWeight
中保存的就是可以放进背包的最大重量,以及hasInput
保存的是最大重量的物品组合
测试代码:
javascript
findWeight(0, [], 0);
console.log(maxWeight);
// 8.9
console.log(hasInput);
// [ 3.2, 2, 0.6, 3.1 ]
可以放入的最大重量就是 8.9 ,大家可以试着找一找还有没有更大的
是不是很简单
加上物品价值这个变量
在不大于背包总容量的最大的情况下,找到价值最大的组合
虽然这里多了一个物品价值这个变量,但代码还是一样:
javascript
const weight = [4, 3.2, 2, 0.6, 6, 3.1, 0.8]; // 物品的重量数组
const packageWeight = 9; // 背包的容量
let maxWeight = 0; // 最大价值
let hasInput = []; // 已放入背包的物品的重量数组
const values = [1.3, 3.2, 2.2, 2, 4, 1.5, 3.3]; // 物品的价值
let maxValue = 0; // 最大价值
const findWeight2 = (index, currentInput, currentWeight, currentValue) => { // 定义递归函数
if (index == values.length || currentWeight == packageWeight) { // 如果物品索引已经到达了物品数组的末尾或者当前背包的重量已经达到了背包的容量
if (currentValue > maxValue) { // 如果当前物品的价值大于之前的最大价值
maxValue = currentValue; // 更新最大价值和已放入背包的物品的重量数组
maxWeight = currentWeight;
hasInput = currentInput;
}
return; // 返回函数,结束递归
}
findWeight2(index + 1, currentInput, currentWeight, currentValue); // 不将物品放入背包,物品索引加1,当前背包的重量保持不变,已放入背包的物品的重量数组不添加当前物品的重量,以及当前物品的价值保持不变
if (currentWeight + weight[index] <= packageWeight) { // 如果当前背包的重量加上当前物品的重量小于等于背包的容量
findWeight2(index + 1, [...currentInput, weight[index]], currentWeight + weight[index], currentValue + values[index]); // 将物品放入背包,物品索引加1,当前背包的重量加上当前物品的重量,已放入背包的物品的重量数组添加当前物品的重量,以及当前物品的价值也加上当前物品的价值
}
}; // 递归调用函数,直到找到一种物品组合使得背包内物品的总价值最大且不超过背包的容量
代码还是一样的,改变的只是存储 maxWeight
,maxValue
的判断标准,这里是currentValue > maxValue
表示,只要你的价值更高,就可以更新 maxValue
,maxWeight
。要知道 maxValue
和maxWeight
不是一回事,所以在得到maxValue
后,maxWeight
并不是最大的。
测试代码
javascript
findWeight2(0, [], 0, 0);
console.log(maxWeight);
console.log(maxValue);
console.log(hasInput);
// 6.6
// 10.7
// [ 3.2, 2, 0.6, 0.8 ]
最大价值是10.7
, 对应的最大重量为6.6
,相较于上面的 maxWeight--8.9
差了不少。
差在哪里呢?小伙伴们可以根据打印出来的
hasInput
自己观察一下
总结
这篇文章分享了第二个回溯算法--0-1 背包。介绍了 0-1 背包的问题,和应用场景,之后再不断逐步分析 0-1 背包的问题,以及解题思路。代码注释也很详细,如果还有不明白的小朋友可以评论区留言
你觉得这篇文章怎么样?我每天都会分享一篇算法小练习,喜欢就点赞+关注吧