247 lines
10 KiB
YAML
247 lines
10 KiB
YAML
title: Target Sum
|
|
slug: target-sum
|
|
difficulty: medium
|
|
leetcode_id: 494
|
|
leetcode_url: https://leetcode.com/problems/target-sum/
|
|
categories:
|
|
- arrays
|
|
- dynamic-programming
|
|
patterns:
|
|
- slug: dynamic-programming
|
|
is_optimal: true
|
|
- slug: backtracking
|
|
is_optimal: false
|
|
|
|
function_signature: "def find_target_sum_ways(nums: list[int], target: int) -> int:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { nums: [1, 1, 1, 1, 1], target: 3 }
|
|
expected: 5
|
|
- input: { nums: [1], target: 1 }
|
|
expected: 1
|
|
- input: { nums: [1, 2, 1], target: 0 }
|
|
expected: 2
|
|
hidden:
|
|
- input: { nums: [1], target: 2 }
|
|
expected: 0
|
|
- input: { nums: [0, 0, 0, 0, 0], target: 0 }
|
|
expected: 32
|
|
- input: { nums: [2, 3, 5], target: 0 }
|
|
expected: 0
|
|
- input: { nums: [1, 0], target: 1 }
|
|
expected: 2
|
|
- input: { nums: [1, 2, 3, 4, 5], target: 3 }
|
|
expected: 3
|
|
- input: { nums: [100], target: -200 }
|
|
expected: 0
|
|
- input: { nums: [7, 9, 3, 8, 0, 2, 4, 8, 3, 9], target: 0 }
|
|
expected: 0
|
|
|
|
description: |
|
|
You are given an integer array `nums` and an integer `target`.
|
|
|
|
You want to build an **expression** out of nums by adding one of the symbols `'+'` and `'-'` before each integer in nums and then concatenate all the integers.
|
|
|
|
For example, if `nums = [2, 1]`, you can add a `'+'` before `2` and a `'-'` before `1` and concatenate them to build the expression `"+2-1"`.
|
|
|
|
Return *the number of different expressions that you can build, which evaluates to* `target`.
|
|
|
|
constraints: |
|
|
- `1 <= nums.length <= 20`
|
|
- `0 <= nums[i] <= 1000`
|
|
- `0 <= sum(nums[i]) <= 1000`
|
|
- `-1000 <= target <= 1000`
|
|
|
|
examples:
|
|
- input: "nums = [1,1,1,1,1], target = 3"
|
|
output: "5"
|
|
explanation: "There are 5 ways to assign symbols to make the sum of nums be target 3: -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."
|
|
- input: "nums = [1], target = 1"
|
|
output: "1"
|
|
explanation: "There is only one way: +1 = 1."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine you have a set of coins, and for each coin you must decide whether to put it in a "positive pile" or a "negative pile". The question becomes: how many ways can you split the coins so that the positive pile minus the negative pile equals the target?
|
|
|
|
This reframing reveals a powerful insight. Let `P` be the sum of numbers assigned `+` and `N` be the sum assigned `-`. We know:
|
|
- `P + N = total` (all numbers are used)
|
|
- `P - N = target` (the goal)
|
|
|
|
Adding these equations: `2P = total + target`, so `P = (total + target) / 2`.
|
|
|
|
**The problem transforms into a subset sum problem**: find how many subsets of `nums` sum to exactly `P`. This is a classic dynamic programming pattern!
|
|
|
|
Think of it like this: instead of tracking all possible sums from `-total` to `+total` (which could be huge), we only need to count subsets that reach one specific target sum. This dramatically reduces the problem space.
|
|
|
|
approach: |
|
|
We solve this using **Dynamic Programming (Subset Sum Count)**:
|
|
|
|
**Step 1: Transform the problem**
|
|
|
|
- Calculate `total = sum(nums)`
|
|
- Calculate `subset_sum = (total + target) / 2`
|
|
- If `(total + target)` is odd, return `0` — no valid split exists
|
|
- If `total + target < 0`, return `0` — target is unreachable
|
|
|
|
|
|
|
|
**Step 2: Initialise the DP array**
|
|
|
|
- Create `dp` array of size `subset_sum + 1`
|
|
- `dp[s]` represents the number of ways to form sum `s`
|
|
- Set `dp[0] = 1` — there's exactly one way to form sum `0` (use no elements)
|
|
|
|
|
|
|
|
**Step 3: Fill the DP table**
|
|
|
|
- For each number `num` in `nums`:
|
|
- Iterate `s` from `subset_sum` down to `num` (reverse order is crucial!)
|
|
- Update: `dp[s] += dp[s - num]`
|
|
- This adds the count of ways to reach `s - num` (if we include `num`)
|
|
|
|
|
|
|
|
**Step 4: Return the result**
|
|
|
|
- Return `dp[subset_sum]` — the number of subsets that sum to our target
|
|
|
|
|
|
|
|
The reverse iteration in Step 3 ensures each number is used at most once per subset. If we iterated forward, we'd count the same number multiple times.
|
|
|
|
common_pitfalls:
|
|
- title: Brute Force Exponential Blowup
|
|
description: |
|
|
The naive approach tries all `2^n` combinations of `+` and `-` signs using recursion or backtracking.
|
|
|
|
With `n = 20`, this means up to `2^20 = 1,048,576` combinations. While this might pass for small inputs, it's inefficient and the DP approach is much faster.
|
|
|
|
More importantly, the brute force approach doesn't reveal the elegant mathematical structure of the problem.
|
|
wrong_approach: "Recursively try all 2^n sign combinations"
|
|
correct_approach: "Transform to subset sum and use DP"
|
|
|
|
- title: Forward Iteration in DP
|
|
description: |
|
|
When filling the DP array, you might be tempted to iterate `s` from `0` to `subset_sum`. This is wrong!
|
|
|
|
Forward iteration allows the same element to be counted multiple times. For example, with `num = 2`, updating `dp[2]` first would affect `dp[4]` in the same iteration, treating `2` as if it could be used twice.
|
|
|
|
Always iterate in reverse (from `subset_sum` down to `num`) to ensure each element is considered only once.
|
|
wrong_approach: "for s in range(num, subset_sum + 1)"
|
|
correct_approach: "for s in range(subset_sum, num - 1, -1)"
|
|
|
|
- title: Forgetting Edge Cases
|
|
description: |
|
|
Several edge cases can trip you up:
|
|
|
|
- If `(total + target)` is odd, no valid partition exists — you can't split integers into two groups with a non-integer difference
|
|
- If `target > total` or `target < -total`, the target is unreachable
|
|
- If `total + target < 0`, the subset sum would be negative, which is impossible
|
|
|
|
Handle these before starting the DP computation.
|
|
|
|
- title: Zeros in the Array
|
|
description: |
|
|
Zeros double the count of ways! A zero can be assigned either `+` or `-` without changing the sum.
|
|
|
|
With `k` zeros in the array, each valid subset has `2^k` variations. The DP approach handles this automatically since `dp[s] += dp[s - 0]` effectively doubles the count.
|
|
|
|
key_takeaways:
|
|
- "**Problem transformation**: Recognising that `+/-` assignment is equivalent to subset partitioning unlocks an efficient solution"
|
|
- "**Subset sum pattern**: Counting subsets that sum to a target is a foundational DP pattern — memorise the `dp[s] += dp[s - num]` recurrence"
|
|
- "**Reverse iteration trick**: When each element can only be used once, iterate the DP array in reverse to avoid double-counting"
|
|
- "**Mathematical insight**: Always look for ways to simplify the state space — transforming from tracking sums in `[-total, total]` to just `[0, subset_sum]` is a huge optimisation"
|
|
|
|
time_complexity: "O(n * subset_sum). We process each of the `n` numbers once, and for each number we update up to `subset_sum` entries in the DP array."
|
|
space_complexity: "O(subset_sum). We use a 1D DP array of size `subset_sum + 1`. This can be at most `O(total)` where `total = sum(nums)`."
|
|
|
|
solutions:
|
|
- approach_name: Dynamic Programming (Subset Sum)
|
|
is_optimal: true
|
|
code: |
|
|
def find_target_sum_ways(nums: list[int], target: int) -> int:
|
|
total = sum(nums)
|
|
|
|
# Edge cases: impossible to reach target
|
|
if (total + target) % 2 != 0: # Can't split into equal parts
|
|
return 0
|
|
if total + target < 0: # Target too negative
|
|
return 0
|
|
|
|
# Transform: find subsets that sum to this value
|
|
subset_sum = (total + target) // 2
|
|
|
|
# dp[s] = number of ways to form sum s
|
|
dp = [0] * (subset_sum + 1)
|
|
dp[0] = 1 # One way to form sum 0: use nothing
|
|
|
|
for num in nums:
|
|
# Iterate in reverse to avoid using same element twice
|
|
for s in range(subset_sum, num - 1, -1):
|
|
dp[s] += dp[s - num]
|
|
|
|
return dp[subset_sum]
|
|
explanation: |
|
|
**Time Complexity:** O(n * subset_sum) — For each number, we update the DP array.
|
|
|
|
**Space Complexity:** O(subset_sum) — 1D DP array.
|
|
|
|
We transform the problem into counting subsets that sum to `(total + target) / 2`. The 1D DP array counts ways to form each possible sum, updated in reverse order to ensure each number is used once.
|
|
|
|
- approach_name: Recursion with Memoisation
|
|
is_optimal: false
|
|
code: |
|
|
def find_target_sum_ways(nums: list[int], target: int) -> int:
|
|
from functools import lru_cache
|
|
|
|
@lru_cache(maxsize=None)
|
|
def count_ways(index: int, current_sum: int) -> int:
|
|
# Base case: processed all numbers
|
|
if index == len(nums):
|
|
return 1 if current_sum == target else 0
|
|
|
|
# Try adding current number with + or -
|
|
add = count_ways(index + 1, current_sum + nums[index])
|
|
subtract = count_ways(index + 1, current_sum - nums[index])
|
|
|
|
return add + subtract
|
|
|
|
return count_ways(0, 0)
|
|
explanation: |
|
|
**Time Complexity:** O(n * total_sum) — Each unique (index, sum) state is computed once.
|
|
|
|
**Space Complexity:** O(n * total_sum) — Memoisation cache for all states.
|
|
|
|
This approach directly models the problem: at each index, we can add or subtract the current number. Memoisation prevents recomputing the same (index, current_sum) pairs. While intuitive, it uses more space than the subset sum DP approach.
|
|
|
|
- approach_name: Brute Force (Backtracking)
|
|
is_optimal: false
|
|
code: |
|
|
def find_target_sum_ways(nums: list[int], target: int) -> int:
|
|
count = 0
|
|
|
|
def backtrack(index: int, current_sum: int) -> None:
|
|
nonlocal count
|
|
|
|
# Base case: used all numbers
|
|
if index == len(nums):
|
|
if current_sum == target:
|
|
count += 1
|
|
return
|
|
|
|
# Try + and - for current number
|
|
backtrack(index + 1, current_sum + nums[index])
|
|
backtrack(index + 1, current_sum - nums[index])
|
|
|
|
backtrack(0, 0)
|
|
return count
|
|
explanation: |
|
|
**Time Complexity:** O(2^n) — Every number has two choices, leading to exponential combinations.
|
|
|
|
**Space Complexity:** O(n) — Recursion stack depth.
|
|
|
|
This brute force approach explicitly tries all `2^n` combinations of `+` and `-` signs. While correct and easy to understand, it's inefficient for larger inputs. Included to show the problem's natural recursive structure before optimisation.
|