代码随想录算法训练营第四十一天 | 0-1背包问题
46.携带研究材料
二维数组
动态规划五部曲:
- dp[i][j]:下标i表示背包装0-i的物品(任取),j表示当前背包的最大容量,dp[i][j]表示容量为j时,装0-i的物品的最大价值
- 递推公式:dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])
- dp[i-1][j] 表示j的容量全部装0-i-1的物品,不装物品i的最大价值
- dp[i-1][j-weight[i]] 表示背包空出装物品i的容量的最大价值,再加上value[i],表示容量为j时,装物品j的最大价值
- 两者取最大值,就是dp[i][j]的最大价值,如果不装i价值最大,则取前者;如果装物品i价值最大,则取后者
- 初始化:
- 由递推公式可以看出dp[i][j]取决于左面和和上面的最大值
- 当背包容量为0时,最大价值都为0:
for(int row = 0; row < M; ++row) { dp[row][0] = 0; }
- 当背包只装物品0时,最大价值取决去背包是否能装下物品0
for(int col = weights[0]; col <= N; ++col) { dp[0][col] = value[0]; }
- 遍历顺序:由于dp[i][j]取决于dp[i-1][j]和dp[i-1][j-weight[i]] + value[i]所以先遍历背包和先遍历物品都可以
- 打印dp数组
#include <iostream>
#include <vector>
using namespace std;
int main() {
int M; // 研究材料的种类
int N; // 背包容量
while(cin >> M >> N) {
vector<int> weights(M, 0); // 存储材料所占空间
vector<int> value(M, 0); // 存储材料的价值
// 循环输入weights和value
for(int i = 0; i < M; ++i) cin >> weights[i];
for(int i = 0; i < M; ++i) cin >> value[i];
// dp[i][j]: 背包容量为j时,物品0-i任取,背包的价值最大为多少
vector<vector<int>> dp(M, vector<int>(N+1, 0));
// 递推公式dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])
// 背包容量为j时,不装物品i和装物品i中取最大的
// 初始化
// 背包容量为0时最大价值为0
// for(int i = 0; i < M; ++i) dp[i][0] = 0;
// 只装物品0取决于背包是否能够容纳物品0
for(int j = 0; j <= N; ++j) {
if(j >= weights[0]) dp[0][j] = value[0];
}
// 先遍历物品,再遍历背包
for(int i = 1; i < M; ++i) {
for(int j = 1; j <= N; ++j) {
if(j >= weights[i]) dp[i][j] = max(dp[i-1][j], dp[i-1][j - weights[i]] + value[i]);
else dp[i][j] = dp[i-1][j];
}
}
// for(auto vec : dp) {
// for(auto val : vec) cout << val << "\t";
// cout << endl;
// }
cout << dp[M-1][N];
}
return 0;
}
滚动数组
- dp[j]:容量为j的背包所能装物品的最大价值
- 递推公式:dp[j] = max(dp[j], dp[j-weights[i] + value[i])
- 初始化:由递推公式可知,dp数组每一次更新都依赖于本身的值,所以本身的值在初始化时不应过大,使得dp数组无法更新,所以初始化为0即可
- 遍历顺序:先遍历物品后遍历背包,且背包必须倒序遍历;、
- 背包为什么要倒序遍历:
递推公式dp[j-weight[i]] + value[i],由于j >= j - weight[i] 所以背包从小到大遍历dp[j - weighs[i]]这个值已经填过了,会影响到dp[j];试想dp[2] = dp[2-1] + value[1] 相当于将两个物品1装入了背包 - 为什么先遍历物品:
dp[j] = dp[j-weights[i] + value[i], 首先遍历最大容量的背包,此时并没有物品已经装入,是一个空的背包,所以j-weights[i] <= j的值全是0,这样相当于将所有物品放进取试一下装哪一个价值最大,结束后,背包将只装有一个物品
- 背包为什么要倒序遍历:
- 打印dp数组
#include <iostream>
#include <vector>
using namespace std;
int main() {
int M, N;
cin >> M >> N;
vector<int> weights(M);
vector<int> value(M);
for(int i = 0; i < M; ++i) cin >> weights[i];
for(int i = 0; i < M; ++i) cin >> value[i];
// dp[j]: 容量为j时,dp数组的最大价值
vector<int> dp(N + 1);
// 递推公式dp[j] = max(dp[j], dp[j-weights[i]] + value[i])
// 初始化
for(int j = 0; j < N; ++j) {
dp[j] = 0;
}
// 遍历顺序
for(int i = 0; i < M; ++i) {
for(int j = N; j >=0; --j) {
if(j < weights[i]) continue;
else dp[j] = max(dp[j], dp[j-weights[i]] + value[i]);
}
}
// for(int i = 0; i < N; ++i) cout << dp[i];
cout << dp[N];
return 0;
}
416.分割等和子集
问题抽象:物品i的重量nums[i], 价值也是nums[i]
- dp[j]: 当前子集的和
- 递推公式:p[j] = max(dp[j], dp[j-nums[i]] + nums[i]);
- 初始化:默认初始化为0
- 遍历顺序:先遍历物品,再遍历背包;背包倒序遍历
- 打印dp数组
class Solution {
public:
bool canPartition(vector<int>& nums) {
int target = 0;
for(int val : nums) target += val;
if(target % 2 == 1) return false;
target = target >> 1;
vector<int> dp(target + 1, 0);
for(int i = 0; i < nums.size(); ++i) {
for(int j = target; j >= 0; --j) {
if(j < nums[i]) continue;
else dp[j] = max(dp[j], dp[j-nums[i]] + nums[i]);
}
}
return dp[target] == target;
}
};