力扣: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背包,来解决这个问题了。
动规五部曲分析如下:
- 确定dp数组以及下标的含义
01背包中,dp[j] 表示: 容量为j的背包,所背的物品价值最大可以为dp[j]。
本题中每一个元素的数值既是重量,也是价值。
套到本题,dp[j]表示 背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dp[j]。
那么如果背包容量为target, dp[target]就是装满 背包之后的重量,所以 当 dp[target] == target 的时候,背包就装满了。
值得注意的是本题中的"物品"重量和价值相等
- 确定递推公式
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]);
- dp数组如何初始化
在01背包,一维dp如何初始化,已经讲过,
从dp[j]的定义来看,首先dp[0]一定是0。
如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。
这样才能让dp数组在递推的过程中取得最大的价值,而不是被初始值覆盖了。
本题题目中 只包含正整数的非空数组,所以非0下标的元素初始化为0就可以了。
代码如下:
# 创建一个长度为10001的数组dp,用于记录是否可以找到和为i的子集dp = [0] * 10001
- 确定遍历顺序
在前篇博客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])
- 举例推导dp数组
dp[j]的数值一定是小于等于j的。
如果dp[j] == j 说明,集合中的子集总和正好可以凑成总和j,理解这一点很重要。
用例1,输入[1,5,11,5] 为例,如图:
最后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 Trueelse:# 否则返回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背包问题了。
思路跟第一题的思路一样,动态规划五部曲基本都一样这里就不详细讲解了,最后一点有所出入
- 举例推导dp数组
举例,输入:[2,4,1,1],此时target = (2 + 4 + 1 + 1)/2 = 4 ,dp数组状态图如下:
最后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)