1. 程式人生 > 其它 >01揹包問題詳解

01揹包問題詳解

本文詳細整理了常見的幾類揹包問題的多種解法。內容涉及01揹包問題,完全揹包問題,多重揹包問題,分組揹包問題等。

引言

揹包問題是動態規劃(DP)的一類問題。

揹包問題的核心其實就是組合問題,在一個揹包中有若干物品,在某種限制條件下,選出最好的組合。

01揹包問題

特點:每件物品最多隻能用一次。

有 N 件物品和一個容量是 V 的揹包。每件物品只能使用一次。
第 i 件物品的體積是 vi,價值是 wi。
求解將哪些物品裝入揹包,可使這些物品的總體積不超過揹包容量,且總價值最大。
輸出最大價值。

輸入格式
第一行兩個整數,N,V,用空格隔開,分別表示物品數量和揹包容積。
接下來有 N 行,每行兩個整數 vi,wi,用空格隔開,分別表示第 i 件物品的體積和價值。

輸出格式
輸出一個整數,表示最大價值。

資料範圍
0<N,V≤1000
0<vi,wi≤1000
輸入樣例
4 5
1 2
2 4
3 4
4 5
輸出樣例:
8

思路:

如果採用暴力列舉每一件物品放或者不放進揹包,有兩種選擇,所以時間複雜度為\(O(2^n)\),非常大。

接下來考慮動態規劃求解。

題解一:先嚐試二維解法。

我們可以定義一個二維陣列dp儲存最大價值,其中dp[i][j] 表示前i 件物品體積不超過j (即此時揹包容量

為j)的情況下能達到的最大價值。

在我們遍歷到第i 件物品時,在當前揹包總容量為j 的情況下,

  1. 如果我們不將物品i 放入揹包,那麼dp[i][j]= dp[i-1][j],即前i 個物品的最大價值等於只取前i-1 個

    物品時的最大價值;

  2. 如果我們將物品i 放入揹包,假設第i 件物品體積為wi,價值為vi,那麼我們得到

    dp[i][j] = dp[i-1][j-w[i]] + v[i]。我們只需在遍歷過程中對這兩種情況取最大值即可,總時間複雜度和空間複雜度都為\(O(NV)\)

綜合上面提到的2種選擇策略,我們可以得到狀態轉移方程

dp[i][j] = max{dp[i-1][j],dp[i-1][j-w[i]] + v[i]}

確定初始化邊界,dp[0][0] = 0 .

注意理解誤區

dp[i][j]裡的i不是表示選擇了前i個物品,而是表示對前i個物品做出兩中策略的選擇;

裡面的j不是表示當前物品的總體積等於j,而是表示前i 件物品體積不超過j 。

程式碼:(二維樸素做法)

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int dp[N][N]; // dp[0][0] = 0
int v[N],w[N];
int n,m;

int main(){
    cin >> n >> m;
    
    for (int i = 1;i <= n;i++) cin >> v[i] >> w[i];
    
    for (int i = 1;i <= n;i++)
        for (int j = 0;j <= m;j++){
            dp[i][j] = dp[i-1][j];
            if (j - v[i] >= 0){
                dp[i][j] = max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
            }
        }
    
    cout << dp[n][m];
    return 0;
}

題解二:再嘗試一維優化。

也就是對二維做法等價變形得到一維做法。

我們可以進一步對0-1 揹包進行空間優化,將空間複雜度降低為\(O(V)\)。時間複雜度已經不能再優化了。

從二維變成一維,相當於把二維中第一個維度變成迴圈滾動只有1行的陣列dp[N]

如果我們仍然從左往右計算dp[j],那麼可能存在汙染,因為後面的資料根據前面遞推而來,在滾動的時候可能要用到dp[i-1](即上一次迴圈的資料時,實際上這個位置的資料已經在這次迴圈時被更新過了,用到的是dp[i]的資料,那麼就出錯了。

只有通過逆序列舉v,即從右往左滾動陣列,這次計算dp[i]時依然根據上次迴圈遞推而來,而且dp[i-v[i]]並沒有被汙染,才能得到正確結果。

我們注意到在處理資料時,我們是一個物品一個物品,一個一個體積的列舉。

因此我們可以不必開兩個陣列記錄體積和價值,而是邊輸入邊處理。這樣可以進一步壓縮空間。

程式碼:

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int dp[N];
int n,m;
int v,w;
int main(){
    cin >> n >> m;
    for (int i = 1;i <= n;i++){
        cin >> v >> w; // 邊輸入邊處理
        for (int j = m;j >= v;j--){
            dp[j] = max(dp[j],dp[j-v]+w);
        }
    }    
    cout << dp[m];
    return 0;
}