01背包
Description
一个旅行者有一个最多能装 M 公斤的背包,现在有 n 件物品,它们的重量分别是W1,W2,...,Wn,它们的价值分别为C1,C2,...,Cn,求旅行者能获得最大总价值。
Input
第一行:两个整数,M(背包容量,M≤200)和N(物品数量,N≤30);
第2..N+1行:每行二个整数Wi,Ci,表示每个物品的重量和价值。
Output
仅一行,一个数,表示最大总价值。
于是我们可以用一个二维数组dp[n][m]来存储状态。dp[i][j]就表示当背包容量为j时对第i个物品进行决策的最优值。决策就是选与不选。若是不选的话dp[i][j]就可以由dp[i - 1][j]更新而来;若是选的话,则需要给第i个物品腾出空间,所以dp[i][j]就由dp[i - 1][j - w[i]]更新而来。于是就有了状态转移方程:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]])。最后的答案便是dp[n][m]。
不过还有一个小细节需要处理:若当前背包容量不足以装下第i个物品时,则不选,此时dp[i][j]就由dp[i - 1][j]更新而来。
于是这题便有了代码:
#include <iostream>
#define maxm 200 + 5
#define maxn 30 + 5
using namespace std;
int w[maxn], c[maxn];
int n, m;
int dp[maxn][maxm];
int main(){
cin >> m >> n;
for(int i = 1; i <= n; i++)
cin >> w[i] >> c[i];
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
if(j >= w[i]){
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + c[i]);
}else{
dp[i][j] = dp[i - 1][j];
}
cout << dp[n][m] << endl;
return 0;
}
不过认真思考之后不难得出,对第i个物品做决策时只需要由第i-1个物品更新而来,所以存储第1~i-2个物品的决策的数组就浪费掉了,因为每次做决策只需要两层数组。因此我们可以尝试用滚动数组来进行空间的优化:
#include <iostream>
#define maxm 200 + 5
#define maxn 30 + 5
using namespace std;
int w[maxn], c[maxn];
int n, m;
int dp[2][maxm];
int main(){
cin >> m >> n;
for(int i = 1; i <= n; i++)
cin >> w[i] >> c[i];
int p = 0;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(j >= w[i]){
dp[p][j] = max(dp[1 - p][j], dp[1 - p][j - w[i]] + c[i]);
}else{
dp[p][j] = dp[1 - p][j];
}
}
p = 1 - p;//如果p=1,1-p可以得到0;如果p=0,1-p可以得到1
}
cout << dp[1 - p][m] << endl;//最后一层循环对p多操作了一次,所以退回去
return 0;
}
但是我们现在可不可以多这样一个操作:当前一层数组更新完后,将当前的数组复制到下一层数组,然后下一层数组以自己为基础进行更新呢?貌似是可以的:
但如果这个物品的价值足够大,会发生什么呢?
如图,这样的话一个物品就被选择了两次或以上,这是违背了题意的.不过真的不能够让数组以自己为基础更新自己吗?并不.由于dp[i][j]必须由之前的数据更新而来,所以我们把dp[i][j]之前的东西放到最后更新就好了嘛.即倒序循环.然后动态数组复制到下一层的操作干脆就可以省略了,直接用一维数组:
#include <iostream>
#define maxm 200 + 5
#define maxn 30 + 5
using namespace std;
int w[maxn], c[maxn];
int m, n;
int dp[maxm];
int main(){
cin >> m >> n;
for(int i = 1; i <= n; i++)
cin >> w[i] >> c[i];
for(int i = 1; i <= n; i++)
for(int j = m; j >= w[i]; j--)
dp[j] = max(dp[j], dp[j - w[i]] + c[i]);
cout << dp[m] << endl;
return 0;
}
截止现在,我们空间上已经在做了非常大的优化了:从一开始O(NM)的二维数组到都来的O(2M)的滚动数组,再到现在的O(M)的一位数组,简直优化了不要太多.但时间上是否也可以优化呢?
显然是可以的.假设我们现在已经对第i-1个物品进行了决策,并且背包的总容量足够大.那么,对于剩下的第i~n个物品的决策最多需要多少空间呢?也就是全部选的情况,就需要∑(i, n)wi的容量.那么,我们此时就只需要更新数组的第m-∑(i,n)wi位到第m位就可以了,不必再更新第wi到第m-∑(i,n)wi位省去了几次循环.不过这里有个前提,就是背包容量足够大,即m-∑(i,n)wi>wi.下面给出参考代码:
#include <iostream>
#define maxm 200 + 5
#define maxn 30 + 5
using namespace std;
int w[maxn], c[maxn];
int m, n;
int dp[maxm], sum[maxn];
int main(){
cin >> m >> n;
for(int i = 1; i <= n; i++)
cin >> w[i] >> c[i];
for(int i = n; i >= 1; i--){
sum[i] = sum[i + 1] + w[i];
}//求和.sum[i]表示第i个物品到第n个物品的重量和.倒着加可以一边循环过
int tmp;
for(int i = 1; i <= n; i++){
tmp = max(m - sum[i], w[i]);//循环之前先确定下限,否则循环时每次都会求一遍
for(int j = m; j >= tmp; j--)
dp[j] = max(dp[j], dp[j - w[i]] + c[i]);
}
cout << dp[m] << endl;
return 0;
}
美滋滋~
01背包就讲解到这里下面是一些01背包的变式:
1.背包是否能够装满
由于这题不涉及物品价值,所以可以将dp数组定义为bool类型.dp[j]=true表示容量为j的背包能够被装满.于是,如果容量为j-w[i]的背包能够装满,那么容量为j的背包也就能够被装满.我们只需在循环之前把dp[0]初始化成true就可以了.下面给出参考代码:
#include <iostream>
#define maxm 200 + 5
#define maxn 30 + 5
using namespace std;
int w[maxn];
int m, n;
bool dp[maxm];
int main(){
cin >> m >> n;
for(int i = 1; i <= n; i++)
cin >> w[i];
dp[0] = true;
for(int i = 1; i <= n; i++)
for(int j = m; j >= w[i]; j--) if(dp[j - w[i]])
dp[j] = true;
cout << (dp[m] ? "yes" : "no") << endl;
return 0;
}
2.找方案.即最大价值是由哪些物品得来的
首先这是01背包的变式,所以01背包的代码还是照常打上去.既然要找出方案,那我们就需要从所有物品的决策中推导.而之前优化出的一位数组虽然省了空间,却丢失了之前做的决策.所以我们这里只能使用二维数组.那么我们尝试从dp[1][0]开始推导,却发现缺少信息.所以很显然我们需要倒着推,从dp[n][m]开始.那么在容量为m的背包中对第n个物品进行决策,假如决策是"选",那么就将n存储起来,然后问题转变为容量为m-w[i]的背包里对n-1个物品进行决策,以此类推;假如决策时"不选",那么问题就转变为了在容量为m的背包里,对第n-1个物品进行决策.最后把存储的内容输出就行了.
不过还有些小细节需要处理:
(1)怎么存答案?--------显然,我们需要对最先存储的答案最后输出,所以可以用一个栈(stack)来存储.
(2)如果有多种方案,要求输出字典序最小的怎么办?--------由于我们是倒着推导,所以我们对于第i个物品应该尽量地不选为什么呢?如果选了,那么答案里就会有i;而不选的话就会从第i个物品之前的物品进行推导,它们的字典序明显是小于i的.
于是就有了代码:
#include <iostream>
#include <stack>
#define maxm 200 + 5
#define maxn 30 + 5
using namespace std;
int w[maxn], c[maxn];
int m, n;
int dp[maxn][maxm];
stack<int> ans;
int main(){
cin >> m >> n;
for(int i = 1; i <= n; i++)
cin >> w[i] >> c[i];
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
if(j >= w[i]){
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + c[i]);
}else{
dp[i][j] = dp[i - 1][j];
}
for(int i = n; i >= 1; i--)
if(dp[i][m] != dp[i - 1][m]){
ans.push(i);
m -= w[i];
}
while(!ans.empty()){
cout << ans.top() << " ";
ans.pop();
}
return 0;
}