192 lines
9.2 KiB
YAML
192 lines
9.2 KiB
YAML
title: Combination Sum III
|
|
slug: combination-sum-iii
|
|
difficulty: medium
|
|
leetcode_id: 216
|
|
leetcode_url: https://leetcode.com/problems/combination-sum-iii/
|
|
categories:
|
|
- arrays
|
|
- recursion
|
|
patterns:
|
|
- backtracking
|
|
|
|
description: |
|
|
Find all valid combinations of `k` numbers that sum up to `n` such that the following conditions are true:
|
|
|
|
- Only numbers `1` through `9` are used.
|
|
- Each number is used **at most once**.
|
|
|
|
Return *a list of all possible valid combinations*. The list must not contain the same combination twice, and the combinations may be returned in any order.
|
|
|
|
constraints: |
|
|
- `2 <= k <= 9`
|
|
- `1 <= n <= 60`
|
|
|
|
examples:
|
|
- input: "k = 3, n = 7"
|
|
output: "[[1,2,4]]"
|
|
explanation: "1 + 2 + 4 = 7. There are no other valid combinations."
|
|
- input: "k = 3, n = 9"
|
|
output: "[[1,2,6],[1,3,5],[2,3,4]]"
|
|
explanation: "1 + 2 + 6 = 9, 1 + 3 + 5 = 9, 2 + 3 + 4 = 9. There are no other valid combinations."
|
|
- input: "k = 4, n = 1"
|
|
output: "[]"
|
|
explanation: "There are no valid combinations. Using 4 different numbers in the range [1,9], the smallest sum we can get is 1+2+3+4 = 10, and since 10 > 1, there are no valid combinations."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine you have a row of numbered boxes from 1 to 9, and you need to pick exactly `k` boxes such that their numbers add up to `n`. You can only pick each box once, and order doesn't matter — picking boxes 1, 2, 4 is the same as picking 4, 2, 1.
|
|
|
|
This is a classic **combination problem** where you're exploring all possible subsets of a fixed set. The key insight is that you can build combinations incrementally: start with an empty selection, then for each number from 1 to 9, decide whether to include it or skip it.
|
|
|
|
Think of it like walking through a decision tree. At each node, you choose to either:
|
|
- **Include** the current number and move forward
|
|
- **Skip** the current number and move forward
|
|
|
|
When your selection reaches exactly `k` numbers and they sum to `n`, you've found a valid combination. If the sum exceeds `n` or you've used too many numbers, you backtrack and try a different path.
|
|
|
|
The constraint that we only use numbers 1-9 keeps the search space small — at most 2<sup>9</sup> = 512 possible subsets — making this problem tractable with backtracking.
|
|
|
|
approach: |
|
|
We solve this using **Backtracking** to explore all valid combinations:
|
|
|
|
**Step 1: Set up the recursive function**
|
|
|
|
- Create a helper function `backtrack(start, remaining_sum, current_combination)`
|
|
- `start`: The next number to consider (1 through 9)
|
|
- `remaining_sum`: How much more we need to reach target `n`
|
|
- `current_combination`: Numbers we've picked so far
|
|
|
|
|
|
|
|
**Step 2: Define the base cases**
|
|
|
|
- If `current_combination` has exactly `k` numbers AND `remaining_sum == 0`, we found a valid combination — add a copy to results
|
|
- If `current_combination` has `k` numbers but sum isn't `n`, or if we've exhausted all numbers (start > 9), backtrack
|
|
|
|
|
|
|
|
**Step 3: Explore choices with pruning**
|
|
|
|
- For each number `i` from `start` to 9:
|
|
- If `i > remaining_sum`, skip it and all larger numbers (pruning)
|
|
- Otherwise, add `i` to `current_combination`
|
|
- Recurse with `backtrack(i + 1, remaining_sum - i, current_combination)`
|
|
- Remove `i` from `current_combination` (backtrack)
|
|
|
|
|
|
|
|
**Step 4: Start the recursion**
|
|
|
|
- Call `backtrack(1, n, [])` and return the collected results
|
|
|
|
|
|
|
|
The key optimisation is **pruning**: if the current number already exceeds the remaining sum needed, we can skip all larger numbers since they would only increase the overshoot.
|
|
|
|
common_pitfalls:
|
|
- title: Generating Duplicate Combinations
|
|
description: |
|
|
Without careful ordering, you might generate the same combination multiple times. For example, [1,2,4] and [2,1,4] are the same combination.
|
|
|
|
The fix is to always process numbers in increasing order by only considering numbers greater than or equal to `start`. This ensures each combination is generated exactly once in sorted order.
|
|
wrong_approach: "Consider all numbers 1-9 at each step"
|
|
correct_approach: "Only consider numbers from `start` to 9"
|
|
|
|
- title: Forgetting to Backtrack
|
|
description: |
|
|
After exploring a path that includes a number, you must remove that number before exploring paths that exclude it. Forgetting this step corrupts the `current_combination` list.
|
|
|
|
Always pair "add to combination" with "remove from combination" after the recursive call returns.
|
|
wrong_approach: "Add number but never remove it"
|
|
correct_approach: "Remove the number after recursive call (backtrack)"
|
|
|
|
- title: Not Copying the Combination When Adding to Results
|
|
description: |
|
|
In Python, lists are mutable. If you append `current_combination` directly to results, all entries will reference the same list, which gets modified during backtracking.
|
|
|
|
Always append a copy: `results.append(current_combination.copy())` or `results.append(list(current_combination))`.
|
|
wrong_approach: "results.append(current_combination)"
|
|
correct_approach: "results.append(current_combination.copy())"
|
|
|
|
- title: Missing the Pruning Optimisation
|
|
description: |
|
|
Without pruning, you explore branches that can never lead to a valid solution. For instance, if you need `remaining_sum = 3` and you're at number 5, there's no point continuing since 5 > 3 and all subsequent numbers are even larger.
|
|
|
|
Adding `if i > remaining_sum: break` significantly reduces unnecessary exploration.
|
|
|
|
key_takeaways:
|
|
- "**Backtracking template**: This problem follows the classic backtracking pattern — make a choice, recurse, undo the choice"
|
|
- "**Avoid duplicates by ordering**: Processing elements in sorted order and only considering elements >= start prevents generating the same combination twice"
|
|
- "**Prune aggressively**: Early termination when a branch cannot lead to a valid solution dramatically improves performance"
|
|
- "**Foundation for harder problems**: This pattern extends to Combination Sum I, II, IV, and other subset/permutation problems"
|
|
|
|
time_complexity: "O(C(9, k) * k). We explore at most C(9, k) combinations (9 choose k), and each valid combination takes O(k) time to copy. Since k <= 9 and C(9, k) <= 126, this is effectively constant for this problem."
|
|
space_complexity: "O(k). The recursion depth is at most k, and we use O(k) space for the current combination. The output space for storing results is not counted."
|
|
|
|
solutions:
|
|
- approach_name: Backtracking with Pruning
|
|
is_optimal: true
|
|
code: |
|
|
def combination_sum3(k: int, n: int) -> list[list[int]]:
|
|
results = []
|
|
|
|
def backtrack(start: int, remaining: int, combination: list[int]) -> None:
|
|
# Found a valid combination
|
|
if len(combination) == k and remaining == 0:
|
|
results.append(combination.copy())
|
|
return
|
|
|
|
# Too many numbers or exhausted search space
|
|
if len(combination) == k or start > 9:
|
|
return
|
|
|
|
# Try each number from start to 9
|
|
for num in range(start, 10):
|
|
# Pruning: if current number exceeds remaining sum, skip rest
|
|
if num > remaining:
|
|
break
|
|
|
|
# Include this number and recurse
|
|
combination.append(num)
|
|
backtrack(num + 1, remaining - num, combination)
|
|
# Backtrack: remove the number we just added
|
|
combination.pop()
|
|
|
|
backtrack(1, n, [])
|
|
return results
|
|
explanation: |
|
|
**Time Complexity:** O(C(9, k) * k) — We explore combinations of k numbers from 1-9, copying each valid one.
|
|
|
|
**Space Complexity:** O(k) — Recursion depth and combination list are bounded by k.
|
|
|
|
The backtracking explores the decision tree of including/excluding each number. Pruning when `num > remaining` cuts off entire subtrees, and processing numbers in order prevents duplicates.
|
|
|
|
- approach_name: Iterative with Bitmask
|
|
is_optimal: false
|
|
code: |
|
|
def combination_sum3(k: int, n: int) -> list[list[int]]:
|
|
results = []
|
|
|
|
# Iterate through all 2^9 = 512 subsets
|
|
for mask in range(1, 1 << 9):
|
|
combination = []
|
|
total = 0
|
|
|
|
# Check which bits are set (which numbers to include)
|
|
for i in range(9):
|
|
if mask & (1 << i):
|
|
combination.append(i + 1) # Numbers are 1-indexed
|
|
total += i + 1
|
|
|
|
# Check if this subset matches our criteria
|
|
if len(combination) == k and total == n:
|
|
results.append(combination)
|
|
|
|
return results
|
|
explanation: |
|
|
**Time Complexity:** O(2^9 * 9) = O(4608) — Check all 512 subsets, each taking O(9) to process.
|
|
|
|
**Space Complexity:** O(k) — Each combination uses O(k) space.
|
|
|
|
This approach treats each subset as a bitmask where bit i indicates whether number (i+1) is included. While less elegant than backtracking, it's simple and the small search space (512 subsets) makes it practical. No pruning is applied, so it explores all subsets regardless of validity.
|