携带研究材料(二维DP)
题目描述
小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的空间,并且具有不同的价值。
小明的行李空间为 N,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料只能选择一次,并且只有选与不选两种选择,不能进行切割。
输入描述
第一行包含两个正整数,第一个整数 M 代表研究材料的种类,第二个正整数 N,代表小明的行李空间。
第二行包含 M 个正整数,代表每种研究材料的所占空间。
第三行包含 M 个正整数,代表每种研究材料的价值。
输出描述
输出一个整数,代表小明能够携带的研究材料的最大价值。
输入示例
6 1
2 2 3 1 5 2
2 3 1 5 4 3
输出示例
5
解题思路
假设我们手中有三份珠宝,对应的重量和价值对应关系如下:
weight | 1 | 3 | 4 |
---|---|---|---|
value | 15 | 20 | 30 |
在背包最大容量为5的情况下,对题目进行分析:
-
确定dp数组以及下标的含义
- dp[i][j]表示前
i
个物品在背包容量为j
时的最大价值
- dp[i][j]表示前
-
确定递推公式
-
递推公式:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
- 如果当前背包容量
j
小于第i
个物品的重量weight[i]
,或是选择不将第i个物品放入背包之中,此时背包的最大价值取决于前i-1
个物品的价值所得到的结果:dp[i][j] = dp[i - 1][j]
。 - 如果选择将第
i
个物品放入背包,那么背包的容量将减少weight[i]
,并且我们需要在前i-1
个物品得到的最大价值基础上加上第i
个物品的价值value[i]
。 - 最终,
dp[i][j]
取上述两种情况的最大值,得到此时前i个物品在当前背包容量j下得到的最大价值总和。
- 如果当前背包容量
-
-
dp数组初始化
-
初始化
dp
数组为0
,表示在没有任何物品或背包容量为0
时的最大价值为0
。 -
对于第一个物品(即
i = 0
),如果背包容量j
大于等于第一个物品的重量weight[0]
,则最大价值为第一个物品的价值value[0]
,对应代码:for (int j = weight[0]; j <= bagWeight; j++) { dp[0][j] = value[0]; }
初始化后的dp表如下所示:
-
-
确定遍历顺序
-
以dp[1][3]和dp[1][4]为例,分析在递推公式中dp[i][j]的依赖关系:
dp[1][3] = max(dp[0][3],dp[0][0]+value[1]) = 20
dp[1][4] = max(dp[0][4],dp[0][1]+value[1]) = 35
-
可以发现dp[i][j]依赖于正上方的值,和左上方的值,因此合理的遍历顺序应该为先遍历商品,在遍历背包容量,但是本题也可以先遍历背包再遍历商品,遍历顺序取决于所依赖的值是否被提前赋值了。
-
-
举例推导dp数组
-
根据上述思路,填补dp数组:
-
代码实现
测试地址:https://kamacoder.com/problempage.php?pid=1046
#include <bits/stdc++.h>
using namespace std;
int n, bagWeight;
void solve() {
// 初始化物品的重量和价值数组
vector<int> weight(n, 0);
vector<int> value(n, 0);
// 输入每个物品的重量
for (int i = 0; i < n; i++) {
cin >> weight[i];
}
// 输入每个物品的价值
for (int j = 0; j < n; j++) {
cin >> value[j];
}
// 初始化动态规划数组 dp,dp[i][j] 表示前 i 个物品在背包容量为 j 时的最大价值
vector<vector<int>> dp(weight.size(), vector<int>(bagWeight + 1, 0));
// 初始化第一个物品的情况
for (int j = weight[0]; j <= bagWeight; j++) {
dp[0][j] = value[0];
}
// 动态规划填表
for (int i = 1; i < weight.size(); i++) {
for (int j = 0; j <= bagWeight; j++) {
if (j < weight[i])
// 如果当前背包容量小于第 i 个物品的重量,则不放入该物品
dp[i][j] = dp[i - 1][j];
else
// 否则,比较放入和不放入第 i 个物品的价值,取最大值
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
// 输出最终结果,即在背包容量为 bagWeight 时能装入的最大价值
cout << dp[weight.size() - 1][bagWeight] << endl;
}
int main() {
// 循环读取输入,直到没有输入为止
while (cin >> n >> bagWeight) {
solve();
}
return 0;
}
携带研究材料(一维DP)
题目描述
小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的空间,并且具有不同的价值。
小明的行李空间为 N,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料只能选择一次,并且只有选与不选两种选择,不能进行切割。
输入描述
第一行包含两个正整数,第一个整数 M 代表研究材料的种类,第二个正整数 N,代表小明的行李空间。
第二行包含 M 个正整数,代表每种研究材料的所占空间。
第三行包含 M 个正整数,代表每种研究材料的价值。
输出描述
输出一个整数,代表小明能够携带的研究材料的最大价值。
输入示例
6 1
2 2 3 1 5 2
2 3 1 5 4 3
输出示例
5
解题思路
-
确定dp数组以及下标的含义
dp[j]
:表示背包容量为j
时,可以装入的最大价值。
-
确定递推公式
- 递推公式:
dp[j] = max(dp[j], dp[j - costs[i]] + values[i])
: - 在背包容量为
j
的前提下,不装入第i
个物品(即dp[j]
)和装入第i
个物品(即dp[j - costs[i]] + values[i]
,表示背包容量减少,但价值增加)之间,选择一个价值最大的。
- 递推公式:
-
dp数组初始化
- 初始化
dp
数组的长度为N + 1
(背包的最大容量加1),并且初始化所有元素为0
。表示在没有任何物品或背包容量为0时的最大价值为0。
- 初始化
-
确定遍历顺序
- 对于物品的遍历,是正序遍历,对于背包容量的遍历,是逆序遍历
- 逆序遍历的原因:因为每个物品只能使用一次,为了防止同一个物品被多次选中,我们需要确保在选择第
i
个物品放入背包时,不会影响到其在容量更大的背包中的选择情况。
-
举例推导dp数组
-
通过一维dp,来遍历背包,最终得到结果如下:
-
代码实现
测试地址:https://kamacoder.com/problempage.php?pid=1046
#include <vector>
#include <iostream>
using namespace std;
int main() {
int M, N; // M代表物品的数量,N代表背包的容量
cin >> M >> N; // 输入物品数量和背包容量
vector<int> costs(M, 0); // 存储每个物品的成本(或重量)
vector<int> values(M, 0); // 存储每个物品的价值
// 输入所有物品的成本和价值
for (int i = 0; i < M; i++) {
cin >> costs[i]; // 输入第i个物品的成本
}
for (int j = 0; j < M; j++) {
cin >> values[j]; // 输入第j个物品的价值
}
vector<int> dp(N + 1, 0); // 动态规划数组,初始化为0。dp[j]表示在容量为j的背包中能装入物品的最大价值
// 使用动态规划填表
for (int i = 0; i < M; i++) { // 遍历所有物品
for (int j = N; j >= costs[i]; j--) { // 逆序遍历背包容量,确保每个物品只被计算一次
// 使用递推公式更新dp数组,决定是否将第i个物品加入到背包中
dp[j] = max(dp[j], dp[j - costs[i]] + values[i]);
}
}
cout << dp[N] << endl; // 输出最大价值,即背包容量为N时的最大价值
return 0;
}
分割等和子集
题目描述
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
解题思路
-
确定dp数组以及下标的含义
dp[j]
表示从数组中选取若干个元素,它们的总和最多为j
时,这些元素的所能达到的最大总和。
-
确定递推公式
-
递推公式:
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
-
公式说明:
-
dp[j]
保存了在不超过j
这个和的条件下,我们能够达到的最大元素和。 -
nums[i]
是当前正在考虑是否加入子集以达到目标和j
的元素值。 -
dp[j - nums[i]]
表示在我们选择包含当前元素nums[i]
之前,达到剩余和j - nums[i]
的最大值。这个值加上nums[i]
就等于如果包括当前元素,我们能达到的一个新的和值。 -
max(dp[j], dp[j - nums[i]] + nums[i])
比较了两种情况:一种是不包括当前元素nums[i]
时已经可以达到的最大和(dp[j]
),另一种是如果包括当前元素nums[i]
,能够达到的新的可能最大和(dp[j - nums[i]] + nums[i]
)。从这两个值中选取最大值作为新的dp[j]
。
-
-
-
dp数组初始化
- 因为题目的取值范围给出了数组之和最大值为20000,因此可以初始化目标和为10001,并初始化dp数组所有值为0,表示没有选择任何元素时,所有可能的总和都是0,也可以确保在遍历的过程中初始值不会被覆盖
-
确定遍历顺序
- 遍历数组元素的时候采用正序遍历
- 在计算当前元素能到达的最大和的时候采用倒序遍历,以确保每个元素只使用了一次
-
举例推导dp数组
-
以用例1,输入[1,5,11,5]为例,推导dp数组,如下所示:
-
代码实现
测试地址:
class Solution {
public:
bool canPartition(vector<int>& nums) {
int targetSum = 0;
for (int num : nums) { // 计算数组元素总和
targetSum += num;
}
if (targetSum % 2 == 1) // 如果总和为奇数,则不能平分为两个和相等的子集
return false;
int target = targetSum / 2; // 目标和为数组总和的一半
vector<int> dp(10001,0); // 创建动态规划数组,用于存储可能达到的各个和的最大值
for (int i = 0; i < nums.size(); i++) { // 遍历数组中的每个数
for (int j = target; j >= nums[i];
j--) { // 逆序遍历,从target到nums[i]
// 更新dp[j]为当前和j和减去nums[i]再加上nums[i](即选择当前数字)的最大值
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
if (dp[target] ==
target) { // 如果dp[target]等于target,则表示可以分割为两个和相等的子集
return true;
}
return false;
}
};