通過前面三篇動態規劃文章的介紹,相信你們對動態規劃、分治、貪心有了充分的理解,對動態規劃的 3 個核心問題、其本質也有了瞭解。java
紙上得來終覺淺,絕知此事要躬行。面試
那麼今天開始咱們來聊聊具體的那些面試時常考的題目。算法
(尚未看過前三篇文章的同窗齊姐叫你補課啦~)數組
(一):初識動態規劃spa
月黑風高的夜晚,張三開啓了法外狂徒模式:他揹着一個可裝載重量爲 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 = 0 或 w = 0,那麼:
dp0 = dp... = 0
解釋:
對前 0 個物品而言,不管揹包容量等於多少,裝入的價值爲 0;當揹包容量爲 0 時,不管裝入前多少個物品(由於一個都裝不進去),揹包裏的價值依舊爲 0。
根據這個定義,咱們求的最終答案就是dp[N][W]
咱們如今找出了狀態,並找到了 base case,那麼狀態之間該如何轉移呢(狀態轉移方程)?
dpi 表示:對於前i
個物品,當前揹包的容量爲w
,這種狀況下能夠裝的最大價值是dp[i][w]
。
思考:對於當前第 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
咱們首先初始化一行和一列 0,分別對應dp0 和 dpi。
那麼第一個問號處應該填什麼呢?
咱們根據上述表述的狀態轉移關係來判斷:
當前第一個物品的重量 4 > 揹包容量,故裝不進去,因此繼承上一個結果。
上一個結果是什麼呢?
就是第 i - 1個物品,也就是第 0 個,和W = 1時的價值:
if (wt[i] > W) { dp[i][W] = dp[i-1][W]; //繼承上一個結果 }
此時方框裏的值爲 0,故第一個問號這裏應該填 0
如今咱們走到了當揹包容量 W = 2 的時候,此時當前 i (依舊第一個物品)可否裝進揹包裏呢?
咱們發現 4 > 2,此時仍是裝不進去,那麼一樣繼承上一個結果。
上一個結果是 i 不變(依舊是第 0 個物品),W = 2,因此結果依舊爲 0。
如今來到 W = 3,發現依舊裝不進去,因此填 0。
下一步到 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:
Option B:
此時第一個物品的重量爲 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
下一步咱們來到 W = 5 這裏,此時依舊是第一個物品,質量 4 < 5(揹包容量),咱們能夠裝裏邊。
而後咱們在
Option A:
Option B:
此時第一個物品的重量爲 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 循環
for(int i = 1;i <=N;i++){ ... }
而後寫入狀態轉移方程
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 上並無這個原題,因此對於揹包問題,最重要的是它的變種。
揹包問題是一大類問題的統稱,很大一部分動態規劃的題深層剖析均可以轉換爲揹包問題。
因此還須要理解體會揹包問題的核心思想,再將此種思想運用到其它一類揹包問題的問題上。
那麼揹包問題還有哪些變化呢?咱們下期見~