引言
背包問題是動態規划(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;
}