questions C
This commit is contained in:
191
backend/data/questions/combination-sum-iii.yaml
Normal file
191
backend/data/questions/combination-sum-iii.yaml
Normal file
@@ -0,0 +1,191 @@
|
||||
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.
|
||||
Reference in New Issue
Block a user