Facebook Pixel

494. Target Sum

Problem Description

You have an integer array nums and an integer target. Your task is to assign either a '+' or '-' sign before each integer in the array, then calculate the sum of the resulting expression.

For example, given nums = [2, 1]:

  • You could assign '+' before 2 and '-' before 1, creating the expression "+2-1" which evaluates to 1
  • Or you could assign '-' before 2 and '+' before 1, creating the expression "-2+1" which evaluates to -1
  • There are other combinations as well

The goal is to count how many different ways you can assign the signs such that the final sum equals the target value.

The solution transforms this problem into a subset sum problem. The key insight is that if we denote the sum of elements assigned negative signs as x, and the total sum of all elements as s, then:

  • Elements with positive signs sum to s - x
  • The expression evaluates to (s - x) - x = s - 2x
  • For this to equal target, we need s - 2x = target, which gives us x = (s - target) / 2

This means we need to find how many ways we can select elements from nums that sum to (s - target) / 2.

The dynamic programming approach uses a 2D table f[i][j] where:

  • i represents considering the first i elements from nums
  • j represents the target sum we want to achieve
  • f[i][j] stores the number of ways to select elements from the first i elements that sum to j

The state transition works by considering whether to include the current element or not:

  • If we don't include nums[i-1]: f[i][j] = f[i-1][j]
  • If we include nums[i-1] (when j >= nums[i-1]): f[i][j] += f[i-1][j - nums[i-1]]

The base case is f[0][0] = 1 (one way to get sum 0 with 0 elements: select nothing), and the answer is f[m][n] where m is the length of nums and n = (s - target) / 2.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • No: The problem involves an array of integers and sign assignments, not nodes and edges as in graph problems.

Need to solve for kth smallest/largest?

  • No: We're not looking for the kth smallest or largest value, but rather counting the number of ways to achieve a target sum.

Involves Linked Lists?

  • No: The problem works with an integer array, not linked lists.

Does the problem have small constraints?

  • Yes: Looking at typical constraints for this problem (array length up to 20, values up to 1000), we need to explore all possible combinations of '+' and '-' signs. With n elements, there are 2^n possible combinations, which is manageable for small n.

Brute force / Backtracking?

  • Yes: Backtracking is a valid approach here. We can recursively try assigning '+' or '-' to each element, exploring all possible combinations and counting those that sum to the target.

Conclusion: The flowchart suggests using a backtracking approach for the Target Sum problem. This makes sense because:

  1. We need to explore all possible sign assignments (2^n possibilities)
  2. Each element requires a binary choice ('+' or '-')
  3. We can recursively build expressions and backtrack when needed
  4. The small constraints make exhaustive search feasible

While the provided solution uses Dynamic Programming (which is more efficient), backtracking is indeed a valid approach that directly models the problem statement - trying all possible sign combinations and counting those that achieve the target sum.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

While backtracking can solve this problem by trying all 2^n combinations of signs, we can find a more elegant approach by reframing the problem mathematically.

Let's think about what happens when we assign signs to numbers. If we split the array into two groups - one with '+' signs and one with '-' signs, we're essentially computing: sum(positive_group) - sum(negative_group) = target.

Let's call the sum of the negative group x. Since all numbers together sum to s, the positive group sums to s - x. Our equation becomes: (s - x) - x = target s - 2x = target x = (s - target) / 2

This is a key insight! Instead of thinking about assigning signs, we can think about: "Which numbers should we pick to form the negative group such that their sum equals (s - target) / 2?"

This transforms our original problem into a classic subset sum problem: "How many ways can we select elements from the array to sum to a specific value?"

For this to work, we need (s - target) / 2 to be a non-negative integer. This means:

  • s >= target (we can't reach a target larger than the sum of all positive numbers)
  • s - target must be even (so division by 2 yields an integer)

The subset sum problem is a perfect fit for dynamic programming. We can build up solutions incrementally: for each number, we decide whether to include it in our subset or not. By tracking how many ways we can achieve each possible sum using the first i elements, we can eventually find how many ways to achieve our target sum using all elements.

This transformation from a sign assignment problem to a subset selection problem is the key that unlocks an efficient solution, reducing our conceptual complexity from exploring all sign combinations to counting subset combinations that achieve a specific sum.

Solution Approach

Based on our mathematical transformation, we need to find how many ways we can select elements from nums to sum to (s - target) / 2. Let's implement this using dynamic programming.

Step 1: Initial Checks

s = sum(nums)
if s < target or (s - target) % 2:
    return 0

We first calculate the total sum s. If s < target or (s - target) is odd, it's impossible to achieve the target, so we return 0.

Step 2: Define the DP State

m, n = len(nums), (s - target) // 2
f = [[0] * (n + 1) for _ in range(m + 1)]
  • m: length of the array
  • n: our transformed target sum (s - target) / 2
  • f[i][j]: number of ways to select elements from the first i elements of nums to sum to j

Step 3: Base Case

f[0][0] = 1

There's exactly one way to achieve sum 0 with 0 elements: select nothing.

Step 4: Fill the DP Table

for i, x in enumerate(nums, 1):
    for j in range(n + 1):
        f[i][j] = f[i - 1][j]
        if j >= x:
            f[i][j] += f[i - 1][j - x]

For each element nums[i-1] (accessed as x in the code), and for each possible sum j:

  • Don't include the current element: f[i][j] = f[i - 1][j]
    • The number of ways remains the same as without this element
  • Include the current element (only if j >= x): f[i][j] += f[i - 1][j - x]
    • Add the number of ways to achieve sum j - x using the first i - 1 elements

The state transition equation combines both choices: f[i][j] = f[i - 1][j] + f[i - 1][j - nums[i - 1]]

Step 5: Return the Result

return f[m][n]

The answer is f[m][n] - the number of ways to select elements from all m elements to achieve sum n.

Time and Space Complexity:

  • Time: O(m * n) where m is the array length and n is the target sum
  • Space: O(m * n) for the DP table

This DP approach efficiently counts all valid subset selections without explicitly generating them, transforming an exponential backtracking problem into a polynomial-time solution.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with nums = [1, 1, 1, 1, 1] and target = 3.

Step 1: Transform the problem

  • Total sum s = 1 + 1 + 1 + 1 + 1 = 5
  • We need to find elements to assign negative signs such that the expression equals 3
  • Using our formula: x = (s - target) / 2 = (5 - 3) / 2 = 1
  • So we need to find how many ways to select elements that sum to 1

Step 2: Set up the DP table

  • m = 5 (array length), n = 1 (transformed target)
  • Create table f[6][2] (indices 0 to 5 for elements, 0 to 1 for sums)
  • Initialize f[0][0] = 1 (one way to get sum 0 with no elements)

Step 3: Fill the DP table

Processing element by element:

Element 1 (value = 1):

  • For sum 0: f[1][0] = f[0][0] = 1 (don't include)
  • For sum 1: f[1][1] = f[0][1] + f[0][0] = 0 + 1 = 1 (can include to make sum 1)

Element 2 (value = 1):

  • For sum 0: f[2][0] = f[1][0] = 1
  • For sum 1: f[2][1] = f[1][1] + f[1][0] = 1 + 1 = 2 (two ways: include first OR second)

Element 3 (value = 1):

  • For sum 0: f[3][0] = f[2][0] = 1
  • For sum 1: f[3][1] = f[2][1] + f[2][0] = 2 + 1 = 3 (three ways: any single element)

Element 4 (value = 1):

  • For sum 0: f[4][0] = f[3][0] = 1
  • For sum 1: f[4][1] = f[3][1] + f[3][0] = 3 + 1 = 4 (four ways: any single element)

Element 5 (value = 1):

  • For sum 0: f[5][0] = f[4][0] = 1
  • For sum 1: f[5][1] = f[4][1] + f[4][0] = 4 + 1 = 5 (five ways: any single element)

Step 4: Result f[5][1] = 5, meaning there are 5 ways to achieve target sum 3.

Verification: The 5 ways correspond to choosing which single element gets a negative sign:

  1. -1 + 1 + 1 + 1 + 1 = 3
  2. 1 - 1 + 1 + 1 + 1 = 3
  3. 1 + 1 - 1 + 1 + 1 = 3
  4. 1 + 1 + 1 - 1 + 1 = 3
  5. 1 + 1 + 1 + 1 - 1 = 3

Each way corresponds to selecting exactly one element (to sum to 1) for the negative group, while the rest remain positive.

Solution Implementation

1class Solution:
2    def findTargetSumWays(self, nums: List[int], target: int) -> int:
3        """
4        Find the number of ways to assign + or - signs to elements in nums to get target sum.
5      
6        This problem can be transformed into a subset sum problem:
7        Let P be the sum of numbers with + sign, N be the sum of numbers with - sign
8        P - N = target and P + N = sum(nums)
9        Therefore: 2*N = sum(nums) - target, so N = (sum(nums) - target) / 2
10      
11        The problem becomes: find number of subsets with sum equal to N
12        """
13        total_sum = sum(nums)
14      
15        # Check if it's possible to achieve the target
16        # 1. The absolute target cannot exceed total sum
17        # 2. (total_sum - target) must be even to have an integer N
18        if total_sum < abs(target) or (total_sum - target) % 2 != 0:
19            return 0
20      
21        # Calculate the target sum for the negative subset
22        num_elements = len(nums)
23        negative_target = (total_sum - target) // 2
24      
25        # dp[i][j] represents number of ways to select from first i elements 
26        # to get sum j
27        dp = [[0] * (negative_target + 1) for _ in range(num_elements + 1)]
28      
29        # Base case: empty subset has sum 0, there's one way to achieve it
30        dp[0][0] = 1
31      
32        # Fill the DP table
33        for i in range(1, num_elements + 1):
34            current_num = nums[i - 1]  # Current number (0-indexed in nums)
35          
36            for j in range(negative_target + 1):
37                # Option 1: Don't include current number
38                dp[i][j] = dp[i - 1][j]
39              
40                # Option 2: Include current number (if possible)
41                if j >= current_num:
42                    dp[i][j] += dp[i - 1][j - current_num]
43      
44        return dp[num_elements][negative_target]
45
1class Solution {
2    public int findTargetSumWays(int[] nums, int target) {
3        // Calculate the total sum of all numbers
4        int totalSum = Arrays.stream(nums).sum();
5      
6        // Check if it's possible to achieve the target
7        // If total sum is less than target or the difference is odd, it's impossible
8        if (totalSum < Math.abs(target) || (totalSum - target) % 2 != 0) {
9            return 0;
10        }
11      
12        // Transform the problem into subset sum problem
13        // We need to find subsets with sum = (totalSum - target) / 2
14        // This comes from: positiveSum - negativeSum = target
15        // and positiveSum + negativeSum = totalSum
16        // Solving these equations: negativeSum = (totalSum - target) / 2
17        int numCount = nums.length;
18        int targetSum = (totalSum - target) / 2;
19      
20        // dp[i][j] represents the number of ways to select from first i numbers
21        // to achieve a sum of j
22        int[][] dp = new int[numCount + 1][targetSum + 1];
23      
24        // Base case: empty subset has sum 0, there's one way to achieve it
25        dp[0][0] = 1;
26      
27        // Fill the dp table
28        for (int i = 1; i <= numCount; i++) {
29            for (int j = 0; j <= targetSum; j++) {
30                // Option 1: Don't include the current number
31                dp[i][j] = dp[i - 1][j];
32              
33                // Option 2: Include the current number if possible
34                if (j >= nums[i - 1]) {
35                    dp[i][j] += dp[i - 1][j - nums[i - 1]];
36                }
37            }
38        }
39      
40        // Return the number of ways to achieve targetSum using all numbers
41        return dp[numCount][targetSum];
42    }
43}
44
1class Solution {
2public:
3    int findTargetSumWays(vector<int>& nums, int target) {
4        // Calculate the total sum of all numbers
5        int totalSum = accumulate(nums.begin(), nums.end(), 0);
6      
7        // Check if target is achievable
8        // The difference (totalSum - target) must be non-negative and even
9        // because we need to partition nums into two subsets P (positive) and N (negative)
10        // where sum(P) - sum(N) = target and sum(P) + sum(N) = totalSum
11        // This gives us sum(N) = (totalSum - target) / 2
12        if (totalSum < target || (totalSum - target) % 2 != 0) {
13            return 0;
14        }
15      
16        int numsSize = nums.size();
17        // Calculate the target sum for the negative subset
18        int negativeSum = (totalSum - target) / 2;
19      
20        // dp[i][j] represents the number of ways to select from first i numbers
21        // to achieve sum j
22        int dp[numsSize + 1][negativeSum + 1];
23        memset(dp, 0, sizeof(dp));
24      
25        // Base case: empty subset has sum 0, there's one way to achieve it
26        dp[0][0] = 1;
27      
28        // Fill the dp table
29        for (int i = 1; i <= numsSize; ++i) {
30            for (int j = 0; j <= negativeSum; ++j) {
31                // Case 1: Don't include current number
32                dp[i][j] = dp[i - 1][j];
33              
34                // Case 2: Include current number if possible
35                if (j >= nums[i - 1]) {
36                    dp[i][j] += dp[i - 1][j - nums[i - 1]];
37                }
38            }
39        }
40      
41        // Return the number of ways to achieve the negative subset sum
42        return dp[numsSize][negativeSum];
43    }
44};
45
1/**
2 * Finds the number of ways to assign '+' or '-' to elements in nums to achieve target sum
3 * @param nums - Array of non-negative integers
4 * @param target - Target sum to achieve
5 * @returns Number of different expressions that evaluate to target
6 */
7function findTargetSumWays(nums: number[], target: number): number {
8    // Calculate total sum of all numbers
9    const totalSum: number = nums.reduce((accumulator, current) => accumulator + current, 0);
10  
11    // Check if solution is possible
12    // The difference (totalSum - target) must be even for a valid partition
13    if (totalSum < target || (totalSum - target) % 2 !== 0) {
14        return 0;
15    }
16  
17    // Transform problem to subset sum problem
18    // Find subsets with sum = (totalSum - target) / 2
19    const arrayLength: number = nums.length;
20    const targetSubsetSum: number = Math.floor((totalSum - target) / 2);
21  
22    // Initialize DP table
23    // dp[i][j] represents number of ways to achieve sum j using first i elements
24    const dp: number[][] = Array.from(
25        { length: arrayLength + 1 }, 
26        () => Array(targetSubsetSum + 1).fill(0)
27    );
28  
29    // Base case: empty subset has sum 0 in exactly one way
30    dp[0][0] = 1;
31  
32    // Fill DP table
33    for (let i = 1; i <= arrayLength; i++) {
34        for (let j = 0; j <= targetSubsetSum; j++) {
35            // Case 1: Don't include current element
36            dp[i][j] = dp[i - 1][j];
37          
38            // Case 2: Include current element if possible
39            if (j >= nums[i - 1]) {
40                dp[i][j] += dp[i - 1][j - nums[i - 1]];
41            }
42        }
43    }
44  
45    // Return number of ways to achieve target subset sum
46    return dp[arrayLength][targetSubsetSum];
47}
48

Time and Space Complexity

The time complexity is O(m × n), where m is the length of the nums array (i.e., len(nums)) and n is (s - target) // 2, where s is the sum of all elements in nums. The algorithm uses a nested loop structure - the outer loop iterates through all m elements of the array, and the inner loop iterates through all possible sums from 0 to n. Therefore, the total number of operations is proportional to m × n.

The space complexity is O(m × n). The algorithm creates a 2D DP table f with dimensions (m + 1) × (n + 1) to store the number of ways to achieve each possible sum using a subset of the first i elements. This table requires O((m + 1) × (n + 1)) space, which simplifies to O(m × n).

Common Pitfalls

1. Forgetting to Handle Edge Cases with Target Validation

One of the most common mistakes is not properly validating whether the target is achievable before starting the DP computation. This leads to incorrect results or unnecessary computation.

Pitfall Example:

# Incorrect: Missing validation
def findTargetSumWays(self, nums: List[int], target: int) -> int:
    s = sum(nums)
    n = (s - target) // 2  # This might be negative or fractional!
  
    # Proceeding with DP without checking validity...
    dp = [[0] * (n + 1) for _ in range(len(nums) + 1)]
    # ...

Issues:

  • If s < target, then n becomes negative, causing array indexing errors
  • If (s - target) is odd, integer division gives an incorrect target
  • If abs(target) > s, it's mathematically impossible to achieve the target

Solution: Always validate before proceeding:

s = sum(nums)
# Check both absolute value constraint and even/odd constraint
if abs(target) > s or (s - target) % 2 != 0:
    return 0
n = (s - target) // 2

2. Space Optimization Opportunity Missed

While the 2D DP solution is correct, it uses O(m * n) space which can be optimized to O(n) since each row only depends on the previous row.

Current Approach (2D array):

dp = [[0] * (n + 1) for _ in range(m + 1)]

Optimized Approach (1D array):

def findTargetSumWays(self, nums: List[int], target: int) -> int:
    s = sum(nums)
    if abs(target) > s or (s - target) % 2 != 0:
        return 0
  
    n = (s - target) // 2
    dp = [0] * (n + 1)
    dp[0] = 1
  
    for num in nums:
        # Iterate backwards to avoid using updated values
        for j in range(n, num - 1, -1):
            dp[j] += dp[j - num]
  
    return dp[n]

Key insight: When updating dp[j], we need dp[j - num] from the previous iteration. By iterating backwards, we ensure we're using the old values before they get updated.

3. Incorrect Index Mapping

A subtle but critical error occurs when developers confuse the indexing between the nums array (0-indexed) and the DP table (where row i represents considering the first i elements).

Pitfall Example:

for i in range(1, m + 1):
    for j in range(n + 1):
        # Wrong: using nums[i] instead of nums[i-1]
        if j >= nums[i]:  # IndexError when i == m!
            dp[i][j] = dp[i-1][j] + dp[i-1][j - nums[i]]

Solution: Always remember that when i represents "first i elements", the current element is nums[i-1]:

for i in range(1, m + 1):
    current_num = nums[i - 1]  # Correct indexing
    for j in range(n + 1):
        dp[i][j] = dp[i - 1][j]
        if j >= current_num:
            dp[i][j] += dp[i - 1][j - current_num]

4. Handling Arrays with Zeros

Arrays containing zeros require special attention because zeros can be included or excluded without changing the sum, effectively doubling the number of ways for each zero.

Example: nums = [0, 0, 1], target = 1

  • The subset {1} achieves sum 1
  • But we can include 0, 1, or 2 zeros with this subset
  • Total ways: 2^2 = 4 ways

The provided DP solution handles this correctly because:

  • When processing a zero (x = 0), both transitions lead to the same state
  • dp[i][j] = dp[i-1][j] + dp[i-1][j-0] = 2 * dp[i-1][j]

This naturally accounts for the multiplicative effect of zeros without special handling.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How would you design a stack which has a function min that returns the minimum element in the stack, in addition to push and pop? All push, pop, min should have running time O(1).


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More