494. 目标和(C++)

题目

给定一个非负整数数组,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 位整数存下。

分析与题解

暴力解法

可以把数组中的每个数字前面都用负号和正号进行讨论,然后进行组合的求解。

定义的递归函数声明如下:

int dfs(vector<int>& nums, int S, int index)

除了引用传递数组外,我们将给定的目标值进行值传递,并且从数组的首位下标进行讨论。

int dfs(vector<int>& nums, int S, int index) {
		int ans = 0;
        // 分别对当前下标的元素赋予+/-号,进行递归遍历
        // 需要注意
        // 当前元素取负数,S添加的是+号
        // 当前元素取正数,S添加的是-号
        ans += dfs(nums, S + nums[index], index + 1);
        ans += dfs(nums, S - nums[index], index + 1);
        }

从数组的首位元素开始,分别在元素的前面添加+/-号。这时候给定的target就会进行波动:

  • 当元素前面添加+号时,对应的是s - nums[index]
  • 当元素的前面添加-号时,对应的是s - nums[index]

当前元素筛选引起S值变化后,我们就可以将下标位置向后挪动一位,再次递归地引用该函数。

最后设置递归的中止条件:

if (S == 0 && nums.size() == index) return 1;
// 如果在目标值没凑够的情况下,index已经遍历完,则没找到
if (index == nums.size()) return 0;

注意两个中止条件的顺序。因为数组下标是从0开始讨论的,当index = nums.size()时,数组元素已经全部被讨论,此时会出现两种情况:

  • 如果S值经过加减已经为0的时候,说明已经查询到满足条件的结果
  • 如果S值此时不为0,但数组已经遍历结束,说明未找到满足条件的结果

完整代码如下:

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        int sum = 0;
        for (int num : nums)
            sum += num;
        if (sum < S) return 0;

        return dfs(nums, S, 0);
    }

    int dfs(vector<int>& nums, int S, int index) {
        // 设置中止条件
        if (S == 0 && nums.size() == index) return 1;
        // 如果在目标值没凑够的情况下,index已经遍历完,则没找到
        if (index == nums.size()) return 0;

        int ans = 0;
        // 分别对当前下标的元素赋予+/-号,进行递归遍历
        // 需要注意
        // 当前元素取负数,S添加的是+号
        // 当前元素取正数,S添加的是-号
        ans += dfs(nums, S + nums[index], index + 1);
        ans += dfs(nums, S - nums[index], index + 1);
        return ans;
    }
};

01背包问题

其实我们可以考虑成将数组划分为两个集合:P表示正数的集合,N表示负数的集合,Total表示整个集合,S代表设定的目标值。题设条件即为:

\[P - N = S \]

将负数集移动至右边,并且两边同时增加一个正数集合:

\[P + P = N + S + P\\ 2 * P = Total + S\\ P = (Total + S) / 2 \]

所以目标和问题就转化成从nums数组中挑选凑出(Total + S) / 2数值的组合。于是就成了经典的背包问题。

二维数组作为状态量

首先确定状态量为vector<vector<int>> dp,任一元素dp[i][j]表示使用前i元素能凑到j数值的组合数。

接下来确定base casedp[i][0]对于数值0,使用任意个元素能否凑到是不确定的。dp[0][i]对于任意数值i,使用0个元素能否凑到也是不确定的。唯一能确定的就是dp[0][0]] = 1,即对于数值0,0个元素凑到目标数值的组合数为1.

因为不存在元素,所以不存在添加+/-号的选项

即确定初始化代码:

dp[0][0] = 1;

最后关于组合数的计算分两种情况进行讨论,假设此时遍历到数组nums的第i个元素,求取的目标状态是dp[i][j]

注意第i个元素对应的下标为i-1

  • 如果nums[i-1] > j ,即第i个元素比目标值还大。那么必然第i个元素不在凑目标值的组合中,如果前i-1个元素能够凑齐即可。此时dp[i][j] = dp[i-1][j]
  • 如果nums[i] <= j,即第i个元素小于等于目标值。那么除了考虑前i-1个元素凑齐目标值的情况,还需要考虑在使用第i个元素后,剩余的j - nums[i-1]能否被前i-1个元素凑齐。即为dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]]

完整代码如下:

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        int n = nums.size();
        int sum = 0;
        for (int num : nums)
            sum += num;
        // 如果数组总仍小于目标值,不可能达成目标
        if (sum < S) return 0;
        // 如果数组总和和目标值不为偶数,不可能达成目标
        // (见上文的公式推导)
        if ((sum + S) % 2 != 0) return 0;
        // 更新sum值为背包容量
        sum = (sum + S) / 2;

        vector<vector<int>> dp(n + 1, vector<int>(sum + 1, 0));
        // 初始化base case
        dp[0][0] = 1;

        for (int i = 1; i < n + 1; i++) {
            for (int j = 0; j < sum + 1; j++) {
                // 注意第i个元素在nums对应的下标为i - 1
                int val = nums[i - 1];
                if (j < val)
                    dp[i][j] = dp[i - 1][j];
                else if (j >= val)
                    dp[i][j] = dp[i - 1][j] + dp[i - 1][j - val];
            }
        }
        return dp[n][sum];

    }
};

二维数组进行空间优化

我们发现在for循环嵌套中,每次对于当前元素讨论后,我们只需要取dp[i-1][j]dp[i-1][j-nums[i-1]]的值,即循环中只需要保留上一行的状态值即可。所以我们考虑只开辟一个一维数组并不断使用上一行的状态值进行更新。

for (int& num : nums) {
            for (int j = sum; j >= 0; j--) {
                if (j >= num)
                    dp[j] += dp[j - num];
            }
        }

因为不需要知道当前元素在数组中的具体下标,所以直接使用for循环引用传递遍历所有元素即可。另外需要注意的是,由于推导公式是dp[j] += dp[j - num];,整个遍历过程中前面的状态值会影响到后面的状态值表现,如果提前将前面的i-1行状态值更新为第i行最新的状态值,会对未遍历完的第i行的状态值造成影响。因此对于状态值的遍历进行倒叙遍历

完整代码如下:

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        int n = nums.size();
        int sum = 0;
        for (int num : nums)
            sum += num;
        // 如果数组总仍小于目标值,不可能达成目标
        if (sum < S) return 0;
        // 如果数组总和和目标值不为偶数,不可能达成目标
        // (见上文的公式推导)
        if ((sum + S) % 2 != 0) return 0;
        // 更新sum值为背包容量
        sum = (sum + S) / 2;

        vector<int> dp(sum + 1, 0);
        // 初始化base case
        dp[0] = 1;

        for (int& num : nums) {
            for (int j = sum; j >= 0; j--) {
                if (j >= num)
                    dp[j] += dp[j - num];
            }
        }
        return dp[sum];

    }
};

可以看到在for循环嵌套中,我们对于每个num都需要跟当前的目标值j进行比较。本质上就是大于j的num就不要对第i行进行增减,dp[i][j]的值直接就等于dp[i-1][j]的数值,所以我们考虑直接根据num动态改变状态值遍历的下界

for (int& num : nums) {
	for (int j = sum; j >= num; j--)
		dp[j] += dp[j - num];
}

这样改写就不需要再进行讨论,完整代码如下:

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        int n = nums.size();
        int sum = 0;
        for (int num : nums)
            sum += num;
        // 如果数组总仍小于目标值,不可能达成目标
        if (sum < S) return 0;
        // 如果数组总和和目标值不为偶数,不可能达成目标
        // (见上文的公式推导)
        if ((sum + S) % 2 != 0) return 0;
        // 更新sum值为背包容量
        sum = (sum + S) / 2;

        vector<int> dp(sum + 1, 0);
        // 初始化base case
        dp[0] = 1;

        for (int& num : nums) {
            for (int j = sum; j >= num; j--) {
                dp[j] += dp[j - num];
            }
        }
        return dp[sum];

    }
};
posted @ 2020-12-17 00:28  脱线森林`  阅读(201)  评论(0编辑  收藏  举报