力扣:416. 分割等和子集 & 1049. 最后一块石头的重量 II (动态规划)(二合一,一次吃透两道题)

力扣:416. 分割等和子集 & 1049. 最后一块石头的重量 II 用的方法都是01背包解法,思路也是近乎一样,这里就放在一起讲解了(主要讲解第一题,第二题大家可以直接自己AC)。01背包解法详细讲解请见上篇博客01背包问题(二)

416. 分割等和子集

题目:

给你一个 只包含正整数 的 非空 数组 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

思路:

背包问题有多种背包方式,常见的有:01背包、完全背包、多重背包、分组背包和混合背包等等。

要注意题目描述中商品是不是可以重复放入。

即一个商品如果可以重复多次放入是完全背包,而只能放入一次是01背包,写法还是不一样的。

要明确本题中我们要使用的是01背包,因为元素我们只能用一次。

回归主题:首先,本题要求集合里能否出现总和为 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. 确定递推公式

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]);

  1. dp数组如何初始化

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

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

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

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

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

代码如下:

        # 创建一个长度为10001的数组dp,用于记录是否可以找到和为i的子集
        dp = [0] * 10001  
  1. 确定遍历顺序

在前篇博客01背包讲解中可知如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!

代码如下:

        for i in range(len(nums)):  
            # 从target到nums[i]遍历,更新dp数组
            for j in range(target, nums[i] - 1, -1):  
                # 更新dp[j]的值
                dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])  
  1. 举例推导dp数组

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

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

用例1,输入[1,5,11,5] 为例,如图:
力扣:416. 分割等和子集 & 1049. 最后一块石头的重量 II (动态规划)(二合一,一次吃透两道题)_第1张图片
最后dp[11] == 11,说明可以将这个数组分割成两个子集,使得两个子集的元素和相等。

代码及详细注释:

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        # 创建一个长度为10001的数组dp,用于记录是否可以找到和为i的子集
        dp = [0] * 10001  
        # 如果nums的和为奇数,则无法分割成两个和相等的子集
        if sum(nums) % 2 == 1:  
            return False
        # 计算目标和
        target = sum(nums) // 2  
        # 遍历nums中的每个数字
        for i in range(len(nums)):  
            # 从target到nums[i]遍历,更新dp数组
            for j in range(target, nums[i] - 1, -1):  
                # 更新dp[j]的值
                dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])  
        # 如果dp[target]等于target,则表示可以找到和为target的子集
        if dp[target] == target:  
            return True
        else:
            # 否则返回False,表示无法找到和为target的子集
            return False  

  • 时间复杂度:O(n^2)
  • 空间复杂度:O(n)

1049. 最后一块石头的重量 II

题目:

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

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

如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。

示例 1:

输入:

stones = [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],这就是最优值。

示例 2:

输入:

stones = [31,26,33,21,40]

输出:

5

提示:

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

思路:

本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了。

思路跟第一题的思路一样,动态规划五部曲基本都一样这里就不详细讲解了,最后一点有所出入

  1. 举例推导dp数组

举例,输入:[2,4,1,1],此时target = (2 + 4 + 1 + 1)/2 = 4 ,dp数组状态图如下:

力扣:416. 分割等和子集 & 1049. 最后一块石头的重量 II (动态规划)(二合一,一次吃透两道题)_第2张图片
最后dp[target]里是容量为target的背包所能背的最大重量。

那么分成两堆石头,一堆石头的总重量是dp[target],另一堆就是sum - dp[target]。

在计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的。

那么相撞之后剩下的最小石头重量就是 sum - dp[target]) - dp[target]

代码及详细注释:

class Solution:
    def lastStoneWeightII(self, stones: List[int]) -> int:
        # 创建一个长度为1501的数组dp,用于记录是否可以找到和为i的子集
        dp = [0] * 1501
        # 计算目标和
        target = sum(stones) // 2
        # 遍历stones中的每个石头重量
        for i in range(len(stones)):
            # 从target到stones[i]遍历,更新dp数组
            for j in range(target, stones[i] - 1, -1):
                # 更新dp[j]的值
                dp[j] = max(dp[j], dp[j - stones[i]] + stones[i])
        # 返回石头总重量减去两个子集的总和
        return sum(stones) - dp[target] - dp[target]

  • 时间复杂度:O(m × n) , m是石头总重量(准确的说是总重量的一半),n为石头块数
  • 空间复杂度:O(m)

你可能感兴趣的:(算法,python,leetcode,动态规划,算法,python)