01揹包問題 之 動態規劃(通俗解釋)

01揹包問題 (問題描述)

給定 n 件物品,物品的重量爲 w[i],物品的價值爲 c[i]。現挑選物品放入揹包中,假定揹包能承受的最大重量爲 V,問應該如何選擇裝入揹包中的物品,使得裝入揹包中物品的總價值最大?

 

一個有趣的例子

假設你是一個小偷,揹着一個可裝下4磅東西的揹包,你可以偷竊的物品如下:

 

爲了讓偷竊的商品價值最高,你該選擇哪些商品?

重量價值表
物品名 重(磅) 價值(美元)
吉他 1 1500
音響 4 3000
筆記本 3 2000

 

直接上手嘗試組合在這種數量比較少的情況還可行,直接可得出偷 吉他和筆記本,總重4磅,價值3500美元。然而要是換一個問題,比如最近 B 站首屆bilibili 1024 安全挑戰賽的一道題如下:

3.期末考試結束了,老師決定帶學生們去捲餅店喫烤鴨餅。老師看到大餅和鴨子,搞了一個活動:每人可以拿走一張餅,誰捲到的食物美味程度總和最高,誰就能獲得稱號:卷王之王!Vita很想得到“卷王之王”稱號,他的大餅可以裝下大小總和不超過500的食物,現在有7塊鴨肉和6根黃瓜,每份食物都有它的大小和美味程度。 每塊鴨肉的大小:85、86、73、66、114、51、99 每塊鴨肉的美味程度:71、103、44、87、112、78、36 每根黃瓜的大小:35、44、27、41、65、38 每塊黃瓜的美味程度:41、46、13、74、71、27 老師要求大餅裏至少有一塊鴨肉和一根黃瓜。請問,Vita捲到的食物美味程度總和最大是多少?(本題由UP主@小學生Vita君提供)

A. 593   B.612    C.496   D. 584

直接嘗試組合恐怕不是一件容易的事,我們需要一個更便捷的策略來輔助我們解決這樣的問題。

這裏要嘗試解釋的,就是耳熟能詳的動態規劃,百度百科裏這樣解釋動態規劃的基本思想:

動態規劃算法通常用於求解具有某種最優性質的問題。在這類問題中,可能會有許多可行解。每一個解都對應於一個值,我們希望找到具有最優值的解。動態規劃算法與分治法類似,其基本思想也是將待求解問題分解成若干個子問題,先求解子問題,然後從這些子問題的解得到原問題的解。與分治法不同的是,適合於用動態規劃求解的問題,經分解得到子問題往往不是互相獨立的。若用分治法來解這類問題,則分解得到的子問題數目太多,有些子問題被重複計算了很多次。如果我們能夠保存已解決的子問題的答案,而在需要時再找出已求得的答案,這樣就可以避免大量的重複計算,節省時間。我們可以用一個表來記錄所有已解的子問題的答案。不管該子問題以後是否被用到,只要它被計算過,就將其結果填入表中。這就是動態規劃法的基本思路。

至於爲啥叫 “動態”,百度百科裏這樣解釋其概念:

在現實生活中,有一類活動的過程,由於它的特殊性,可將過程分成若干個互相聯繫的階段,在它的每一階段都需要作出決策,從而使整個過程達到最好的活動效果。因此各個階段決策的選取不能任意確定,它依賴於當前面臨的狀態,又影響以後的發展。當各個階段決策確定後,就組成一個決策序列,因而也就確定了整個過程的一條活動路線.這種把一個問題看作是一個前後關聯具有鏈狀結構的多階段過程就稱爲多階段決策過程,這種問題稱爲多階段決策問題。在多階段決策問題中,各個階段採取的決策,一般來說是與時間有關的,決策依賴於當前狀態,又隨即引起狀態的轉移,一個決策序列就是在變化的狀態中產生出來的,故有“動態”的含義,稱這種解決多階段決策最優化的過程爲動態規劃方法

這些定義雖然很準確,但是確實有一定程度的抽象,如果在理解的時候能夠把這個理解具體化,那就好理解一些。所以接下來還是回到上面那個小偷的例子,一步一步構建出動態規劃的核心:狀態表格。

 

關於構建狀態表格

網格的各行表示商品,各列代表不同容量(1~4磅)的揹包

 

1. 在填第一行時,我們可選的商品只有吉他;在填第二行時,我們可選的商品則可以有吉他和音響;同理,在填第三行時,可選吉他,音響,筆記本電腦

2. 最後一行的最後一列,對應於這裏的第三行第四列所填寫的數字,表示在揹包容器爲 4 的情況下,從吉他,音響,筆記本選出的最佳組合的價值。 

3. 用動態規劃解決 01揹包問題 的巧妙之處類似於遞歸的優雅簡潔,你要做的就是將一個看似複雜的問題,分解爲自相似的子問題

n! = n *(n-1)!       //求一個數 n 的階乘,可以將其分解爲: 使用 n 乘以 n-1 的階乘

這裏揹包問題的分解方式是:求能裝4磅揹包裝什麼價值最高,分解爲假設我先拿下一個筆記本電腦(3磅),剩下的容量(1磅) 能夠裝什麼價值最高(子問題)

這裏構建的表格,每一個格子就是一個問題(求能裝某個磅數重量的揹包裝什麼價值最高),而該問題的子問題答案,總能在前面已經構建的表格中找到答案

越往前問題就越小,最前面的一行自然是最最簡單的,所以,這裏構建表格的方式是:

4. 我們將一行一行從上往下填

 

開始構建表格

第一行

 

這一行,我們可選的商品只有吉他

第一個單元格表示揹包的的容量爲1磅。吉他的重量也是1磅,這意味着它能裝入揹包!因此這個單元格包含吉他,價值爲1500美元。

 

 與這個單元格一樣,每個單元格都將包含當前可裝入揹包的所有商品。

來看下一個單元格。這個單元格表示揹包容量爲2磅,完全能夠裝下吉他!

 

 這行的其他單元格也一樣。別忘了,這是第一行,只有吉他可供你選擇,換而言之,你假裝現在還沒發偷竊其他兩件商品

 

 此時你很可能心存疑惑:原來的問題說的額是4磅的揹包,我們爲何要考慮容量爲1磅、2磅等得揹包呢?前面說過,動態規劃從小問題着手,逐步解決大問題。這裏解決的子問題將幫助你解決大問題。

 

別忘了,你要做的是讓揹包中商品的價值最大。這行表示的是當前的最大價值。它指出,如果你有一個容量4磅的揹包,可在其中裝入的商品的最大價值爲1500美元。

你知道這不是最終解。隨着算法往下執行,你將逐步修改最大價值。

第二行

你現在處於第二行,可以偷竊的商品有吉他和音響。

我們先來看第一個單元格,它表示容量爲1磅的揹包。在此之前,可裝入1磅揹包的商品最大價值爲1500美元。

 

該不該偷音響呢?

揹包的容量爲1磅,顯然不能裝下音響。由於容量爲1磅的揹包裝不下音響,因此最大價值依然是1500美元。

 

 接下來的兩個單元格的情況與此相同。在這些單元格中,揹包的容量分別爲2磅和3磅,而以前的最大價值爲1500美元。由於這些揹包裝不下音響,因此最大的價值保持不變。

 

 揹包容量爲4磅呢?終於能夠裝下音響了!原來最大價值爲1500美元,但如果在揹包中裝入音響而不是吉他,價值將爲3000美元!因此還是偷音響吧。

 

 你更新了最大價值。如果揹包的容量爲4磅,就能裝入價值至少3000美元的商品。在這個網格中,你逐步地更新最大價值。

 

 

第三行:

下面以同樣的方式處理筆記本電腦。筆記本電腦重3磅,沒法將其裝入1磅或者2磅的揹包,因此前兩個單元格的最大價值仍然是1500美元。

 

 對於容量爲3磅的揹包,原來的最大價值爲1500美元,但現在你可以選擇偷竊價值2000美元的筆記本電腦而不是吉他,這樣新的最大價值將爲2000美元。

 

 對於容量爲4磅的揹包,情況很有趣。這是非常重要的部分。當前的最大價值爲3000美元,你可不偷音響,而偷筆記本電腦,但它只值2000美元。

 

 價值沒有原來高,但是等一等,筆記本電腦的重量只有3磅,揹包還有1磅的重量沒用!

 

 在1磅的容量中,可裝入的商品的最大價值是多少呢?你之前計算過。

 

 根據之前計算的最大價值可知,在1磅的容量中可裝入吉他,價值1500美元。因此,你需要做如下的比較:

 

 

你可能始終心存疑惑:爲何計算小揹包可裝入的商品的最大價值呢?但願你現在明白了其中的原因!餘下了空間時,你可根據這些子問題的答案來確定餘下的空間可裝入哪些商品。筆記本電腦和吉他的總價值爲3500美元,因此偷它們是更好的選擇。
 
最終的網格類似於下面這樣。

 

 答案如下:將吉他和筆記本電腦裝入揹包時價值更高,爲3500美元。

 

你可能認爲,計算最後一個單元格的價值時,我使用了不同的公式。那是因爲填充之前的單元格時,我故意避開了一些複雜的因素。其實,計算每個單元格的價值時,使用的公式都相同。這個公式如下。

 

同樣的容量,在原來考慮的商品之上,多考慮一個商品,只有兩種可能:“要麼考慮先加入當前商品,要麼不考慮直接還是用原先的組合”。哪種更好,選擇哪個。

這個,就是動態規劃求解揹包問題的核心祕訣。

 

第四行:

知道這個祕密後,我們再來添加一個商品,鞏固一下這個計算方式:

現在假設還有第四件商品可偷——一個iPhone

 

 此時需要重新執行前面所做的計算嗎?不需要。別忘了,動態規劃逐步計算最大價值。到目前爲止,計算出的最大價值如下:

 

這意味着揹包容量爲4磅時,你最多可偷價值3500美元的商品。但這是以前的情況,下面再添加表示iPhone的行。

 

 我們還是從第一個單元格開始。

“要麼考慮先加入當前商品,要麼不考慮直接還是用原先的組合”

iPhone可裝入容量爲1磅的揹包,放下之後填滿沒有剩餘空間。之前的最大價值爲1500美元,但iPhone價值2000美元,因此該偷iPhone而不是吉他。

 

 

在下一個單元格中,

“要麼考慮先加入當前商品,要麼不考慮直接還是用原先的組合”

2磅容器,考慮 1磅的 iPhone,剩下 1 磅空間,1500美元的吉他,共可裝入iPhone和吉他,比 剛剛不用 iPhone 好。

 

 對於第三個單元格,

“要麼考慮先加入當前商品,要麼不考慮直接還是用原先的組合”

3磅容器,考慮 1磅的 iPhone,剩下 2 磅空間,根據前面的評估,也是剩下1500美元的吉他,也是裝入iPhone和吉他 比 剛剛不用 iPhone 好。

 

對於最後一個單元格,情況比較有趣。

同樣,“要麼考慮先加入當前商品,要麼不考慮直接還是用原先的組合”

當前的最大價值爲3500美元,但你可以偷iPhone,這將餘下3磅的容量。

 

 3磅容量的最大價值爲2000美元!再加上iPhone價值2000美元,總價值爲4000美元。新的最大價值誕生了!

最終的網格如下。

 

代碼實現參考:

#include <vector>
#include <iostream>

//解決方案
class solution
{
public:
    solution(int value):totalValue(value) {}
    int totalValue;             //選擇的物品的總價值
    std::vector<size_t> items;  //選擇的物品的項
    int containerValue;         //容器容量
};

//構建網格
solution buildNet(const std::vector<size_t>& w, const std::vector<int>& v, size_t total)
{
    size_t row = w.size();      //可選擇的物體數量
    size_t column = total;      //總容量

    std::vector<std::vector<solution>> net;
    net = std::vector<std::vector<solution>>(row+1, std::vector<solution>(column+1, 0));  //初始化多第一行和第一列,便於通用公式

    for (size_t r = 1; r <= row; ++r)
    {
        for (size_t c = 1; c <= column; ++c)
        {
            size_t weightCurrent = w[r - 1]; //當前物品重
            int valueCurrent = v[r - 1];  //當前物品價值
            if (weightCurrent <= c)       //如果單獨放得下
            {
                int valueIncludeCurrent = valueCurrent + net[r - 1][c - weightCurrent].totalValue;
                if (valueIncludeCurrent > net[r - 1][c].totalValue) //加入當前物品價值更高,則更新方案
                {
                    net[r][c] = valueIncludeCurrent;

                    net[r][c].items = net[r - 1][c - weightCurrent].items; //得到之前的序列
                    net[r][c].items.push_back(r);                          //添加自己到序列後
                }
                else
                    net[r][c] = net[r - 1][c];
            }
            else
                net[r][c] = net[r - 1][c];
        }
    }

    net[row][column].containerValue = total;
    return net[row][column];
}

//打印選擇的最佳方案
void printVector(const std::vector<size_t>& w, const std::vector<int>& v,const solution & s)
{
    std::cout << "Input: ";
    for (size_t i = 0; i < w.size(); ++i)
    {
        std::cout << w[i] << " (" << v[i] << ");" ;
    }
    std::cout << "Container: " << s.containerValue << std::endl;

    const std::vector<size_t>& items = s.items;
    int totalV = s.totalValue;

    size_t totalW = 0;
    size_t totalV2 = 0;
    for (auto r : items)
    {
        size_t w0 = w[r-1];
        int v0 = v[r-1];
        std::cout << w0 << " (" << v0 << ");" << std::endl;

        totalW += w0;
        totalV2 += v0;
    }
    std::cout << "Total: " << totalW << " (" << totalV << " -> check:" << totalV2 << ")";
    std::cout << std::endl << std::endl;
}

int main()
{
     std::vector<size_t> w = {1,4,3,1};
     std::vector<int> v = { 1500,3000,2000,2000};
     solution maxValue = buildNet(w, v, 4);
     printVector(w, v, maxValue);

     std::vector<size_t> w2 = { 85, 86, 73, 66, 114, 51, 99 };
     std::vector<int> v2 = { 71,103,44, 87, 112, 78, 36 };
     solution duck = buildNet(w2, v2, 500);
     printVector(w2, v2, duck);

     std::vector<size_t> w3 = { 35, 44,  27,  41, 65, 38 };
     std::vector<int> v3 = { 41, 46, 13, 74, 71, 27 };
     solution cucumber = buildNet(w3, v3, 500);
     printVector(w3, v3, cucumber);

     std::vector<size_t> w4 = w2;  w4.insert(w4.end(), w3.begin(), w3.end());
     std::vector<int> v4 = v2; v4.insert(v4.end(), v3.begin(), v3.end());
     solution duckCucumber = buildNet(w4, v4, 500);
     printVector(w4, v4, duckCucumber);

    return 0;
}
01揹包算法 動態規劃實現

 

 

 


 

參考1:(0-1揹包問題 - 簡書 by 我沒有三顆心臟):https://www.jianshu.com/p/a66d5ce49df5

參考2:(動態規劃之01揹包問題 - 簡書 by kkbill):https://www.cnblogs.com/kkbill/p/12081172.html

本文原文地址:https://www.cnblogs.com/BensonLaur/p/15464045.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章