[leetcode]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。

提示:

数组非空,且长度不会超过 20 。 初始的数组的和不会超过 1000 。 保证返回的最终结果能被 32 位整数存下。

关键词

回溯,带备忘录的回溯,动态规划,背包问题

解题

回溯

看这题,第一反应就是回溯。

回溯有个基本框架

int backtrace(vector<int> nums, 选择路径){
    if (满足退出条件)
        return result;

    for (auto 选择: 选择列表){
        做选择
        backtrace(nums, 选择路径);
        撤销选择
    }
}

写个基础回溯应该问题不大

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        return backtrack(nums, 0, (long)S, 0);
    }

    int backtrack(vector<int> &nums, int i, long res, int result){
        if (i == nums.size()){
            if (res == 0)
                result += 1;
            return result;
        }
        result = backtrack(nums, i+1, res+nums[i], result);
        result = backtrack(nums, i+1, res-nums[i], result);
        return result;
    }
};

带备忘录的回溯

这个问题一定有重复子问题,那我把子问题的答案记录下来。这个解法要稍微设计一下,怎么记录子问题。

比较好的方法是用map,信息应该包含:对第i个数做选择,剩下来的数需要凑出来的数值res,有几种凑法。相当于要记三个信息,有点头大!

好消息是 一旦确定 i 和 res,就能获得方案数!即map为string和int的映射表,string包含i和res信息即可。

class Solution {
public:
    unordered_map<string, int> memo;

    int findTargetSumWays(vector<int>& nums, int S) {
        return backtrack(nums, 0, (long)S);
    }

    int backtrack(vector<int> &nums, int i, long res){
        if (i == nums.size()){
            if (res == 0)
                return 1; //这里需要换,返回的是已明确的方案数
            return 0;
        }
        string key = to_string(i) + '_' + to_string(res); //这里要想一下
        if (memo.find(key) != memo.end())
            return memo[key];

        //result应是所有方案的和
                int result = backtrack(nums, i+1, res+nums[i]) + backtrack(nums, i+1, res-nums[i]);
        memo[key] = result;
        return result;
    }
};

背包&动态规划

我的数学思维能力需要专项训练的样子,目前还不行

A: 所有采用+的数的集合,B:所有采用-的数的集合

sum(A) - sum(B) = S

sum(A) + sum(A) = S + sum(B) + sum(A)

sum(A) = (S + sum)/2

小可爱发现什么了!没有错,问题变成了 从整体集合中挑选数,使它们的和为一个常数,有多少种方案

典型的背包问题了,用动态规划来解。

动态规划三步走:确定定义,写状态转移函数,写base case。

  • 确定定义

    dp[i][j]:前i个数中任意挑选,和为j的有多少种方案

  • 状态转移:

    dp[i][j] = dp[i-1][j] (不放第i个数的方案数)+ dp[i-1][j-第i个数](放第i个数的方案数)

  • base case:

    dp[:][0] = 1

写代码的时候有个问题

我的i表示前i个数里挑选,i=0的时候表示没有数的时候

而nums里的第i个数下标是i-1

因此,正确的状态转移方程是:

dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]]

我花时间比较多的点在于,我错误写成了如下:

dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]]

明确这些了来写代码

class Solution {
public:
    /*
    A: 所有采用+的数的集合,B:所有采用-的数的集合
    sum(A) - sum(B) = S
    sum(A) + sum(A) = S + sum(B) + sum(A)
    sum(A) = (S + sum)/2
    */

    int findTargetSumWays(vector<int>& nums, int S) {
        long sum = 0;
        for(auto x:nums) sum+=x;
        if (S > sum || (sum + S)%2!=0) return 0;
        int target = (sum + S)/2;
        return subset(nums, target);
    }

    int subset(vector<int>& nums, int target){
        //背包问题
        //dp[i][j]: 在前i个数中选择恰好装满背包j有几种方法
        //dp[nums.size()-1][target]是结果
        //dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]] 第i个物品不装的方案个数:dp[i-1][j],第i个物品装的方案个数:dp[i-1][j-nums[i-1]]

        //初始化
        int size = nums.size();
        vector<vector<int>> dp(size+1, vector<int>(target+1, 0));
        for(int i = 0; i <= size; i++) dp[i][0] = 1;
        for(int i = 1; i <= size; i++){
            for(int j = 0; j <= target; j++){
                if (j-nums[i-1]>=0)
                    dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]];
                else
                    dp[i][j] = dp[i-1][j];
            }
        }
        return dp[nums.size()][target];
    }
};

动态规划的维度压缩

是的,没有错!

可以看出,每个dp[i]仅依赖于dp[i-1],那这就是可以做路径压缩的呀小可爱们

道理是这样:即然我要更新dp[i],只需要dp[i-1]的信息,那我就可以无脑删掉关于[i]的下标,使用一维数组实现维度压缩

这个时候状态议程变成了:

dp[j] = dp[j] + dp[j-nums[i-1]]

为了保证我每次迭代的时候,dp[j]和dp[j-nums[i-1]]都是上一轮的值,j需要从后往前更新。即我更新dp[j]时,dp[j-nums[i-1]]是i-1轮的值,而非第i轮的值,那j只能从大往小走了,要从小往大走的话,上一轮的dp[j-nums[i-1]]就没了

道理理清了,来写代码

class Solution {
public:
    /*
    A: 所有采用+的数的集合,B:所有采用-的数的集合
    sum(A) - sum(B) = S
    sum(A) + sum(A) = S + sum(B) + sum(A)
    sum(A) = (S + sum)/2
    */

    int findTargetSumWays(vector<int>& nums, int S) {
        long sum = 0;
        for(auto x:nums) sum+=x;
        if (S > sum || (sum + S)%2!=0) return 0;
        int target = (sum + S)/2;
        return subset(nums, target);
    }

    int subset(vector<int>& nums, int target){
        //背包问题
        //dp[i][j]: 在前i个数中选择恰好装满背包j有几种方法
        //dp[nums.size()-1][target]是结果
        //dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]] 第i个物品不装的方案个数:dp[i-1][j],第i个物品装的方案个数:dp[i-1][j-nums[i-1]]

        //初始化
        int size = nums.size();
        vector<int> dp(target+1, 0);
        for(int i = 0; i <= size; i++) dp[0] = 1;

        for(int i = 1; i <= size; i++){
            for(int j = target; j >=0; j--){
                if (j-nums[i-1]>=0)
                    dp[j] = dp[j] + dp[j-nums[i-1]];
                else
                    dp[j] = dp[j];
            }
        }
        return dp[target];
    }
};

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343