有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。类似于这种类型的就是经典的01背包问题。
对于01背包问题,我们通常使用动态规划来求解,当然也可以用回溯算法来求解,但是回溯算法的时间复杂度为 指数级别,若题目中的测试数据过大,则可能会超时。
46. 携带研究材料(第六期模拟笔试)
时间限制:5.000S 空间限制:128MB
小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的空间,并且具有不同的价值。
小明的行李空间为 N,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料只能选择一次,并且只有选与不选两种选择,不能进行切割。
第一行包含两个正整数,第一个整数 M 代表研究材料的种类,第二个正整数 N,代表小明的行李空间。
第二行包含 M 个正整数,代表每种研究材料的所占空间。
第三行包含 M 个正整数,代表每种研究材料的价值。
输出一个整数,代表小明能够携带的研究材料的最大价值。
6 1
2 2 3 1 5 2
2 3 1 5 4 3
5
小明能够携带 6 种研究材料,但是行李空间只有 1,而占用空间为 1 的研究材料价值为 5,所以最终答案输出 5。
数据范围:
1 <= N <= 1000
1 <= M <= 1000
研究材料占用空间和价值都小于等于 1000
对于动态规划类问题,我们使用动态规划五部曲来进行思考。
第一步、构造动态规划dp数组
vector>dp(m, vector(n + 1, 0)); //构造dp数组
我们在构造dp数组的时候,一定要想清楚dp[i][j]的具体含义。
dp[i][j]代表-->从0~i的物品中任取,放进容量大小为j的背包中,所获得的最大价值。
第二步、明确递推公式
if(space[i] > j) {
dp[i][j] = dp[i - 1][j];
}
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - space[i]] + val[i]);
在space[i] > j即物品i的占用空间大于目前背包的容量时,从0~i的物品中任取,放进容量大小为j的背包中所获得的最大价值 就等于 从0~i - 1的物品中任取,放进容量大小为j的背包中,所获得的最大价值。
在space[i] < j即物品i的占用空间小于目前背包的容量时,我们从两种情况中取最大值,因为要计算的是所获得的最大价值。
所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
第三步、dp数组初始化
//初始化dp数组,对背包容量为0时所存储的最大价值进行初始化--都为0
for(int i = 0; i < m; i++) {
dp[i][0] = 0;
}
for(int i = 0; i <= n; i++) {
if(space[0] <= i) {
dp[0][i] = val[0];
}
}
首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。
再看其他情况。
状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。
dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。
那么很明显当 j < weight[0]的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。
当j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。
第四步、遍历dp数组
for(int i = 1; i < m; i++) { //研究材料
for(int j = 1; j <= n; j++) { //背包容量
if(space[i] > j) {
dp[i][j] = dp[i - 1][j];
}
else
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - space[i]] + val[i]);
}
}
在本题所使用的二维动归中,先遍历材料再遍历背包,亦或者先遍历背包再遍历材料都是可以的。
都会遍历完整个dp数组。自己手动画一下dp数组的图标是最直观的。
第五步、手动模拟 + 举例推导
最后呈上完整代码
#include
using namespace std;
int main() {
int m, n;
cin>>m>>n;
vectorspace(m); //每种材料的所占空间
vectorval(m); //每种材料的研究价值
for(int i = 0; i < m; i++) {
cin>>space[i];
}
for(int i = 0; i < m; i++) {
cin>>val[i];
}
vector>dp(m, vector(n + 1, 0)); //构造dp数组
//初始化dp数组,对背包容量为0时所存储的最大价值进行初始化--都为0
for(int i = 0; i < m; i++) {
dp[i][0] = 0;
}
for(int i = 0; i <= n; i++) {
if(space[0] <= i) {
dp[0][i] = val[0];
}
}
for(int i = 1; i < m; i++) { //研究材料
for(int j = 1; j <= n; j++) { //背包容量
if(space[i] > j) {
dp[i][j] = dp[i - 1][j];
}
else
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - space[i]] + val[i]);
}
}
cout<
#include
using namespace std;
int main() {
int m, n;
cin>>m>>n;
vectorspace(m);
vectorval(m);
vectordp(n + 1);
for(int i = 0; i < m; i++) {
cin>>space[i];
}
for(int i = 0; i < m; i++) {
cin>>val[i];
}
for(int i = 0; i < m; i++) {
for(int j = n; j >= space[i]; j--) {
dp[j] = max(dp[j], dp[j - space[i]] + val[i]);
}
}
cout<
1、一维dp数组的含义
dp[j] 表示容量大小为 j 的背包所能装的物品最大价值。
2、递推公式
利用滚动数组,新的dp[j]将旧的dp[j]覆盖的方式不断更新dp[j]。dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。
dp[j - weight[i]] + value[i] 表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j])
此时dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值。
3、初始化
dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。
那么dp数组除了下标0的位置,初始为0,其他下标应该初始化多少呢?
看一下递归公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。
这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。
那么我假设物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。
4、遍历方式
将二维dp压缩为一维dp后,遍历顺序是固定的,必须倒序遍历背包空间,如果正序遍历,会出现同一个物品被连续放进背包两次的情况,请自己手动模拟一下。
那为什么for循环的内外嵌套顺序不能修改呢?因为如果嵌套顺序互换,每次背包中只会装进去一个物品,不符合题意。
分割等和子集
题目难易:中等
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意: 每个数组中的元素不会超过 100 数组的大小不会超过 200
示例 1:
示例 2:
提示:
本题为典型的01背包问题。我们可以对数组大小为偶数的数组进行求和,取其二分之一,只要在集合中可以找到和为 sum / 2 的子集,那么这个集合就可以被拆为两个子集。
我们可以将条件转化为01背包问题的条件。
即 元素的价值 和 元素所占空间 大小相等。我们要将元素放进容量大小为 sum / 2的背包中,如果背包中所装元素的大小之和 的最大值 为 sum / 2,那么我们返回true,如果不是,则返回 false。
呈上代码:
class Solution {
public:
int sum = 0;
bool canPartition(vector& nums) {
if(nums.size() < 2) return false;
for(auto num : nums) {
sum += num;
}
if(sum % 2 != 0) return false;
int target = sum / 2;
vector>dp(nums.size(), vector(target + 1, 0)); //构造二维dp数组
vectorspace(nums.size());
vectorval(nums.size());
for(int i = 0; i < nums.size(); i++) {
space[i] = nums[i];
val[i] = nums[i];
}
for(int i = 0; i <= target; i++) {
if(space[0] <= i) {
dp[0][i] = val[0]; //初始化dp数组
}
}
for(int i = 1; i < nums.size(); i++) {
for(int j = 1; j <= target; j++) {
if(space[i] > j) {
dp[i][j] = dp[i - 1][j];
}
else {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - space[i]] + val[i]);
}
}
}
if(dp[nums.size() - 1][target] == target) return true;
return false;
}
};