一、题目简述
给定非负整数序列,
a1,a2,...,an
和一个目标数
S
,现在你有两种符号
找出和为目标数S的符号组合方式的种数。
示例1:
- 输入: nums is [1, 1, 1, 1, 1], S is 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
共有5种符号组合方式,是的序列之和为3。
注意:
- 给定序列长度为正且不会超过20。
- 给定序列元素之和不会超过1000。
- 输出将会使用32位整数表示。
函数原型:
int findTargetSumWays(vector<int>& nums, int s)
二、编程思路
令
则状态转移方程为
dp[i][j]=dp[i−1][j−a[i]]+dp[i−1][j+a[i]]
为了避免负数的情况,可以对原始问题进行如下转化:
- 令P为序列中符号为正的数字的集合,N为序列中符号为负的数字的集合,则
- sum(P)−sum(N)=target
- sum(P)+sum(N)+sum(P)−sum(N)=target+sum(P)+sum(N)
- 即 2∗sum(P)=target+sum(P)+sum(N)
- 即在原序列中寻找一个正数集合
P
,使得
sum(P)=(target+sum(P)+sum(N))/2
根据上面推导过程可以看出,
target+sum(P)+sum(N)
一定要是偶数,否者不存在符合条件的P。
则令
dp[i][j]
表示序列中从0到j的子序列中,其和为j的组合数,则有如下状态转移方程:
- dp[i][j]=dp[i−1][j]+dp[i−1][j−nums[i]]
-
dp[i][j]=0,j=0
其中,前半部分代表不选择nums[i]的情况,后半部分代表选择nums[i]的情况。
三、程序设计
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int s) {
int sum = accumulate(nums.begin(), nums.end(), 0);
//(s + sum) & 1,判断s + sum的奇偶;(s + sum) >> 1,即(s + sum)/2
return sum < s || (s + sum) & 1 ? 0 : subsetSum(nums, (s + sum) >> 1);
}
int subsetSum(vector<int>& nums, int s) {
int dp[s + 1] = { 0 };
dp[0] = 1;
for (int n : nums)
for (int i = s; i >= n; i--)
dp[i] += dp[i - n];
return dp[s];
}
};
四、实验心得
- 在动态规划问题中需要找到合适的子问题分解方法,即本题目中dp的含义,确定其变量数目。
- 确定状态转移方程,将问题转换为子问题进行求解。
- 动态规划与递归都采用分治思想。动态规划是一个自底向上的过程,递归是一个自顶向下的过程。
- 动态规划:动态规划的本质是状态和状态转移方程的定义。如果一种状态定义满足无后效性,那么称之为具有最优子结构性质。两种性质都蕴含于状态转移方程之中。动态规划一般针对最优化问题。
- 最优子结构性质:每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到。
- 无后效性:不管之前这个状态是如何得到的。
- 缺点:空间需求大。
- 递归(分治):将原始问题分解为结构相似的子问题去解决,解决通用问题的思想。
- 缺点:子问题可能存在重复。
- 动态规划:动态规划的本质是状态和状态转移方程的定义。如果一种状态定义满足无后效性,那么称之为具有最优子结构性质。两种性质都蕴含于状态转移方程之中。动态规划一般针对最优化问题。