K46. 背包理论基础(二维背包)
代码随想录
1. 思路
背包问题的主要特征为,在有限制的情况下满足最优化,因此可以构造二维dp数组,一个维度记录成本,一个维度记录收益,一步步寻找最优解。
(1)dp数组以及下标含义
dp[i][j]代表0-i的物品,在j的背包容量下,可以形成的最大价值。注意,这里i为序数,第一个第二个物品这样,而j为基数,也就是对应着成本的单位,比如kg。因此,如果有3个物品,成本分别为1、3、5kg,则i取0-2,j取0-5。
(2)确定递推公式
每次更新都有两个可选择的方式,一种是放入这个物品,一种是不放入。如果放入,则放入前背包中的物品个数位i-1,最大容量为j-weight[i],放入后价值+value[i]。如果不放入,则背包保持之前的状态,有i-1件物品,j的最大重量。我们需要选取其中更大的,因此表达式为:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i])
注意,如果j<weight[i],说明第i个物品根本放不进背包,把其他的东西都腾出来也没戏,所以只有一种可能性,就是退回到上一步,也就是dp[i-1][j]。
(3)如何初始化
因为每一步更新涉及左上的元素,因此第一行和第一列必须要初始化。第一列代表重量限制为0的情况下总价值,因为放不了任何东西,所以全部初始化为0。
第一行代表如果只放第一个物品,随着背包重量限制的提升,总价值是什么。不难想到,这一行应该是一个阶梯函数,取值为0和value[i],也就是最大限制下放和不放这个物品的价值。
(4)遍历顺序
有两种遍历顺序,这里都详细讲一下。
按列遍历。这种情况相当于在给定不同背包容量限制下,不断往里放东西,最大的承载。按理说依次往里面放就可以,但是还真不是有大的就放就好,因为它会因为dp[i-1][j-weight[i]]+value[i](dp更新的第二项)挤占之前的空间,如果小的更有性价比,还不如不放。
按行遍历。这种情况相当于在给定物品的情况下,扩大背包容量限制,看能放多少。按理说应该就是这个物品本身,但是会有小成本的加塞,可能让总价值更大,表达为dp[i-1][j](dp更新的第一项),如果小的更有性价比,等到了可以堆叠的时候也不必了。
可以看出,不论如何遍历,都能做到最优化,让性价比达到最优。
(5)实例展示
在展示实例的时候,最核心的就是更小的物品可能更有性价比这个思想。比如,一个3成本的价值20,一个1成本的价值15,后者就更有性价比。
void test_2_wei_bag_problem1() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagweight = 4;
// 二维数组
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
// 初始化
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
cout << dp[weight.size() - 1][bagweight] << endl;
}
int main() {
test_2_wei_bag_problem1();
}
K46. 背包理论基础(一维背包)
代码随想录
1. 思路
滚动数组的核心逻辑是讲之前的二维数组进行压缩。而之所以可以压缩,是因为一些环节出现了冗余,换句话说,删除这些冗余信息后,并不会威胁到需要保留的信息。
(1)从数学角度进行压缩
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i])
a. 在i这个维度上,下一个状态只依赖于i-1这个状态,所以这个维度可以压缩。只需要保证在遍历每个i的时候,将dp[i]更新为dp[i]即可。这也是为什么很多一维动态规划的题目可以压缩成一两个元素不断轮换。
b. 在j这个维度上,因为下一个状态依赖于j-weight[i]这个状态,所以不太好确定,因此需要保留这个维度。同时,因为在更新j的时候,需要j-weight[i]保持上一轮的状态,因此需要从后向前更新,不然应该保留的信息会被改变。
这样问题就明朗了,我们只需要保存j这一个维度,代表背包的容量,从最大容量到最小容量遍历,每一遍代表一个物品。
(2)实际含义的解释
a. 目前的dp数组表示在j这个容量下最大的价值,至于是多少个物品的,得看处于第几轮。每一轮更新一个物品,第i轮第j个元素表示0-i物品放到j这个限制的背包中,所获得的最大价值。
b. 更新的方程为:dp[j] = max(dp[j], dp[j-weight[i]]+value[i])
c. 初始化的时候,全部填充为0。这里涉及到第0轮和第1轮的含义,我们可以让第1轮表示放第一个物品,而第0轮则代表不放物品,这样第一轮就代表二维背包中第一行的初始化,也就是放1个物品可以获得的最大价值。
d. 遍历顺序为,每个物品一轮,每一轮从右向左遍历。
2. 实现
由于在j<weights[i]时,j保持不变,因此在这里体现为dp[j] = dp[j],因此在for循环的时候,可以书写成 for(int j = bagWeight; j >= weight[i]; j--)
此外,注意更新条件中i和j的区别,不要弄混。
void test_1_wei_bag_problem() {
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int bagWeight = 4;
// 初始化
vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
cout << dp[bagWeight] << endl;
}
int main() {
test_1_wei_bag_problem();
}
416. 分割等和子集
代码随想录
1. 思路
这道题可以转化为有没有n个数加起来等于sum/2,这样就转化为了价值和成本都为成本的01背包问题。
只有确定了如下三点,才能把背包问题套到本题上来:
- 背包的限制:背包的体积为sum/2,背包如果正好装满,说明找到了总和为 sum/2 的子集。
- 成本和价值:背包要放入的商品(集合里的元素)重量为元素的数值,价值也为元素的数值
- 是否可以重复:背包中每一个元素是不可重复放入。(一个商品如果可以重复多次放入是完全背包,而只能放入一次是01背包,写法还是不一样的)
这道2. 实现
由于文章来源:https://www.toymoban.com/news/detail-793573.html
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
// dp[i]中的i表示背包内总和
// 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200
// 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了
vector<int> dp(10001, 0);
for (int i = 0; i < nums.size(); i++) {
sum += nums[i];
}
// 也可以使用库函数一步求和
// int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum % 2 == 1) return false;
int target = sum / 2;
// 开始 01背包
for(int i = 0; i < nums.size(); i++) {
for(int j = target; j >= nums[i]; j--) { // 每一个元素一定是不可重复放入,所以从大到小遍历
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
// 集合中的元素正好可以凑成总和target
if (dp[target] == target) return true;
return false;
}
};
文章来源地址https://www.toymoban.com/news/detail-793573.html
到了这里,关于Day 42 动态规划 4的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!