每週一道算法題004:揹包

問題:

假設你要去野營。你有一個容量爲6磅的揹包,需要決定該攜帶下面的哪些東西。其中每樣東西都有相應的價值,價值越大意味着越重要:
•水(重3磅,價值10);
•書(重1磅,價值3);
•食物(重2磅,價值9);
•夾克(重2磅,價值5);
•相機(重1磅,價值6)。
請問攜帶哪些東西時價值最高?

思路:

這是一個典型的揹包問題,求解此類問題,通常會使用動態規劃法。不要被名字嚇到,其實並不難。下面我們來講解該算法。

動態規劃算法都是從網格開始的,我們先畫一個如下的網格:
每週一道算法題004:揹包
網格的每一行表示一件物品,每一列表示不同容量的揹包,這裏從1到6。物品後面的括號裏標註了物品的重量及價值。

下面開始求解。
我們需要從左至右,從上至下填充每一個單元格。
第一個單元格表示,1磅容量的揹包,現在要裝入第一件物品水,我們發現水的重量是3磅,裝不進去,此時,該單元格填充0,表示不能裝入任何物品。
第二個單元格也是一樣。
每週一道算法題004:揹包

到第三個單元格時,揹包容量是3磅,水的重量也是3磅,剛好可以裝下,此時,填充單元格,將水的價值填入。第四個單元格時,揹包容量是4磅,不僅可以裝下水,還有1磅的餘量,但我們目前只有水一件物品,所以餘下的空間也不能用,因此,揹包能裝的最大價值還是3磅的水,價值是10。第五,第六單元格也是如此。
每週一道算法題004:揹包

此時,6磅揹包最大可以攜帶價值爲10的物品。接下來,我們準備裝入第二件物品。
第二件物品是書,重量是1磅,揹包容量是1磅時剛好可以放下。每週一道算法題004:揹包

接着是2磅容量的揹包,也能放下書。但當揹包容量到3時,有情況發生了。
此時我們需要考慮一下,到底是放1磅重的書呢,還是3磅重的水,顯然,水的價值更大。所以10 vs 3的結果明顯是10勝出,所以第二行第三個單元格的最大價值是10。
每週一道算法題004:揹包

第四個單元格的情況又有變化。我們此時的揹包容量是4磅,書的重量是1磅,如果裝了書,那麼餘下的容量是3磅,我們看一下前一行,揹包容量是3磅時的最大價值爲10。這就意味着,這一個單元格可以同時放下書和水,共計4磅重量,價值爲3+10=13。我們跟前一行的第四單元格對比一下,之前的最大價值是10。現在13 vs 10,明顯13勝出,所以,第二行第四單元格我們的最大價值是13。
每週一道算法題004:揹包

以此類推,第二行第五,第六單元格的價值也可以算出來。
每週一道算法題004:揹包

接着我們開始算第三行。
第三行第一單元格是放不下食物的,所以沿用前一行對應單元格的價值3。
第二單元格時,有兩種選擇,一是沿用前一行的物品書,二是放入食物,對比一下價值,很明顯應該放入價值9的食物。
每週一道算法題004:揹包

第三至六單元格我們用之前第二行的算法進行計算,填充如下:
每週一道算法題004:揹包

第四行填充:
每週一道算法題004:揹包

第五行填充:
每週一道算法題004:揹包

當我們填充完後,最後一行最後一個單元格中的價值便是我們能達到的最大價值。

比如最後一行,最後一個單元格。我們的填充方法是:
先取到前一行相同容量揹包的價值,這裏是22;
當前物品的價值是6,重量是1,如果將該物品放入揹包中,則餘下容量是5。
前一行揹包容量爲5時的最大價值是19,即之前5磅揹包最多可以放下價值19的物品,再加上當前物品的價值6,合計能放下價值19+6=25的物品;
22 vs 25,取最大的值,則最終能放下價值25的物品。

我們可以歸納出算法:
每週一道算法題004:揹包

這便是網上流傳的公式:
每週一道算法題004:揹包

解答:

有了算法,我們便可以用代碼來實現:

這裏我們用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}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章