【1.4】动态规划-解目标和

一、题目

给你一个整数数组nums和一个整数target 。
向数组中的每个整数前添加'+'或' - ',然后串联起所有整数,可以构造一个表达式:
例 如 , nums=[2,1] , 可 以 在 2 之 前 添 加 '+' , 在 1 之 前 添 加 ' - ' , 然 后串联起来得到表达式"+2-1"。
返回可以通过上述方法构造的、运算结果等于target的不同表达式的数目

二、求解思路

动态规划解决

我们假设在一些数字前添加“+”,这些数字的和是plusSum。剩下的数字前添加“ - ”,这些数字的和是minusSum。我们要求的是
plusSum-minusSum=target ①
的方案数目。
假设数组中所有元素的和是sum。那么我们可以得到
plusSum+minusSum=sum ②
由公式 和公式 我们可以得到
minusSum*2=sum- target;
我们可以看到如果要让上面等式成立, sum- target必须是偶数
也就是说如果 sum- target 不是偶数,无论怎么添加符号 , 表达式的值都不可能是target,直接返回0。
如果sum- target是偶数,我们只需要找出一些数字让他们的和等于minusSum,也就是( sum- target) /2的方案数。
通 过 上 面 的 分 析 , 这 题 就 变 成 了 从 数 组 中 选 择 一 些 元 素 , 让 他 们 的 和 等 于 ( sumtarget) /2的方案数 。这和0-1背包非常像,具体可以看下 背包问题系列之-基础背 包问题 。
我们定义 dp[i][j]表示从数组前i个元素中选取一些数字,让他们的和等于j的方案数 。很明显我们最终只需要返回dp[length][( sum- target) /2]即可。
其中dp[0][0]=1,表示选择0个元素让他们的和等于0,只有一种方案。
遍 历 到 当 前 数 字 num 的 时 候 , 如 果 当 前 数 字 num 大 于 j , 那 么 我 们 是 不 能 选 择 的 , 所 以 dp[i][j]=dp[i -1][j] 。他表示的意思就是前i个元素中不选择第i个元素,而选择前i个元 素中其他的一些数字,让他们的和等于j的方案数。
如果 当 前 数 字 num 小 于 或 等 于 j , 我 们 可 以 选 择 也 可 以 不 选 择 。 如 果 不 选 择 就 是 dp[i][j]=dp[i -1][j],如果选择就是dp[i][j]=dp[i -1][j -num];那么总的方案数就是
dp[i][j]=dp[i -1][j]+dp[i -1][j -num] 。

递推公式如下

if ( j >= num ) { //不选num和选num
    dp[ i ][ j ] = dp[ i - 1 ][ j ]  + dp[ i - 1 ][ j - num ];
} else { //不能选择num
    dp[ i ][ j ] = dp[ i - 1 ][ j ];
}

三、代码实现

通过上面的分析,我们再来看下最终代码

#include <iostream>
#include <vector>

int findTargetSumWays(std::vector<int>& nums, int target) {
    int length = nums.size();
    // 求数组中所有数字的和
    int sum = 0;
    for (int num : nums) {
        sum += num;
    }
    // 如果所有数字的和小于target,或者sum - target是奇数,
    // 说明无论怎么添加符号,表达式的值都不可能是target
    if (sum < target || ((sum - target) & 1) != 0) {
        return 0;
    }
    // 我们要找到一些元素让他们的和等于capacity的方案数即可。
    int capacity = (sum - target) >> 1;
    // dp[i][j]表示在数组nums的前i个元素中选择一些元素,
    // 使得选择的元素之和等于j的方案数
    std::vector<std::vector<int>> dp(length + 1, std::vector<int>(capacity + 1, 0));
    // 边界条件
    dp[0][0] = 1;
    for (int i = 1; i <= length; i++) {
        for (int j = 0; j <= capacity; j++) {
            // 动态规划公式
            if (j >= nums[i - 1]) { // 不选第i个和选第i个元素
                dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i - 1]];
            }
            else { // 不能选择第i个元素
                dp[i][j] = dp[i - 1][j];
            }
        }
    }
    // 从数组前length个(也就是全部)元素中选择一些元素,让他们的
    // 和等于capacity的方案数。
    return dp[length][capacity];
}



// 这里粘贴之前的 findTargetSumWays 函数定义

int main() {
    std::vector<int> nums = { 1, 1, 1, 1, 1 }; // 示例数组
    int target = 3; // 目标和
    int result = findTargetSumWays(nums, target); // 调用函数计算方案数

    std::cout << "Number of ways to reach target sum: " << result << std::endl;

    return 0;
}

时间复杂度:O(n* capacity),n是数组的长度。
空间复杂度:O(n* capacity),capacity是( sum- target) /2。

        我们看到上面二维数组计算的时候,当前那一行的值只和上一行的有关,所以我们可以改成一维数组,这里要注意嵌套中的第二个for循环要倒叙遍历。因为改成一维数组之后,数组后面的值要依赖前面的(改变之前的),如果从前往后遍历,前面的值被修改了,会导致后面的运行结果错误。如果倒叙,也就是先计算数组后面的值,因为前面的还没有计算,也就是还没有被修改,所以不会导致结果错误。来看下代码

代码优化:

#include <vector>

int findTargetSumWays(std::vector<int>& nums, int target) {
    int length = nums.size();
    // 求数组中所有数字的和
    int sum = 0;
    for (int num : nums) {
        sum += num;
    }
    // 如果所有数字的和小于target,或者sum - target是奇数,
    // 说明无论怎么添加符号,表达式的值都不可能是target
    if (sum < target || ((sum - target) & 1) != 0) {
        return 0;
    }
    // 我们要找到一些元素让他们的和等于capacity的方案数即可。
    int capacity = (sum - target) >> 1;
    std::vector<int> dp(capacity + 1, 0);
    // 边界条件
    dp[0] = 1;
    for (int i = 0; i < length; i++) {
        // 注意,这里要倒序
        for (int j = capacity; j >= nums[i]; j--) {
            // 动态规划公式
            dp[j] += dp[j - nums[i]];
        }
    }
    return dp[capacity];
}

// Main 函数的示例
int main() {
    std::vector<int> nums = {1, 1, 1, 1, 1}; // 示例数组
    int target = 3; // 目标和
    int result = findTargetSumWays(nums, target); // 调用函数计算方案数

    std::cout << "Number of ways to reach target sum: " << result << std::endl;

    return 0;
}

时间复杂度:O(n* capacity)
空间复杂度:O(capacity)

  • 27
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

攻城狮7号

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值