問題:
假設你要去野營。你有一個容量爲6磅的揹包,需要決定該攜帶下面的哪些東西。其中每樣東西都有相應的價值,價值越大意味着越重要:
•水(重3磅,價值10);
•書(重1磅,價值3);
•食物(重2磅,價值9);
•夾克(重2磅,價值5);
•相機(重1磅,價值6)。
請問攜帶哪些東西時價值最高?
思路:
這是一個典型的揹包問題,求解此類問題,通常會使用動態規劃法。不要被名字嚇到,其實並不難。下面我們來講解該算法。
動態規劃算法都是從網格開始的,我們先畫一個如下的網格:
網格的每一行表示一件物品,每一列表示不同容量的揹包,這裏從1到6。物品後面的括號裏標註了物品的重量及價值。
下面開始求解。
我們需要從左至右,從上至下填充每一個單元格。
第一個單元格表示,1磅容量的揹包,現在要裝入第一件物品水,我們發現水的重量是3磅,裝不進去,此時,該單元格填充0,表示不能裝入任何物品。
第二個單元格也是一樣。
到第三個單元格時,揹包容量是3磅,水的重量也是3磅,剛好可以裝下,此時,填充單元格,將水的價值填入。第四個單元格時,揹包容量是4磅,不僅可以裝下水,還有1磅的餘量,但我們目前只有水一件物品,所以餘下的空間也不能用,因此,揹包能裝的最大價值還是3磅的水,價值是10。第五,第六單元格也是如此。
此時,6磅揹包最大可以攜帶價值爲10的物品。接下來,我們準備裝入第二件物品。
第二件物品是書,重量是1磅,揹包容量是1磅時剛好可以放下。
接着是2磅容量的揹包,也能放下書。但當揹包容量到3時,有情況發生了。
此時我們需要考慮一下,到底是放1磅重的書呢,還是3磅重的水,顯然,水的價值更大。所以10 vs 3的結果明顯是10勝出,所以第二行第三個單元格的最大價值是10。
第四個單元格的情況又有變化。我們此時的揹包容量是4磅,書的重量是1磅,如果裝了書,那麼餘下的容量是3磅,我們看一下前一行,揹包容量是3磅時的最大價值爲10。這就意味着,這一個單元格可以同時放下書和水,共計4磅重量,價值爲3+10=13。我們跟前一行的第四單元格對比一下,之前的最大價值是10。現在13 vs 10,明顯13勝出,所以,第二行第四單元格我們的最大價值是13。
以此類推,第二行第五,第六單元格的價值也可以算出來。
接着我們開始算第三行。
第三行第一單元格是放不下食物的,所以沿用前一行對應單元格的價值3。
第二單元格時,有兩種選擇,一是沿用前一行的物品書,二是放入食物,對比一下價值,很明顯應該放入價值9的食物。
第三至六單元格我們用之前第二行的算法進行計算,填充如下:
第四行填充:
第五行填充:
當我們填充完後,最後一行最後一個單元格中的價值便是我們能達到的最大價值。
比如最後一行,最後一個單元格。我們的填充方法是:
先取到前一行相同容量揹包的價值,這裏是22;
當前物品的價值是6,重量是1,如果將該物品放入揹包中,則餘下容量是5。
前一行揹包容量爲5時的最大價值是19,即之前5磅揹包最多可以放下價值19的物品,再加上當前物品的價值6,合計能放下價值19+6=25的物品;
22 vs 25,取最大的值,則最終能放下價值25的物品。
我們可以歸納出算法:
這便是網上流傳的公式:
解答:
有了算法,我們便可以用代碼來實現:
這裏我們用php來實現,首先我們把問題先整理出來,如下:
$goods = array(
array("name" => "水", "weight" => 3, "value" => 10),
array("name" => "書", "weight" => 1, "value" => 3),
array("name" => "食物", "weight" => 2, "value" => 9),
array("name" => "夾克", "weight" => 2, "value" => 5),
array("name" => "相機", "weight" => 1, "value" => 6),
);
$maxWeight = 6; //揹包最大容量
接着,開始寫算法:
$table = array();// 表格
foreach ($goods as $i => $good) {
for ($j = 1; $j <= $maxWeight; $j++) {
// 填充第一行
if ($i == 0) {
if ($j < $good['weight']) {
$table[$i][$j] = 0;
} else {
$table[$i][$j] = $good['value'];
}
} else {
$v1 = $table[$i - 1][$j];
if ($j < $good['weight']) { // 當裝不下時,以前一格爲準
$table[$i][$j] = $v1;
} else {
// 1.前一行同列的值;2.當前物品價值+餘下重量的最大價值。這兩者取最大值
if ($j == $good['weight']) {
$preMax = 0;
} else {
$preMax = $table[$i - 1][$j - $good['weight']];
}
$v2 = $good['value'] + $preMax;
$table[$i][$j] = max($v1, $v2);
}
}
}
}
print_r($table);
我們打印了表格,結果如下,可以對比一下手算的結果:
Array
(
[0] => Array
(
[1] => 0
[2] => 0
[3] => 10
[4] => 10
[5] => 10
[6] => 10
)
[1] => Array
(
[1] => 3
[2] => 3
[3] => 10
[4] => 13
[5] => 13
[6] => 13
)
[2] => Array
(
[1] => 3
[2] => 9
[3] => 12
[4] => 13
[5] => 19
[6] => 22
)
[3] => Array
(
[1] => 3
[2] => 9
[3] => 12
[4] => 14
[5] => 19
[6] => 22
)
[4] => Array
(
[1] => 6
[2] => 9
[3] => 15
[4] => 18
[5] => 20
[6] => 25
)
)
到這裏還沒有結束,我們還得知道要裝哪些物品,所以還需要一個回溯。
從後向前進行逆推,如果當前單元格的價值與前一行同列單元格的價格相同,說明當前物品沒有加入到揹包中,計爲$x[$i]=0;
否則,就計爲$x[$i]=1,並從總重量中減去當前物品的重量;
當回溯到第一件物品時,看下$j的值,如果非負,說明第一件物品是入選物品計爲1,否則計爲0。
$j = $maxWeight;
$n = count($goods);
$x = array();// 物品數組
for ($i = $n - 1; $i >= 0; $i--) {
if ($i > 0) {
if ($table[$i][$j] == $table[$i - 1][$j]) {
$x[$i] = 0;
} else {
$x[$i] = 1;
$j -= $goods[$i]['weight'];// 每次扣減當前物品的重量
}
} else {
$x[$i] = $j >= 0 ? 1 : 0;// 如果最後發現$j是有值的,那便是第1個物品
}
}
ksort($x);// 把回溯的過程改爲順序
foreach ($x as $key => $val) {
if ($val != 0) {
print_r($goods[$key]);
}
}
結果如下:
Array
(
[name] => 水
[weight] => 3
[value] => 10
)
Array
(
[name] => 食物
[weight] => 2
[value] => 9
)
Array
(
[name] => 相機
[weight] => 1
[value] => 6
)
以下是Golang的實現,算法一樣,只是換了種語言實現而已
package main
import (
"fmt"
"math"
)
type Goods struct {
Name string
Weight int
Value int
}
var GoodsList []Goods // 物品列表
var maxWeight = 6 // 揹包最大容昊
func main() {
GoodsList = []Goods{
{Name: "水", Weight: 3, Value: 10},
{Name: "書", Weight: 1, Value: 3},
{Name: "食物", Weight: 2, Value: 9},
{Name: "夾克", Weight: 2, Value: 5},
{Name: "相機", Weight: 1, Value: 6},
}
table := make([][]int, len(GoodsList))
for i, goods := range GoodsList {
table[i] = make([]int, maxWeight+1)
for j := 1; j <= maxWeight; j++ {
if i == 0 {
if goods.Weight > j {
table[i][j] = 0
} else {
table[i][j] = goods.Value
}
} else {
v1 := table[i-1][j]
if goods.Weight > j {
table[i][j] = v1
} else {
preMax := 0
if j == goods.Weight {
preMax = 0
} else {
preMax = table[i-1][j-goods.Weight]
}
v2 := goods.Value + preMax
//fmt.Println(v1, v2, goods)
table[i][j] = int(math.Max(float64(v1), float64(v2)))
}
}
}
}
fmt.Println(table)
goodsNum := len(GoodsList)
j := maxWeight
x := make([]int, goodsNum)
for i := goodsNum - 1; i >= 0; i-- {
if i > 0 {
if table[i][j] == table[i-1][j] {
x[i] = 0
} else {
x[i] = 1
j -= GoodsList[i].Weight
}
} else {
if j >= 0 {
x[i] = 1
} else {
x[i] = 0
}
}
}
for key, value := range x {
if value == 1 {
fmt.Println(key, GoodsList[key])
}
}
}
輸出
[[0 0 0 10 10 10 10] [0 3 3 10 13 13 13] [0 3 9 12 13 19 22] [0 3 9 12 14 19 22] [0 6 9 15 18 20 25]]
0 {水 3 10}
2 {食物 2 9}
4 {相機 1 6}