揹包問題(Knapsack problem)是一種組合優化的NP徹底問題。問題能夠描述爲: 給定一組物品,每種物品都有本身的重量和價格,在限定的總重量內,咱們如何選擇,才能使得物品的總價格最高。javascript
假設山洞裏共有a,b,c,d,e這5件寶物(不是5種寶物),它們的重量分別是2,2,6,5,4, 它們的價值分別是6,3,5,4,6,如今給你個承重爲 10 的揹包, 怎麼裝揹包,能夠才能帶走最多的財富。java
動態規劃一個關鍵的步驟是獲得狀態轉化方程,物體的價值用 v(k)
表示, 重量用 w(k)
表示,f[i, j]
表示將前 i
個物體放入到容量爲 j
的揹包中的最大價值,則有:數組
動態規劃有兩種等價的實現方法:函數
帶備忘的自頂向下法。此方法按照天然的遞歸形式編寫過程,但過程當中會保存每一個子問題的解(一般保存在一個數組或散列表中)。 當須要一個子問題的解時,過程首先檢查是否已經保存過此解。若是是,則直接返回保存的值,從而節省了時間;不然,按一般方式計算 這個子問題。優化
自底向上法。這種方法通常須要恰當定義子問題「規模」的概念,使得任何子問題的求解都只依賴於「更小的」子問題的求解。於是 咱們能夠將子問題按規模排序,按由小至大的順序進行求解。當求解某個子問題時,它所依賴的那些更小的子問題都已求解完畢, 結果已經保存。ui
下面給出一個帶備忘的自頂向下實現:spa
var v = [6,3,5,4,6]
var w = [2,2,6,5,4]
var c = 10
function bag (v, w, c) {
function _bag (v, w, c, f, s) {
// 子問題的規模
var n = v.length
// 子問題已經被求解
if (f[n][c] > 0) {
return f[n][c]
}
// 從剩下的物品中選擇一件
for (var i = 0; i < n; i++) {
var newW = w.slice()
newW.splice(i, 1)
var newV = v.slice()
newV.splice(i, 1)
// 選出來的物品重量大於揹包剩餘容量,則該子問題的解爲0
if (w[i] > c) {
return 0
}
// 不然遞歸求解,獲得子問題的最大的解及當前選擇的物品
var maxValue = v[i] + _bag(newV, newW, c - w[i], f, s)
if (f[n][c] < maxValue) {
f[n][c] = maxValue
s[n][c] = {v: v[i], w: w[i]}
}
}
// 返回子問題的最大解
return f[n][c]
}
var n = v.length
// 記錄最大的價值
var f = []
// 記錄每一步所作的選擇
var s = []
for (var i = 0; i <= n; i++) {
f[i] = []
s[i] = []
for (var j = 0; j <= c; j++) {
f[i][j] = 0
s[i][j] = null
}
}
_bag(v, w, c, f, s)
// 打印兩個二維數組
console.log(f)
console.log(s)
// 從s中獲得所選擇的物品
var selected = []
var i = n
var j = c
var sum = 0
do {
var thing = s[i][j]
if (thing) {
selected.push(thing)
j -= thing.w
i--
}
} while (thing)
return {
maxV: f[n][c],
selected: selected
}
}
複製代碼
說明code
程序中 f
最後以下所示,其中第一行能夠忽略,這麼作只是爲了讓數組索引從 1 開始,跟上面的公式保持一致:cdn
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
null | null | null | null | null | null | null | null | null | null | null |
null | null | null | null | null | null | null | null | null | null | null |
0 | 0 | 0 | null | null | null | null | null | null | null | null |
0 | 0 | 0 | 0 | 0 | null | 6 | null | null | null | null |
null | null | null | null | 6 | 6 | 6 | null | 9 | null | null |
null | null | null | null | null | null | null | null | null | null | 15 |
其中,f[5][10]
就是最後所求的最大價值,即 15。 從上表還能夠知道求解過程當中遞歸求解了哪些問題,即上表中值不爲 null 的那些。 而若是須要知道最後所選擇的物品,還須要藉助 s
:blog
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
null | null | null | null | null | null | null | null | null | null | null |
null | null | null | null | null | null | null | null | null | null | null |
null | null | null | null | null | null | null | null | null | null | null |
null | null | { v: 3, w: 2 } | { v: 3, w: 2 } | { v: 3, w: 2 } | null | { v: 6, w: 4 } | null | null | null | null |
null | null | null | null | { v: 6, w: 4 } | { v: 6, w: 4 } | { v: 6, w: 2 } | null | { v: 3, w: 2 } | null | null |
null | null | null | null | null | null | null | null | null | null | { v: 6, w: 2 } |
其中,s[i][j]
表示將前 i
個物體放入到容量爲 j
的揹包中時所選擇的第一個物品
如今,讓咱們來理一下這個過程:
s[5][10]
表示將前 5 個物品放到容量爲 10 的揹包中,選擇了物品 { v: 6, w: 2 }
s[4][8]
,選擇了物品 { v: 6, w: 4 }
s[3][4]
,選擇了物品 { v: 3, w: 2 }
s[2][2]
,沒有選擇任何物品。{ v: 6, w: 2 }
, { v: 6, w: 4 }
, { v: 3, w: 2 }
下面是自底向上法的實現:
function bag2 (v, w, c) {
var f = []
var s = []
var n = v.length
for (var i = 0; i <= n; i++) {
f[i] = []
s[i] = []
for (var j = 0; j <= c; j++) {
f[i][j] = 0
s[i][j] = 0
}
}
// 遍歷物品
for (var i = 1; i <= n; i++) {
var index = i - 1
// 遍歷容量
for (var j = 0; j <= c; j++) {
// 當前物品放入的狀況
if (w[index] <= j && v[index] + f[i - 1][j - w[index]] > f[i - 1][j]) {
f[i][j] = v[index] + f[i - 1][j - w[index]]
s[i][j] = 1
}
// 當前物品不放入的狀況
else {
f[i][j] = f[i - 1][j]
}
}
}
return{
f: f,
s: s
}
}
複製代碼
說明
首先,注意到這個事實:物品放入的順序不會影響咱們最後的結果。這裏按照題目中的順序依次考察 每一個物品在每一個容量的狀況下是否放入。
仍然用 f
來記錄最大值,用 s
來記錄選擇。
不過這裏的 s[i][j]
只需標記當前物品是否放入便可, 因此 s[i][j]
取值爲 0 或 1。
f
以下所示:
v | w | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
- | - | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
6 | 2 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
3 | 2 | 0 | 0 | 6 | 6 | 9 | 9 | 9 | 9 | 9 | 9 | 9 |
5 | 6 | 0 | 0 | 6 | 6 | 9 | 9 | 9 | 9 | 11 | 11 | 14 |
4 | 5 | 0 | 0 | 6 | 6 | 9 | 9 | 9 | 10 | 11 | 13 | 14 |
6 | 4 | 0 | 0 | 6 | 6 | 9 | 9 | 12 | 12 | 15 | 15 | 15 |
s
以下所示:
v | w | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
- | - | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
6 | 2 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
3 | 2 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
5 | 6 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 |
4 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
6 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 |
一樣,咱們能夠反向推導出最後的選擇:
s[5][10]
爲 1,該物體放入袋中s[4][6]
,爲 0,說明這個物體不放入s[3][6]
,爲 0, 不放入s[2][6]
,爲 1, 放入s[1][4]
, 爲 1, 放入{ v: 6, w: 2 }
, { v: 3, w: 2 }
, { v: 6, w: 4 }
之後碰到動態規劃相關的問題均可以用這個思路來解決了,關鍵在於要構造轉移函數這個模型。 我的感受自頂向下法更加好理解,可是代碼略顯囉嗦了。