經典動態規劃:0-1 揹包問題

前言

通過前面三篇動態規劃文章的介紹,相信你們對動態規劃、分治、貪心有了充分的理解,對動態規劃的 3 個核心問題、其本質也有了瞭解。java

紙上得來終覺淺,絕知此事要躬行。面試

那麼今天開始咱們來聊聊具體的那些面試時常考的題目。算法

(尚未看過前三篇文章的同窗齊姐叫你補課啦~)數組

(一):初識動態規劃spa

(二):動態規劃的 3 個核心問題code

(三):動態規劃的本質繼承

問題背景

月黑風高的夜晚,張三開啓了法外狂徒模式:他揹着一個可裝載重量爲 W 的揹包去地主家偷東西。get

地主家有 N 個物品,每一個物品有重量和價值兩個屬性,其中第 i 個物品的重量爲 wt[i],價值爲 val[i]it

問張三如今用這個揹包裝物品,最多能裝的價值是多少?io

舉例:

N = 3 //地主家有三樣東西

wt = [2,1,3] //每樣東西的重量

val = [4,2,3] //每樣東西的價值

W = 4 //揹包可裝載重量

算法應該返回 6.

由於選擇第一件物品和第二件物品,在重量沒有超出揹包容量下,所選價值最大。

若是每種物品只能選 0 個或 1 個(即要麼將此物品裝進包裏要麼不裝),則此問題稱爲 0-1 揹包問題;若是不限每種物品的數量,則稱爲無界(或徹底)揹包問題。

今天這篇文章咱們只關注 0-1 揹包問題,下一篇文章再聊徹底揹包問題。

那咱們是如何選擇要裝入的物品的?

思路初探

首先,質量很大價值很小的物品咱們先不考慮(放着地主家金銀財寶珍珠首飾不偷,背出來一包煤...,那也就基本告別盜竊行業了...)

而後呢?再考慮質量大價值也大的?仍是質量較小价值也稍小的?

咱們天然而然想到:裝價值/質量 比值最大的,由於這至少能說明,此物品的「價質比」最大(也即貪心算法,每次選擇當前最優)

那麼這樣裝能保證最後裝入揹包裏的價值最優嗎?

咱們先來看一個例子:

假設有 5 個物品,N = 5,每種物品的質量與價值以下:

W : 20, 30, 40, 50, 60

V : 20, 30, 44, 55, 60

V/W: 1, 1, 1.1, 1.1, 1

揹包容量爲 100

若是按上述策略:優先選「價質比」最大的:即第三個和第四個物品

此時質量:40+50=90

價值:44+55 =99

但咱們知道,此題更優的選擇策略是:選第一個,第二個和第四個

此時質量:20+30+50=100

價值:20+30+55=105

因此,咱們的「價質比」這種貪心策略顯然不是最優策略。

讀過一文學懂動態規劃這篇文章的讀者會發現,以前文章中兌換零錢例子咱們最開始也是採起貪心策略,但最後發現貪心不是最優解,由此咱們引出了動態規劃

沒錯,今天這題也正是動態規劃又一經典的應用。

解題思路

根據動以前的文章咱們知道,動態規劃的核心即:狀態狀態轉移方程

那麼此題的狀態是什麼呢?

狀態

何爲狀態?

說白了,狀態就是已知條件

重讀題意咱們發現:此題的已知條件只有兩個:

  • 揹包容量
  • 可選的物品

題目要求的是在知足揹包容量前提下,可裝入的最大價值。

那麼咱們能夠根據上述狀態定義出 dp 數組,即:

dp[i][w] 表示:對於前i個物品,當前揹包的容量爲w,這種狀況下能夠裝的最大價值是dp[i][w]

咱們天然而然的考慮到以下特殊狀況:

i = 0w = 0,那麼:

dp0 = dp... = 0

解釋:
對前 0 個物品而言,不管揹包容量等於多少,裝入的價值爲 0;

當揹包容量爲 0 時,不管裝入前多少個物品(由於一個都裝不進去),揹包裏的價值依舊爲 0。

根據這個定義,咱們求的最終答案就是dp[N][W]

咱們如今找出了狀態,並找到了 base case,那麼狀態之間該如何轉移呢(狀態轉移方程)?

狀態轉移方程

dpi 表示:對於前i個物品,當前揹包的容量爲w,這種狀況下能夠裝的最大價值是dp[i][w]

思考:對於當前第 i 個物品:

  • 若是沒有把第 i 個物品裝入包裏(第 i 個物品質量大於當前揹包容量):那麼很顯然,最大價值dpi應該等於dpi - 1,沒有裝進去嘛,故當前揹包總價值就等於以前的結果,即第i - 1 個物品以前的總價值 。
  • 若是把第 i 個物品裝入了包裏,那麼 dpi應該等於什麼呢?

它應該等於下面二者裏的較大值:

  1. dpi - 1 //前i - 1個物品,揹包所裝的最大價值
  2. dp[i - 1]w - wt[i]] + val [i] //當前第 i 個物品我裝裏邊了,那麼此時揹包裝入的總價值即爲:當前第 i 個物品的價值 val [i] + 第 i 個物品以前,揹包容量爲w - wt[i](w 減去當前第 i 個物品的質量)dp[i - 1]w - wt[i]] 時的價值

上述兩個若是能夠寫成如下代碼:

//若是第i個物品質量大於當前揹包容量
if (wt[i] > W) {
    dp[i][W] = dp[i-1][W];  //繼承上一個結果
} else {
//在「上一個結果價值」和「把當前第i個物品裝入揹包裏所獲得價值」兩者裏選價值較大的
    dp[i][W] = Math.max(dp[i-1][W],dp[i-1][W-wt[i]] + val[i])
}

例子

咱們接來下再用一個具體的例子,來理解狀態和狀態轉移方程

如今咱們有 4 個物品,物品對應的價值與質量分別如上圖左側所示:

6, 4

2,5

1, 4

8, 1

Step 1

咱們首先初始化一行和一列 0,分別對應dp0dpi

那麼第一個問號處應該填什麼呢?

咱們根據上述表述的狀態轉移關係來判斷:

當前第一個物品的重量 4 > 揹包容量,故裝不進去,因此繼承上一個結果。

上一個結果是什麼呢?

就是第 i - 1個物品,也就是第 0 個,和W = 1時的價值:

if (wt[i] > W) {
    dp[i][W] = dp[i-1][W];  //繼承上一個結果
}

此時方框裏的值爲 0,故第一個問號這裏應該填 0

Step 2

如今咱們走到了當揹包容量 W = 2 的時候,此時當前 i (依舊第一個物品)可否裝進揹包裏呢?

咱們發現 4 > 2,此時仍是裝不進去,那麼一樣繼承上一個結果。

上一個結果是 i 不變(依舊是第 0 個物品),W = 2,因此結果依舊爲 0

Step 3

如今來到 W = 3,發現依舊裝不進去,因此填 0。

Step 4

下一步到 W = 4 這裏了,

此時物品重量 4 = 4(揹包容量),能夠裝裏,那麼按照以前狀態轉移關係應該是:

else {
//在「上一個結果價值」和「把當前第i個物品裝入揹包裏所獲得價值」兩者裏選價值較大的
    dp[i][W] = Math.max(dp[i-1][W],dp[i-1][W-wt[i]] + val[i])
}

Option A:

  • 上一個結果 : dpi - 1,即dp0 = 0

Option B:

  • 把當前第 i 個物品裝入揹包裏所獲得價值dp[i - 1]W - wt[i]] + val [i]

此時第一個物品的重量爲 4,揹包容量爲 4,

故要想裝入重量爲 4 的此物品,那麼揹包先前的容量必須爲當前揹包容量 - 當前物品容量:4 - 4 = 0

咱們隨即找到在沒裝入此物品(重量爲 4,價值爲 6)以前的dp[i -1]W - wt[i]] = dp0 = 0

那麼dp[i -1]W - wt[i]] + val [i] = 0 + 6 = 6

6 和 0 選擇一個最大值,因此這裏問號處應填入6

Step 5

下一步咱們來到 W = 5 這裏,此時依舊是第一個物品,質量 4 < 5(揹包容量),咱們能夠裝裏邊。

而後咱們在

Option A:

  • 上一個結果dp0 = 0

Option B:

  • 把當前第 i 個物品裝入揹包裏所獲得價值dp[i -1]W - wt[i]] + val [i]

此時第一個物品的重量爲 4,揹包容量爲 5

故要想裝入重量爲 4 的此物品,那麼揹包先前的容量必須爲:當前揹包容量 - 當前物品容量:5 - 4 = 1

咱們隨即找到在沒裝入此物品(重量爲 4,價值爲 6)以前的dp[i - 1]W - wt[i]] = dp0 = 0

那麼dp[i -1]W - wt[i]] + val [i] = 0 + 6 = 6

選擇一個最大值,即 6,因此此處應該填入 6

咱們根據以上狀態轉系關係,依次能夠填出空格其它值,最後咱們獲得整個 dp 數組:

V W 0 1 2 3 4 5 6
0 0 0 0 0 0 0 0 0
6 4 0 0 0 0 6 6 6
2 5 0 0 0 0 6 6 6
1 4 0 0 0 0 6 6 6
8 1 0 8 8 8 8 14 14

最後的 dp4:考慮前四個物品,揹包容量爲 6 的狀況下,可裝入的最大價值,即爲所求。

(注意:咱們在這裏求的是 0-1 揹包問題,即某一個物品只能選擇 0 個或 1 個,不能多選!)

代碼

根據以上思路,咱們很容易寫出代碼:

兩層 for 循環

  1. 外層循環 i 遍歷物品(即前幾個物品):
for(int i = 1;i <=N;i++){
        ...
}
  1. 內層循環 j 遍歷 1~W(揹包容量)之間的整數值:

而後寫入狀態轉移方程

for(int j = 0;j <= W;j++){
        //外層循環i,若是第i個物品質量大於當前揹包容量
    if (wt[i] > W) {
        dp[i][W] = dp[i-1][W];  //繼承上一個結果
    } else {
        //在「上一個結果價值」和「把當前第i個物品裝入揹包裏所獲得價值」兩者裏選價值較大的
        dp[i][W] = Math.max(dp[i-1][W],dp[i-1][W-wt[i]] + val[i])
    }
}

由此咱們給出完整代碼:

class solution{
        public int knapsackProblem(int[] wt,int[] val,int size){
                //定義dp數組
                int[][] dp = new int[wt.length][size];
                //對於裝入前0個物品而言,dp數組儲存的總價值初始化爲0
                for(int i = 0;i < size;i++){
                    int[0][i] = 0;
                }
                //對於揹包容量W=0時,裝入揹包的總價值初始化爲0
                for(int j = 0;j < size;j++){
                    int[j][0] = 0;
                }
                //外層循環遍歷物品
                for(int i = 1;i <= N;i++){
                    //內層循環遍歷1~W(揹包容量)
                    for(int j = 0;j <= W;j++){
                        //外層循環i,若是第i個物品質量大於當前揹包容量
                        if (wt[i] > W) {
                            dp[i][W] = dp[i-1][W];  //繼承上一個結果
                        } else {
                            //在「上一個結果價值」和「把當前第i個物品裝入揹包裏所獲得價值」兩者裏選價值較大的
                            dp[i][W] = Math.max(dp[i-1][W],dp[i-1][W-wt[i]] + val[i])
                        }
                    }
                }
        }
}

只要咱們定義好了狀態(dp 數組的定義),理清了狀態之間是如何轉移的,最後的代碼水到渠成。

本文所說的這個 0-1 揹包問題,Leetcode 上並無這個原題,因此對於揹包問題,最重要的是它的變種

揹包問題是一大類問題的統稱,很大一部分動態規劃的題深層剖析均可以轉換爲揹包問題。

因此還須要理解體會揹包問題的核心思想,再將此種思想運用到其它一類揹包問題的問題上。

那麼揹包問題還有哪些變化呢?咱們下期見~

相關文章
相關標籤/搜索