【LeetCode动态规划#06】分割等和子集(01背包问题一维写法实战)

这篇具有很好参考价值的文章主要介绍了【LeetCode动态规划#06】分割等和子集(01背包问题一维写法实战)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

分割等和子集

分割等和子集

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例 1:

输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11]

示例 2:

输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。

提示:

1 <= nums.length <= 200
1 <= nums[i] <= 100

思路

题意分析

题目是要找是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

那么只要找到集合里能够出现 sum / 2 的子集总和,就算是可以分割成两个相同元素和子集了。

例如示例1:

输入元素总和是22,要分成两个元素和相等的子集,那这两个子集各自之和肯定都是11,即sum / 2

解题方法

既然这题出现在dp章节了,那肯定就是用dp来做了

但是,如果第一次遇见,应该怎么确定适用dp来解呢?可以先尝试分析一下题目,看看能不能把题目往这边靠

比如,这题中,每个构成子集的元素只能使用一次(关键点1)

这个描述可以联想到01背包问题,那就尝试去抽象一下:

本题可以转换为一个01背包问题,目标是给定一个数组,用数组中的元素去填满一个容量为 sum / 2 的背包(如果能填满的话,就找到了题目要求的子集)

还不够,还要继续确定什么是物品(物品重量),什么是价值

显然,能够作为物品的只有数组里的元素了,那么物品的重量就是元素数值,物品的价值也是元素数值

现在就把01背包问题套进来了,当前问题的条件如下:

  • 背包的体积是 sum / 2
  • 要放入背包的物品是数组元素,其重量为元素数值,重量也为元素数值
  • 背包中每个元素不可重复使用
  • 如果背包正好装满,那么说明找到了总和为 sum / 2 的子集(有一种就行,不用最优)

ok,现在可以五部曲去分析了

五步走

以一维01背包问题为模板来解题,详见

1、确定dp数组含义

回顾一下分析一维数组的背包问题时,dp[j]的定义:在容量为j时,背包能装下的物品的最大价值

根据上面的分析,本题中物品重量是数组元素数值,物品价值还是数组元素数值

那上面的定义可以改为:背包总容量是j,放入物品后,背包的最大重量为dp[j]

即本题dp数组的含义

举个例子,假设背包容量为target,那么dp[target]就是背包装满时的重量

当dp[target] = target,背包就装满了,此时变找到了题解子集

这里会出现一种情况:在所给的背包容量下,没有物品能装满

什么意思?举个例子,输入是数组 [1, 5, 11, 5],target是7

那么,当dp[7]的时候,背包只能放1、5,总价为6,没放满,显然这不是我们需要的结果(不满足dp数组定义),不过好在我们是倒序遍历,在之后的遍历中,dp[6] = 6,满足dp定义,并且我们也找到了一个满足条件的子集[1,5]

2、确定递推公式

其实可以直接套用一维dp数组背包问题的递推公式:dp[j] = max(dp[j], dp[j - wight[i]] + value[i])

本题中,物品重量和价值都是数组元素,所以可以改成:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])

其含义是:

不放入物品时,容量为j的背包最大的价值是dp[j];

放入物品时,容量为j - nums[i]的背包所背的最大价值是dp[j - nums[i]];

3、确定如何初始化

还是参考一维背包问题,当背包容量为0,也就是dp[0]时,里面不可能装东西

因此,dp[0] = 0;

其他部分全部初始化为0(因为题目说了数组元素全是正数),目的是为了防止倒序遍历过程中,上一层的值与当前值叠加(详见:一维dp数组如何初始化)

又因为题目给了:每个数组元素不会超过100,数组本身的大小不会超过200,且元素总和不会超过20000

那么创建dp数组时只需要取一半的大小即可

vector<int> dp(10001, 0);
4、确定遍历顺序

都套一维dp数组的背包问题了,肯定是倒序遍历

原因详见:为什么要倒序遍历?

for(int i = 0; i < nums.size(); ++i){//遍历物品
    for(int j = target; j > 0; --j){//遍历背包容量
        dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
    }
}

代码

根据上面的分析可写出一下代码

步骤:

1、求数组的总和,用于求背包容量target

2、判断当前总和sum是否为偶数,不为偶数就直接返回false

3、根据题目提示定义dp数组,同时进行初始化

4、倒序遍历dp数组,先遍历物品,再遍历背包容量,计算dp[j]

5、判断dp[target] 是否等于 target

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        //求背包容量,也就是target
        //先定义一个sum用于求数组总和
        int sum = 0;
        for(auto num : nums) sum += num;

        //如果sum值不能均分(不是偶数),那么一定无法分为两个和相等的子集,直接返回false
        if(sum % 2 == 1) return false;
        int target = sum / 2;

        //定义dp数组,同时初始化
        vector<int> dp(10001, 0);
        // //初始化dp数组
        // dp[0] = 0;
        //遍历dp数组(注意是倒序遍历)
        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]);
            }
        }
        //判断是否满足题意条件
        if(dp[target] == target) return true;
        return false;
    }
};

二刷问题

1、第二层for循环中的条件需要重新理解一下,也就是遍历背包容量的过程需要重新梳理一下
		//遍历dp数组(注意是倒序遍历)
        for(int i = 0; i < nums.size(); ++i){//遍历物品
            //第一层for只是用来从nums中取物品,这个取的顺序可以是正序的
            //第二层for开始倒序遍历背包,j为当前遍历到的背包容量
            //如果背包容量大于根据i取出的物品容量,那么可以继续放入,否则遍历结束
            for(int j = target; j >= nums[i]; --j){//遍历背包容量
                dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
            }
        }

要记住,我们已经将背包降维,使用两层for的原因不是遍历两个维度,内层循环的遍历是核心

2、dp[target] == target条件有点不理解了

虽然"背包问题"里面有个"背包",但实际上"背包"本身是没有"重量"的

再来看一下dp数组的含义:背包总容量是j,放入物品后,背包的最大重量为dp[j]

dp[target]表示:背包总容量是target,放入物品后,背包的最大重量为dp[target]

换种说法就是,当背包总容量是target时,那么该背包装满的最大重量就是dp[target]

也就是说,在数值上,如果dp[target] == target,代表背包装满了

最后一块石头的重量II

力扣题目链接(opens new window)

题目难度:中等

有一堆石头,每块石头的重量都是正整数。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

如果 x == y,那么两块石头都会被完全粉碎;

如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。

最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。

示例:

  • 输入:[2,7,4,1,8,1]
  • 输出:1

解释:

  • 组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
  • 组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
  • 组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
  • 组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

提示:

  • 1 <= stones.length <= 30
  • 1 <= stones[i] <= 1000

思路

给了一个石头数组stones,要求我们从中两两选取石头相撞(作差),最后只能剩下一块石头,如何使得这块石头的重量最小

由常识可知,如果要两数之差更小,那么这两个数就要越接近越好。解决该问题的一个直观思路是,把所有值相近的石头两两用来相撞(作差)

注意,这里不对选取顺序做要求

因此问题转换为:我们可以先把石头分成两堆,这两堆石头的重量总和尽量接近,然后以"堆"为单位进行作差,得到"最小的可能重量"

这里就可以套01背包了,选择其中一个“堆”看做背包,往里面放石头即可

怎么确定背包的容量呢?计算出石头的总重量sum,然后除以2即可

注意,除2是向下取整

和 分割等和子集 类似,这里放入背包的物品的重量和价值都是数组中元素本身的值

可见,很多时候,背包问题中,所放物品不一定都有价值,所谓价值和重量更多的是一种比喻,可以根据题目的具体情况进行替换

套入01背包问题后,当前问题的条件如下:

  • 背包的容量是 sum / 2
  • 要放入背包的物品是石头数组stones中的元素,其重量和价值均为元素数值
  • 背包中元素不可重复使用
  • 若背包正好装满,则表明石头数组stones被尽可能均分为两个部分,此时可以进行作差找出"最小的可能重量"

五步走

1、确定dp数组含义

老规矩,回忆一下一维01背包的dp含义:dp[j],在容量为j时,背包能装下的物品的最大价值

假设target为求得的背包容量,那么本题dp数组的含义就是:dp[j]是在容量为j时背包能装下的物品的最大价值

2、确定递推公式

一维01背包的递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

这里可以直接套用(不明白的详见:分割等和子集)

dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);

3、初始化dp数组

当背包容量j为0时,此时无法放入任何东西

因此,dp[0]需要初始化为0,即 dp[0] = 0

其余部分初始化为0

原因是我们需要从后向前遍历,因此为了不让初始值覆盖递归累加得到的结果,初始值要设为0(还是详见之前的博客)

4、确定遍历顺序

从后向前遍历

代码

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        //求出总重量
        int sum = 0;
        for(auto stone : stones) sum += stone;

        int target = sum / 2;//求出背包容量大小,注意是向下取整

        //初始化dp数组
        vector<int> dp(1501, 0);//因为dp[0] = 0,所以一块初始化了

        //遍历dp数组(注意是倒序遍历)
        //第一层for循环用于遍历物品,第二层for循环则是尝试将物品放入不同容量下的背包(遍历背包)
        for(int i = 0; i < stones.size(); ++i){//遍历物品
            for(int j = target; j >= stones[i]; --j){//遍历背包容量
                dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }
        //dp数组遍历完成,此时dp[target]就是容量为target的背包所能装下的最大重量
        //石头被分为两堆,一堆重量是dp[target],另一堆是sum - dp[target]
        //又因为target是由sum除以2向下取整得到的,所以sum - dp[target]一定大于等于dp[target](差值就是被取整抹掉的那些部分)
        //所以相撞之后剩下的是两堆石头总和的差值
        return (sum - dp[target]) - dp[target];
    }
};

二刷问题

为什么在创建dp数组时,其大小设置为1501?

设置为1501的原因是为了满足题目中给出的石头重量的范围(1 <= stones[i] <= 1000)和背包容量的要求(1 <= stones.length <= 30)

因为石头重量的范围是1到1000,而背包容量是石头总重量的一半,所以最大的石头总重量是1000 * 30 = 3000,其一半是1500。为了提供一定的缓冲空间,将数组大小设置为1501,以确保能够覆盖到所有可能的重量和容量。文章来源地址https://www.toymoban.com/news/detail-408945.html

到了这里,关于【LeetCode动态规划#06】分割等和子集(01背包问题一维写法实战)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Day42|动态规划part04: 01背包问题,你该了解这些!、滚动数组、416. 分割等和子集

    其他背包,面试几乎不会问,都是竞赛级别的了,leetcode上连多重背包的题目都没有,所以题库也告诉我们,01背包和完全背包就够用了。 而完全背包又是也是01背包稍作变化而来,即:完全背包的物品数量是无限的。 01 背包问题描述 有n件物品和一个最多能背重量为w 的背包

    2024年04月25日
    浏览(38)
  • 01背包问题-递推公式的自我理解与LeetCode 416. 分割等和子集

    学算法好痛苦,完全是对我智力的一次次折磨,看了好多博客,对二维dp数组的理解都是直接搬了代码随想录,搬了随想录又没详细解释,大家都是一眼看懂的吗,好吧() 如果有一个容量为 j 的这样的背包——一个独立的,容量为j的背包。(没把它理解为“剩余容量”) 对于

    2024年02月07日
    浏览(44)
  • 解决背包衍生题目:单词拆分和分割等和子集--动态规划方式深度呈现

    目录 139. 单词拆分 解题思路 代码实现 416. 分割等和子集 二维动态规划 状态压缩(一维) 问题拓展 背包九讲知识总结 相关问题 题目描述 给你一个字符串  s  和一个字符串列表  wordDict  作为字典。请你判断是否可以利用字典中出现的单词拼接出  s  。 注意: 不要求字典中

    2024年02月03日
    浏览(50)
  • 【Day42】代码随想录之动态规划0-1背包_416. 分割等和子集

    动态规划理论基础 动规五部曲: 确定dp数组 下标及dp[i] 的含义。 递推公式:比如斐波那契数列 dp[i] = dp[i-1] + dp[i-2]。 初始化dp数组。 确定遍历顺序:从前到后or其他。 推导dp数组。 出现结果不正确: 打印dp日志和自己想的一样:递推公式、初始化或者遍历顺序出错。 打印

    2024年02月20日
    浏览(68)
  • leetcode416. 分割等和子集(动态规划-java)

    来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/partition-equal-subset-sum 给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。 示例 1: 输入:nums = [1,5,11,5] 输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11] 。

    2024年02月11日
    浏览(37)
  • 算法训练第四十二天|01背包问题 二维 、01背包问题 一维、416. 分割等和子集

    视频链接:https://www.bilibili.com/video/BV1cg411g7Y6/ 参考:https://programmercarl.com/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-1.html 对于面试的话,其实掌握01背包,和完全背包,就够用了,最多可以再来一个多重背包。 而完全背包又是也是01背包稍作变化而来,即:完全

    2024年02月01日
    浏览(46)
  • 动态规划(分割等和子集)

    题目难易:中等 给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。 注意: 每个数组中的元素不会超过 100 数组的大小不会超过 200 示例 1: 输入: [1, 5, 11, 5] 输出: true 解释: 数组可以分割成 [1, 5, 5] 和 [11]. 示例 2: 输入: [1, 2

    2024年02月02日
    浏览(41)
  • 【力扣】416. 分割等和子集 <动态规划、回溯>

    给你一个 只包含正整数的非空数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。 示例 1: 输入:nums = [1,5,11,5] 输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11] 。 示例 2: 输入:nums = [1,2,3,5] 输出:false 解释:数组不能分割成两个元素和

    2024年02月10日
    浏览(39)
  • 力扣hot100:416.分割等和子集(组合/动态规划/STL问题)

    组合数问题 我们思考一下,如果要把数组分割成两个子集,并且两个子集的元素和相等,是否等价于在数组中寻找若干个数使之和等于所有数的一半?是的! 因此我们可以想到,两种方式: ①回溯的方式找到target,但是回溯是阶乘级别的算法,这里会超时。 ②从前往后遍历

    2024年04月28日
    浏览(37)
  • 算法设计与分析实验4 :利用动态规划的方法解决子集等和分割判断问题

    实验4  利用动态规划的方法解决子集等和分割判断问题 一、实验目的 1. 了解动态规划的主要思想。 2. 掌握背包问题解决方法用以解决该问题。 3. 分析核心代码的时间复杂度和空间复杂度。 二、实验内容和要求 题目:给定一个只包含正整数的非空数组。是否可以将这个数组

    2024年04月23日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包