原题链接:494. 目标和
题目:给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
思路:动态规划。
首先,如果我们把 nums
划分成两个子集 A
和 B
,分别代表分配 +
的数和分配 -
的数,那么他们和 target
存在如下关系:
sum(A) - sum(B) = target
sum(A) = target + sum(B)
sum(A) + sum(A) = target + sum(B) + sum(A)
2 * sum(A) = target + sum(nums)
综上,可以推出 sum(A) = (target + sum(nums)) / 2
,也就是把原问题转化成:nums
中存在几个子集 A
,使得 A
中元素的和为 (target + sum(nums)) / 2
?这是一个典型的01背包问题。
下面开始动态规划流程:
第一步要明确两点,「状态」和「选择」
状态就是「背包的容量」和「可选择的物体」,选择就是「装进背包」和「不装进背包」。对应于本题,状态就是「所选子集所有元素的和」和「子集的所有元素」,选择就是「元素放进子集里」和「元素不放进子集里」。
第二部就是明确dp数组的定义
按照背包问题的套路,可以给出如下定义:
dp[i][j] = x
表示,若只在前 i
个物品中选择,若当前背包的容量为 j
,则最多有 x
种方法可以恰好装满背包。
翻译成我们探讨的子集问题就是,若只在 nums
的前 i
个元素中选择,若目标和为 j
,则最多有 x
种方法划分子集。
根据这个定义,显然 dp[0][..] = 0
,因为没有物品的话,根本没办法装背包;dp[..][0] = 1
,因为如果背包的最大载重为 0,「什么都不装」就是唯一的一种装法。
我们所求的答案就是 dp[N][sum]
,即使用所有 N
个物品,有几种方法可以装满容量为 sum
的背包。
第三步,根据「选择」,思考状态转移的逻辑
回想刚才的 dp
数组含义,可以根据「选择」对 dp[i][j]
得到以下状态转移:
如果不把 nums[i]
算入子集,或者说你不把这第 i
个物品装入背包,那么恰好装满背包的方法数就取决于上一个状态 dp[i-1][j]
,继承之前的结果。
如果把 nums[i]
算入子集,或者说你把这第 i
个物品装入了背包,那么只要看前 i - 1
个物品有几种方法可以装满 j - nums[i-1]
的重量就行了,所以取决于状态 dp[i-1][j-nums[i-1]]
。
PS:注意我们说的 i
是从 1 开始算的,而数组 nums
的索引时从 0 开始算的,所以 nums[i-1]
代表的是第 i
个物品的重量,j - nums[i-1]
就是背包装入物品 i
之后还剩下的容量。
由于 dp[i][j]
为装满背包的总方法数,所以应该以上两种选择的结果求和,得到状态转移方程:
dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]];
C++实现如下:
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int S) {
int sum=0;
for(int n:nums) sum+=n;
if(sum<S||(sum+S)%2==1)//和小于S或者和+S为奇数的情况下不符合条件
return 0;
return subsums(nums,(sum+S)/2);
}
int subsums(vector<int>&nums,int sum){
int n=nums.size();
vector<vector<int>> dp(n+1,vector<int> (sum+1,0));
for(int i=0;i<n;i++){
dp[i][0]=1;//背包容量为0的情况下,只有什么都不装这一种方法
}
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{
dp[i][j]=dp[i-1][j];
}
}
}
return dp[n][sum];
}
};