有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi, wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出一个整数,表示最大价值。
0
4 5
1 2
2 4
3 4
4 5
8
有 N 件物品和一个容量为 V 的背包,每种物品都有自己的价值w和体积v,并且只能被选择一次,要求在有限的背包容量下,装入的物品总价值最大。
「0-1 背包」是较为简单的动态规划问题,也是其余背包问题的基础。
动态规划是不断决策求最优解的过程,「0-1 背包」即是不断对第 i 个物品的做出决策, 「0-1」正好代表不选与选两种决定。
版本1 :二维
多种物品,每种物品只有一个.求能获得的最大总价值.
我们考虑是否选择第i件物品时,是需要考虑前i-1件物品对答案的贡献的.
当前的状态依赖于之前的状态,可以理解为从初始状态f[0][0] = 0开始决策,有 N 件物品,则需要 N 次决 策,每一次对第 i 件物品的决策,状态f[i][j]不断由之前的状态更新而来
状态转移方程 :f[i][v]=max(f[i−1][v],f[i−1][v−c[i]]+w[i])
状态转移方程是如何得来的?
对于当前第i件物品,我们需要考虑其是否能让我们得到更优解.显然,根据上面的话
我们选择第i件物品的时候,我们要得到体积为v的背包,我们需要通过填充用i-1件物品填充得到的体积为v-c[i]的背包得到体积为v的背包.
我们需要考虑到v-c[i]的情况.
当不选当前第i件物品的时候,就对应了状态转移方程中的f[i-1][v],
而选择的时候就对应了f[i-1][v-c[i]]+w[i].
得出以下俩种情况 :
(1)当前背包容量不够(j < v[i]),没得选,因此前 i 个物品最优解即为前 i−1 个物品最优解:
对应代码:f[i][j] = f[i - 1][j]。
(2)当前背包容量够,可以选,因此需要决策选与不选第 i 个物品:
例子:
总共:
4件物品 背包容量为5
体积 价值
1 2
2 4
3 4
4 5
则应该选,中间两样东西,体积之和为5,价值之和为8
#include
#include
#include
using namespace std;
const int MAXN = 1005;
int v[MAXN]; // 体积
int w[MAXN]; // 价值
int f[MAXN][MAXN]; // f[i][j], j体积下前i个物品的最大价值
int main()
{
int n, m;
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 = 1; j <= m; j++)
{
// 当前背包容量装不进第i个物品,则价值等于前i-1个物品
if(j < v[i])
f[i][j] = f[i - 1][j];
// 能装,需进行决策是否选择第i个物品
else
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
}
cout << f[n][m] << endl;
return 0;
}
版本2:一维
考虑一维如何写!
仔细观察会发现,二维状态中,我们的状态每次都会传递给i(就是说我们的前几行会变得没用.)
这就给了我们写一维dp的机会啊
将状态f[i][j]优化到一维f[j],实际上只需要做一个等价变形。
为什么可以这样变形呢?我们定义的状态f[i][j]可以求得任意合法的i与j最优解,但题目只需要求得最终状态f[n][m],因此我们只需要一维的空间来更新状态。 都是废话,直接往下看重点
(1)状态f[j]定义:N 件物品,背包容量j下的最优解。
(2)注意枚举背包容量j必须从m开始。
(3)为什么一维情况下枚举背包容量需要逆序?在二维情况下,状态f[i][j]是由上一轮i - 1的状态得来的,f[i][j]与f[i - 1][j]是独立的。而优化到一维后,如果我们还是正序,则有f[较小体积]更新到f[较大体积],则有可能本应该用第i-1轮的状态却用的是第i轮的状态。
当初这句话看了很久,大家刚开始看的时候是不是有点看不太明白
这句话什么意思呢?
f[j]从f[j - w]转移过来,那么这里的f[j - w]是指f[i - 1][j - w]还是f[i][j - w]就尤为重要
,如果是正序循环,这样的f[j - w]就是f[i][j - w], 因为我们在算f[j]之前,f[j - w]也就是f[i - 1][j - w]就已经算出来了,如果是倒叙循环,就可以完美解决这个问题, 此时的f[j - w]还没计算,是上一层的结果,即f[i - 1][j - w],所以需要倒着写.
还不太清楚,让我们往下看一个例子体会一下
例子一:假设有3件物品,背包的总体积为10
物品 体积 价值
i = 1 4 5
i = 2 5 6
i = 3 6 7
如果 f[0][j] 总共0件物品,所以最大价值为 0, 即 f[0][j] == 0 也是成立的
如果 j 层循环是递增的:
for (int i = 1; i <= n; i++) {
for (int j = v[i]; j <= m; j++) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
当还未进入循环时(初始状态):
f[0] = 0; f[1] = 0; f[2] = 0; f[3] = 0; f[4] = 0;
f[5] = 0; f[6] = 0; f[7] = 0; f[8] = 0; f[9] = 0; f[10] = 0;
当进入循环 i == 1 时:
f[4] = max(f[4], f[0] + 5); 即max(0, 5) = 5; 即f[4] = 5;
f[5] = max(f[5], f[1] + 5); 即max(0, 5) = 5; 即f[5] = 5;
f[6] = max(f[6], f[2] + 5); 即max(0, 5) = 5; 即f[6] = 5;
f[7] = max(f[7], f[3] + 5); 即max(0, 5) = 5; 即f[7] = 5;
重点来了!!!
f[8] = max(f[8], f[4] + 5); 即max(0, 5 + 5) = 10; 即f[8] = 10;
这里就已经出错了
因为此时处于 i == 1 这一层,即物品只有一件,不存在单件物品满足价值为10
所以已经出错了。
如果 j 层循环是逆序的:
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]);
}
}
当还未进入循环时:
f[0] = 0; f[1] = 0; f[2] = 0; f[3] = 0; f[4] = 0;
f[5] = 0; f[6] = 0; f[7] = 0; f[8] = 0; f[9] = 0; f[10] = 0;
当进入循环 i == 1 时:w[i] = 5; v[i] = 4;
j = 10:f[10] = max(f[10], f[6] + 5); 即max(0, 5) = 5; 即f[10] = 5;
j = 9 :f[9] = max(f[9], f[5] + 5); 即max(0, 5) = 5; 即f[9] = 5;
j = 8 :f[8] = max(f[8], f[4] + 5); 即max(0, 5) = 5; 即f[8] = 5;
j = 7 :f[7] = max(f[7], f[3] + 5); 即max(0, 5) = 5; 即f[7] = 5;
j = 6 :f[6] = max(f[6], f[2] + 5); 即max(0, 5) = 5; 即f[6] = 5;
j = 5 :f[5] = max(f[5], f[1] + 5); 即max(0, 5) = 5; 即f[5] = 5;
j = 4 :f[6] = max(f[4], f[0] + 5); 即max(0, 5) = 5; 即f[4] = 5;
当进入循环 i == 2 时:w[i] = 6; v[i] = 5;
j = 10:f[10] = max(f[10], f[5] + 6); 即max(5, 11) = 11; 即f[10] = 11;
j = 9 :f[9] = max(f[9], f[4] + 6); 即max(5, 11) = 5; 即f[9] = 11;
j = 8 :f[8] = max(f[8], f[3] + 6); 即max(5, 6) = 6; 即f[8] = 6;
j = 7 :f[7] = max(f[7], f[2] + 6); 即max(5, 6) = 6; 即f[7] = 6;
j = 6 :f[6] = max(f[6], f[1] + 6); 即max(5, 6) = 6; 即f[6] = 6;
j = 5 :f[4] = max(f[5], f[0] + 6); 即max(5, 6) = 6; 即f[5] = 6;
当进入循环 i == 3 时: w[i] = 7; v[i] = 6;
j = 10:f[10] = max(f[10], f[4] + 7); 即max(11, 12) = 12; 即f[10] = 12;
j = 9 :f[9] = max(f[9], f[3] + 6); 即max(11, 6) = 11; 即f[9] = 11;
j = 8 :f[8] = max(f[8], f[2] + 6); 即max(6, 6) = 6; 即f[8] = 6;
j = 7 :f[7] = max(f[7], f[1] + 6); 即max(6, 6) = 6; 即f[7] = 6;
j = 6 :f[6] = max(f[6], f[0] + 6); 即max(6, 6) = 6; 即f[6] = 6;
状态转移方程为:f[j] = max(f[j], f[j - v[i]] + w[i] 。
我们发现,逆序完美的解决了这个问题,看到这里大家是不是对为什么要逆序有一些认识了
还不清楚?,
让我们看第二个例子体会一下:
此时我们手中有一件体积为2,价值为6的物品.
很明显我们可以得到一个填充了2体积,价值为6的背包. (黑色代表体积,红色代表价值)
(中间填充3的过程略过)
此时当我们填充到体积为4的背包的时候,我们发现可以通过填充体积为2的背包得到更大价值.
但是,我们手中只有一件这样的物品,再去填充不就违反了题目规定?
同理,当我们枚举到更大体积的时候,依旧不满足要求.
所以,顺序枚举体积是完全背包做法.(即物品有很多件.
其实根据上面应该不难理解,但是既然说到这里,我还是决定写一下.
同样,我们依旧有一个体积为2的物品。
此时倒叙,我们填充背包.(依旧是到4)
我们发现,我们还是可以通过填充体积为2的背包得到体积为4的背包.
不过这个时候,体积为2的背包中并没有物品.
因此,我们填充体积得到的4的背包,只是得到了体积为2的物品的价值.
再度枚举到体积为2的背包时,我们才填充得到体积为2的背包.
而接下来枚举到的物品中,不会再重复使用这一件物品.
即最终的图会是这样(其他部分略过)
这样我们就能满足题目中的,每个物品只有一件的情况.
而这也是我们01背包倒叙枚举体积的原因.
for(int i = 1; i <= n; i++)
for(int j = m; j >= 0; j--)
{
if(j < v[i])
f[i][j] = f[i - 1][j]; // 优化前
f[j] = f[j]; // 优化后,该行自动成立,可省略。
else
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]); // 优化前
f[j] = max(f[j], f[j - v[i]] + w[i]); // 优化后
}
实际上,只有当枚举的背包容量 >= v[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]);
}
最终代码:
#include
#include
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
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 = m; j >= v[i]; j -- )
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}
如果对小伙伴有帮助的话,不妨点个赞哟!