leetcode 494. 目标和

494. 目标和(深搜、子集划分-动态规划)

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

分析

就是使用深度搜索进行求解。求所有满足的路径的数目。但是问题需要减枝,否则会造成超时。

代码

方法1:造成超时
class Solution {
public:
    int res = 0;
    void help(vector<int>& nums, int S, int sum, int depth){
        
        if(depth==nums.size() && sum == S){
            res++;
            return;
        }
        if(depth>=nums.size()) return;//这个地方一定要取等于号,就是相当于等于nums.size的时候,sum仍然不等于S
        help(nums, S, sum+nums[depth], depth+1);
        help(nums, S, sum-nums[depth], depth+1);
    }
    int findTargetSumWays(vector<int>& nums, int S) {
        //使用深度搜索
        //寻找路径数目,可能伴随着减枝
        help(nums, S, 0, 0);
        return res;
    }
};
方法2:使用备忘录

观察上面的代码,可以发现nums[depth]等于0的时候,两个递归函数本质上一样,因此该递归过程存在重叠子问题。考虑使用哈希表作为备忘录。

class Solution {
public:
    map<pair<int, int>, int> dic;
    int help(vector<int>& nums, int S, int sum, int depth){        
        if(depth==nums.size() &&  S==sum){
            return 1;
        }
        if(depth>=nums.size()) return 0;//这个地方一定要取等于号,就是相当于等于nums.size的时候,sum仍然不等于S

        pair<int, int> key(depth, sum); 
        //key = to_string(depth)+","+to_string(sum);//深度加上当前的总和
        if(dic.count(key)){
            return dic[key]; 
        }
        
        int res = help(nums, S, sum+nums[depth], depth+1) + help(nums, S, sum-nums[depth], depth+1);
        dic[key] = res;
        return res;
    }

    int findTargetSumWays(vector<int>& nums, int S) {
        //使用深度搜索
        //寻找路径数目,可能伴随着减枝
        int res = help(nums, S, 0, 0);
        return res;
    }
};
  • 注意使用备忘录,一般函数要有返回值,每次在处理之前,都要先去备忘录里面判断是否已经求过,如果已经求过,就直接返回,否则则递归求值,并在递归之后再存放到备忘录里面。
方法3:使用动态规划

本题可以考虑为是子集划分问题,而子集划分问题又是一个典型的背包问题。
把nums划分为两个子集A和B,分别代表分配+的数和分配-的数,那么他们就和target存在如下的关系:
s u m ( A ) − s u m ( B ) = t a r g e t sum(A) - sum(B) = target sum(A)sum(B)=target
s u m ( A ) = s u m ( B ) + t a r g e t sum(A) = sum(B) + target sum(A)=sum(B)+target
s u m ( A ) + s u m ( A ) = s u m ( B ) + s u m ( A ) + t a r g e t = 2 ∗ s u m ( A ) = t a r g e t + s u m ( n u m s ) sum(A) + sum(A) = sum(B) + sum(A)+ target = 2* sum(A) = target + sum(nums) sum(A)+sum(A)=sum(B)+sum(A)+target=2sum(A)=target+sum(nums)
从而推出:
sum(A) = (target + sum(nums))/2。
问题转化为求数组中有多少个子集A使得该子集的和为 (target + sum(nums))/2。因此该题可以等价地转化为:有一个背包,容量为 sum,现在给你 N 个物品,第 i 个物品的重量为 nums[i - 1](注意 1 <= i <= N),每个物品只有一个,请问你有几种不同的方法能够恰好装满这个背包?dp[N][sum],即使用所有 N 个物品,有几种方法可以装满容量为 sum 的背包。

int findTargetSumWays(int[] nums, int target) {
    int sum = 0;
    for (int n : nums) sum += n;
    // 这两种情况,不可能存在合法的子集划分
    if (sum < target || (sum + target) % 2 == 1) {
        return 0;
    }
    return subsets(nums, (sum + target) / 2);
}
/* 计算 nums 中有几个子集的和为 sum */
int subsets(int[] nums, int sum) {
    int n = nums.length;
    int[][] dp = new int[n + 1][sum + 1];
    // base case
    for (int i = 0; i <= n; i++) {
        dp[i][0] = 1;
    }
    
    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 {
                // 背包的空间不足,只能选择不装物品 i
                dp[i][j] = dp[i-1][j];
            }
        }
    }
    return dp[n][sum];
}

参考

近似题目416. 分割等和子集
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        //本题是0 1背包问题的变体,问题转化为求一个子集A使得其数字的所有和为nums总和的一半
        int sum = 0;
        int len = nums.size();
        for(auto it:nums) sum+=it;
        if(sum%2==1) return false;
        sum = sum/2;
        vector<vector<int> > dp(len+1, vector<int>(sum+1));
        //定义dp数组dp[i][sum], 前i个数字的选择情况所构成的sum的合法性
        //状态方程
        //dp[i][sum] = dp[i-1][sum] || dp[i-1][sum-num[i-1]];
        //base case:当i等于0的时候
        dp[0][0] = 1;  //其他的dp[0][j]都是0     
        for(int i=1;i<=len;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[len][sum];
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值