当前位置:   article > 正文

Leetcode 416.分割等和子集_子集等和分割判断问题功能说明

子集等和分割判断问题功能说明

题目描述

动态规划(转换为 0-1 背包问题

关于背包问题的介绍,大家可以在互联网上搜索「背包九讲」进行学习,其中「0-1」背包问题是这些问题的基础。「力扣」上涉及的背包问题有「0-1」背包问题、完全背包问题、多重背包问题。

本题解有些地方使用了「0-1」背包问题的描述,因此会不加解释的使用「背包」、「容量」这样的名词。

说明:这里感谢很多朋友在这篇题解下提出的建议,对我的启发很大。本题解的阅读建议是:先浏览代码,然后再看代码之前的分析,能更有效理解知识点和整个问题的思考路径。题解后也增加了「总结」,供大家参考。


转换为 「0 - 1」 背包问题

这道问题是我学习「背包」问题的入门问题,做这道题需要做一个等价转换:是否可以从输入数组中挑选出一些正整数,使得这些数的和 等于 整个数组元素的和的一半。很坦白地说,如果不是我的老师告诉我可以这样想,我很难想出来。容易知道:数组的和一定得是偶数。

本题与 0-1 背包问题有一个很大的不同,即:

  • 0-1 背包问题选取的物品的容积总量 不能超过 规定的总量;
  • 本题选取的数字之和需要 恰好等于 规定的和的一半。

这一点区别,决定了在初始化的时候,所有的值应该初始化为 false。 (《背包九讲》的作者在介绍 「0-1 背包」问题的时候,有强调过这点区别。)

「0 - 1」 背包问题的思路

作为「0-1 背包问题」,它的特点是:「每个数只能用一次」。解决的基本思路是:物品一个一个选,容量也一点一点增加去考虑,这一点是「动态规划」的思想,特别重要。
在实际生活中,我们也是这样做的,一个一个地尝试把候选物品放入「背包」,通过比较得出一个物品要不要拿走。

具体做法是:画一个 len 行,target + 1 列的表格。这里 len 是物品的个数,target 是背包的容量。len 行表示一个一个物品考虑,target + 1多出来的那 1 列,表示背包容量从 0 开始考虑。很多时候,我们需要考虑这个容量为 0 的数值。

 

状态与状态转移方程

 

最终得到dp[n−1][target] 即为答案。

参考代码 1

  • C++
  1. class Solution {
  2. public:
  3. bool canPartition(vector<int>& nums) {
  4. int n = nums.size();
  5. if (n < 2) {
  6. return false;
  7. }
  8. int sum = accumulate(nums.begin(), nums.end(), 0);
  9. int maxNum = *max_element(nums.begin(), nums.end());
  10. if (sum & 1) {
  11. return false;
  12. }
  13. int target = sum / 2;
  14. if (maxNum > target) {
  15. return false;
  16. }
  17. vector<vector<int>> dp(n, vector<int>(target + 1, 0));
  18. for (int i = 0; i < n; i++) {
  19. dp[i][0] = true;
  20. }
  21. dp[0][nums[0]] = true;
  22. for (int i = 1; i < n; i++) {
  23. int num = nums[i];
  24. for (int j = 1; j <= target; j++) {
  25. if (j >= num) {
  26. dp[i][j] = dp[i - 1][j] | dp[i - 1][j - num];
  27. } else {
  28. dp[i][j] = dp[i - 1][j];
  29. }
  30. }
  31. }
  32. return dp[n - 1][target];
  33. }
  34. };

复杂度分析


 

考虑空间优化(重要)

说明:这个技巧很常见、很基础,请一定要掌握。

「0-1 背包问题」常规优化:「状态数组」从二维降到一维,减少空间复杂度。

  • 在「填表格」的时候,当前行只参考了上一行的值,因此状态数组可以只设置 22 行,使用「滚动数组」的技巧「填表格」即可;

  • 实际上,在「滚动数组」的基础上还可以优化,在「填表格」的时候,当前行总是参考了它上面一行 「头顶上」 那个位置和「左上角」某个位置的值。因此,我们可以只开一个一维数组,从后向前依次填表即可。

友情提示:这一点在刚开始学习的时候,可能会觉得很奇怪。理解的办法是:拿题目中的示例,画一个表格,自己模拟一遍程序是如何「填表」的行为,就很清楚为什么状态数组降到 1 行的时候,需要「从后前向」填表。

  • 「从后向前」 写的过程中,一旦 nums[i] <= j 不满足,可以马上退出当前循环,因为后面的 j 的值肯定越来越小,没有必要继续做判断,直接进入外层循环的下一层。相当于也是一个剪枝,这一点是「从前向后」填表所不具备的。

说明:如果对空间优化技巧还有疑惑的朋友,本题解下的精选评论也解释了如何理解这个空间优化的技巧,请大家前往观看。

参考代码 3:只展示了使用一维表格,并且「从后向前」填表格的代码。

  • C++
    1. class Solution {
    2. public:
    3. bool canPartition(vector<int>& nums) {
    4. int n = nums.size();
    5. if (n < 2) {
    6. return false;
    7. }
    8. int sum = 0, maxNum = 0;
    9. for (auto& num : nums) {
    10. sum += num;
    11. maxNum = max(maxNum, num);
    12. }
    13. if (sum & 1) {
    14. return false;
    15. }
    16. int target = sum / 2;
    17. if (maxNum > target) {
    18. return false;
    19. }
    20. vector<int> dp(target + 1, 0);
    21. dp[0] = true;
    22. for (int i = 0; i < n; i++) {
    23. int num = nums[i];
    24. for (int j = target; j >= num; --j) {
    25. dp[j] |= dp[j - num];
    26. }
    27. }
    28. return dp[target];
    29. }
    30. };


 

总结

「0-1 背包」问题是一类非常重要的动态规划问题,一开始学习的时候,可能会觉得比较陌生。建议动笔计算,手动模拟填表的过程,其实就是画表格。这个过程非常重要,自己动手填过表,更能加深体会程序是如何执行的,也能更好地理解「空间优化」技巧的思路和好处。

在编写代码完成以后,把数组 dp 打印出来,看看是不是与自己手算的一样。以加深体会动态规划的设计思想:「不是直接面对问题求解,而是从一个最小规模的问题开始,新问的最优解均是由比它规模还小的子问题的最优解转换得到,在求解的过程中记录每一步的结果,直至所要求的问题得到解」。


最后思考为什么题目说是正整数,有 00 是否可以,有实数可以吗,有负数可以吗?

  • 00 的存在意义不大,放在哪个子集都是可以的;
  • 实数有可能是无理数,也可能是无限不循环小数,在计算整个数组元素的和的一半,要除法,然后在比较两个子集元素的和是否相等的时候,就会遇到精度的问题;
  • 再说负数,负数其实也是可以存在的,但要用到「回溯搜算法」解决。

相关问题

「力扣」上的 0-1 背包问题

  • 「力扣」第 416 题:分割等和子集(中等);
  • 「力扣」第 474 题:一和零(中等);
  • 「力扣」第 494 题:目标和(中等);
  • 「力扣」第 879 题:盈利计划(困难);

「力扣」上的 完全背包问题

  • 「力扣」第 322 题:零钱兑换(中等);
  • 「力扣」第 518 题:零钱兑换 II(中等);
  • 「力扣」第 1449 题:数位成本和为目标值的最大数字(困难)。

这里要注意鉴别:「力扣」第 377 题,不是「完全背包」问题。

参考资料

分割等和子集 - 分割等和子集 - 力扣(LeetCode)

动态规划(转换为 0-1 背包问题) - 分割等和子集 - 力扣(LeetCode)

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/空白诗007/article/detail/744952
推荐阅读
相关标签
  

闽ICP备14008679号