416. 分割等和子集¶
力扣链接(中等):https://leetcode.cn/problems/partition-equal-subset-sum
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
示例 2:
提示:
1 <= nums.length <= 2001 <= 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数组无误。