Problem
You are given an array of non-negative integers and a target value. Assign either a + or a - sign before each number, then evaluate the resulting expression. Return the total number of distinct sign assignments that produce the target sum.
- Input:
nums = [1, 1, 1, 1, 1],target = 3 - Output:
5 - Explanation: There are 5 ways:
+1+1+1+1-1,+1+1+1-1+1,+1+1-1+1+1,+1-1+1+1+1,-1+1+1+1+1β each sums to 3.
A counter-example: nums = [1, 2], target = 10 β output 0, because the max reachable sum is Β±3, never 10.
Intuition
At every position you make a binary choice β add or subtract β so brute force blows up exponentially. The key insight is that the problem can be reframed as a subset-sum count: if you call the numbers assigned + the "positive subset" P and the rest the "negative subset" N, then sum(P) - sum(N) = target and sum(P) + sum(N) = total, which gives sum(P) = (total + target) / 2. Counting subsets of nums that add up to that fixed value is a classic 0/1 knapsack that runs in O(n Β· S) time and O(S) space.
Approach 1 β Recursion with Memoization
At each index, recurse with both the + and - choice, caching on (index, current_sum) to avoid recomputing the same subproblem.
- Define
dfs(index, current_sum)returning the number of ways fromindexonward to reachtarget. - Base case: if
index == len(nums), return 1 ifcurrent_sum == target, else 0. - Check the memo table before computing; return early on a hit.
- Recurse into both
current_sum + nums[index](assign +) andcurrent_sum - nums[index](assign -), sum the results. - Store and return the combined count.
1def findTargetSumWays(nums: list[int], target: int) -> int:
2 memo = {}
3
4 def dfs(index: int, current_sum: int) -> int:
5 if index == len(nums):
6 return 1 if current_sum == target else 0
7 if (index, current_sum) in memo:
8 return memo[(index, current_sum)]
9 add_ways = dfs(index + 1, current_sum + nums[index])
10 subtract_ways = dfs(index + 1, current_sum - nums[index])
11 memo[(index, current_sum)] = add_ways + subtract_ways
12 return memo[(index, current_sum)]
13
14 return dfs(0, 0)Time: O(n Β· S) β each (index, sum) pair is computed once; total sums range over [βtotal, +total], giving at most 2Β·nΒ·total unique states.
Space: O(n Β· S) β memo table plus recursion stack depth n.
Approach 2 β Bottom-up DP (Subset Sum Reformulation)
Reformulate: let subset_sum = (total + target) / 2, then count subsets of nums that add to exactly subset_sum. Use a 1-D DP array where dp[j] holds the number of subsets summing to j, iterating each number in reverse to enforce the 0/1 constraint.
- Compute
total = sum(nums). Return 0 ifabs(target) > totalor(total + target)is odd β no valid assignment exists. - Set
subset_sum = (total + target) // 2. - Initialize
dp[0] = 1(one way to reach sum 0: pick nothing), all others 0. - For each
num, iteratejfromsubset_sumdown tonum, addingdp[j - num]todp[j]. - Return
dp[subset_sum].
1def findTargetSumWays(nums: list[int], target: int) -> int:
2 total = sum(nums)
3 # No valid assignment if target is unreachable or subset_sum is fractional
4 if abs(target) > total or (total + target) % 2 != 0:
5 return 0
6
7 subset_sum = (total + target) // 2
8 dp = [0] * (subset_sum + 1)
9 dp[0] = 1 # empty subset always sums to 0
10
11 for num in nums:
12 # Reverse iteration prevents using num more than once per assignment
13 for j in range(subset_sum, num - 1, -1):
14 dp[j] += dp[j - num]
15
16 return dp[subset_sum]Time: O(n Β· S) β same asymptotic as memoization, where S = (total + target) / 2 β€ total.
Space: O(S) β single 1-D DP array; no recursion overhead.
Complexity Summary
| Approach | Time | Space | When to use |
|---|---|---|---|
| Recursion with Memoization | O(n Β· S) | O(n Β· S) | More intuitive starting point; works even when subset_sum is not well-defined |
| Bottom-up DP (Subset Sum) | O(n Β· S) | O(S) | Preferred in production β no recursion overhead and minimal memory |
Common Mistakes
- Skipping the early-exit checks in Approach 2 β if
abs(target) > total,subset_sumwould be negative; if(total + target)is odd,subset_sumis fractional. Either omission causes a wrong answer or an array index error. - Iterating the inner DP loop forward (ascending j) instead of backward β ascending order lets each
numcontribute to later indices in the same pass, effectively counting it multiple times and turning the problem into unbounded knapsack. The count explodes incorrectly. - Keying the memo only on
indexβ the return value depends on bothindexandcurrent_sum; caching on index alone causes the first-reached sum at that index to be returned for all subsequent sums, producing wildly wrong counts. - Treating a negative
targetas impossible β since you freely assign signs, a negative target is valid as long asabs(target) β€ total. The guard condition isabs(target) > total, nottarget < 0. - Forgetting that
total + targetcan overflow for large inputs β in Java and C++, iftotalandtargetare both nearINT_MAX, the addition overflows. Uselongor check bounds before the addition.
Related Problems
partition-equal-subset-sumβ same 0/1 knapsack structure, but asks whether a subset exists rather than counting themcoin-change-iiβ count combinations summing to a target, but with unbounded (reusable) itemssubsetsβ enumerates all subsets; the brute-force layer underneath this problemdecode-waysβ counting distinct paths through a sequence, similar memoized DFS shapeword-breakβ another counting-paths DP problem where memoizing(index, state)is the key move