LeetCode第 494 题:目标和(C++)

494. 目标和 - 力扣(LeetCode)
在这里插入图片描述

注意几个点:

  • 数组是非负的
  • 一个元素只能用一次(01背包)
  • 每个元素都是必选的(要么+要么-)
  • 初始的数组的和不会超过 1000

其实一开始容易想到这个状态:
dp[i][j]表示前i个元素能够凑成和值j的方法数:
d p [ i ] [ j ] = d p [ i − 1 ] [ j − n u m s [ i − 1 ] ] + d p [ i − 1 ] [ j + n u m s [ i − 1 ] ] dp[i][j] = dp[i-1][ j-nums[i-1]] + dp[i-1][j+nums[i-1]] dp[i][j]=dp[i1][jnums[i1]]+dp[i1][j+nums[i1]]

因为这题元素只能选一次,只有两种选择(+/-),所以状态转移还是比较单一的,但是写代码的时候就出现问题了,因为和值为负数的情况没有考虑到。比如例子中实际上dp[2][0] = 2,因为前两个元素凑成和值0有两种选择:(+1, -1), (-1, +1),但是上面的转移方程考虑不到负数的情况,所以会出错。

所以负数的情况需要考虑,而数组的下标又不能为负,所以可以采取数组下标统一加上一个偏置的方法,避免下标为负的情况。

所以原本错误的代码是这样的(01背包套模板即可):

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        if(S > 1000 || S < -1000)   return 0;
        int n = nums.size();
        //dp[i][j]表示前i个元素能够凑成和值j的方法数
        vector<vector<int>> dp(n+1, vector<int>(S+1, 0));
        dp[0][0] = 1;//前0个元素组成和值10的方法数为1
        for(int i = 1; i <= n; ++i){//考虑前i个物品
            for(int j = 0; j <= S; ++j){//能够组成的和值-1000 ~ 1000
                //两个if条件是为了保证下标不越界
                if(j - nums[i-1] >= 0) dp[i][j] += dp[i-1][j - nums[i-1]];
                if(j + nums[i-1] <= S) dp[i][j] += dp[i-1][j + nums[i-1]];
            }
        }
        return dp[n][S];
    }
};

加上偏置之后,就变成这样:

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        if(S > 1000 || S < -1000)   return 0;
        int n = nums.size();
        //dp[i][j]表示前i个元素能够凑成和值j-1000的方法数
        vector<vector<int>> dp(n+1, vector<int>(2001, 0));
        dp[0][1000] = 1;//前0个元素组成和值1000-1000=0的方法数为1
        for(int i = 1; i <= n; ++i){//考虑前i个物品
            for(int j = -1000; j <= 1000; ++j){//能够组成的和值-1000 ~ 1000
                //和值-1000 ~ 1000映射到数组中对应下标是0 ~ 2001 
                //两个if条件是为了保证下标不越界
                if(j - nums[i-1] >= -1000) dp[i][j+1000] = dp[i-1][j+1000 - nums[i-1]];
                if(j + nums[i-1] <= 1000) dp[i][j+1000] += dp[i-1][j+1000 + nums[i-1]];
            }
        }
        return dp[n][S+1000];
    }
};

那么其实也不用非要那么大的数组,最大的偏置最多就是数组的全部和值:

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if(S > 1000 || S < -1000 || S > sum)   return 0;
        int n = nums.size();
        //dp[i][j]表示前i个元素能够凑成和值j-sum的方法数
        vector<vector<int>> dp(n+1, vector<int>(2*sum+1, 0));
        dp[0][sum] = 1;//前0个元素组成和值sum-sum=0的方法数为1
        for(int i = 1; i <= n; ++i){//考虑前i个物品
            for(int j = -sum; j <= sum; ++j){//能够组成的和值-sum ~ sum
                //和值-sum ~ sum映射到数组中对应下标是0 ~ 2*sum+1
                //两个if条件是为了保证下标不越界
                if(j - nums[i-1] >= -sum) dp[i][j+sum] = dp[i-1][j+sum - nums[i-1]];
                if(j + nums[i-1] <= sum) dp[i][j+sum] += dp[i-1][j+sum + nums[i-1]];
            }
        }
        return dp[n][S+sum];
    }
};

观察一下可以进行状态压缩,使用滚动数组即可,因为每一行只与前一行有关。

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if(S > 1000 || S < -1000 || S > sum)   return 0;
        int n = nums.size();
        //dp[i][j]表示前i个元素能够凑成和值j-sum的方法数
        vector<vector<int>> dp(2, vector<int>(2*sum+1, 0));
        dp[0][sum] = 1;//前0个元素组成和值sum-sum=0的方法数为1
        int cur = 0;
        for(int i = 1; i <= n; ++i){//考虑前i个物品
            cur = i%2;
            for(int j = -sum; j <= sum; ++j){//能够组成的和值-sum ~ sum
                //和值-sum ~ sum映射到数组中对应下标是0 ~ 2*sum+1
                //两个if条件是为了保证下标不越界
                if(j - nums[i-1] >= -sum) dp[cur][j+sum] = dp[1-cur][j+sum - nums[i-1]];
                if(j + nums[i-1] <= sum) dp[cur][j+sum] += dp[1-cur][j+sum + nums[i-1]];
            }
        }
        return dp[n%2][S+sum];
    }
};

最后,总感觉这个题可以进行转换,像是LeetCode第 416 题:分割等和子集(C++)_zj-CSDN博客那样可以转为0-1背包问题,但是又没想到怎么转,直到看了这个题解:

换一下角度,可以转换为典型的01背包问题 - 目标和 - 力扣(LeetCode)

原理其实就是挑选一部分元素作为正数,剩下的作为负数,两部分相差即为S,既然S已知,那知道正数的那一部分,负数的那一部分自然也就知道了。所有我们只需要计算正数的那一部分就可以了。

所有的数先分成两种:
符号为正的数的总和设为一个背包的容量x,
符号为负的数的总和设为一个背包的容量y
sum为数组的总和,则 x + y = s u m x+y = sum x+y=sum,同时根据题意也有 x − y = S x-y = S xy=S。所以解出 x = ( S + s u m ) / 2 x=(S+sum)/2 x=(S+sum)/2
那么,题目转换为背包问题:给定数组 nums 和一个容量为x的背包,求有多少种方式让背包装满。

对于01背包问题来说,元素就不是必选的了,可选可不选

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if(S > 1000 || S < -1000 || S > sum)   return 0;//这一行得有
        if((S + sum)%2) return 0;//为奇数的话,下面就算不出有效的背包容量
        int cap = (S + sum)/2;//正数的背包容量
        int n = nums.size();
        //dp[i][j]表示前i个元素能够凑成和值j的方法数
        vector<vector<int>> dp(n+1, vector<int>(cap+1, 0));
        dp[0][0] = 1;//前0个元素组成和值0的方法数为1
        for(int i = 1; i <= n; ++i){//考虑前i个物品
            for(int j = 0; j <= cap; ++j){
                //if条件是为了保证下标不越界
                //元素可以选或者不选
                if(j - nums[i-1] >= 0) dp[i][j] = dp[i-1][j - nums[i-1]] + dp[i-1][j];
                else dp[i][j] = dp[i-1][j];//只能继承之前的值
            }
        }
        return dp[n][cap];
    }
};

也可以进行状态压缩,注意后向前遍历避免值覆盖就行。

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        int sum = accumulate(nums.begin(), nums.end(), 0);
        if(S > 1000 || S < -1000 || S > sum)   return 0;
        if((S + sum)%2) return 0;//为奇数的话,下面就算不出有效的背包容量
        int n = nums.size();
        int cap = (S+sum)/2;
        //dp[j]表示能够装满容量j的最大方法数
        vector<int> dp(cap+1, 0);
        dp[0] = 1;
        for(int i = 1; i <= n; ++i){
            for(int j = cap; j >= nums[i-1]; --j){
                dp[j] += dp[j-nums[i-1]];
            }
        }
        return dp[cap];
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值