动态规划 目标和

494. 目标和

问题描述

给定一个非负整数数组 nums 和一个整数 target,向数组中的每个整数前添加 '+''-',然后串联成表达式,求共有多少种组合方式使得表达式运算结果等于 target

示例

输入: nums = [1,1,1,1,1], target = 3
输出: 5
解释: 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

算法思路

动态规划(0-1背包问题)

  1. 数学推导
    • 设正数子集和为 S1,负数子集和为 S2(取绝对值)。
    • S1 - S2 = targetS1 + S2 = sumsumnums 总和)。
    • 两式相加得:2S1 = target + sumS1 = (target + sum) / 2
  2. 目标转换
    • 问题转化为在 nums 中选取若干元素,使其和等于 (target + sum) / 2 的方案数。
  3. 边界条件
    • target > sum(target + sum) 为奇数 → 无解,返回 0。
    • (target + sum) < 0 → 取绝对值(实际由边界条件保证非负)。

动态规划

  1. 状态定义
    • dp[j] 表示和为 j 的方案数。
  2. 状态转移
    • 遍历每个数 num,倒序更新 jSnum
      dp[j] = dp[j] + dp[j - num]
  3. 初始化
    • dp[0] = 1(和为 0 的方案数为 1,不选任何数字)。

代码实现

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        // 计算数组总和
        for (int num : nums) {
            sum += num;
        }
        // 边界条件1:target绝对值大于sum,无解
        if (Math.abs(target) > sum) {
            return 0;
        }
        // 边界条件2:sum+target为奇数,无解
        if ((sum + target) % 2 != 0) {
            return 0;
        }
        // 计算目标子集和S
        int S = (sum + target) / 2;
        // 确保S非负(实际由边界条件保证)
        if (S < 0) {
            S = -S; // 实际不会发生,因Math.abs(target)<=sum且(sum+target)为偶数
        }
        // 初始化dp数组:dp[j]表示和为 j 的方案数
        int[] dp = new int[S + 1];
        dp[0] = 1; // 空集的和为0,方案数为1
        
        // 遍历每个数字
        for (int num : nums) {
            // 从S到num倒序遍历,避免重复使用同一数字
            for (int j = S; j >= num; j--) {
                // 状态转移:选或不选当前数字
                dp[j] += dp[j - num];
            }
        }
        return dp[S];
    }
}

算法分析

  • 时间复杂度:O(n×S)
    其中 n 为数组长度,S 为目标子集和。
  • 空间复杂度:O(S)
    使用一维 DP 数组。

算法过程

输入nums = [1,1,1,1,1], target = 3

  1. 计算总和sum = 5
  2. 计算目标子集和
    S = (3 + 5) / 2 = 4
  3. 初始化 DP 数组
    dp = [1,0,0,0,0]dp[0]=1
  4. 遍历数字
    • num=1:更新 j=4→1
      dp[4] += dp[3] → 0
      dp[3] += dp[2] → 0
      dp[2] += dp[1] → 0
      dp[1] += dp[0]=1dp[1]=1
      dp=[1,1,0,0,0]
    • num=1:更新 j=4→1
      dp[4] += dp[3]=0
      dp[3] += dp[2]=0
      dp[2] += dp[1]=1dp[2]=1
      dp[1] += dp[0]=1dp[1]=2
      dp=[1,2,1,0,0]
    • 后续同理,最终 dp[4]=5
  5. 结果:5

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 测试用例1: 标准示例
    int[] nums1 = {1,1,1,1,1};
    System.out.println("Test 1: " + solution.findTargetSumWays(nums1, 3)); // 5
    
    // 测试用例2: 目标值过大
    int[] nums2 = {1,2,3};
    System.out.println("Test 2: " + solution.findTargetSumWays(nums2, 10)); // 0
    
    // 测试用例3: 目标和为负数
    int[] nums3 = {1,2,3};
    System.out.println("Test 3: " + solution.findTargetSumWays(nums3, -2)); // 1(方案:-1+2-3=-2 → 1种)
    
    // 测试用例4: 无解(奇偶性不匹配)
    int[] nums4 = {1,2,4};
    System.out.println("Test 4: " + solution.findTargetSumWays(nums4, 1)); // 1(方案:-1-2+4=1)
    
    // 测试用例5: 空数组
    int[] nums5 = {};
    System.out.println("Test 5: " + solution.findTargetSumWays(nums5, 0)); // 1(空表达式)
    
    // 测试用例6: 目标和为0
    int[] nums6 = {1,1,1,1};
    System.out.println("Test 6: " + solution.findTargetSumWays(nums6, 0)); // 6
}

关键点

  1. 问题转化
    • 将添加 +/- 问题转化为子集和问题(背包问题)。
  2. 边界条件
    • (target + sum) 必须为非负偶数。
  3. 动态规划核心
    • 状态转移:dp[j] += dp[j - num]
    • 倒序遍历避免重复计数。
  4. 初始化
    • dp[0] = 1(和为 0 的方案数为 1)。

常见问题

  1. 为什么需要倒序遍历 j
    防止同一数字被重复使用(每个数字只能选一次)。
  2. 如何处理 target 为负数?
    数学推导中 (target + sum) 可处理负数,但需保证其为非负偶数。
  3. 为什么 dp[0]=1
    表示不选任何数字时,和为 0 的方案数为 1(空表达式)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值