题目描述
小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的空间,并且具有不同的价值。
小明的行李空间为 N,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料只能选择一次,并且只有选与不选两种选择,不能进行切割。
输入描述
第一行包含两个正整数,第一个整数 M 代表研究材料的种类,第二个正整数 N,代表小明的行李空间。
第二行包含 M 个正整数,代表每种研究材料的所占空间。
第三行包含 M 个正整数,代表每种研究材料的价值。
输出描述
输出一个整数,代表小明能够携带的研究材料的最大价值。
输入示例
6 1
2 2 3 1 5 2
2 3 1 5 4 3
输出示例
5
提示信息
小明能够携带 6 种研究材料,但是行李空间只有 1,而占用空间为 1 的研究材料价值为 5,所以最终答案输出 5。
数据范围:
1 <= N <= 1000
1 <= M <= 1000
研究材料占用空间和价值都小于等于 1000
思路1:二维dp数组
经典的零一背包问题,定义一个二维的dp数组,行是物品的编号,从 0 ∼ M − 1 0\sim M-1 0∼M−1,列是背包的容量,从 0 ∼ N 0\sim N 0∼N,所以我们定义的二维dp数组行数为 M M M,列数为 N + 1 N+1 N+1。另外我定义了一个一维数组 w e i g h t weight weight存放物品的质量,再定义一个一维数组 v a l u e value value存放物品的价值,它们的维度都是 M M M。
怎么初始化dp数组呢:
首先是数组的第一行,我们把行索引固定为 0 0 0,列索引 j j j从0递增到 N N N,对应dp数组每一个元素是 d p [ 0 ] [ j ] dp[0][j] dp[0][j],如果此时的 j < w e i g h t [ 0 ] j < weight[0] j<weight[0],意味着背包放不下第一个物品(前一项是背包的容量,后一项是第一个物品的质量),这时候 d p [ 0 ] [ j ] dp[0][j] dp[0][j]赋值为 0 0 0,否则都赋值为 v a l u e [ 0 ] value[0] value[0](第一个物品的价值)。
接着是数组的第一列,列索引固定为 0 0 0,表示背包的容量为 0 0 0,这时候无论物品的质量是多少,都放不进背包,这一列的所有元素价值都为 0 0 0。
剩余的元素行索引的范围从 1 ∼ M − 1 1\sim M-1 1∼M−1,列索引从 1 ∼ N 1\sim N 1∼N,其dp数组的递推公式为:
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]) dp[i][j]=max(dp[i−1][j],dp[i−1][j−weight[i]]+value[i])
这里可能出现数组越界的情况:
j
−
i
t
e
m
[
i
]
[
0
]
<
0
j - item[i][0]\lt 0
j−item[i][0]<0
所以dp数组的递推公式我们改为
d p [ i ] [ j ] = { m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) i f j − w e i g h t [ i ] ≥ 0 d p [ i − 1 ] [ j ] i f j − w e i g h t [ i ] < 0 dp[i][j]=\left\{\begin{aligned} &max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]) &if \quad j - weight[i]\ge 0\\ &dp[i - 1][j] & if \quad j - weight[i]\lt 0\end{aligned}\right. dp[i][j]={max(dp[i−1][j],dp[i−1][j−weight[i]]+value[i])dp[i−1][j]ifj−weight[i]≥0ifj−weight[i]<0
我们看到这个递推公式中 d p [ i ] [ j ] ( 1 < = i < M , 1 < = j < = N ) dp[i][j](1<=i<M, 1<=j<=N) dp[i][j](1<=i<M,1<=j<=N)和它的原始初始值没有关系,我们可以初始化为任意值,为了方便起见我们先把这个dp数组全部赋值为0。然后赋值第一行就可以了。
vector<vector<int>> dp(M, vector<int>(N + 1, 0));
for (int i = item[0][0]; i <= N; ++i)
dp[0][i] = item[0][1];
这里的遍历顺序我们可以先遍历背包再遍历物品(先列后行),也可以先遍历物品再遍历背包(先行后列)。因为当前的元素的值取决于它的左上角的值,这些值都是确保存在的。
完整代码(先遍历物品再遍历背包):
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int M;
int N;
cin >> M >> N;
vector<int> weight(M);
vector<int> value(M);
for (int i = 0; i < M; ++i)
cin >> weight[i];
for (int i = 0; i < M; ++i)
cin >> value[i];
vector<vector<int>> dp(M, vector<int>(N + 1, 0));
for (int i = weight[0]; i <= N; ++i)
dp[0][i] = value[0];
for (int i = 1; i < M; ++i)
{
for (int j = 1; j <= N; ++j)
{
if (j < weight[i])
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
cout << dp[M-1][N];
return 0;
}
注意这里的背包的物品也不需要排序。
我们也可以先遍历背包再遍历物品,因为这两种遍历方式一种是先行后列,一种是先列后行,它们都可以保证当前遍历的元素上一行一直有值。因为递推公式里我们需要用到上一行的历史数据。
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int M;
int N;
cin >> M >> N;
vector<int> weight(M);
vector<int> value(M);
for (int i = 0; i < M; ++i)
cin >> weight[i];
for (int i = 0; i < M; ++i)
cin >> value[i];
vector<vector<int>> dp(M, vector<int>(N + 1, 0));
for (int i = weight[0]; i <= N; ++i)
dp[0][i] = value[0];
for (int j = 1; j <= N; ++j) //先遍历背包,列
{
for (int i = 1; i < M; ++i) //后遍历物品,行
{
if (j < weight[i])
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
cout << dp[M-1][N];
return 0;
}
思路2:滚动一维dp数组
我们也可以使用滚动一维dp数组来解决这个问题。相当于把二维数组做了一个压缩,我们需要先遍历物品,再遍历背包,并且物品是正序遍历,背包是逆序遍历,注意这里的顺序是不能改变的,否则不是使用上一行的历史数据(可以和二维dp数组对比)。我们的动态规划的递推公式为:
d p [ j ] = { m a x ( d p [ j ] , d p [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) i f j − w e i g h t [ i ] ≥ 0 d p [ j ] i f j − w e i g h t [ i ] < 0 dp[j] = \left\{\begin{aligned} &max(dp[j], dp[j - weight[i]] + value[i]) &if \quad j - weight[i]\ge 0\\ &dp[j] & if \quad j - weight[i]\lt 0\end{aligned}\right. dp[j]={max(dp[j],dp[j−weight[i]]+value[i])dp[j]ifj−weight[i]≥0ifj−weight[i]<0
这里的i还是代表物品的编号,j代表背包的容量。
为什么要使用倒序遍历背包呢?虽然是一维数组,但是性质和二维背包差不多。从每一个元素往前看,因为是倒序遍历,所以前面的元素都还没被更新过,看到的都是“上一层的元素”,倒序遍历的前提下前面的元素都没有改变,相当于是二维dp数组上一层的元素,元素只使用了一次,所以不会重复。
因为二维数组是根据左上元素来求的,而一维数组自然就是靠左侧来求的。倒序的时候左边元素再刷新前都是上一层的数据,但正序就不一样了,正序的时候,左边的元素刚刚刷新过,也就是左边的元素已经是本层的了,意味着什么这样会导致一个物品反复加好几次。遍历的顺序也不能改变,因为一维数组如果先遍历背包再遍历物品同一个背包容量一直被更新(物品的编号在增加),遍历下一个背包容量的时候所知道的数据就是是最后一个编号对应的背包的最大价值,而不是上一个编号对应的背包的最大价值,和我们遍历的逻辑就不符合了。
初始化很简单,因为我们要放置把我们的结果忽略掉(可能取 d p [ j − w e i g h t [ i ] ] + v a l u e [ i ] dp[j - weight[i]] + value[i] dp[j−weight[i]]+value[i])我们把一维数组全部初始化为0就可以了。
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int M;
int N;
cin >> M >> N;
vector<int> weight(M);
vector<int> value(M);
for (int i = 0; i < M; ++i)
cin >> weight[i];
for (int i = 0; i < M; ++i)
cin >> value[i];
vector<int> dp(N+1, 0);
for (int i = 0; i < M; ++i)
{
for (int j = N; j >= 1; --j)
{
if (j < weight[i])
dp[j] = dp[j];
else
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[N];
return 0;
}
这里的代码可以精简,主要是第二个for循环我们可以把j >=1
改为j >= weight[i]
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int M;
int N;
cin >> M >> N;
vector<int> weight(M);
vector<int> value(M);
for (int i = 0; i < M; ++i)
cin >> weight[i];
for (int i = 0; i < M; ++i)
cin >> value[i];
vector<int> dp(N+1, 0);
for (int i = 0; i < M; ++i)
{
for (int j = N; j >= weight[i]; --j)
{
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[N];
return 0;
}
注意使用了一维的dp数组,我们就不能使用先背包后物品的遍历顺序了(先列后行),因为一维的dp数组不能保证上一行的历史数据保存下来,只有先行后列我们才能使用到上一行的历史数据。
一维的dp数组在写法上更加的简洁,我们要记住,先物品再背包的遍历顺序,以及物品正序,背包逆序,代码就不难写了。