动态规划-背包01问题推理与实践
动态规划-背包01问题推理与实践
背包01问题描述:
有storage大小的背包和sizes.size()数量的物品,每个物品i对应的物品大小为sizes[i],价值为values[i],在不超过storage大小的情况下,如何装载物品使背包中的values和最大.
物品大小: vector<int> sizes;
物品价值: vector<int> values;
背包容量: <int> storage;
理解(状态转移公式的推理):
构建二维数组dp[i][j],定义式为 std::vector<std::vector<int>> dp(sizes.size(),std::vector<int>(storage + 1,0));默认值为0;
dp[i][j] 的意义是在前 i 个物品中选择任意少于等于 i 个物品,其总大小不超过 j 的最大价值和.
则同理 dp[i - 1][j] 为在前 i - 1 物品中选择任意少于等于 i - 1 个物品,其总大小不超过 j 的最大价值和,此即为子状态.
假设求取dp[i][j],则考虑两种情况:
①第i个物品取的可能
②第i个物品不取的可能
针对上述情况进行分类讨论:
①已知dp[i-1][j],此时背包已满,则应腾出某物品以装在第 i 物品,则装载前必须知道dp[i - 1][j - sizes[i]]的值,即背包扣除第i个物品的大小后的最大价值装载方式[1],则可推导出dp[i][j] = dp[i - 1][j - sizes[i]] + values[i];
②已知dp[i - 1][j],此时第 i 个物品的价值过低或大小过大,不适合替换背包中的物品,则推理出dp[i][j] = dp[i - 1][j];
因为无法知道上述两种情况何时会发生,应该取两者的较大值:
dp[i][j] = std::max(dp[i - 1][j - sizes[i]] + values[i],dp[i - 1][j]);
此即为背包01问题的状态转移公式.
代码实现dp数组如下[2]:
//初始化第一个物品放入背包的子状态,即dp[0][j],此处的j代表的是>=第一个物品的大小的位置处,都填充values[0],注意子数组的额存放索引为增长的storage. for(int j = sizes[0]; j <= storage; j++) { dp[0][j] = values[0]; } //构建dp数组 for(int i = 1; i < sizes.size(); i++) { for(int j = 0; j <= storage; j++) { //该装载的物品过大,即使只装它一个也装不下,直接返回不装载的情况即可 if(j < sizes[i]) dp[i][j] = dp[i - 1][j]; else dp[i][j] = std::max( dp[i - 1][j - sizes[i]] + values[i], dp[i - 1][j] ); } }
完整代码(对应2. 01背包问题 - AcWing题库):
#include <iostream> #include <vector> #include <algorithm> int main(int argc,char** argv) { std::vector<int> sizes; std::vector<int> values; int storage; int size; std::cin >> size >> storage; for(int i = 0; i < size; i++) { int t_size; int t_value; std::cin >> t_size >> t_value; sizes.push_back(t_size); values.push_back(t_value); } std::vector<std::vector<int>> dp(sizes.size(),std::vector<int>(storage + 1,0)); for(int j = sizes[0]; j <= storage; j++) { dp[0][j] = values[0]; } for(int i = 1; i < sizes.size(); i++) { for(int j = 0; j <= storage; j++) { if(j < sizes[i]) dp[i][j] = dp[i - 1][j]; else dp[i][j] = std::max( dp[i - 1][j - sizes[i]] + values[i], dp[i - 1][j] ); } } //Debug查看部分 // for(int i = 0; i < size; i++) { // for(int j = 0; j <= storage; j++) { // std::cout << "[" << i << "]" << "[" << j << "]" << " = " << dp[i][j] << std::endl; // } // } std::cout << dp[size - 1][storage]; return 0; }
一维数组简化理解:
构建dp[j]一维数组,定义式为 std::vector<std::vector<int>> dp(storage + 1,0);
dp[j]数组意义为背包容量为j时,背包的最大价值为dp[j];
一维数组的遍历代码:
for(int i = 0; i < sizes.size(); i++) { for(int j = storage; j >= sizes[i]; j--) { dp[j] = std::max( dp[j - sizes[i]] + values[i], dp[j] ); } }
这里复用dp[j]的推理是,外层循环可以理解为依次遍历当次放入的物品 i 与放入该物品时容量为 j 的最大价值计算,即dp[j]为复用上一层循环 i = i - 1, j = j 时的最大价值,所变化的是考虑了新加入的 i 物品;内层循环反向遍历的理由因此可以推理得出,若正向进行遍历时,外层的i物品会被内层多次放入背包中(多次被遍历到),而反向遍历则不会重复遍历到,即举例以下情况:
// i:(in) 1 2 3...sizes.size() - 1 当 i = 2时 // j:(in) size[i] size[i] + 1 size[i] + 2...storage 当 j = x - 1(实际值在此例中不重要,合理即可)时 dp[j] = dp[j - sizes[i]] + values[i] == dp[x - 1] = dp[x - 1 - sizes[2]] + values[2]; //当 i = 2时 //当 j = x时 dp[j] = dp[j - sizes[i]] + values[i] == dp[x] = dp[x - sizes[2]] + values[2]; //观察可得: 在j:容量增长的情况下重复考虑了 i = 2 的情况,即重复加入了背包 //此时我们反向考虑 //i:(in) 与上文相同, 当i = 2时 //j:(in) storage... size[i] 当 j = x + 1(同上)时 dp[j] == dp[x + 1] = dp[x + 1 - size[2]] + values[2]; //当 i = 2时 //当 j = x时 dp[j] == dp[x] = dp[x - sizes[2]] + values[2]; //观察可得: 在j:容量递减的情况下,dp[x + 1] 依赖于上一次外层循环的dp[x + 1 - sizes[2]]值,而与此次循环的dp[x]值无关,故不会出现重复加入的情况
一维数组的初始化方式:
一维数组dp[j]不需要像二维数组dp[i][j]那样繁琐的初始化
仅仅只需要设置整体默认值(一般为0)即可,可参考定义式[3]
完整代码(对应416. 分割等和子集(LeetCode)):
class Solution { public: bool canPartition(std::vector<int>& nums) { int sum{0}; for(int elem : nums) { sum += elem; } if(sum % 2 != 0) return false; int target = sum / 2; std::vector<int> dp(target + 1,0); for(int i = 0; i < nums.size(); i++) { 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; else return false; } }
例题理解(对应494. 目标和(LeetCode)):
给你一个非负整数数组 nums
和一个整数 target
。
向数组中的每个整数前添加 '+'
或 '-'
,然后串联起所有整数,可以构造一个 表达式 :
- 例如,
nums = [2, 1]
,可以在2
之前添加'+'
,在1
之前添加'-'
,然后串联起来得到表达式"+2-1"
。
返回可以通过上述方法构造的、运算结果等于 target
的不同 表达式 的数目。
示例 1:
输入:nums = [1,1,1,1,1], target = 3 输出:5 解释:一共有 5 种方法让最终目标和为 3 。 -1 + 1 + 1 + 1 + 1 = 3 +1 - 1 + 1 + 1 + 1 = 3 +1 + 1 - 1 + 1 + 1 = 3 +1 + 1 + 1 - 1 + 1 = 3 +1 + 1 + 1 + 1 - 1 = 3
示例 2:
输入:nums = [1], target = 1 输出:1
提示:
1 <= nums.length <= 20
0 <= nums[i] <= 1000
0 <= sum(nums[i]) <= 1000
-1000 <= target <= 1000
解题思路:
将nums数组的选择抽取划分为题干对应的两种状态,"+" 和 "-";我们称 "+" 的成员集成为left数组,"-" 的成员集成为right数组;
显然可见:
① int sum{0}; for: sum += nums.elems; sum = sum(left) + sum(right); sum => const //对nums数组成员求和,其值为定值,且可转换为left和 right的成员和 ② sum(left) - sum(right) = target; target => const //满足题目要求的left和right,其相减值为定值
①和②可推理出
target + sum = sum(left) * 2 => sum(left) = (target + sum) / 2; //依此可得,sum(left)也为定值
推导出,当我们满足sum(left)为 (target + sum) / 2时,便可满足题干要求,抽象为当我们装满容量为sum(left)的背包时,dp[left]所存储的值即为解
确定背包定义式: std::vector<int> dp (storage + 1,0); storage = (sum + target) / 2;
确定背包状态转移公式: dp[j] = dp[j] + dp[j - nums[i]] + nums[i] == dp[j] += dp[j - nums[i]] + nums[i];解释为在背包容量为j时,当前dp[j]的值为装入选择该i物品加入left数组和不加入left数组后的满足次数;
该题目与许多其他背包问题的思维大致相同,都是将题目问题转换为可理解的背包01问题.
完整代码:
class Solution { public: int findTargetSumWays(std::vector<int>& nums, int target) { int sum{0}; for(int elem : nums) { sum += elem; } if((target + sum) % 2 != 0 || abs(target) > sum) return 0; int storage = (sum + target) / 2; std::vector<int> dp (storage + 1,0); dp[0] = 1; for(int i = 0; i < nums.size(); i++) { for(int j = storage; j >= nums[i]; j--) { dp[j] += dp[j - nums[i]]; } } return dp[storage]; } };
文章参考于: 代码随想录,CSDN,CNBlog等各优秀编辑者,开发者的优质文章作品.
--2024.11.10 Neko 总结
这里是在推理在storage为 j - sizes[i] 的情况下的最大价值(dp[i - 1][j - sizes[i]),从而求得装载第i个物品后总sizes <= storage的最大价值,因为dp[i - 1][ j -sizes[i]]为子状态,其已经定义且为已知结果,故能推导出选择第i个物品时的最大价值 ↩︎
这里的dp[i][j] 初始化代码为 (sizes.size(),std::vector<int>(storage + 1,0)); 此处为兼容语言数组特性,将对其自然数字记录方式;第i个物品的大小在sizes[i - 1]处存放,而重量则符合自然数字规律(0-storage) ↩︎
C++的此定义式本身也初始化了所有元素(为0),语法请自行查略 ↩︎
本文作者:NekoBlog
本文链接:https://www.cnblogs.com/NekoBlog/p/18537978
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
2023-11-10 Visual Studio-OpenGL基础环境配置