代码随想录刷题Day42 | 动态规划:01背包理论基础、动态规划:01背包理论基础(滚动数组)、416. 分割等和子集
今日任务
动态规划:01背包理论基础
题目链接(acwing):01背包问题
主要思路
- 每个物品分为放入和不放入两种状态
- dp数组
d[i][j]
表示在第[0, i]
个物品中选择几个物品放进容量为j
的背包时,背包中物品最大价值 - 有两种情况
- 第一种是当第
i
个物品的体积大于当前背包容量时(Volume[i] > j
),此时必定无法向背包放入第i
个物品:dp[i][j] = dp[i - 1][j]
- 第二种是当
volume[i] <= j
,可以向背包放入第i
个物品,此时也有两种情况- 可以继续选择不放入第
i
个物品,此时dp[i][j] = dp[i - 1][j]
- 可以选择放入第
i
个物品,此时dp[i][j] = dp[i - 1][j - Volume[i]] + Wealth[i]
- 由于我们要使背包中物品价值最大,所以最终递推公式为:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - Volume[i]])
- 可以继续选择不放入第
- 第一种是当第
最终代码
#include <iostream>
using namespace std;
const int N = 1005;
int Volume[N], Wealth[N];
int dp[N][N]; // dp[i][j] 表示从第[0, i]个物品中取物品,其体积总和不超过j时最大价值
// dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - Volume[i]] + wealth[i]) 不放第i个物品和放第i个物品
int main() {
int n, v;
cin >> n >> v;
for (int i = 0; i < n; ++i) {
cin >> Volume[i] >> Wealth[i];
}
// 正序遍历
// 先初始化dp数组
// 当只从第0个物品开始挑选时有两种情况
// 1.容量j小于物体体积Volume[0], 那么背包不能放东西,此时dp[0][j] = 0;
for (int j = 0; j < Volume[0]; ++j) dp[0][j] = 0;
// 2.容量j大于等于物体体积Volume[0],此时可以放入物体0,所以dp[0][j] = Wealth[0]
for (int j = Volume[0]; j <= v; ++j) dp[0][j] = Wealth[0];
for (int i = 1; i < n; ++i) { // 遍历物品
for (int j = 0; j <= v; ++j) { // 遍历背包容量
if (j < Volume[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - Volume[i]] + Wealth[i]);
}
}
cout << dp[n - 1][v];
return 0;
}
动态规划:01背包理论基础(滚动数组)
题目链接同上一题
主要思想
- 根据上一题的推导,我们将第
i - 1
层的值拷贝到第i
层时,依旧可以可行:dp[i][j] = max(dp[i][j], dp[i][j - Volume[i]] + Wealth[i])
- 所以我们可以删掉
i
这个维度,直接用dp[j]
来表示背包容量为j
时,背包所能容纳的物品的最大价值 - 递推公式同样分为两种情况:
- 一种不放物品
i
,此时取自己原本的值:dp[j] = dp[j]
- 一种放物品
i
,此时不取自己的值:dp[j] = dp[j - Volume[i]] + Wealth[i]
- 最后取最大值:
dp[j] = max(dp[j], dp[j - Volume[i]] + Wealth[i])
- 一种不放物品
- 注意:倒序遍历是为了保证物品i只被放入一次!
- 如果是正序遍历,由于每次都要用到上一层(
i - 1
层)的dp[j]
(dp[i - 1][j]
) 的值,每次是从 左往右逐步扩大取物品区间[0, i]
,这就可能导致上一层的 **dp[j]
(dp[i - 1][j]
)**已经更新为这一层的dp[j]
(dp[i][j]
);而我们在计算第i
层时,仍然在取前面的dp[j]
(dp[i][j]
)进行计算时,原本是要取用dp[i - 1][j]
的dp[j]
值,但是用的是更新后的d[i][j]
值,会导致有的物品多次放入背包 - 如果逆序遍历,由于先更新右边的值,再往左走时,左边用的是更左边的
dp[j]
值来进行更新,不会使用到已经更新的值,所以不会出现重复
- 如果是正序遍历,由于每次都要用到上一层(
![image-20230919200000017](https://i-blog.csdnimg.cn/blog_migrate/bd968f0456ecf515f5213637df67d730.png)
#include <iostream>
using namespace std;
const int N = 1005;
int dp[N], Volume[N], Wealth[N];
int main() {
int n, v;
cin >> n >> v;
for (int i = 0; i < n; ++i) cin >> Volume[i] >> Wealth[i];
for (int i = 0; i < n; ++i) {
for (int j = v; j >= Volume[i]; --j) {
dp[j] = max(dp[j], dp[j - Volume[i]] + Wealth[i]);
}
}
cout << dp[v];
return 0;
}
416. 分割等和子集
题目链接:416. 分割等和子集
主要思想
- 背包的体积为
sum / 2
- 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
- 背包如果正好装满,说明找到了总和为
sum / 2
的子集。 - 背包中每一个元素是不可重复放入。
- 当
dp[sum / 2] == sum / 2
时,说明背包正好放满,说明该集合可以划分为两个和相等的子集
class Solution {
public:
// 背包的体积为sum / 2
// 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
// 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
// 背包中每一个元素是不可重复放入。
// 当dp[sum / 2] == sum / 2时,说明背包正好放满,说明该集合可以划分为两个和相等的子集
bool canPartition(vector<int>& nums) {
int sum = 0;
for (auto x : nums) sum += x;
if (sum % 2 != 0) return false;
sum /= 2;
vector<int> dp(sum + 1, 0); // dp[j] 表示容量为j时装入背包的最大数值和
for (int i = 0; i < nums.size(); ++i) {
for (int j = sum; j >= nums[i]; --j) {
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
return dp[sum] == sum;
}
};