分割等和子集
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
动态规划,递推
背包问题。
特殊情况:n<2或总和为奇数或最大值大于target。
dp[i][j]
表示在前 i+1 个元素中是否可以找到和为 j 的子集。任何 dp[i][0]
都为 true,因为和为0的子集就是空集。
class Solution {
public boolean canPartition(int[] nums) {
int n = nums.length;
// 特殊情况
if (n < 2) {
return false;
}
int sum = 0, maxNum = 0;
// 求最大值和总和
for (int num : nums) {
sum += num;
maxNum = Math.max(maxNum, num);
}
// 特殊情况:总和为奇数
if (sum % 2 != 0) {
return false;
}
// 特殊情况:最大值大于和的一半,说明最大值不行,剩下的值也不行(小于target)。
int target = sum / 2;
if (maxNum > target) {
return false;
}
// 初始化
// dp[i][j] 表示在前 i+1 个元素[0...i]中是否可以找到和为 j 的子集。重点。
boolean[][] dp = new boolean[n][target + 1];
// 初始化 dp 的第一列,任何 dp[i][0] 都为 true,因为和为0的子集就是空集。重点。
for (int i = 0; i < n; i++) {
dp[i][0] = true;
}
// 初始化第一行中的第nums[0]个元素。dp数组的第一行中的其它值都是false。重点。
dp[0][nums[0]] = true;
// 开始递推。先遍历物品再遍历容量。
for (int i = 1; i < n; i++) {
int num = nums[i];
// 不能从num开始遍历,不然j<num时dp[i][j]是默认值,应该是dp[i-1][j]才对。重点。
// 滚动更新的时候才可以从num开始遍历。
for (int j = 1; j <= target; j++) {
if (j >= num) {
// 如果当前和 j 大于等于当前元素 num,可以选择当前元素或不选择当前元素
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - num];
} else {
// 如果当前和 j 小于当前元素 num,只能不选择当前元素,即和上一轮的值一样。
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n - 1][target];
}
}
- 时间复杂度:O(n×target),其中 n 是数组的长度, target 是整个数组的元素和的一半。需要计算出所有的状态, 每个状态在进行转移时的时间复杂度为 O(1)。
- 空间复杂度:O(n×target),其中 target 是整个数组的元素和的一半。空间复杂度取决于 dp 数组, 在不进行空间优化的情况下,空间复杂度是 O(n×target),在进行空间优化的情况下,空间复杂度可以降到 O(target)。
动态规划优化空间-数组递推+倒序遍历+滚动更新。
之所以要倒序遍历,是因为dp[j]依赖的是上一轮的dp[j-num],如果从前往后遍历的话,dp[j]依赖的就是本轮的dp[j-num]了
class Solution {
public boolean canPartition(int[] nums) {
int n = nums.length;
// 特殊情况
if (n < 2) {
return false;
}
int sum = 0, maxNum = 0;
// 求最大值
for (int num : nums) {
sum += num;
maxNum = Math.max(maxNum, num);
}
// 特殊情况:总和为奇数
if (sum % 2 != 0) {
return false;
}
// 特殊情况:最大值大于target
int target = sum / 2;
if (maxNum > target) {
return false;
}
// 初始化
boolean[] dp = new boolean[target + 1];
dp[0] = true;
// 正序遍历物品,倒序遍历容量。
for (int i = 0; i < n; i++) {
int num = nums[i];
// 之所以要倒序遍历j,是因为dp[j]依赖的是上一轮的dp[j-num],如果从前往后遍历的话,
// dp[j]依赖的就是本轮的dp[j-num]了。就好像拖地要倒着拖不能正着拖,不然又踩脏了。
for (int j = target; j >= num; --j) {
dp[j] |= dp[j - num];
}
}
return dp[target];
}
}
- 时间复杂度: O(n× target ) ,其中 n 是数组的长度, target 是整个数组的元素和的一半。需要计算出所有的状态,每个状态在进行转移时的时间复杂度为 O(1) 。
- 空间复杂度: O (target),其中 target 是整个数组的元素和的一半。空间复杂度取决于 dp 数组,在不进行空间优化的情况下,空间复杂度是 O(n× target ,在进行空间优化的情况下,空间复杂度可以降到 O( target ) 。