目 录CONTENT

文章目录

LeetCode 494.目标和

小王同学
2024-01-03 / 0 评论 / 1 点赞 / 155 阅读 / 0 字

LeetCode 494.目标和

力扣传送门(opens new window)

难度:中等

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

解题思路:

方法一:回溯
数组 nums 的每个元素都可以添加符号 + 或-,因此每个元素有 2 种添加符号的方法,n 个数共有 2^n
种添加符号的方法,对应 2^n 种不同的表达式。当 n 个元素都添加符号之后,即得到一种表达式,如果表达式的结果等于目标数 target,则该表达式即为符合要求的表达式。

可以使用回溯的方法遍历所有的表达式,回溯过程中维护一个计数器 count,当遇到一种表达式的结果等于目标数 target 时,将 count 的值加 1。遍历完所有的表达式之后,即可得到结果等于目标数 target 的表达式的数目。

public int findTargetSumWays(int[] nums, int target) {
    backtrack(nums, target, 0, 0);
    return count;
}

public void backtrack(int[] nums, int target, int index, int sum) {
    if (index == nums.length) {
        if (sum == target) {
            count++;
        }
    } else {
        backtrack(nums, target, index + 1, sum + nums[index]);
        backtrack(nums, target, index + 1, sum - nums[index]);
    }
}

}
复杂度分析

时间复杂度:O(2n),其中 nnn 是数组 nums 的长度。回溯需要遍历所有不同的表达式,共有 2^n种不同的表达式,每种表达式计算结果需要 O(1)的时间,因此总时间复杂度是 O(2^n)。

空间复杂度:O(n),其中 nnn 是数组 nums 的长度。空间复杂度主要取决于递归调用的栈空间,栈的深度不超过 n。

方法二:动态规划

这里如何转化为01背包问题呢。

假设加法的总和为x,那么减法对应的总和就是sum - x。

比如说[1,1,1,1,1]这个例子,target为3,那我们的加法总和为1+1+1+1,减法总和为1,分成两部分

所以我们要求的是 x - (sum - x) = target

x = (target + sum) / 2

此时问题就转化为,装满容量为x的背包,有几种方法

这里的x,就是bagSize,也就是我们后面要求的背包容量。

大家看到(target + sum) / 2 应该担心计算的过程中向下取整有没有影响。

这么担心就对了,例如sum 是5,S是2的话其实就是无解的,所以:

if ((S + sum) % 2 == 1) return 0; // 此时没有方案

同时如果 S的绝对值已经大于sum,那么也是没有方案的。

if (abs(S) > sum) return 0; // 此时没有方案

再回归到01背包问题,为什么是01背包呢?

因为每个物品(题目中的1)只用一次!

这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。

本题则是装满有几种方法。其实这就是一个组合问题了。

  1. 确定dp数组以及下标的含义

dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法

  1. 确定递推公式

有哪些来源可以推出dp[j]呢?

只要搞到nums[i],凑成 j 就有dp[j - nums[i]] 种方法。

例如:dp[j],j 为4,

  • 已经有一个1(nums[i]) 的话,有 dp[4-1]种方法 凑成 容量为4的背包
  • 已经有一个2(nums[i]) 的话,有 dp[4-2]种方法 凑成 容量为4的背包
  • 已经有一个3(nums[i]) 的话,有 dp[4-3]种方法 凑成 容量为4的背包
  • 已经有一个4(nums[i]) 的话,有 dp[4-4]种方法 凑成 容量为4的背包

那么凑整dp[4]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。

所以求组合类问题的公式,都是类似这种:

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

这个公式在后面在讲解背包解决排列组合问题的时候还会用到!

  1. dp数组如何初始化

从递推公式可以看出,在初始化的时候dp[0] 一定要初始化为1,因为dp[0]是在公式中一切递推结果的起源,如果dp[0]是0的话,递推结果将都是0。

手动推导

01-xuwq.jpg

这里我理解的是,首先选出nums[0],nums[0]的值为1

  • 背包容量为5的时候,当前只有重量1的物品,有dp[5]+dp[5-1]种方法
  • 背包容量为4的时候,当前只有重量1的物品,有dp[4]+dp[4-1]种方法
  • 背包容量为3的时候,当前只有重量1的物品,有dp[3]+dp[3-1]种方法
  • 背包容量为2的时候,当前只有重量1的物品,有dp[2]+dp[2-1]种方法
  • 背包容量为1的时候,当前只有重量1的物品,有dp[1]+dp[1-1]种方法

对于每个 j 值,我们将 dp[j] 累加上 dp[j - nums[i]] 的值。这是因为我们可以选择使用当前元素 nums[i] 或不选择它,以得到和为 j。通过累加 dp[j - nums[i]] 的方案数量,我们将选择了其他数字得到和为 j - nums[i] 的方案数量加到了选择了当前数字得到和为 j 的方案数量中,以确保我们考虑了所有可能的选择。

02-hupi.jpg

在选择出nums[1]的基础上,又有了nums[2]

  • 背包容量为5的时候,当前有重量1和2的物品,有dp[5]+dp[5-2]种方法
  • 背包容量为4的时候,当前有重量1和2的物品,有dp[4]+dp[4-2]种方法
  • 背包容量为3的时候,当前有重量1和2的物品,有dp[3]+dp[3-2]种方法
  • 背包容量为2的时候,当前有重量1和2的物品,有dp[2]+dp[2-2]种方法

03-rqny.jpg

在选择出nums[1],nums[2]的基础上,又有了nums[3]

  • 背包容量为5的时候,当前有重量1,2,3的物品,有dp[5]+dp[5-3]种方法
  • 背包容量为4的时候,当前有重量1,2,3的物品,有dp[4]+dp[4-3]种方法
  • 背包容量为3的时候,当前有重量1,2,3的物品,有dp[3]+dp[3-3]种方法

这里的话举个例子:拿第一条来说,dp[5]+dp[5-3]种方法,指的是:

当我的背包此时已经装了nums[1],nums[2]的物品,

我要装满5的背包容量,有几种装法,一种是装nums[3](即dp[5-3]),一种是不装nums3(即dp[5]),这样会好理解一些。最后我们整理思考一下,也就是我之前装满背包为2的方法有dp[5-3]种方法,我现在有了nums[3],想要装满背包为5的方法有dp5(不装nums[3],这里不装nums[3]显然是凑不成5的)+dp[5-3](装nums[3],这里一旦装了nums[3],就凑成5,也就是dp[2]种方法,解释的有点乱,大家可以思考一下)

代码实现:

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int i = 0; i < nums.length; i++) sum += nums[i];
	//如果target过大 sum将无法满足
        if ( target < 0 && sum < -target) return 0;
        if ((target + sum) % 2 != 0) return 0;
        int size = (target + sum) / 2;
        if(size < 0) size = -size;
        int[] dp = new int[size + 1];
        dp[0] = 1;
        for (int i = 0; i < nums.length; i++) {
            for (int j = size; j >= nums[i]; j--) {
                ////本层装满方案数=不放i物品装满方案数 + 放i物品装满方案数
                dp[j] =dp[j] + dp[j - nums[i]];
            }
        }
        return dp[size];
    }
}

代码跟踪:

/**
     * 举例推导dp数组
     * 输入:nums: [1, 1, 1, 1, 1], S: 3
     *
     * bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4
     * @param nums
     * @param target
     * @return
     */
---------------(i = 0)-------------------

dp[4]+=dp[4-nums[0]]
dp[4-nums[0]]=0
nums[0]=1
dp[4]=0

dp[3]+=dp[3-nums[0]]
dp[3-nums[0]]=0
nums[0]=1
dp[3]=0

dp[2]+=dp[2-nums[0]]
dp[2-nums[0]]=0
nums[0]=1
dp[2]=0

dp[1]+=dp[1-nums[0]]
dp[1-nums[0]]=1
nums[0]=1
dp[1]=1

---------------(i = 1)-------------------

dp[4]+=dp[4-nums[1]]
dp[4-nums[1]]=0
nums[1]=1
dp[4]=0

dp[3]+=dp[3-nums[1]]
dp[3-nums[1]]=0
nums[1]=1
dp[3]=0

dp[2]+=dp[2-nums[1]]
dp[2-nums[1]]=1
nums[1]=1
dp[2]=1

dp[1]+=dp[1-nums[1]]
dp[1-nums[1]]=1
nums[1]=1
dp[1]=2

---------------(i = 2)-------------------

dp[4]+=dp[4-nums[2]]
dp[4-nums[2]]=0
nums[2]=1
dp[4]=0

dp[3]+=dp[3-nums[2]]
dp[3-nums[2]]=1
nums[2]=1
dp[3]=1

dp[2]+=dp[2-nums[2]]
dp[2-nums[2]]=2
nums[2]=1
dp[2]=3

dp[1]+=dp[1-nums[2]]
dp[1-nums[2]]=1
nums[2]=1
dp[1]=3

---------------(i = 3)-------------------

dp[4]+=dp[4-nums[3]]
dp[4-nums[3]]=1
nums[3]=1
dp[4]=1

dp[3]+=dp[3-nums[3]]
dp[3-nums[3]]=3
nums[3]=1
dp[3]=4

dp[2]+=dp[2-nums[3]]
dp[2-nums[3]]=3
nums[3]=1
dp[2]=6

dp[1]+=dp[1-nums[3]]
dp[1-nums[3]]=1
nums[3]=1
dp[1]=4

---------------(i = 4)-------------------

dp[4]+=dp[4-nums[4]]
dp[4-nums[4]]=4
nums[4]=1
dp[4]=5

dp[3]+=dp[3-nums[4]]
dp[3-nums[4]]=6
nums[4]=1
dp[3]=10

dp[2]+=dp[2-nums[4]]
dp[2-nums[4]]=4
nums[4]=1
dp[2]=10

dp[1]+=dp[1-nums[4]]
dp[1-nums[4]]=1
nums[4]=1
dp[1]=5
5

Process finished with exit code 0
1
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区