Target Sum

Problem

Given an integer array nums and an integer target, return the number of ways to assign '+' and '-' signs to each element such that the resulting sum equals target.

  • 1 ≤ nums.length ≤ 20
  • 0 ≤ nums[i] ≤ 1000
  • −1000 ≤ target ≤ 1000

Example

Input: nums = [1,1,1,1,1], target = 3
Output: 5

The brute-force approach tries all 2^n combinations of '+' and '-' assignments, which is exponential and inefficient. The optimal approach uses recursion with memoization to avoid redundant calculations. Starting at index 0 with total 0, the algorithm explores two branches: adding nums[0] and subtracting nums[0]. It recursively continues this for each index, accumulating totals. Memoization caches results for each (index, total) pair to prevent recomputation. For example, at index 0 and total 0, it explores total 1 and total -1 at index 1. This branching continues until the end of the array. When index reaches the length of nums, it checks if total equals target; if yes, it counts as 1 way, else 0. Summing all valid ways yields the final answer 5.

Approach

Straightforward Solution

A brute-force approach tries all 2^n sign combinations by recursively exploring '+' and '-' for each element, resulting in exponential time complexity and redundant computations.

Core Observation

The problem asks for counting all possible sign assignments that sum to a target, which naturally forms a decision tree with overlapping subproblems defined by the current index and running total.

Path to Optimal

Recognizing overlapping subproblems, the solution caches results for each (index, total) state using memoization. This pruning avoids recomputing the number of ways for the same state multiple times, reducing the complexity drastically.

Optimal Approach

Use a top-down recursive function dp(i, total) that returns the number of ways to reach target starting from index i with current sum total. At each step, recursively explore adding and subtracting nums[i]. Memoize results keyed by (i, total) to avoid redundant work. The final answer is dp(0, 0).

Time

O(n * m)

There are n indices and at most m distinct totals (bounded by sum of nums), each state computed once and cached, resulting in polynomial time.

Space

O(n * m)

Memoization dictionary stores results for each (index, total) pair, which can be up to n times the range of possible totals, leading to O(n*m) auxiliary space.

Pattern Spotlight

Dynamic Programming (Top-Down Memoization)

When counting the number of ways to reach a target with choices at each step, model the problem as a state space defined by index and accumulated total, and cache results to avoid exponential recomputation.

Solution

1class Solution:
2 def findTargetSumWays(self, nums: list[int], target: int) -> int:
3 memo = {}
4
5 def dp(i, total):
6 if i == len(nums):
7 return 1 if total == target else 0
8 if (i, total) in memo:
9 return memo[(i, total)]
10
11 add = dp(i + 1, total + nums[i])
12 subtract = dp(i + 1, total - nums[i])
13
14 memo[(i, total)] = add + subtract
15 return memo[(i, total)]
16
17 return dp(0, 0)

Step-by-Step Solution

1

Initialize Memoization Cache to Store Computed States

3memo = {}

Objective

To create a dictionary that caches the number of ways to reach the target from each (index, total) state.

Key Insight

Memoization is essential to avoid redundant recursive calls. By storing results keyed by (i, total), the algorithm ensures that each unique state is computed once, transforming exponential recursion into efficient dynamic programming.

Interview Quick-Check

Core Logic

Memoization cache keys are tuples of (index, current total), uniquely identifying subproblems.

Common Pitfalls & Bugs

Forgetting to memoize or incorrectly keying the cache leads to exponential time and stack overflow.

2

Recursively Explore Adding and Subtracting Current Number

5def dp(i, total):
6 if i == len(nums):
7 return 1 if total == target else 0
11 add = dp(i + 1, total + nums[i])
12 subtract = dp(i + 1, total - nums[i])
14 memo[(i, total)] = add + subtract
15 return memo[(i, total)]

Objective

To recursively compute the number of ways to reach the target by either adding or subtracting the current number at index i.

Key Insight

At each index, the problem branches into two choices: add or subtract the current number. The total number of ways is the sum of ways from both branches. This exhaustive exploration covers all valid sign assignments.

Interview Quick-Check

Core Logic

The recursion explores both adding and subtracting nums[i], summing their results to count all valid expressions.

State & Boundaries

The base case occurs when index reaches the length of nums, returning 1 if total equals target, else 0.

Common Pitfalls & Bugs

Not handling the base case correctly or missing one of the two recursive calls leads to incorrect counts.

3

Return Final Count from Starting State

17return dp(0, 0)

Objective

To initiate the recursive exploration from index 0 and total 0, returning the total number of valid expressions.

Key Insight

Starting from the initial state with no numbers processed and total sum zero, the recursion explores all sign assignments. The returned value represents the total count of expressions evaluating to target.

Interview Quick-Check

Core Logic

The initial call dp(0, 0) triggers the entire memoized recursion tree.

Line Analysis

This solution has 5 Critical lines interviewers watch for.

Line 7 Critical
return 1 if total == target else 0

Return 1 if total equals target, else 0, at the base case.

This line correctly counts one valid expression when the accumulated sum matches the target, serving as the foundation for counting all valid paths.

Line 3 Critical
memo = {}

Initialize a memoization dictionary to cache results of subproblems.

This cache prevents redundant computations by storing the number of ways to reach the target from each (index, total) state, transforming exponential recursion into polynomial time.

Line 8 Critical
if (i, total) in memo:

Check if the current state (index, total) has been computed before.

This memoization lookup avoids recomputing the number of ways for the same state, drastically reducing the recursion tree size.

Line 9 Critical
return memo[(i, total)]

Return the cached result for the current state if available.

Returning cached results immediately prunes redundant recursive calls, ensuring efficient computation.

Line 14 Critical
memo[(i, total)] = add + subtract

Store the sum of both recursive results in the memoization cache.

Caching the combined count for the current state ensures that future calls with the same parameters return instantly, preventing exponential blowup.

Line 5 Core
def dp(i, total):

Define the recursive function dp with parameters index and current total.

This function models the problem as a state space where each state is defined by the current index and accumulated total, enabling systematic exploration of all sign assignments.

Line 6 Core
if i == len(nums):

Check if the recursion has processed all numbers.

Reaching the end of the array is the base case that determines if the current total matches the target, which decides if this path counts as a valid expression.

Line 11 Core
add = dp(i + 1, total + nums[i])

Recursively compute ways by adding nums[i] to the total.

Exploring the branch where the current number is added captures all expressions with a '+' sign at this position.

Line 12 Core
subtract = dp(i + 1, total - nums[i])

Recursively compute ways by subtracting nums[i] from the total.

Exploring the branch where the current number is subtracted captures all expressions with a '-' sign at this position.

Line 15 Core
return memo[(i, total)]

Return the cached total number of ways for the current state.

Returning the memoized result completes the recursion step, propagating counts up the call stack.

Line 17 Core
return dp(0, 0)

Start the recursive exploration from index 0 and total 0.

This initial call triggers the entire memoized recursion, covering all possible sign assignments and returning the final count.

Test Your Understanding

Why is memoization necessary in this recursive solution?

Because without memoization, the recursion explores many overlapping subproblems multiple times, leading to exponential time; memoization caches results for each state to ensure each is computed once, reducing complexity to polynomial.

Related Problems

Dynamic Programming pattern

Don't just read it. Drill it.

Reconstruct Target Sum from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Target Sum drill

or drill a free problem