- 由 zhongluqiang创建, 最后修改于4月 03, 2022
01背包
题目链接:2. 01背包问题 - AcWing题库
有N件物品和一个容量为V的背包,第i件物品的费用是c[i],价值是w[i],求将哪些物品装入背包可使总费用不超过背包的容量,且价值之和最大。
重点:每件物品仅有一件,可以取或不取。
二维DP解法
定义dp[i][j]表示将第i件物品放入一个容量为j的背包的价值,则其状态转移方程如下:
dp[i][j] = max{dp[i-1][j], dp[i-1][j-c[i]] + w[i]}
上面的这个方程的含义如下:
- 将第
i件物品放入容量为j的背包的最大价值,只需要考虑第i件物品是否放入背包,有放和不放两种情况。 - 如果不放第
i件物品,那么总价值就是前i-1件物品放入容量为j的背包的价值,也就是dp[i-1][j]。 - 如果放入第
i件物品,那么总价值就是第i件物品的价值w[i],加上前i-1件物品放入容量为j-c[i]的背包的价值dp[i-1][j-c[i]]。 - 以上两者取最大值,即为第
i件物品放入容量为j的背包的最大价值。
示例代码实现如下:
#include <iostream>
using namespace std;
#define MAXN 1005
int c[MAXN]; // 物品费用
int w[MAXN]; // 物品价值
int dp[MAXN][MAXN]; // dp[i][j]表示第i件物品放入容量为j的背包的价值
int main() {
int n, v; // 物品数,背包容量
cin >> n >> v;
for(int i = 1; i <= n; i++) {
cin >> c[i] >> w[i];
}
// 注意这里隐含设置了取0件物品和背包容量为0时dp数组对应的值全为0的情况
// for(int i = 0; i <= v; i++) dp[0][i] = 0;
// for(int i = 0; i <= n; i++) dp[i][0] = 0;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= v; j++) {
if(j < c[i]) {
// 当前背包容量j装不下第i个物品,则价值等于前i-1个物品
dp[i][j] = dp[i-1][j];
} else {
// 能装,按方程进行决策
// 注意,当到j恰好能装下全部的前i件物品时,
// 后续的j可直接设置成f[i][j-1],因为后续的j表示背包容量已经有冗余了
dp[i][j] = max(dp[i-1][j], dp[i-1][j-c[i]] + w[i]);
}
}
}
cout << dp[n][v];
}
优化空间复杂度
根据状态转移方程可知,第i件物品的计算过程只与第i-1件物品有关,由于可以想到使用滚动数组的方式,将空间复杂度降为从二维降至一维。
- 定义状态
dp[j],表示N件物品在背包容量为j下的最优解。 - 枚举容量,
j必须从V枚举到0,也就是逆序。 为什么采用逆序,参考以下解释:
状态方程:
dp[j] = max(dp[j], dp[j - c[i]] + w[i])假如枚举到:
i = 3, j = 8, c[3] = 5, w[3] = 1二维:
dp[3][8] = max(dp[2][8], dp[2][3] + w[3])此时的dp[2][8]和dp[2][3]都是上一轮的状态值一维:
dp[8] = max(dp[8], dp[3] + w[3])我们要保证dp[8]和dp[3]都是上一轮的状态值按照逆序的顺序,一维
dp数组的更新顺序为:dp[8], dp[7], dp[6], ... , dp[3]也就是说,在本轮更新的值,不会影响本轮中其他未更新的值!较小的
index对应的状态是上一轮的状态值!如果按照顺序进行更新,
dp[3] = max(dp[3], dp[0] + w[0]),对dp[3]的状态进行了更新,那么在更新dp[8]时,用到的dp[3]就不是上一轮的状态了,不满足动态规划的要求。简单来说,如果从小到大更新
由于j,则先更新dp[3],后面再更新dp[8]时会用到已经更新过的dp[3],而不是上一轮的dp[3],而如果按从大到小更新j,则先更新dp[8],此时用到的dp[3]还是上一轮的dp[3]。dp[j]的更新依赖于上一轮的dp[j-c[i]],这就需要保证在更新dp[j]时,所有比j小的dp都是未更新过的。
示例代码:
#include <iostream>
using namespace std;
#define MAXN 1005
int c[MAXN]; // 物品费用
int w[MAXN]; // 物品价值
int dp[MAXN]; // dp[j]表示N个物品在容量j下的最优解
int main() {
int n, v;
cin >> n >> v;
for(int i = 1; i <= n; i++) {
cin >> c[i] >> w[i];
}
for(int i = 1; i <= n; i++) {
for(int j = v; j >= 0; j--) {
if(j < c[i]) {
dp[j] = dp[j]; // 左边的dp[j]是第i轮的值,右边的dp[j]是第i-1轮的值
} else {
dp[j] = max(dp[j], dp[j-c[i]] + w[i]);
}
}
}
cout << dp[v];
}
以上代码的循环部分可进一步简化:
#include <iostream>
using namespace std;
#define MAXN 1005
int c[MAXN]; // 物品费用
int w[MAXN]; // 物品价值
int dp[MAXN]; // dp[j]表示N个物品在容量j下的最优解
int main() {
int n, v;
cin >> n >> v;
for(int i = 1; i <= n; i++) {
cin >> c[i] >> w[i];
}
for(int i = 1; i <= n; i++) {
for(int j = v; j >= c[i]; j--) {
dp[j] = max(dp[j], dp[j-c[i]] + w[i]); // 左边的dp[j]是第i轮的值,右边的dp[j-c[i]]是第i-1轮的值
}
}
cout << dp[v];
}
优化输入
可以使用在线处理输入的方式,边输入边计算,而不必开两个数组来保存c和w,如下:
#include <iostream>
using namespace std;
#define MAXN 1005
int dp[MAXN];
int main() {
int n, v;
cin >> n >> v;
for(int i = 1; i <= n; i++) {
int c, w;
cin >> c >> w;
for(int j = v; j >= c; j--) {
dp[j] = max(dp[j], dp[j-c] + w);
}
}
cout << dp[v];
}
- 无标签