動態規划之背包問題


后台天天有人問背包問題,這個問題其實不難啊,如果我們號動態規划系列的十幾篇文章你都看過,借助框架,遇到背包問題可以說是手到擒來好吧。無非就是狀態 + 選擇,也沒啥特別之處嘛。

今天就來說一下背包問題吧,就討論最常說的 0-1 背包問題。描述:

給你一個可裝載重量為 W 的背包和 N 個物品,每個物品有重量和價值兩個屬性。其中第 i 個物品的重量為 wt[i],價值為 val[i],現在讓你用這個背包裝物品,最多能裝的價值是多少?

舉個簡單的例子,輸入如下:

N = 3, W = 4
wt = [2, 1, 3]
val = [4, 2, 3]

算法返回 6,選擇前兩件物品裝進背包,總重量 3 小於 W,可以獲得最大價值 6。

題目就是這么簡單,一個典型的動態規划問題。這個題目中的物品不可以分割,要么裝進包里,要么不裝,不能說切成兩塊裝一半。這就是 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]

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]

第三步,根據「選擇」,思考狀態轉移的邏輯

簡單說就是,上面偽碼中「把物品 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 開始的,所以對 valwt 的取值是 i-1

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 數組的定義,就可以理所當然地確定狀態轉移了。

PS:我最近精心制作了一份電子書《labuladong的算法小抄》,分為「動態規划」「數據結構」「算法思維」「高頻面試」四個章節,共 60 多篇原創文章,絕對精品,限時開放下載,可掃碼到我的公眾號 labuladong 后台回復關鍵詞「pdf」即可下載:

目錄


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM