题目描述

✅ LCR 102. 目标和

image-20260929004412526

image-20260929004412527

题意分析

给每个数组位置添加一个正号或负号,统计表达式结果等于 target 的方案数。提示允许元素为 0;相同数值位于不同位置时,仍是不同的赋号选择。

设数组总和为 $S$,正号部分的和为 $P$,负号部分的绝对值之和为 $N$。由 $P-N=target$、$P+N=S$,得到 $P=(S+target)/2$。选出哪些位置放进正号子集后,其余位置的负号也随之确定,因此可以改为统计和为 $P$ 的子集数量。

解法:转成正号集合的 01 背包计数

核心思路

[!blue]

先保证 $P$ 是合法的非负整数。代码用 s = sum + target,若它为负或为奇数就返回 0;再令 p = s / 2,若 p > sum 也无解。这几项检查同时排除了目标在总和范围之外和奇偶不匹配的情况。

定义 dp[j] 为使用已处理位置、选出和为 j 的子集数量。初始 dp[0] = 1,代表一个元素都不选的唯一空集,其余计数为 0。处理元素 x 时,不选它的方案已经保存在 dp[j] 中;选它的方案则是在旧的和为 j - x 的每个子集上加入当前位置,所以执行 dp[j] += dp[j - x]。

每个位置最多选择一次,因此容量倒序。对正数 x,j - x 比 j 小,尚未在本轮更新,保证前驱方案没有使用当前位置。即使多个位置的值相同,也要逐个处理,因为选取的位置不同对应不同表达式。

当 x = 0 时,转移变为 dp[j] += dp[j],将已有方案恰好翻倍:当前零可以属于正号子集,也可以属于负号部分。两种表达式数值相同,但按题意都应计数。循环必须包含 j = 0,这样总和为 0 的输入也能正确统计所有赋号方式。

最后返回 dp[p]。所有被统计的子集都对应唯一的正负号分配,反过来每个合法表达式也会落入其中,因此没有遗漏或重复。

解题步骤

  1. 求总和,计算 s = sum + target,排除负数和奇数。
  2. 令 p = s / 2,若 p > sum,返回 0。
  3. 创建计数数组并设置 dp[0] = 1。
  4. 逐个处理 x,让 j 从 p 递减到 x,执行计数累加。
  5. 返回 dp[p]。

代码实现

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;

        for (int x : nums) {
            sum += x;
        }

        // 正号那堆的和 p 满足 p - (sum - p) = target,即 2p = sum + target。
        int s = sum + target;

        if (s < 0 || s % 2 != 0) {
            return 0;
        }

        int p = s / 2;

        if (p > sum) {
            return 0;
        }

        int[] dp = new int[p + 1];

        dp[0] = 1;

        for (int x : nums) {
            // 倒序保证 dp[j - x] 尚未计入本轮的 x。
            for (int j = p; j >= x; --j) {
                dp[j] += dp[j - x];
            }
        }

        return dp[p];
    }
}
func findTargetSumWays(nums []int, target int) int {
    sum := 0
    for _, x := range nums {
        sum += x
    }

    // 正号那堆的和 p 满足 p - (sum - p) = target,即 2p = sum + target。
    s := sum + target
    if s < 0 || s%2 != 0 {
        return 0
    }
    p := s / 2
    if p > sum {
        return 0
    }

    dp := make([]int, p+1)
    dp[0] = 1

    for _, x := range nums {
        // 倒序保证 dp[j-x] 尚未计入本轮的 x。
        for j := p; j >= x; j-- {
            dp[j] += dp[j-x]
        }
    }

    return dp[p]
}

复杂度分析

  • 时间复杂度:$O(n(P+1))$,其中 $n$ 为元素个数、$P=(S+target)/2$;保留加一项也覆盖 $P=0$ 时逐个处理零的情况。
  • 空间复杂度:$O(P+1)$,保存 0 到正号目标和的计数。

关键点总结

[!green]

  • 子集按数组位置区分,不能将重复值去重。
  • 计数初值为一个空方案,转移用加法;不能用布尔可达状态代替。
  • 每个零都会将当前计数翻倍,不应过滤零或跳过容量 0。
  • 数组最多 20 个元素,赋号方案总数不超过 $2^{20}$,现有整数计数足够。

易错点总结

[!yellow]

  • 未检查范围与奇偶就建立背包,可能得到无效或错误的目标容量。
  • 正序更新会重复选择当前正数,无法对应每个位置只赋一个符号。
  • 将 dp[0] 设为 0 会失去空方案,所有计数都无法启动。
  • 把零的正负号合并成一种,会少计不同的表达式。

相似题目

题目 难度 关联与区别
416. 分割等和子集 中等 同样将问题转成子集和,本题需要方案数量而不是可达布尔值。
518. 零钱兑换 II 中等 同样做容量计数,原题硬币可重复使用,本题每个下标只选择一次,更新方向不同。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/71463337
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!