動態規劃解0-1揹包問題

喝茶謝謝發表於2022-05-24

動態規劃解0-1揹包問題

動態規劃解0-1揹包問題是一個十分典型案例,我從網上查詢好多相關資料,但是大部分都深奧難懂,並不適合初學演算法的小白,其中涉及的遞推關係式、填表,以及最後的二維表簡化為一維表的優化過程,好多都是一筆帶過,所以,今天就盡我所能,來敘述一下對於0-1揹包問題使用動態規劃來求解。
要解決0-1揹包問題,首先我們們要解決的是什麼是動態規劃。

動態規劃

先說一說什麼是動態規劃。
動態規劃演算法與分治演算法類似,其基本思想也是將待求解問題分解成若干個子問題,先求解子問題,然後從這些子問題的解得到原問題的解。
這裡又涉及到分治演算法,那就簡單概述一下分治演算法。
分治演算法簡而言之就是將一個大問題分成若干個小問題,通過求解小問題的最優解,進而推出所求解問題的最優解。
這麼乍一看,可能會覺得分治演算法與動態規劃相類似,實際上,他們有一個最本質的區別,那就是子問題的型別不同。
分治演算法的子問題是相互獨立的,子問題與子問題之間並沒有什麼關聯。而動態規劃的子問題就有意思了,他們是重疊的子問題。這麼單純的用文字來描述可能有點難以理解,我們借用一張圖來宣告

從這張圖我們可以看出,當我們對所求問題進行子問題劃分時,會產生很對相同的子問題,這些子問題在計算機中我們已經求得結果一次,再另一個子問題中,我們如果再讓計算機求解相同的子問題,顯然有點不太地道。
而這種一遍一遍求解相同子問題,如果當問題為n時,顯然我們付出的時間是相當大的。
為了解決這個問題,動態規劃表示,可以用一張陣列來記錄下我們之前已經求解的子問題結果,當我們再次呼叫時,只需先從陣列中查詢是否有所求子問題的結果,如果有,皆大歡喜,直接把答案調出來使用,如果沒有,就將該問題的解儲存進去,為了下一次查詢提供結果。
這麼看來,我們就是在用空間換時間,實際上就是如此。
簡單來說,求解動態規劃的核心是窮舉,因為要求最值,所以要把所有可行的答案窮舉出來,然後在其中找最值。而在我們窮舉的過程中,我們會把碰到的解相同的子問題用一個陣列記錄下來。

通過上述所言,我們講清楚了什麼是動態規劃。

進而,我們可以總結出,要想使用動態規劃,我們的問題必不可少兩個基本要素是:

  • 最優子結構性質
  • 重疊子問題

當我們的問題具備這兩個基本要素後,我們便可以考慮使用動態規劃來求解問題。

0-1揹包問題

實現我們來了解一下什麼是0-1揹包問題。
在學習到動態規劃演算法之前,相想必我們也接觸過貪心演算法。
而在貪心演算法中,我們也肯定接觸過一個經典案例,那就是用貪心演算法求解揹包問題。
沒接觸過也沒有關係,我們只是用貪心演算法解揹包問題來類比一下,所以我們只需要瞭解用貪心演算法解決揹包問題的大體思路
用貪心演算法解揹包問題的基本步驟: 首先計算每種物品單位重量的價值v[i]/w[i],然後,依貪心選擇策略,將盡可能多的單位重量價值最高的物品裝入揹包。若將這種物品全部裝入揹包後,揹包內的物品總重量未超過C,則選擇單位重量價值次高的物品並儘可能多地裝入揹包。依此策略一直地進行下去,直到揹包裝滿為止。
貪心演算法解決揹包問題,無疑是一種十分方便的方法,但是在0-1揹包問題中,我們增加了一條限制,就是我們並不能取得平均價值,比如說一個蘋果重5斤,價值是5,平均價值是1,但是當揹包還剩下2斤的容量時,我們不可能裝下2/5個蘋果,所以,就衍生出來了我們的0-1揹包問題:

有n個物品,它們有各自的體積和價值,現有給定容量的揹包,如何讓揹包裡裝入的物品具有最大的價值總和?

那麼我們就來解決一下這個不可分割的揹包問題。
解題之前先要梳理思路,該題的思路是一般動態規劃的解題思路,就是將大問題化成子問題,然後動態填表的過程。而該題的核心是如何動態填表。

揹包問題抽象化

把揹包問題抽象化,Vi表示第 i 個物品的價值,Wj表示第 i 個物品的體積(重量),設dp[i][j]為揹包為j時前i個物品的總價值。
也就是說,dp[1]2]的值就是我們揹包為2時,裝入第一個物品時的總價值。
在這裡不得不說一句,我們的i,j最好是從1開始取值,因為在我們的遞推方程中,我們可以看到[j-1],[i-1],這時如果我們從0開始取值,我們就會發現出現陣列越界的現象。
在這裡可能會很難看出我們的狀態轉移方程,我們不妨打表來推出我們的dp陣列。(dp就是我們的遞推陣列,i表示物品,j表示揹包容量,dp值為總價值)
這樣我們就可以通過打表
首先初始化邊界,dp(0,j)=dp(i,0)=0由於我們會用到dp[i-1][j-1],如果我們的i,j從0開始迭代,就容易出現越界,所以這裡我們通常迭代從1開始。

W0-W4是物品編號,M0-M8是揹包的容量,表中綠色區域表示dp值,即該揹包容量下,前n個物品可裝入的最大價值。

通過打表,當我們打到M3,W2時,我們發現一個問題,就是在dp[2][3],我們有兩種選擇:即容量為3,我們可以選W2,也可以選擇W1,因為dp為可裝入物品的價值的最大值,這是我們需要比較W1與W2的價值,選擇價值大的一個,如果價值W2>W1,則dp[2][3]=W2的價值,否則dp的值是W1的價值,在此時,我們可以看到W2>W1,所以我們填入W2的價值。

如此繼續打表

我們可以看到,當我們的揹包容量不足以裝下新的物品時,實際上它的值是相同揹包容量下,前n-1個物品的最大價值。即dp[i][j]=dp[i-1][j];
當可以裝下新加入的物品時,得到新的dp遞推公式。
但是在得到遞推公式之前我們需要知道,我們的dp[i][j]值是最大值,也就是說。我們需要比較,即max(dp[i][j-wi]+新物品的價值,dp[i-1][j]),取最值。
這樣,通過打表的方式,我們得到了遞推公式。
做到這裡,我們就已經將0-1揹包問題解決。
接下來我們來實現程式碼

int W[30],C[30];//w是物品重量,C是物品價值,測試數量不能超過30
int main()
{
    int,m,n;//m為揹包容量,n為物品數量
    scanf("%d%d",&m&n);
    for(int i=0;i<n;i++){
        scanf("%d%d",&w[i]&c[i]);
    }
    for(int i = 1;i<=n;i++){
        for(int j =1;j<=m;j++){
            if(j>w[i]){
                dp[i][j]=dp[i-1][j];
            }else{
                dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+c[i]);
            }
        }
    }
}

程式碼優化

通過程式碼,我們實現了動態規劃解決0-1揹包問題。但是我們不難發現,這樣通過一個二維表十分的浪費空間,我們可不可以優化程式碼呢。

我們來看遞推公式,不難發現,實際上,
dp二維表可以化為一維表。
j > w[i]時,dp[i]=dp[i-1]
j < w[i]時,dp[j] = dp[j - w[i]] + v[i];

#include<stdio.h>
int main() {
    //動態規劃解0-1揹包問題
    //遞推公式:dp[j]=dp[j-w[i]]+v[i]
    int m, n;//m為揹包容量,n為物品數量,n小於20;
    int w[20], v[20],dp[20];//w[i]為第i件物品的重量,v[i]是第i件物品的價值,dp[]為遞推陣列
    scanf("%d %d", &m, &n); //m為揹包容量,n為物品數量,n小於20;
    for (int j = 0; j<= m; j++) {
        dp[j] = 0; //dp置零
    }
    for (int i = 1; i <= n; i++) {
        scanf("%d %d", &w[i], &v[i]);
    }
    for (int i = 1; i <= n; i++) {
        for (int j = m; j >= 0; j--) {
            if (j >= w[i]) {
            dp[j] = dp[j - w[i]] + v[i];
            }
        }
    }
    printf("%d", dp[m]);
    return 0;
}

由此我們用動態規劃將0-1揹包問題講了個大概。

相關文章