01揹包問題
篇幅可能有點長,但請耐心看一下,你會覺得物有所值的。本文以後還會擴展,因爲我還沒有想到完全揹包與多重揹包打印物品編號的方法。如果有高人知道,勞煩在評論區指教一下。
注意,由於社區不支持LaTex數學公式,你們看到${xxxx}$,就自己將它們過濾吧。
1.1 問題描述:
有${n}$件物品和${1}$個容量爲W的揹包。每種物品均只有一件,第${i}$件物品的重量爲${weights[i]}$,價值爲${values[i]}$,求解將哪些物品裝入揹包可使價值總和最大。
對於一種物品,要麼裝入揹包,要麼不裝。所以對於一種物品的裝入狀態只是1或0, 此問題稱爲01揹包問題。
1.2 問題分析:
數據:物品個數${n=5}$,物品重量${weights=[2,2,6,5,4]}$,物品價值${values=[6,3,5,4,6]}$,揹包總容量${W=10}$。
我們設置一個矩陣${f}$來記錄結果,${f(i, j)}$ 表示可選物品爲 ${i…n}$ 揹包容量爲 ${j(0
W |
V |
I |
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 |
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 |
根據第一行,我們得到如下方程

然後我們看第二行,確定確定${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 |
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,或者放物品1、2,或者放物品0、2。物品0、1的價值,我們在表中就可以看到是9,至於其他兩種情況我們姑且不顧,我們目測就知道是最優值是${6+5=11}$, 恰恰我們的公式也能正確計算出來。當${j=10}$時,剛好三個物品都能裝下,它們的總值爲14,即${f(2,10)=14}$
第三行的結果如下:
W |
V |
I |
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 |
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)}$的遞歸式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
//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
)
|
1.3 各種優化:
合併循環
現在方法裏面有兩個大循環,它們可以合併成一個。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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 是不是能適用於下面這一行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}$的情況,方程與其他教科書的一模一樣了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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
]
}
|
W |
V |
I |
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就行了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
//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
=
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}$一樣的效果。這是一個非常大的空間優化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
//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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
//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
)
|
注意,這種解法由於丟棄了之前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實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
//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
)
|

1.4 遞歸法解01揹包
由於這不是動態規則的解法,大家多觀察方程就理解了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
//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
]
)
}
}
)
|

完全揹包問題
2.1 問題描述:
有${n}$件物品和${1}$個容量爲W的揹包。每種物品沒有上限,第${i}$件物品的重量爲${weights[i]}$,價值爲${values[i]}$,求解將哪些物品裝入揹包可使價值總和最大。
2.2 問題分析:
最簡單思路就是把完全揹包拆分成01揹包,就是把01揹包中狀態轉移方程進行擴展,也就是說01揹包只考慮放與不放進去兩種情況,而完全揹包要考慮 放0、放1、放2…的情況,
這個k當然不是無限的,它受揹包的容量與單件物品的重量限制,即${j/weights[i]}$。假設我們只有1種商品,它的重量爲20,揹包的容量爲60,那麼它就應該放3個,在遍歷時,就0、1、2、3地依次嘗試。
程序需要求解${n*W}$個狀態,每一個狀態需要的時間爲${O(W/w_i)}$,總的複雜度爲${O(nW*Σ(W/w_i))}$。
我們再回顧01揹包經典解法的核心代碼
現在多了一個k,就意味着多了一重循環
javascript的完整實現:
2.3 O(nW)優化
我們再進行優化,改變一下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的完整實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
//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
)
|
我們可以繼續優化此算法,可以用一維數組寫
我們用${f(j)}$表示當前可用體積j的價值,我們可以得到和01揹包一樣的遞推式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
//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
)
|
多重揹包問題
3.1 問題描述:
有${n}$件物品和${1}$個容量爲W的揹包。每種物品最多有numbers[i]件可用,第${i}$件物品的重量爲${weights[i]}$,價值爲${values[i]}$,求解將哪些物品裝入揹包可使價值總和最大。
3.2 問題分析:
多重揹包就是一個進化版完全揹包。在我們做完全揹包的第一個版本中,就是將它轉換成01揹包,然後限制k的循環
直接套用01揹包的一維數組解法
1
2
3
4
5
6
7
8
9
10
11
12
|
function
knapsack
(
weights
,
values
,
numbers
,
W
)
{
var
n
=
weights
.
length
;
var
f
=
new
Array
(
W
+
1
)
.
fill
(
0
)
for
(
var
i
=
0
;
i
<
n
;
|