2. 01揹包問題
一、知識架構
(1) \(01\)揹包
\(N\)個物品(\(0\)不選擇,\(1\)選擇\(1\)次),容量\(V\)的揹包(上限),\(v_i\)表示物品的體積,\(w_i\)表示價值
如何組裝揹包,在\(V\)的上限限制情況下,使得價值最大,求價值最大值。
總結:每個物品只有1個,可以選或不選,求在容量限制下的重量(價值)最大值。
(2) 完全揹包
每個物品有無限個,求價值最大是多少。
(3) 多重揹包
每個物品個數不一樣,不是無限,也不是隻有一個,有\(k_i\)的個數限制。
(4) 分組揹包問題
物品有\(n\)組,各一組有若干個。每一組最多選擇一個物品,比如水果組選擇了蘋果,就不能選香蕉。
注意:不一定要裝滿揹包
二、01揹包的遞迴表示法
#include <bits/stdc++.h> using namespace std; const int N=110; int w[N],v[N],n,m; //dfs函式定義的意義: 在已經放入揹包i個物品,剩餘的揹包體積為j的情況下,計算可以獲取到的最大價值 int dfs(int i,int j){ int res=0; //儲存每一步求得的當前最大價值 if(i==n+1) return res; // 當到n+1個物品時就開始回溯 if(w[i]>j) res=dfs(i+1,j); //若當前物品的重量大於揹包容量時,就無法放入揹包。此時,放棄第i個物品,走到i+1個物品前進行下一步嘗試 else res=max(dfs(i+1,j),dfs(i+1,j-w[i])+v[i]);//若能放下,則取放入或不放入兩種情況中較大值 return res; } int main(){ scanf("%d %d",&n,&m); //物品個數與揹包容量 //讀入每個物品的體積和價值 for(int i=1;i<=n;i++) scanf("%d %d",&w[i],&v[i]); cout<<dfs(0,m)<<endl; return 0; }
遞迴表示法的缺點:
因為有大量的重複計算,所以,不出意外的在大資料量的情況下,\(TLE\)了!我們需要找一個辦法進行優化。
三、記憶化搜尋
#include <bits/stdc++.h> using namespace std; const int N = 1010; int w[N], v[N], n, m, dp[N][N]; //引入dp陣列儲存已經計算過的結果 int dfs(int i, int j) { //判斷這種情況是否被計算過,若被計算過就可以直接返回結果不用進行下面的一系列判斷計算 if (dp[i][j] >= 0) return dp[i][j]; int res = 0; if (i == n + 1) return res; if (w[i] > j) res = dfs(i + 1, j); else res = max(dfs(i + 1, j), dfs(i + 1, j - w[i]) + v[i]); dp[i][j] = res; //儲存求出結果 return res; } int main() { //初始化陣列元素為-1 memset(dp, -1, sizeof(dp)); cin >> n >> m; for (int i = 1; i <= n; i++) cin >> w[i] >> v[i]; cout << dfs(0, m) << endl; return 0; }
我們由此又可以得出打表的方法即:
for(int i = 1;i<= n;i++)
for(int j = 1;j <= v;j++)
dp[i][j]=max(dp[i-1][j] ,dp[i-1][j-w[i]] + v[i]);
四、二維陣列法
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main() {
//物品個數n
cin >> n >> m; //m:揹包容積,就是大V
//讀入體積和重量
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
if (j >= v[i]) f[i][j] = max(f[i-1][j], f[i - 1][j - v[i]] + w[i]); //兩種情況取最大值
else f[i][j] = f[i - 1][j];
}
cout << f[n][m] << endl;
return 0;
}
2、為什麼要用一維陣列優化01揹包問題
因為我們看到上面的儲存方式,其實第\(i\)件物品選擇或不選擇兩種方式,都只和\(i-1\)時的情況相關聯,也就是說在\(i-1\)再以前的狀態和資料與\(i\)無關,換句話說,就是沒啥用了,可以清除掉了。這樣一路走一路依賴它前面一行就OK,可以把二維陣列優化為一維。
那麼,怎麼個優化法呢?答案是用一個一維陣列。
這裡最核心的問題是為什麼容量的遍歷要從大到小,和上面二維的不一樣了呢??
這是因為第\(i\)個要依賴於第\(i-1\)個,如果從小到大,那麼\(i-1\)先變化了,而\(i\)說的依賴於\(i-1\),其實是在沒變化前的\(i-1\) 資訊,這麼直接來就錯了。那怎麼才能對呢?就是從大到小反向遍歷,這樣\(i-1\)都是以前的資訊,還沒變,就OK了!有點繞啊,但還算好理解!
五、一維陣列法
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main() {
//物品個數n
cin >> n >> m; //m:揹包容積,就是大V
//讀入體積和重量
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++)
for (int j = m; j >=0; j--) {
if (j >= v[i]) f[j] = max(f[j], f[j - v[i]] + w[i]); //兩種情況取最大值
else f[j] = f[j];
}
cout << f[m] << endl;
return 0;
}
感覺上面的程式碼怪怪的,可以簡寫成下面的樣子:
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main() {
//物品個數n
cin >> n >> m; //m:揹包容積,就是大V
//讀入體積和重量
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++)
for (int j = m; j >=v[i]; j--)
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}
4、01揹包之恰好裝滿
恰好裝滿:
求最大值時,除了\(dp[0]\) 為0,其他都初始化為無窮小 -0x3f3f3f3f
求最小值時,除了\(dp[0]\) 為0,其他都初始化為無窮大 0x3f3f3f3f
不必恰好裝滿: 全初始化為0
為什麼呢?因為初始化的陣列,實際上是在沒有任何物品可以放入揹包的情況下的合法狀態。如果要求揹包恰好裝滿,那麼此時只有容量為0的揹包可以在什麼都不裝且價值為0的情況下被“恰好裝滿”,其他容量的揹包均沒有合法的解,因此屬於未定義的狀態,應該設定為負無窮大。如果揹包不需要被裝滿,那麼任何容量的揹包都有合法解,那就是“什麼都不裝”。這個解的價值為0,所以初始狀態的值都是0。
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main() {
//物品個數n
cin >> n >> m; //m:揹包容積,就是大V
//初始化f[0]=0,其它設定負無窮
memset(f,-0x3f,sizeof f);
f[0]=0;
//讀入體積和重量
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++)
for (int j = m; j >=v[i]; j--) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
cout << f[m] << endl;
return 0;
}