打算好好学一下算法,先拿背包问题入手。可是网上许多教程都是C++或java或python,大部分做者都是在校生,虽然算法很强,可是彻底没有工程意识,全局变量满天飞,变量名不明因此。我查了许多资料,花了一个星期才搞懂,最开始的01背包耗时最多,之前只会枚举(就是普通的for循环,暴力地一步步遍历下去),递归与二分,而动态规划所讲的状态表与状态迁移方程为我打开一扇大门。javascript
篇幅可能有点长,但请耐心看一下,你会以为物有所值的。本文之后还会扩展,由于我尚未想到彻底背包与多重背包打印物品编号的方法。若是有高人知道,劳烦在评论区指教一下。html
注意,因为社区不支持LaTex数学公式,大家看到${xxxx}$,就本身将它们过滤吧。java
有${n}$件物品和${1}$个容量为W的背包。每种物品均只有一件,第${i}$件物品的重量为${weights[i]}$,价值为${values[i]}$,求解将哪些物品装入背包可以使价值总和最大。python
对于一种物品,要么装入背包,要么不装。因此对于一种物品的装入状态只是1或0, 此问题称为01背包问题。es6
数据:物品个数${n=5}$,物品重量${weights=[2,2,6,5,4]}$,物品价值${values=[6,3,5,4,6]}$,背包总容量${W=10}$。算法
咱们设置一个矩阵${f}$来记录结果,${f(i, j)}$ 表示可选物品为 ${i...n}$ 背包容量为 ${j(0<=j<=W)}$ 时, 背包中所放物品的最大价值。编程
w | v | i\j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2 | 6 | 0 | |||||||||||
2 | 3 | 1 | |||||||||||
6 | 5 | 2 | |||||||||||
5 | 4 | 3 | |||||||||||
4 | 6 | 4 |
咱们先看第一行,物品0的体积为2,价值为6,当容量为0时,什么也放不下,所以第一个格式只能填0,程序表示为${f(0,0) = 0}$或者${f[0][0] = 0}$。 当${j=1}$时,依然放不下${w_0}$,所以依然为0,${f(0, 1) = 0}$。 当${j=2}$时,能放下${w_0}$,因而有 ${f(0, 2)\ = \ v_0=6}$。 当${j=3}$时,也能放下${w_0}$,但咱们只有一个物品0,所以它的值依然是6,因而一直到${j=10}$时,它的值都是${v_0}$。数组
w | v | i\j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2 | 6 | 0 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
2 | 3 | 1 | |||||||||||
6 | 5 | 2 | |||||||||||
5 | 4 | 3 | |||||||||||
4 | 6 | 4 |
根据第一行,咱们获得以下方程post
当背包容量少于物品価积时,总价值为0,不然为物品的价值优化
而后咱们看第二行,肯定肯定${f(1,0...10)}$这11个元素的值。当${j=0}$ 时,依然什么也放不下,值为0,但咱们发觉它是上方格式的值同样的,${f(1,0)=0}$。 当${j=1}$时,依然什么也放不下,值为0,但咱们发觉它是上方格式的值同样的,${f(1,1)=0}$. 当${j=2}$时,它能够选择放入物品1或不放。
若是选择不放物品1,背包里面有物品0,最大价值为6。
若是选择放入物品1,咱们要用算出背包放入物品1后还有多少容量,而后根据容量查出它的价值,再加上物品1的价值,即${f(0,j-w_1)+v_1}$ 。因为咱们的目标是尽量装最值钱的物品, 所以放与不放, 咱们须要经过比较来决定,因而有
显然${v_1=2,v_0=6}$, 所以这里填${v_0}$。 当${j=3}$时, 状况相同。 当${j=4}$,能同时放下物品0与物品1,咱们这个公式的计算结果也合乎咱们的预期, 获得${f(1,4)=9}$。 当${j>4}$时, 因为背包只能放物品0与物品1,那么它的最大价值也一直停留在${v_0+v_1=9}$
w | v | i\j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2 | 6 | 0 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
2 | 3 | 1 | 0 | 0 | 6 | 6 | 9 | 9 | 9 | 9 | 9 | 9 | 9 |
6 | 5 | 2 | |||||||||||
5 | 4 | 3 | |||||||||||
4 | 6 | 4 |
咱们再看第三行,当${j=0}$时,什么都放不下,${f(2,0)=0}$。当${j=1}$时,依然什么也放不下,${f(2,1)=0}$,当${j=2}$时,虽然放不下${w_2}$,但咱们根据上表得知这个容号时,背包能装下的最大价值是6。继续计算下去,其实与上面推导的公式结果是一致的,说明公式是有效的。当${j=8}$时,背包能够是放物品0、1,或者放物品一、2,或者放物品0、2。物品0、1的价值,咱们在表中就能够看到是9,至于其余两种状况咱们姑且不顾,咱们目测就知道是最优值是${6+5=11}$, 偏偏咱们的公式也能正确计算出来。当${j=10}$时,恰好三个物品都能装下,它们的总值为14,即${f(2,10)=14}$
第三行的结果以下:
w | v | i\j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2 | 6 | 0 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
2 | 3 | 1 | 0 | 0 | 6 | 6 | 9 | 9 | 9 | 9 | 9 | 9 | 9 |
6 | 5 | 2 | 0 | 0 | 6 | 6 | 9 | 9 | 9 | 9 | 11 | 11 | 14 |
5 | 4 | 3 | |||||||||||
4 | 6 | 4 |
整理一下第1,2行的适用方程:
咱们根据此方程,继续计算下面各列,因而获得
w | v | i\j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2 | 6 | 0 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
2 | 3 | 1 | 0 | 0 | 6 | 6 | 9 | 9 | 9 | 9 | 9 | 9 | 9 |
6 | 5 | 2 | 0 | 0 | 6 | 6 | 9 | 9 | 9 | 9 | 11 | 11 | 14 |
5 | 4 | 3 | 0 | 0 | 6 | 6 | 9 | 9 | 9 | 10 | 11 | 13 | 14 |
4 | 6 | 4 | 0 | 0 | 6 | 6 | 9 | 9 | 12 | 12 | 15 | 15 | 15 |
至此,咱们就能够获得解为15.
咱们最后根据0-1背包问题的最优子结构性质,创建计算${f(i,j)}$的递归式:
//by 司徒正美 function knapsack(weights, values, W){ var n = weights.length -1 var f = [[]] for(var j = 0; j <= W; j++){ if(j < weights[0]){ //若是容量不能放下物品0的重量,那么价值为0 f[0][j] = 0 }else{ //不然等于物体0的价值 f[0][j] = values[0] } } for(var j = 0; j <= W; j++){ for(var i = 1; i <= n; i++ ){ if(!f[i]){ //建立新一行 f[i] = [] } if(j < weights[i]){ //等于以前的最优值 f[i][j] = f[i-1][j] }else{ f[i][j] = Math.max(f[i-1][j], f[i-1][j-weights[i]] + values[i]) } } } return f[n][W] } var a = knapsack([2,2,6,5,4],[6,3,5,4,6],10) console.log(a)
如今方法里面有两个大循环,它们能够合并成一个。
function knapsack(weights, values, W){ var n = weights.length; var f = new Array(n) for(var i = 0 ; i < n; i++){ f[i] = [] } for(var i = 0; i < n; i++ ){ for(var j = 0; j <= W; j++){ if(i === 0){ //第一行 f[i][j] = j < weights[i] ? 0 : values[i] }else{ if(j < weights[i]){ //等于以前的最优值 f[i][j] = f[i-1][j] }else{ f[i][j] = Math.max(f[i-1][j], f[i-1][j-weights[i]] + values[i]) } } } } return f[n-1][W] }
而后咱们再认真地思考一下,为何要孤零零地专门处理第一行呢?f[i][j] = j < weights[i] ? 0 : values[i]
是否是能适用于下面这一行f[i][j] = Math.max(f[i-1][j], f[i-1][j-weights[i]] + values[i])
。Math.max能够轻松转换为三元表达式,结构极其类似。而看一下i-1的边界问题,有的书与博客为了解决它,会添加第0行,所有都是0,而后i再往下挪。其实咱们也能够添加一个${-1}$行。那么在咱们的方程中就不用区分${i==0}$与${0>0}$的状况,方程与其余教科书的如出一辙了!
function knapsack(weights, values, W){ var n = weights.length; var f = new Array(n) f[-1] = new Array(W+1).fill(0) for(var i = 0 ; i < n ; i++){ //注意边界,没有等号 f[i] = new Array(W).fill(0) for(var j=0; j<=W; j++){//注意边界,有等号 if( j < weights[i] ){ //注意边界, 没有等号 f[i][j] = f[i-1][j] }else{ f[i][j] = Math.max(f[i-1][j], f[i-1][j-weights[i]]+values[i]);//case 3 } } } return f[n-1][W] }
w | v | i\j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
X | X | -1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2 | 6 | 0 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | |
2 | 3 | 1 | 0 | 0 | 6 | 6 | 9 | 9 | 9 | 9 | 9 | 9 | 9 | |
6 | 5 | 2 | 0 | 0 | 6 | 6 | 9 | 9 | 9 | 9 | 11 | 11 | 14 | |
5 | 4 | 3 | 0 | 0 | 6 | 6 | 9 | 9 | 9 | 10 | 11 | 13 | 14 | |
4 | 6 | 4 | 0 | 0 | 6 | 6 | 9 | 9 | 12 | 12 | 15 | 15 | 15 |
负一行的出现能够大大减小了在双层循环的分支断定。是一个很好的技巧。
注意,许多旧的教程与网上文章,经过设置二维数组的第一行为0来解决i-1的边界问题(好比下图)。固然也有一些思惟转不过来的缘故,他们还在坚持数字以1开始,而咱们新世代的IT人已经确立从0开始的编程思想。
上面讲解了如何求得最大价值,如今咱们看到底选择了哪些物品,这个在现实中更有意义。许多书与博客不多提到这一点,就算给出的代码也不对,估计是在设计状态矩阵就出错了。
仔细观察矩阵,从${f(n-1,W)}$逆着走向${f(0,0)}$,设i=n-1,j=W,若是${f(i,j)}$==${f(i-1,j-w_i)+v_i}$说明包里面有第i件物品,所以咱们只要当前行不等于上一行的总价值,就能挑出第i件物品,而后j减去该物品的重量,一直找到j = 0就好了。
//by 司徒正美 function knapsack(weights, values, W){ var n = weights.length; var f = new Array(n) f[-1] = new Array(W+1).fill(0) var selected = []; for(var i = 0 ; i < n ; i++){ //注意边界,没有等号 f[i] = [] //建立当前的二维数组 for(var j=0; j<=W; j++){ //注意边界,有等号 if( j < weights[i] ){ //注意边界, 没有等号 f[i][j] = f[i-1][j]//case 1 }else{ f[i][j] = Math.max(f[i-1][j], f[i-1][j-weights[i]]+values[i]);//case 2 } } } var j = W, w = 0 for(var i=n-1; i>=0; i--){ if(f[i][j] > f[i-1][j]){ selected.push(i) console.log("物品",i,"其重量为", weights[i],"其价格为", values[i]) j = j - weights[i]; w += weights[i] } } console.log("背包最大承重为",W," 如今重量为", w, " 总价值为", f[n-1][W]) return [f[n-1][W], selected.reverse() ] } var a = knapsack([2,3,4,1],[2,5,3, 2],5) console.log(a) var b = knapsack([2,2,6,5,4],[6,3,5,4,6],10) console.log(b)
所谓滚动数组,目的在于优化空间,由于目前咱们是使用一个${i*j}$的二维数组来储存每一步的最优解。在求解的过程当中,咱们能够发现,当前状态只与前一行的状态有关,那么更以前存储的状态信息已经无用了,能够舍弃的,咱们只须要存储当前状态和前一行状态,因此只需使用${2*j}$的空间,循环滚动使用,就能够达到跟${i*j}$同样的效果。这是一个很是大的空间优化。
//by 司徒正美 function knapsack(weights, values, W){ var n = weights.length var lineA = new Array(W+1).fill(0) var lineB = [], lastLine = 0, currLine var f = [lineA, lineB]; //case1 在这里使用es6语法预填第一行 for(var i = 0; i < n; i++){ currLine = lastLine === 0 ? 1 : 0 //决定当前要覆写滚动数组的哪一行 for(var j=0; j<=W; j++){ f[currLine][j] = f[lastLine][j] //case2 等于另外一行的同一列的值 if( j>= weights[i] ){ var a = f[lastLine][j] var b = f[lastLine][j-weights[i]] + values[i] f[currLine][j] = Math.max(a, b);//case3 } } lastLine = currLine//交换行 } return f[currLine][W]; } var a = knapsack([2,3,4,1],[2,5,3, 2],5) console.log(a) var b = knapsack([2,2,6,5,4],[6,3,5,4,6],10) console.log(b)
咱们还能够用更hack的方法代替currLine, lastLine
//by 司徒正美 function knapsack(weights, values, W){ var n = weights.length var f = [new Array(W+1).fill(0),[]], now = 1, last //case1 在这里使用es6语法预填第一行 for(var i = 0; i < n; i++){ for(var j=0; j<=W; j++){ f[now][j] = f[1-now][j] //case2 等于另外一行的同一列的值 if( j>= weights[i] ){ var a = f[1-now][j] var b = f[1-now][j-weights[i]] + values[i] f[now][j] = Math.max(a, b);//case3 } } last = f[now] now = 1-now // 1 - 0 => 1;1 - 1 => 0; 1 - 0 => 1 .... } return last[W]; } var a = knapsack([2,3,4,1],[2,5,3, 2],5) console.log(a) var b = knapsack([2,2,6,5,4],[6,3,5,4,6],10) console.log(b)
注意,这种解法因为丢弃了以前N行的数据,所以很难解出挑选的物品,只能求最大价值。
观察咱们的状态迁移方程:
weights为每一个物品的重量,values为每一个物品的价值,W是背包的容量,i表示要放进第几个物品,j是背包现时的容量(假设咱们的背包是魔术般的可放大,从0变到W)。
咱们假令i = 0
f中的-1就变成没有意义,由于没有第-1行,而weights[0], values[0]继续有效,${f(0,j)}$也有意义,由于咱们所有放到一个一维数组中。因而:
这方程后面多加了一个限制条件,要求是从大到小循环。为何呢?
假设有物体${\cal z}$容量2,价值${v_z}$很大,背包容量为5,若是j的循环顺序不是逆序,那么外层循环跑到物体${\cal z}$时, 内循环在${j=2}$时 ,${\cal z}$被放入背包。当${j=4}$时,寻求最大价值,物体z放入背包,${f(4)=max(f(4),f(2)+v_z) }$, 这里毫无疑问后者最大。 但此时${f(2)+v_z}$中的${f(2)}$ 已经装入了一次${\cal z}$,这样一来${\cal z}$被装入两次不符合要求, 若是逆序循环j, 这一问题便解决了。
javascript实现:
//by 司徒正美 function knapsack(weights, values, W){ var n = weights.length; var f = new Array(W+1).fill(0) for(var i = 0; i < n; i++) { for(var j = W; j >= weights[i]; j--){ f[j] = Math.max(f[j], f[j-weights[i]] +values[i]); } console.log(f.concat()) //调试 } return f[W]; } var b = knapsack([2,2,6,5,4],[6,3,5,4,6],10) console.log(b)
因为这不是动态规则的解法,你们多观察方程就理解了:
//by 司徒正美 function knapsack(n, W, weights, values, selected) { if (n == 0 || W == 0) { //当物品数量为0,或者背包容量为0时,最优解为0 return 0; } else { //从当前所剩物品的最后一个物品开始向前,逐个判断是否要添加到背包中 for (var i = n - 1; i >= 0; i--) { //若是当前要判断的物品重量大于背包当前所剩的容量,那么就不选择这个物品 //在这种状况的最优解为f(n-1,C) if (weights[i] > W) { return knapsack(n - 1, W, weights, values, selected); } else { var a = knapsack(n - 1, W, weights, values, selected); //不选择物品i的状况下的最优解 var b = values[i] + knapsack(n - 1, W - weights[i], weights, values, selected); //选择物品i的状况下的最优解 //返回选择物品i和不选择物品i中最优解大的一个 if (a > b) { selected[i] = 0; //这种状况下表示物品i未被选取 return a; } else { selected[i] = 1; //物品i被选取 return b; } } } } } var selected = [], ws = [2,2,6,5,4], vs = [6,3,5,4,6] var b = knapsack( 5, 10, ws, vs, selected) console.log(b) //15 selected.forEach(function(el,i){ if(el){ console.log("选择了物品"+i+ " 其重量为"+ ws[i]+" 其价值为"+vs[i]) } })
有${n}$件物品和${1}$个容量为W的背包。每种物品没有上限,第${i}$件物品的重量为${weights[i]}$,价值为${values[i]}$,求解将哪些物品装入背包可以使价值总和最大。
最简单思路就是把彻底背包拆分红01背包,就是把01背包中状态转移方程进行扩展,也就是说01背包只考虑放与不放进去两种状况,而彻底背包要考虑 放0、放一、放2...的状况,
这个k固然不是无限的,它受背包的容量与单件物品的重量限制,即${j/weights[i]}$。假设咱们只有1种商品,它的重量为20,背包的容量为60,那么它就应该放3个,在遍历时,就0、一、二、3地依次尝试。
程序须要求解${n*W}$个状态,每个状态须要的时间为${O(W/w_i)}$,总的复杂度为${O(nW*Σ(W/w_i))}$。
咱们再回顾01背包经典解法的核心代码
for(var i = 0 ; i < n ; i++){ for(var j=0; j<=W; j++){ f[i][j] = Math.max(f[i-1][j], f[i-1][j-weights[i]]+values[i])) } } }
如今多了一个k,就意味着多了一重循环
for(var i = 0 ; i < n ; i++){ for(var j=0; j<=W; j++){ for(var k = 0; k < j / weights[i]; k++){ f[i][j] = Math.max(f[i-1][j], f[i-1][j-k*weights[i]]+k*values[i])) } } } }
javascript的完整实现:
function completeKnapsack(weights, values, W){ var f = [], n = weights.length; f[-1] = [] //初始化边界 for(var i = 0; i <= W; i++){ f[-1][i] = 0 } for (var i = 0;i < n;i++){ f[i] = new Array(W+1) for (var j = 0;j <= W;j++) { f[i][j] = 0; var bound = j / weights[i]; for (var k = 0;k <= bound;k++) { f[i][j] = Math.max(f[i][j], f[i - 1][j - k * weights[i]] + k * values[i]); } } } return f[n-1][W]; } //物品个数n = 3,背包容量为W = 5,则背包能够装下的最大价值为40. var a = completeKnapsack([3,2,2],[5,10,20], 5) console.log(a) //40
咱们再进行优化,改变一下f思路,让${f(i,j)}$表示出在前i种物品中选取若干件物品放入容量为j的背包所得的最大价值。
因此说,对于第i件物品有放或不放两种状况,而放的状况里又分为放1件、2件、......${j/w_i}$件
若是不放, 那么${f(i,j)=f(i-1,j)}$;若是放,那么当前背包中应该出现至少一件第i种物品,因此f(i,j)中至少应该出现一件第i种物品,即${f(i,j)=f(i,j-w_i)+v_i}$,为何会是${f(i,j-w_i)+v_i}$?
由于咱们要把当前物品i放入包内,由于物品i能够无限使用,因此要用${f(i,j-w_i)}$;若是咱们用的是${f(i-1,j-w_i)}$,${f(i-1,j-w_i)}$的意思是说,咱们只有一件当前物品i,因此咱们在放入物品i的时候须要考虑到第i-1个物品的价值${f(i-1,j-w_i)}$;可是如今咱们有无限件当前物品i,咱们不用再考虑第i-1个物品了,咱们所要考虑的是在当前容量下是否再装入一个物品i,而${(j-w_i)}$的意思是指要确保${f(i,j)}$至少有一件第i件物品,因此要预留c(i)的空间来存放一件第i种物品。总而言之,若是放当前物品i的话,它的状态就是它本身"i",而不是上一个"i-1"。
因此说状态转移方程为:
与01背包的相比,只是一点点不一样,咱们也不须要三重循环了
javascript的完整实现:
function unboundedKnapsack(weights, values, W) { var f = [], n = weights.length; f[-1] = []; //初始化边界 for (let i = 0; i <= W; i++) { f[-1][i] = 0; } for (let i = 0; i < n; i++) { f[i] = []; for (let j = 0; j <= W; j++) { if (j < weights[i]) { f[i][j] = f[i - 1][j]; } else { f[i][j] = Math.max(f[i - 1][j], f[i][j - weights[i]] + values[i]); } } console.log(f[i].concat());//调试 } return f[n - 1][W]; } var a = unboundedKnapsack([3, 2, 2], [5, 10, 20], 5); //输出40 console.log(a); var b = unboundedKnapsack([2, 3, 4, 7], [1, 3, 5, 9], 10); //输出12 console.log(b);
咱们能够继续优化此算法,能够用一维数组写
咱们用${f(j)}$表示当前可用体积j的价值,咱们能够获得和01背包同样的递推式:
function unboundedKnapsack(weights, values, W) { var n = weights.length, f = new Array(W + 1).fill(0); for(var i=0; i< n; ++i){ for(j = weights[i]; j <= W; ++j) { var tmp = f[j-weights[i]]+values[i]; f[j] = (f[j] > tmp) ? f[j] : tmp; } } console.log(f)//调试 return f[W]; } var a = unboundedKnapsack([3, 2, 2], [5, 10, 20], 5); //输出40 console.log(a); var b = unboundedKnapsack([2, 3, 4, 7], [1, 3, 5, 9], 10); //输出12 console.log(b);
有${n}$件物品和${1}$个容量为W的背包。每种物品最多有numbers[i]件可用,第${i}$件物品的重量为${weights[i]}$,价值为${values[i]}$,求解将哪些物品装入背包可以使价值总和最大。
多重背包就是一个进化版彻底背包。在咱们作彻底背包的第一个版本中,就是将它转换成01背包,而后限制k的循环
直接套用01背包的一维数组解法
function knapsack(weights, values, numbers, W){ var n = weights.length; var f= new Array(W+1).fill(0) for(var i = 0; i < n; i++) { for(var k=0; k<numbers[i]; k++)//其实就是把这类物品展开,调用numbers[i]次01背包代码 for(var j=W; j>=weights[i]; j--)//正常的01背包代码 f[j]=Math.max(f[j],f[j-weights[i]]+values[i]); } return f[W]; } var b = knapsack([2,3,1 ],[2,3,4],[1,4,1],6) console.log(b)
其实说白了咱们最朴素的多重背包作法是将有数量限制的相同物品当作多个不一样的0-1背包。这样的时间复杂度为${O(W*Σn(i))}$, W为空间容量 ,n(i)为每种背包的数量限制。若是这样会超时,咱们就得考虑更优的拆分方法,因为拆成1太多了,咱们考虑拆成二进制数,对于13的数量,咱们拆成1,2,4,6(有个6是为了凑数)。 19 咱们拆成1,2,4,8,4 (最后的4也是为了凑和为19)。通过这个样的拆分咱们能够组合出任意的小于等于n(i)的数目(二进制啊,必然能够)。j极大程度缩减了等效为0-1背包时候的数量。 大概可使时间复杂度缩减为${O(W*log(ΣN(i))}$;
定理:一个正整数n能够被分解成1,2,4,…,2^(k-1),n-2^k+1(k是知足n-2^k+1>0的最大整数)的形式,且1~n以内的全部整数都可以惟一表示成1,2,4,…,2^(k-1),n-2^k+1中某几个数的和的形式。 证实以下: (1) 数列1,2,4,…,2^(k-1),n-2^k+1中全部元素的和为n,因此若干元素的和的范围为:[1, n]; (2)若是正整数t<= 2^k – 1,则t必定能用1,2,4,…,2^(k-1)中某几个数的和表示,这个很容易证实:咱们把t的二进制表示写出来,很明显,t能够表示成n=a0*2^0+a1*2^1+…+ak*2^(k-1),其中ak=0或者1,表示t的第ak位二进制数为0或者1. (3)若是t>=2^k,设s=n-2^k+1,则t-s<=2^k-1,于是t-s能够表示成1,2,4,…,2^(k-1)中某几个数的和的形式,进而t能够表示成1,2,4,…,2^(k-1),s中某几个数的和(加数中必定含有s)的形式。 (证毕!)
function mKnapsack(weights, values, numbers, W) { var kind = 0; //新的物品种类 var ws = []; //新的物品重量 var vs = []; //新的物品价值 var n = weights.length; /** * 二进制分解 * 100=1+2+4+8+16+32+37,观察能够得出100之内任何一个数均可以由以上7个数选择组合获得, * 因此对物品数目就不是从0都100遍历,而是0,1,2,4,8,16,32,37遍历,时间大大优化。 */ for (let i = 0; i < n; i++) { var w = weights[i]; var v = values[i]; var num = numbers[i]; for (let j = 1; ; j *= 2) { if (num >= j) { ws[kind] = j * w; vs[kind] = j * v; num -= j; kind++; } else { ws[kind] = num * w; vs[kind] = num * v; kind++; break; } } } //01背包解法 var f = new Array(W + 1).fill(0); for (let i = 0; i < kind; i++) { for (let j = W; j >= ws[i]; j--) { f[j] = Math.max(f[j], f[j - ws[i]] + vs[i]); } } return f[W]; } var b = mKnapsack([2,3,1 ],[2,3,4],[1,4,1],6) console.log(b) //9