当前位置: 首页 > article >正文

代码随想录算法训练营第三十五天 | 416. 分割等和子集

416. 分割等和子集  

  • 题目链接:力扣题目链接
  • 文章讲解:代码随想录 

  • 视频讲解:动态规划之背包问题,这个包能装满吗?| LeetCode:416.分割等和子集

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

注意: 每个数组中的元素不会超过 100 数组的大小不会超过 200

示例 1:

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

示例 2:

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

提示:

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

思路

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

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

首先,本题要求集合里能否出现总和为 sum / 2 的子集。

那么来一一对应一下本题,看看背包问题如何来解决。

只有确定了如下四点,才能把01背包问题套到本题上来。

  • 背包的体积为sum / 2
  • 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
  • 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
  • 背包中每一个元素是不可重复放入。

以上分析完,我们就可以套用01背包,来解决这个问题了。

动规五部曲分析如下:

1.确定dp数组以及下标的含义

01背包中,dp[j] 表示: 容量为j的背包,所背的物品价值最大可以为dp[j]。

本题中每一个元素的数值既是重量,也是价值。

套到本题,dp[j]表示 背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dp[j]

那么如果背包容量为target, dp[target]就是装满 背包之后的重量,所以 当 dp[target] == target 的时候,背包就装满了。

有录友可能想,那还有装不满的时候?

拿输入数组 [1, 5, 11, 5],举例, dp[7] 只能等于 6,因为 只能放进 1 和 5。

而dp[6] 就可以等于6了,放进1 和 5,那么dp[6] == 6,说明背包装满了。

2.确定递推公式

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

本题,相当于背包里放入数值,那么物品i的重量是nums[i],其价值也是nums[i]。

所以递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);

3.dp数组如何初始化

在01背包,一维dp如何初始化,已经讲过,

从dp[j]的定义来看,首先dp[0]一定是0。

如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。

这样才能让dp数组在递推的过程中取得最大的价值,而不是被初始值覆盖了

本题题目中 只包含正整数的非空数组,所以非0下标的元素初始化为0就可以了。

4.确定遍历顺序

在动态规划:关于01背包问题,你该了解这些!(滚动数组)中就已经说明:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!

5.举例推导dp数组

dp[j]的数值一定是小于等于j的。

如果dp[j] == j 说明,集合中的子集总和正好可以凑成总和j,理解这一点很重要。

用例1,输入[1,5,11,5] 为例,如图:

416.分割等和子集2

最后dp[11] == 11,说明可以将这个数组分割成两个子集,使得两个子集的元素和相等。

方法一: 动态规划-卡哥版一维数组

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        if sum(nums) % 2 != 0:
            return False
        target = sum(nums) // 2
        dp = [0] * (target + 1)
        for num in nums:
            for j in range(target, num-1, -1):
                dp[j] = max(dp[j], dp[j-num] + num)
        return dp[-1] == target

方法二:动态规划(二维数组)

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        
        total_sum = sum(nums)

        if total_sum % 2 != 0:
            return False

        target_sum = total_sum // 2
        dp = [[False] * (target_sum + 1) for _ in range(len(nums) + 1)]

        # 初始化第一行(空子集可以得到和为0)
        for i in range(len(nums) + 1):
            dp[i][0] = True

        for i in range(1, len(nums) + 1):
            for j in range(1, target_sum + 1):
                if j < nums[i - 1]:
                    # 当前数字大于目标和时,无法使用该数字
                    dp[i][j] = dp[i - 1][j]
                else:
                    #如果某个物品的重量小于j,那就可以看该物品是否放入背包
                    #dp[i - 1][j]表示该物品不放入背包,如果在 [0, i - 1] 这个子区间内已经有一部分元素,使得它们的和为 j ,那么 dp[i][j] = true;
                    #dp[i - 1][j - nums[i]]表示该物品放入背包。如果在 [0, i - 1] 这个子区间内就得找到一部分元素,使得它们的和为 j - nums[i]。
                    dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i - 1]]

        return dp[len(nums)][target_sum]

方法三:动态规划(一维数组)

class Solution:
    def canPartition(self, nums: List[int]) -> bool:

        total_sum = sum(nums)

        if total_sum % 2 != 0:
            return False

        target_sum = total_sum // 2
        dp = [False] * (target_sum + 1)
        dp[0] = True

        for num in nums:
            # 从target_sum逆序迭代到num,步长为-1
            for i in range(target_sum, num - 1, -1):
                dp[i] = dp[i] or dp[i - num]
        return dp[target_sum]

心得收获 

这里有个点注意,在求target是要用//得到的是整数类型的数值,如果用/就会得到float类型的数值

方法二中,dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i - 1]]可以好好理解一下,刚开始我有点没有看明白怎么理解,其实就是对应了两种情况:

1.如果取当前的数字nums[i-1],就要看上一层的结果中dp[i - 1][j - nums[i - 1]]是否能够填满背包

2.如果不取当前数值,就要看dp[i - 1][j]是否刚好填满背包


http://www.kler.cn/a/281751.html

相关文章:

  • 深入理解分页及 PageHelper 使用细节与注意事项
  • Java基础夯实——2.4 线程的生命周期
  • 本地文件如何推送到git仓库
  • 鸿蒙北向开发环境安装指南
  • 在ubuntu上安装ubuntu22.04并ros2 humble版本的docker容器记录
  • ChromeDriver驱动下载地址更新(保持最新最全)
  • 华为自研仓颉编程语言测试版上线,计划持续到10月21号
  • 氢能源时代的守护者:氢气传感器在储存与使用中的关键角色
  • 【Linux】第十七章 多路转接(select+poll+epoll)
  • uniapp(微信小程序如何使用单选框、复选框)
  • DevExpress 表格再新增行后滚动条自动移动到新增行
  • 建筑业AI的崛起The Rise of AI and Machine Learning in Construction
  • Android Compose 下拉选择框 ExposedDropdownMenu下拉选择
  • 超越传统:探索Visual Basic在操作系统插件开发的新境界
  • 少儿编程Python系列课程——003python注释
  • Ubuntu 22安装和配置PyCharm详细教程(图文详解)
  • 歌曲分享平台|基于SprinBoot+vue的原创歌曲分享平台系统(源码+数据库+文档)
  • Android实现自定义方向盘-8自定义view的相关问题
  • KOLLMORGEN科尔摩根驱动器AKD-P00607-NBPN-0000
  • 三防平板:定制化服务的趋势——以智慧医疗为例
  • 【Java】—— Java面向对象基础:Java中类的构造器与属性初始化,Student类的实例
  • 一、基于Vue3的开发-环境搭建【pnpm】安装
  • Java-多线程IO工具类
  • Matlab矩阵基础操作
  • LLM大模型入门天花板!《大模型入门:技术原理与实战应用》一本书让你轻松入门大模型(附PDF)
  • 什么是Dropout在机器学习中?