416. 分割等和子集-M
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.
题解
思路:
向01背包问题转化。本题虽然是分成两个大小相同的组。也就是说,每个组元素的和应该是sum(nums)/2。
那么我们的问题就可以转化为:从nums数组里找一些数,使他们的和为sum(nums)/2。我们记这个和为target = sum(nums)/2
和背包问题的不同指出:
- 背包问题是希望我们的总和最大。所以 d p [ i ] [ j ] dp[i][j] dp[i][j]记录的是前i个物品中进行选择,使其在容量j下,记录所能达到的价值最大值。
- 此问题希望我们选取综合等于target。所以 d p [ i ] [ j ] dp[i][j] dp[i][j]记录的是前i个数进行选择,是他们的和为j。如果可以,则记录true。
状态转移方程:
注意nums[i-1]是代表第i个数的大小。因为nums数组和dp数组有个错位。
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
o
r
d
p
[
i
−
1
]
[
j
−
n
u
m
s
[
i
−
1
]
]
dp[i][j] = dp[i-1][j] \quad or \quad dp[i-1][j-nums[i-1]]
dp[i][j]=dp[i−1][j]ordp[i−1][j−nums[i−1]]
二维dp数组解法:
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
int n = nums.length;
for(int i=0; i<n; i++){
sum+=nums[i];
}
if(sum%2==1){
return false;
}
int target = sum/2;
//要把i = 0, j=0的行列都给空出来。所以要n+1维
boolean[][] dp = new boolean[n+1][target+1];
// for(int i = 0; i<=n; i++){
// dp[i][0] = true;
// }
dp[0][0] = true;
for(int i=1; i<=n; i++){
for(int j = 0; j<=target; j++){
dp[i][j] = dp[i-1][j] ;
//注意我们要处理第i个数,结果存放在dp[i][j]
//第i个在nums中的下表是i-1
if(nums[i-1]<=j){
dp[i][j] = dp[i][j] || dp[i-1][j-nums[i-1]];
}
}
}
return dp[n][target];
}
}
空间复杂度优化:
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
int n = nums.length;
for(int i=0; i<n; i++){
sum+=nums[i];
}
if(sum%2==1){
return false;
}
int target = sum/2;
boolean[] dp = new boolean[target+1];
dp[0] = true;
for(int i=1; i<=n; i++){
for(int j = target; j>=nums[i-1]; j--){
// dp[i][j] = dp[i-1][j] ;
// if(nums[i-1]<=j){
// dp[i][j] = dp[i][j] || dp[i-1][j-nums[i-1]];
// }
dp[j] = dp[j] || dp[j-nums[i-1]];
}
}
return dp[target]; //返回最后一个
}
}
494. 目标和
给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
示例:
输入:nums: [1, 1, 1, 1, 1], S: 3
输出:5
解释:
-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3
一共有5种方法让最终目标和为3。
提示:
数组非空,且长度不会超过 20 。
初始的数组的和不会超过 1000 。
保证返回的最终结果能被 32 位整数存下。
题解
仍然是一个01背包问题。
对于每个数字,都有+/-两种选择。我们用 d p [ i ] [ j ] dp[i][j] dp[i][j]记录前i个元素,总和为j的时候的方法数量。
递推公式:
d
p
[
0
]
[
0
]
=
1
,
表
示
0
个
数
总
和
为
0
时
,
只
有
一
个
钟
方
案
。
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
−
n
u
m
s
[
i
]
]
+
d
p
[
i
−
1
]
[
j
+
n
u
m
s
[
i
]
]
dp[0][0] =1, 表示0个数总和为0时,只有一个钟方案。\\ dp[i][j] = dp[i-1][j-nums[i]] + dp[i-1][j+nums[i]]
dp[0][0]=1,表示0个数总和为0时,只有一个钟方案。dp[i][j]=dp[i−1][j−nums[i]]+dp[i−1][j+nums[i]]
由于总和不超过1000,所以我们计算的j的范围从-1000到1000。 由于数组不能有负的索引,故把-1000到+1000映射到了0-2001
class Solution {
public int findTargetSumWays(int[] nums, int S) {
int n = nums.length;
int [][]dp = new int[n+1][2000+3];
if(S>1000){
return 0;
}
//初始化,注意这里没有引用第0维,所以不能初始化为0
dp[0][1000] = 1;
for(int i=1; i<=n; i++){
for(int j = 0; j<=2000; j++){
if(j-nums[i-1]>=0 && j+nums[i-1] <=2000){
dp[i][j] = dp[i-1][j-nums[i-1]] + dp[i-1][j+nums[i-1]];
}else if(j-nums[i-1]<0){
dp[i][j] = dp[i-1][j+nums[i-1]];
}else if(j+nums[i-1]>2000){
dp[i][j] = dp[i-1][j-nums[i-1]];
}
}
}
return dp[n][1000+S];
}
}
空间复杂度优化到O(2n)
class Solution {
public int findTargetSumWays(int[] nums, int S) {
int n = nums.length;
int []dpPre = new int[2000+1];
if(S>1000){
return 0;
}
//初始化,注意这里没有引用第0维,所以不能初始化为0
dpPre[1000] =1;
for(int i=1; i<=n; i++){
int []dp = new int[2000+1];
for(int j = 2000; j>=0; j--){
if(j-nums[i-1]>=0 && j+nums[i-1] <=2000){
dp[j] = dpPre[j-nums[i-1]] + dpPre[j+nums[i-1]];
}else if(j-nums[i-1]<0){
dp[j] = dpPre[j+nums[i-1]];
}else if(j+nums[i-1]>2000){
dp[j] = dpPre[j-nums[i-1]];
}
}
dpPre = dp;
}
return dpPre[1000+S];
}
}