【LeetCode動態規劃#05】揹包問題的理論分析(基於程式碼隨想錄的個人理解,多圖)

dayceng發表於2023-03-26

揹包問題

問題描述

揹包問題是一系列問題的統稱,具體包括:01揹包完全揹包、多重揹包、分組揹包等(僅需掌握前兩種,後面的為競賽級題目)

下面來研究01揹包

實際上即使是最經典的01揹包,也不會直接出現在題目中,一般是融入到其他的題目背景中再考察

因為是學習原理,所以先跳過最原始的問題模板來學。

01揹包的原始題意是:(標準的揹包問題)

有n件物品和一個最多能背重量為 w 的揹包。第 i 件物品的重量是 weight[i] ,得到的價值是 value[i]每件物品只能用一次,求解將哪些物品裝入揹包裡物品價值總和最大

(01揹包問題可以使用暴力解法,每一件物品其實只有兩個狀態,或者不取,所以可以使用回溯法搜尋出所有的情況,那麼時間複雜度就是O(2^n),這裡的n表示物品數量。因為暴力搜尋的時間複雜度是指數級別的,所以才需要透過dp來進行最佳化)

根據上面的描述可以舉出以下例子

二維dp陣列01揹包

揹包最大重量為4。

物品為:

重量 價值
物品0 1 15
物品1 3 20
物品2 4 30

問揹包能背的物品最大價值是多少?

五部曲分析一波

五步走

1、確定dp陣列含義

該問題中的dp陣列應該是二維的,所以先定義一個dp[i][j]

該陣列的含義是什麼?

含義:任取編號(下標)為[0, i]之間的物品放進容量為j的揹包裡

動態規劃-揹包問題1

2、確定遞推公式

確定遞推公式之前,要明確dp[i][j]可以由哪幾個方向推匯出

當前揹包的狀態取決於放不放物品i,下面分別討論

(1)不放物品i

dp[i - 1][j]

(2)放物品i

dp[i - 1][j - weight[i]] + value[i] (物品i的價值)

我來解釋一下上面的式子是什麼意思

先回顧一下dp[i][j]的含義:從下標為[0, i]的物品裡任意取,放進容量為j的揹包,價值總和最大是多少。

那麼可以有上述兩個方向推出來dp[i][j]

情況1:不放物品i。此時我們已經認為物品i不會被放到揹包中,那麼根據dp[i][j]的定義,任取物品的範圍應該變成[0, i-1]

也就是從下標為[0, i-1]的物品裡任意取,放進容量為j的揹包,價值總和最大是多少,即dp[i - 1][j]

再看情況2:放物品i。因為要放物品i,那就不需要再遍歷到i了(相當於已經放入揹包的東西下次就不遍歷了)

根據dp[i][j]的定義,任取物品的範圍也應該變成[0, i-1]

但是,因為情況2是要將物品i放入揹包,此時揹包的容量也要發生變化

根據dp[i][j]的定義,揹包的容量應該要減去物品i的重量 weight[i] ,即dp[i - 1][j - weight[i]]

此時dp[i - 1][j - weight[i]]只是做好了準備放入物品i的工作,實際上物品i並沒有放入,因此該式子的含義是:揹包容量為j - weight[i]的時候不放物品i的最大價值

所以要再加上物品i本身的價值 value[i] ,才能求出揹包放物品i得到的最大價值

即:dp[i - 1][j - weight[i]] + value[i]

根據dp[i][j]的定義,我們最後要求價值總和最大物品放入方式

因此遞推公式應該是: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

從不放物品i和放物品i兩個方向往dp[i][j]推,取最後結果最大的那種方式(即最優的方式)

3、確定dp陣列初始化方式

可以把dp陣列試著畫出來,然後假設要求其中一個位置,思考可以從哪個方向將其推出,而這些方向最開始又是由哪些方向推得的,進而確定dp陣列中需要初始化的部分

將本題的dp陣列畫出來如下:

假設有一個要求的元素dp[x][x],根據前面對遞推公式的討論可知,該元素一定是由兩個方向推過來求得的。

也就是情況1、情況2,那麼對應到圖中就是從上到下推過來的,是情況1(dp[i - 1][j]

情況2(dp[i - 1][j - weight[i]])在圖中體現得不是十分確定,但是大致方向是從左上角往下推過來的

這兩個方向的源頭分別指向綠色區域橙色區域

那麼這兩個區域就是要初始化的區域,怎麼初始化呢?

先說橙色區域,從dp[i][j]的定義出發,如果揹包容量j為0的話,即dp[i][0],無論是選取哪些物品,揹包價值總和一定為0。

所以橙色區域區域需要初始化為0

再說綠色區域,狀態轉移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出 i 是由 i-1 推匯出來,那麼 i 為 0 的時候就一定要初始化

dp[0][j],即:i 為0,存放 編號0 的物品的時候,各個容量的揹包所能存放的最大價值。

很明顯當 j < weight[0]的時候,dp[0][j] 應該是 0,因為揹包容量比編號0的物品重量還小。

j >= weight[0]時,dp[0][j] 應該是 value[0] ,因為揹包容量放足夠放編號0物品。

兩個區域的初始化情況對應到圖中如下:

初始化程式碼:

for (int j = 0 ; j < weight[0]; j++) { //橙色區域
    dp[0][j] = 0;
}
// 正序遍歷
for (int j = weight[0]; j <= bagweight; j++) {//綠色區域
    dp[0][j] = value[0];
}

以上兩個區域實際上屬於“含0下標區域”,其他的“非0下標區域”也需要初始化(沒想清楚為什麼有時要初始化完整個dp陣列,有時又不用)

“非0下標區域”初始化為任何值都可以

還是拿前面的圖來看

dp[x][x]這個位置為例,其初始化成100、200都無所謂,因為這個位置的dp值是由其上面和左上兩個方向上的情況推匯出來的,只取決於這裡個方向最開始的初始化值。

(例如dp[x][x]這裡初始化為100,我從上面推導下來之後會用推導值將100覆蓋)

4、確定遍歷方式

該問題中dp陣列有兩個維度:物品、揹包容量,先遍歷哪個呢?

直接說結論,都行,但是先遍歷物品更好理解

(具體看程式碼隨想錄解釋

兩種過程的圖如下:

(這裡需要重申一下揹包問題的條件:每個物品只能用一次,要求的是怎麼裝揹包裡的價值最大

先遍歷物品再遍歷揹包容量(固定物品編號,遍歷揹包容量

​ 挑一個節點來說一下(圖中的紅框部分),此時的遍歷順序是先物後包,物品1(重3價20)在0~4種容量中放置的結果如圖所示

​ 因為固定了物品1,此時揹包容量為0、1、2的情況都是放不下物品1的(又也放不下物品3),所以只能放物品0(此為最佳選擇)

​ 當遍歷到揹包容量為3時,可以放下物品1了,那此處的最佳選擇就是放一個物品1,所以此處的dp陣列值變為20

​ 其餘位置分析方法同理

先遍歷揹包容量再遍歷物品(固定揹包容量,遍歷物品編號

​ 有了前面的例子,這裡就很好理解了,就是從上往下遍歷,固定住當前揹包的容量,遍歷物品,看看能不能放入,能放的話最優選擇應該放哪個

​ 還是拿紅框部分來說,此時揹包容量固定為3

​ 第一次遍歷,物品0可以裝下,此時最優選擇就是放物品0,揹包總價是15;

​ 第二次遍歷,物品1可以裝下,此時最優選擇就是放物品1,揹包總價是20;

​ 第二次遍歷,物品2裝不下,此時最優選擇就是放物品1,揹包總價還是20;

​ 其餘位置分析方法同理

完整c++測試程式碼(卡哥)

void test_2_wei_bag_problem1() {
    vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int bagweight = 4;

    // 二維陣列
    vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));

    // 初始化
    for (int j = weight[0]; j <= bagweight; j++) {
        dp[0][j] = value[0];
    }

    // weight陣列的大小 就是物品個數
    for(int i = 1; i < weight.size(); i++) { // 遍歷物品
        for(int j = 0; j <= bagweight; j++) { // 遍歷揹包容量
            if (j < weight[i]) dp[i][j] = dp[i - 1][j];
            else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

        }
    }

    cout << dp[weight.size() - 1][bagweight] << endl;
}

int main() {
    test_2_wei_bag_problem1();
}

相關文章