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。
分析
就是使用深度搜索进行求解。求所有满足的路径的数目。但是问题需要减枝,否则会造成超时。
代码
方法1:造成超时
class Solution {
public:
int res = 0;
void help(vector<int>& nums, int S, int sum, int depth){
if(depth==nums.size() && sum == S){
res++;
return;
}
if(depth>=nums.size()) return;//这个地方一定要取等于号,就是相当于等于nums.size的时候,sum仍然不等于S
help(nums, S, sum+nums[depth], depth+1);
help(nums, S, sum-nums[depth], depth+1);
}
int findTargetSumWays(vector<int>& nums, int S) {
//使用深度搜索
//寻找路径数目,可能伴随着减枝
help(nums, S, 0, 0);
return res;
}
};
方法2:使用备忘录
观察上面的代码,可以发现nums[depth]等于0的时候,两个递归函数本质上一样,因此该递归过程存在重叠子问题。考虑使用哈希表作为备忘录。
class Solution {
public:
map<pair<int, int>, int> dic;
int help(vector<int>& nums, int S, int sum, int depth){
if(depth==nums.size() && S==sum){
return 1;
}
if(depth>=nums.size()) return 0;//这个地方一定要取等于号,就是相当于等于nums.size的时候,sum仍然不等于S
pair<int, int> key(depth, sum);
//key = to_string(depth)+","+to_string(sum);//深度加上当前的总和
if(dic.count(key)){
return dic[key];
}
int res = help(nums, S, sum+nums[depth], depth+1) + help(nums, S, sum-nums[depth], depth+1);
dic[key] = res;
return res;
}
int findTargetSumWays(vector<int>& nums, int S) {
//使用深度搜索
//寻找路径数目,可能伴随着减枝
int res = help(nums, S, 0, 0);
return res;
}
};
- 注意使用备忘录,一般函数要有返回值,每次在处理之前,都要先去备忘录里面判断是否已经求过,如果已经求过,就直接返回,否则则递归求值,并在递归之后再存放到备忘录里面。
方法3:使用动态规划
本题可以考虑为是子集划分问题,而子集划分问题又是一个典型的背包问题。
把nums划分为两个子集A和B,分别代表分配+的数和分配-的数,那么他们就和target存在如下的关系:
s
u
m
(
A
)
−
s
u
m
(
B
)
=
t
a
r
g
e
t
sum(A) - sum(B) = target
sum(A)−sum(B)=target
s
u
m
(
A
)
=
s
u
m
(
B
)
+
t
a
r
g
e
t
sum(A) = sum(B) + target
sum(A)=sum(B)+target
s
u
m
(
A
)
+
s
u
m
(
A
)
=
s
u
m
(
B
)
+
s
u
m
(
A
)
+
t
a
r
g
e
t
=
2
∗
s
u
m
(
A
)
=
t
a
r
g
e
t
+
s
u
m
(
n
u
m
s
)
sum(A) + sum(A) = sum(B) + sum(A)+ target = 2* sum(A) = target + sum(nums)
sum(A)+sum(A)=sum(B)+sum(A)+target=2∗sum(A)=target+sum(nums)
从而推出:
sum(A) = (target + sum(nums))/2。
问题转化为求数组中有多少个子集A使得该子集的和为 (target + sum(nums))/2。因此该题可以等价地转化为:有一个背包,容量为 sum,现在给你 N 个物品,第 i 个物品的重量为 nums[i - 1](注意 1 <= i <= N),每个物品只有一个,请问你有几种不同的方法能够恰好装满这个背包?dp[N][sum],即使用所有 N 个物品,有几种方法可以装满容量为 sum 的背包。
int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int n : nums) sum += n;
// 这两种情况,不可能存在合法的子集划分
if (sum < target || (sum + target) % 2 == 1) {
return 0;
}
return subsets(nums, (sum + target) / 2);
}
/* 计算 nums 中有几个子集的和为 sum */
int subsets(int[] nums, int sum) {
int n = nums.length;
int[][] dp = new int[n + 1][sum + 1];
// base case
for (int i = 0; i <= n; i++) {
dp[i][0] = 1;
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= sum; j++) {
if (j >= nums[i-1]) {
// 两种选择的结果之和
dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]];
} else {
// 背包的空间不足,只能选择不装物品 i
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][sum];
}
近似题目416. 分割等和子集
class Solution {
public:
bool canPartition(vector<int>& nums) {
//本题是0 1背包问题的变体,问题转化为求一个子集A使得其数字的所有和为nums总和的一半
int sum = 0;
int len = nums.size();
for(auto it:nums) sum+=it;
if(sum%2==1) return false;
sum = sum/2;
vector<vector<int> > dp(len+1, vector<int>(sum+1));
//定义dp数组dp[i][sum], 前i个数字的选择情况所构成的sum的合法性
//状态方程
//dp[i][sum] = dp[i-1][sum] || dp[i-1][sum-num[i-1]];
//base case:当i等于0的时候
dp[0][0] = 1; //其他的dp[0][j]都是0
for(int i=1;i<=len;i++){
for(int j=0;j<=sum;j++){
if(j>=nums[i-1])
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]];
else
dp[i][j] = dp[i-1][j];
}
}
return dp[len][sum];
}
};