赞
踩
关于背包问题的介绍,大家可以在互联网上搜索「背包九讲」进行学习,其中「0-1」背包问题是这些问题的基础。「力扣」上涉及的背包问题有「0-1」背包问题、完全背包问题、多重背包问题。
本题解有些地方使用了「0-1」背包问题的描述,因此会不加解释的使用「背包」、「容量」这样的名词。
说明:这里感谢很多朋友在这篇题解下提出的建议,对我的启发很大。本题解的阅读建议是:先浏览代码,然后再看代码之前的分析,能更有效理解知识点和整个问题的思考路径。题解后也增加了「总结」,供大家参考。
这道问题是我学习「背包」问题的入门问题,做这道题需要做一个等价转换:是否可以从输入数组中挑选出一些正整数,使得这些数的和 等于 整个数组元素的和的一半。很坦白地说,如果不是我的老师告诉我可以这样想,我很难想出来。容易知道:数组的和一定得是偶数。
本题与 0-1 背包问题有一个很大的不同,即:
这一点区别,决定了在初始化的时候,所有的值应该初始化为 false
。 (《背包九讲》的作者在介绍 「0-1 背包」问题的时候,有强调过这点区别。)
作为「0-1 背包问题」,它的特点是:「每个数只能用一次」。解决的基本思路是:物品一个一个选,容量也一点一点增加去考虑,这一点是「动态规划」的思想,特别重要。
在实际生活中,我们也是这样做的,一个一个地尝试把候选物品放入「背包」,通过比较得出一个物品要不要拿走。
具体做法是:画一个 len
行,target + 1
列的表格。这里 len
是物品的个数,target
是背包的容量。len
行表示一个一个物品考虑,target + 1
多出来的那 1
列,表示背包容量从 0
开始考虑。很多时候,我们需要考虑这个容量为 0
的数值。
最终得到dp[n−1][target] 即为答案。
参考代码 1:
- class Solution {
- public:
- bool canPartition(vector<int>& nums) {
- int n = nums.size();
- if (n < 2) {
- return false;
- }
- int sum = accumulate(nums.begin(), nums.end(), 0);
- int maxNum = *max_element(nums.begin(), nums.end());
- if (sum & 1) {
- return false;
- }
- int target = sum / 2;
- if (maxNum > target) {
- return false;
- }
- vector<vector<int>> dp(n, vector<int>(target + 1, 0));
- for (int i = 0; i < n; i++) {
- dp[i][0] = true;
- }
- dp[0][nums[0]] = true;
- for (int i = 1; i < n; i++) {
- int num = nums[i];
- for (int j = 1; j <= target; j++) {
- if (j >= num) {
- dp[i][j] = dp[i - 1][j] | dp[i - 1][j - num];
- } else {
- dp[i][j] = dp[i - 1][j];
- }
- }
- }
- return dp[n - 1][target];
- }
- };

复杂度分析:
说明:这个技巧很常见、很基础,请一定要掌握。
「0-1 背包问题」常规优化:「状态数组」从二维降到一维,减少空间复杂度。
在「填表格」的时候,当前行只参考了上一行的值,因此状态数组可以只设置 22 行,使用「滚动数组」的技巧「填表格」即可;
实际上,在「滚动数组」的基础上还可以优化,在「填表格」的时候,当前行总是参考了它上面一行 「头顶上」 那个位置和「左上角」某个位置的值。因此,我们可以只开一个一维数组,从后向前依次填表即可。
友情提示:这一点在刚开始学习的时候,可能会觉得很奇怪。理解的办法是:拿题目中的示例,画一个表格,自己模拟一遍程序是如何「填表」的行为,就很清楚为什么状态数组降到 1 行的时候,需要「从后前向」填表。
nums[i] <= j
不满足,可以马上退出当前循环,因为后面的 j
的值肯定越来越小,没有必要继续做判断,直接进入外层循环的下一层。相当于也是一个剪枝,这一点是「从前向后」填表所不具备的。说明:如果对空间优化技巧还有疑惑的朋友,本题解下的精选评论也解释了如何理解这个空间优化的技巧,请大家前往观看。
参考代码 3:只展示了使用一维表格,并且「从后向前」填表格的代码。
- class Solution {
- public:
- bool canPartition(vector<int>& nums) {
- int n = nums.size();
- if (n < 2) {
- return false;
- }
- int sum = 0, maxNum = 0;
- for (auto& num : nums) {
- sum += num;
- maxNum = max(maxNum, num);
- }
- if (sum & 1) {
- return false;
- }
- int target = sum / 2;
- if (maxNum > target) {
- return false;
- }
- vector<int> dp(target + 1, 0);
- dp[0] = true;
- for (int i = 0; i < n; i++) {
- int num = nums[i];
- for (int j = target; j >= num; --j) {
- dp[j] |= dp[j - num];
- }
- }
- return dp[target];
- }
- };

「0-1 背包」问题是一类非常重要的动态规划问题,一开始学习的时候,可能会觉得比较陌生。建议动笔计算,手动模拟填表的过程,其实就是画表格。这个过程非常重要,自己动手填过表,更能加深体会程序是如何执行的,也能更好地理解「空间优化」技巧的思路和好处。
在编写代码完成以后,把数组 dp
打印出来,看看是不是与自己手算的一样。以加深体会动态规划的设计思想:「不是直接面对问题求解,而是从一个最小规模的问题开始,新问的最优解均是由比它规模还小的子问题的最优解转换得到,在求解的过程中记录每一步的结果,直至所要求的问题得到解」。
最后思考为什么题目说是正整数,有 00 是否可以,有实数可以吗,有负数可以吗?
「力扣」上的 0-1 背包问题:
「力扣」上的 完全背包问题:
这里要注意鉴别:「力扣」第 377 题,不是「完全背包」问题。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。