questions S-W
This commit is contained in:
218
backend/data/questions/target-sum.yaml
Normal file
218
backend/data/questions/target-sum.yaml
Normal file
@@ -0,0 +1,218 @@
|
||||
title: Target Sum
|
||||
slug: target-sum
|
||||
difficulty: medium
|
||||
leetcode_id: 494
|
||||
leetcode_url: https://leetcode.com/problems/target-sum/
|
||||
categories:
|
||||
- arrays
|
||||
- dynamic-programming
|
||||
patterns:
|
||||
- dynamic-programming
|
||||
- backtracking
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user