打算好好學一下算法,先拿揹包問題入手。可是網上許多教程都是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