代码随想录算法训练营 Day35 动态规划Ⅲ 0-1背包问题
动态规划
背包问题(0-1 背包问题)
0-1 背包:n 个物品,每个物品只有一个
完全背包:n 种物品,每个物品有无限个
多重背包:n 种物品,每个物品个数不相同
暴力解法
场景题目类型给出表格,背包最大容量 n,说怎么装利益最大化
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
暴力解法就是穷举(回溯)当装满了背包统计价值再试试其他的,这样穷举所有可能情况,得出最佳结论
动态规划思路
Dp 数组定义
Dp 说明 dp[i][j]
在 [0,i]
物品种类种任取放一个物品进容量为 J 背包里所能取到的最大价值总和
递推公式确定
dp[i][j]=std::max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])
递推公式取最大值
- 不放物品i:背包容量为j,里面不放这个物品i的最大价值是
dp[i - 1][j]
不放物品 i,i-1 表示排除这个物品,所能获得的最大价值。 - 放物品i:背包空出物品i的容量后,背包容量为
j - weight[i]
此时dp[i - 1][j - weight[i]]
为背包容量为j - weight[i]
且不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i]
(物品i的价值),就是背包放物品i得到的最大价值
放了物品 i,i-1 表示除了这个物品的其他物品,j-weight[i]
表示当前物品需要占据的重量
初始化 Dp 数组
根据递推公式得出要初始化上方与左方的数值,类似于上一道题的路径问题
初始化路径时上方 dp[0][j]
和左方 dp[i][0]
都要正确初始化,其他地方数值初始化没有关系
遍历顺序
0-1 背包问题两层 for 循环都可以遍历(先遍历物品再遍历背包)
判断是否可以正确遍历看递推公式数据来源是否稳定,先便利物品再遍历背包和先遍历背包再遍历物品
最后实际计算时数据来源都是从上方与左方,两种遍历顺序(横着遍历、竖着遍历)
都在计算前填充了上方与左方的数据因此遍历顺序可以交换
滚动数组思路
使用压缩了的一维 dp 数组用于记录
相当于把原来的二维数组沿着物品方向压缩,只留下不同背包容量下能装物品的最大价值
循环更新这个数组,故称滚动数组
可以理解为 dp 计算的时候是将第一层数据拷贝覆盖到下一层计算
Dp 数组
Dp[j]
Dp 数组中表示不同容量 j 下能装的物品的最大价值
递推公式确定
dp[j] = std::max(dp[j], dp[j - value[i]] + value[i])
取不放物品 i 与能放下物品 i 时的最大价值 + 物品 i 价值作为递推公式
可以理解为:要么不要物品 i,要么把背包空一空让他能装得下物品 i,物品 i 放进去看那个价值最大
初始化 dp 数组
dp[0]=0 dp[j]=0
0 初始化值为 0,1 初始化值为 0,如果初始化值很大就会覆盖计算结果,因此保证非负数最小值 0
遍历顺序
先遍历物品,再倒叙遍历背包,正序遍历背包会重复装东西
因为一维dp的写法,背包容量一定是要倒序遍历,如果遍历背包容量放在上一层,那么每个dp[j]
就只会放入一个物品,即:背包里只放入了一个物品。
for(int i = 0; i < m; ++i) for (j=n; j>=0;--j)
倒叙为了保证物品不会被覆盖,因为 dp 是从前一个数据中退出来的
初始化时数据是正确数据,如果正序遍历背包会导致 dp 读取到之前的数据产生“左脚踩右脚起飞”的 bug,因此倒叙遍历保证数据正确
二维不受影响是因为二维 dp 数组数据是来源上一层数据,而不是当前层数据
一维正序会导致当前层数据覆盖了上一层数据,因此要倒叙利用上一层数据
题目
46. 携带研究材料(第六期模拟笔试)
二维数组实现方法
具体五部分析在上面的动态规划思路部分里面
注意遍历时的起始点 i = 1与终止点设置
#include<iostream>
#include<vector>int main() {// 输入数据处理int m = 0, n = 0;std::cin >> m >> n;std::vector<int> Space(m, 0);std::vector<int> Value(m, 0);for (int i = 0; i < m; ++i) std::cin >> Space[i];for (int j = 0; j < m; ++j) std::cin >> Value[j];// DP准备 +1 是因为结果都是从上一次中推导的,如果不+1相当于丢掉了[j-1]这个情况因此要+1std::vector<std::vector<int>> dp(m, std::vector<int>(n+1, 0));// DP初始化 j直接从Space[0]之后赋值,之前默认赋值为0了for (int j = Space[0]; j <= n; ++j) dp[0][j] = Value[0];// 循环遍历 先遍历物品再遍历背包for (int i = 1; i < m; ++i) {for (int j = 1; j <= n; ++j) {// 装不下if (j < Space[i]) dp[i][j] = dp[i-1][j];// 递推公式 不带物品i到当前最大能得到多少价值,带物品i当前能得到多少价值 取最大else dp[i][j] = std::max(dp[i-1][j], dp[i-1][j-Space[i]] + Value[i]);}}std::cout << dp[m-1][n] << std::endl;return 0;
}
46. 携带研究材料(第六期模拟笔试)
滚动数组实现方法
具体分析思路参考滚动数组思路部分
注意遍历时的起始点与终止点设置
#include<iostream>
#include<vector>int main() {// 公式化数据处理int m, n;std::cin >> m >> n;std::vector<int> Space(m);std::vector<int> Value(m);for (int i = 0; i < m; ++i) std::cin >> Space[i];for (int i = 0; i < m; ++i) std::cin >> Value[i];// 初始化dp数组 非负最小值std::vector<int> dp(n+1, 0);// 遍历顺序 先遍历物品再遍历背包 如果先遍历背包再遍历物品结果就是只能放进去一个物品for (int i = 0; i < m; ++i) {// 从后往前遍历是因为当前数据的计算依赖于前一次计算的数据,如果正序遍历会覆盖之前的数据for (int j = n; j >= Space[i]; --j) {// 如果背包容量比当前物品还小就不用比了dp[j] = std::max(dp[j], dp[j- Space[i]] + Value[i]);}}std::cout << dp[n] << std::endl;return 0;
}
416. 分割等和子集 - 力扣(LeetCode)
0-1 背包应用题,很难看出来,但是一旦看出来就好理解了
可以吧给出的数组看作是价值与重量两个数组,只不过他们的值相等
于是问题被转化为背包容量为(数组和/2)如何装最大价值的数据?
1. Dp 数组表示当前容量 j 下能装的最大价值是多少
2. 递推公式采用 0-1 背包递推公式
3. 初始化采用 0-1 背包初始化
4. 遍历顺序倒序遍历,防止新数据覆盖旧数据
bool canPartition(vector<int>& nums) {// 求和计算背包容量int sum = 0;for (int i = 0; i < nums.size(); ++i) sum += nums[i];// 剪枝 若为奇数不可能拆分if (sum % 2 == 1) return false;int target = sum/2;// 定义背包数组 数组元素不超过100 大小不超过200 总和不超过20000背包容量为10001即可 背包元素表示当前物体背包总价值std::vector<int> dp(10001, 0);// 初始化已定义 数据遍历for (int i = 0; i < nums.size(); ++i) {// 倒叙遍历 最大位置启动 j太小了装不下就放弃for (int j = target; j >= nums[i]; --j) {dp[j] = std::max(dp[j], dp[j - nums[i]] + nums[i]);}}if (dp[target] == target) return true;return false;
}