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 的情況下,
-
如果我們不將物品i 放入揹包,那麼
dp[i][j]= dp[i-1][j]
,即前i 個物品的最大價值等於只取前i-1 個物品時的最大價值;
-
如果我們將物品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;
}