經典動態規劃:0-1 揹包問題
-----------
後臺天天有人問揹包問題,這個問題其實不難啊,如果我們號動態規劃系列的十幾篇文章你都看過,藉助框架,遇到揹包問題可以說是手到擒來好吧。無非就是狀態 + 選擇,也沒啥特別之處嘛。
今天就來說一下揹包問題吧,就討論最常說的 0-1 揹包問題。描述:
給你一個可裝載重量為 W
的揹包和 N
個物品,每個物品有重量和價值兩個屬性。其中第 i
個物品的重量為 wt[i]
,價值為 val[i]
,現在讓你用這個揹包裝物品,最多能裝的價值是多少?
舉個簡單的例子,輸入如下:
N = 3, W = 4
wt = [2, 1, 3]
val = [4, 2, 3]
演算法返回 6,選擇前兩件物品裝進揹包,總重量 3 小於 W
題目就是這麼簡單,一個典型的動態規劃問題。這個題目中的物品不可以分割,要麼裝進包裡,要麼不裝,不能說切成兩塊裝一半。這就是 0-1 揹包這個名詞的來歷。
解決這個問題沒有什麼排序之類巧妙的方法,只能窮舉所有可能,根據我們「動態規劃詳解」中的套路,直接走流程就行了。
動規標準套路
看來我得每篇動態規劃文章都得重複一遍套路,歷史文章中的動態規劃問題都是按照下面的套路來的。
第一步要明確兩點,「狀態」和「選擇」。
先說狀態,如何才能描述一個問題局面?只要給幾個物品和一個揹包的容量限制,就形成了一個揹包問題呀。所以狀態有兩個,就是「揹包的容量」和「可選擇的物品」。
再說選擇,也很容易想到啊,對於每件物品,你能選擇什麼?選擇就是「裝進揹包」或者「不裝進揹包」嘛
明白了狀態和選擇,動態規劃問題基本上就解決了,只要往這個框架套就完事兒了:
for 狀態1 in 狀態1的所有取值:
for 狀態2 in 狀態2的所有取值:
for ...
dp[狀態1][狀態2][...] = 擇優(選擇1,選擇2...)
PS:此框架出自歷史文章 團滅 LeetCode 股票問題。
第二步要明確 dp
陣列的定義。
首先看看剛才找到的「狀態」,有兩個,也就是說我們需要一個二維 dp
陣列。
dp[i][w]
的定義如下:對於前 i
個物品,當前揹包的容量為 w
,這種情況下可以裝的最大價值是 dp[i][w]
。
比如說,如果 dp[3][5] = 6
PS:為什麼要這麼定義?便於狀態轉移,或者說這就是套路,記下來就行了。建議看一下我們的動態規劃系列文章,幾種套路都被扒得清清楚楚了。
根據這個定義,我們想求的最終答案就是 dp[N][W]
。base case 就是 dp[0][..] = dp[..][0] = 0
,因為沒有物品或者揹包沒有空間的時候,能裝的最大價值就是 0。
細化上面的框架:
int dp[N+1][W+1]
dp[0][..] = 0
dp[..][0] = 0
for i in [1..N]:
for w in [1..W]:
dp[i][w] = max(
把物品 i 裝進揹包,
不把物品 i 裝進揹包
)
return dp[N][W]
PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,全部發布在 labuladong的演算法小抄,持續更新。建議收藏,按照我的文章順序刷題,掌握各種演算法套路後投再入題海就如魚得水了。
第三步,根據「選擇」,思考狀態轉移的邏輯。
簡單說就是,上面偽碼中「把物品 i
裝進揹包」和「不把物品 i
裝進揹包」怎麼用程式碼體現出來呢?
這就要結合對 dp
陣列的定義和我們的演算法邏輯來分析了:
先重申一下剛才我們的 dp
陣列的定義:
dp[i][w]
表示:對於前 i
個物品,當前揹包的容量為 w
時,這種情況下可以裝下的最大價值是 dp[i][w]
。
如果你沒有把這第 i
個物品裝入揹包,那麼很顯然,最大價值 dp[i][w]
應該等於 dp[i-1][w]
,繼承之前的結果。
如果你把這第 i
個物品裝入了揹包,那麼 dp[i][w]
應該等於 dp[i-1][w - wt[i-1]] + val[i-1]
。
首先,由於 i
是從 1 開始的,所以 val
和 wt
的索引是 i-1
時表示第 i
個物品的價值和重量。
而 dp[i-1][w - wt[i-1]]
也很好理解:你如果裝了第 i
個物品,就要尋求剩餘重量 w - wt[i-1]
限制下的最大價值,加上第 i
個物品的價值 val[i-1]
。
綜上就是兩種選擇,我們都已經分析完畢,也就是寫出來了狀態轉移方程,可以進一步細化程式碼:
for i in [1..N]:
for w in [1..W]:
dp[i][w] = max(
dp[i-1][w],
dp[i-1][w - wt[i-1]] + val[i-1]
)
return dp[N][W]
最後一步,把偽碼翻譯成程式碼,處理一些邊界情況。
我用 C++ 寫的程式碼,把上面的思路完全翻譯了一遍,並且處理了 w - wt[i-1]
可能小於 0 導致陣列索引越界的問題:
int knapsack(int W, int N, vector<int>& wt, vector<int>& val) {
// base case 已初始化
vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= N; i++) {
for (int w = 1; w <= W; w++) {
if (w - wt[i-1] < 0) {
// 這種情況下只能選擇不裝入揹包
dp[i][w] = dp[i - 1][w];
} else {
// 裝入或者不裝入揹包,擇優
dp[i][w] = max(dp[i - 1][w - wt[i-1]] + val[i-1],
dp[i - 1][w]);
}
}
}
return dp[N][W];
}
至此,揹包問題就解決了,相比而言,我覺得這是比較簡單的動態規劃問題,因為狀態轉移的推導比較自然,基本上你明確了 dp
陣列的定義,就可以理所當然地確定狀態轉移了。
_____________
我的 線上電子書 有 100 篇原創文章,手把手帶刷 200 道力扣題目,建議收藏!對應的 GitHub 演算法倉庫 已經獲得了 70k star,歡迎標星!