416. 分割等和子集¶
力扣链接(中等):https://leetcode.cn/problems/partition-equal-subset-sum
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
示例 2:
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 100
个人题解¶
转换为 01 背包问题¶
- 要分割为两个等和子集,可以转化为在两个容量相等的背包中放价值相等的物品;
- 每个元素只能使用一次,考虑转换为 01 背包问题;
- 背包要放入集合中的元素,重量为元素值,价值也为元素值;
- 如果背包正好装满,说明找到了总和为总值一半的子集。
动态规划(滚动数组)¶
一维数组
dp
的背包其实和二维数组逻辑相同,只不过二维数组每遍历一个物品i
产生的每一层dp[i][]
,都会被下一层从后向前覆盖,将二维降至一维,故又称为滚动数组。请对比二维和一维dp
遍历顺序的异同。
-
dp[j]
表示容量为j
的背包能够装下物品的最大价值,在本题中价值等于重量,即dp[j] = j
代表背包刚好装满; -
01 背包问题通用递推公式
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
具体到本题应该为dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
; -
初始化
dp[0] = 0
,很显然,一个容量为0
的背包无法放入任何物品。需要注意的是,如果物品价值有负数,则还需要将数组其它部分初始化为INT_MIN
这样才会被更大的价值覆盖,反之初始化为0
即可; -
一维数组的背包写法与二维数组不同,由于
dp[j]
依赖dp[j - weight[i]]
,为了使得每次取得状态不会和上一次重合,保证每种物品只会取得一次,所以j
要从后向前遍历。与此同时,外层循环必须遍历物品,而不能遍历背包容量,否则每个背包dp[j]
只能装一种物品。以下案例对j
遍历方向进行了对比:由此可见,从前向后遍历会导致dp[j]
已经被取过一次的物品0
再次装入背包,这不满足 01 背包的基本要求,完全背包则与之相反,在此可以手动推导一下非滚动数组为什么不存在这个问题。我个人针对此问题总结了一句话“常规
dp
数组是靠dp[i - 1]
来获取上一层dp
数组,所以不受j
遍历顺序影响,而滚动dp
数组则是靠max(dp[j], dp[j - weight[i]] + value[i])
原地获取上一层dp
数组”,所谓上一层dp
即指枚举物品i - 1
完成后的dp
; -
手动推导
dp
数组无误。