216. Combination Sum III
Problem Description
This problem asks you to find all valid combinations of exactly k
numbers that add up to a target sum n
, with specific constraints:
Constraints:
- You can only use numbers from
1
to9
- Each number can be used at most once (no repetition within a combination)
- The final result should contain all possible valid combinations
- No duplicate combinations should appear in the result
- The order of combinations in the output doesn't matter
For example, if k = 3
and n = 7
, you need to find all combinations of exactly 3 different numbers from 1-9 that sum to 7. Valid combinations would include [1, 2, 4]
since 1 + 2 + 4 = 7
and uses exactly 3 different numbers.
The solution uses a depth-first search (DFS) with backtracking approach. The dfs(i, s)
function explores combinations by:
i
: the current number being considered (starting from 1)s
: the remaining sum needed to reach the targett
: the current combination being builtans
: stores all valid combinations found
The algorithm makes decisions at each step:
- Base case: If
s = 0
and we've used exactlyk
numbers, we've found a valid combination - Pruning conditions: Stop exploring if:
- Current number
i > 9
(exceeded available numbers) - Current number
i > s
(number too large for remaining sum) - Already have
k
or more numbers in current combination
- Current number
- Recursive exploration: For each valid number, either:
- Include it in the combination and continue with
dfs(i + 1, s - i)
- Skip it and continue with
dfs(i + 1, s)
- Include it in the combination and continue with
The backtracking ensures all possible combinations are explored while avoiding duplicates by always moving forward (considering numbers in increasing order).
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 finding combinations of numbers that sum to a target, not traversing nodes and edges in a graph structure.
Need to solve for kth smallest/largest?
- No: We're not looking for the kth smallest or largest element. We need to find all valid combinations that meet specific criteria.
Involves Linked Lists?
- No: The problem deals with combinations of numbers from 1-9, not linked list operations.
Does the problem have small constraints?
- Yes: The constraints are quite small - we can only use numbers 1 through 9, and we need exactly
k
numbers. With at most 9 numbers to choose from and typical values ofk
being small (usually 2-9), the search space is limited.
Brute force / Backtracking?
- Yes: Given the small constraints and the need to explore all possible combinations systematically, backtracking is the appropriate approach. We need to:
- Try including each number in our combination
- Check if it leads to a valid solution
- Backtrack if it doesn't work or after recording a valid combination
- Ensure we explore all possibilities without repetition
Conclusion: The flowchart correctly leads us to use a Backtracking approach. This makes sense because we need to systematically explore all possible combinations of k
numbers from 1-9 that sum to n
, which is a classic backtracking scenario where we build solutions incrementally and abandon paths that cannot lead to valid solutions.
Intuition
When we need to find all combinations that satisfy certain conditions, we're essentially exploring a decision tree. At each step, we face a choice: include the current number or skip it. This naturally leads to a backtracking approach.
Think of it like building a combination step by step. Starting with number 1, we ask: "Should I include 1 in my combination?" If yes, we now need k-1
more numbers that sum to n-1
. If no, we still need k
numbers that sum to n
, but we move on to consider number 2.
The key insight is that we can prune many branches early. If we're considering number i
and i > s
(where s
is the remaining sum needed), there's no point including i
because it would make our sum too large. Similarly, if we've already selected k
numbers, we can stop exploring that path.
The backtracking pattern emerges from this decision-making process:
- Choose: Decide whether to include the current number
- Explore: Recursively solve the smaller problem with updated parameters
- Unchoose: Remove the number and try the alternative path
By systematically trying all valid paths and backtracking when we hit dead ends, we guarantee finding all valid combinations. The beauty of this approach is that we avoid generating invalid combinations in the first place through early pruning.
The recursive function dfs(i, s)
elegantly captures this process - i
tracks which number we're considering, and s
tracks how much sum we still need. When s
becomes 0 and we've used exactly k
numbers, we've found a valid combination. The backtracking ensures we explore all possibilities without missing any or creating duplicates.
Solution Approach
The solution implements a depth-first search with backtracking using the approach described in the reference. Let's walk through the implementation details:
Core Function Design:
The main function combinationSum3
sets up the necessary variables and calls the helper function dfs(i, s)
:
ans
: stores all valid combinations foundt
: temporary list that tracks the current combination being builtdfs(1, n)
: starts the search from number 1 with target sumn
The DFS Helper Function:
The dfs(i, s)
function represents:
i
: the current number being considered (from 1 to 9)s
: the remaining sum needed to reach the target
Base Case and Success Condition:
if s == 0:
if len(t) == k:
ans.append(t[:])
return
When the remaining sum s
reaches 0, we check if we've used exactly k
numbers. If yes, we've found a valid combination and add a copy of t
to our answer list.
Pruning Conditions:
if i > 9 or i > s or len(t) >= k:
return
We stop exploring the current path if:
i > 9
: We've exhausted all available numbers (1-9)i > s
: The current number is larger than the remaining sum neededlen(t) >= k
: We've already selectedk
or more numbers (no point continuing)
Backtracking Logic:
t.append(i) dfs(i + 1, s - i) t.pop() dfs(i + 1, s)
For each valid number i
, we have two choices:
- Include it: Add
i
to the current combinationt
, then recursively search with the next numberi + 1
and reduced sums - i
- Exclude it: After backtracking (removing
i
witht.pop()
), try the path without includingi
by callingdfs(i + 1, s)
Why This Works:
The algorithm systematically explores all possible combinations by:
- Always considering numbers in increasing order (prevents duplicates)
- Using backtracking to explore both inclusion and exclusion of each number
- Pruning invalid paths early to improve efficiency
- Building combinations incrementally and only recording those that meet all criteria
The time complexity is bounded by O(2^9)
since we make binary decisions for at most 9 numbers, though pruning significantly reduces the actual runtime.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with k = 2
and n = 5
. We need to find all combinations of exactly 2 numbers from 1-9 that sum to 5.
Initial State:
ans = []
(stores valid combinations)t = []
(current combination being built)- Start with
dfs(1, 5)
(consider number 1, need sum of 5)
Step 1: dfs(1, 5)
- Check conditions:
s ≠ 0
,i = 1 ≤ 9
,i = 1 ≤ s = 5
,len(t) = 0 < k = 2
✓ - Choice 1: Include 1
- Add 1 to
t
:t = [1]
- Call
dfs(2, 4)
(next number is 2, remaining sum is 5-1=4)
- Add 1 to
Step 2: dfs(2, 4) with t = [1]
- Check conditions:
s ≠ 0
,i = 2 ≤ 9
,i = 2 ≤ s = 4
,len(t) = 1 < k = 2
✓ - Choice 1: Include 2
- Add 2 to
t
:t = [1, 2]
- Call
dfs(3, 2)
(next number is 3, remaining sum is 4-2=2)
- Add 2 to
Step 3: dfs(3, 2) with t = [1, 2]
- Check conditions:
s ≠ 0
, butlen(t) = 2 ≥ k = 2
✗ - Return (we already have k numbers)
Back to Step 2: Try excluding 2
- Remove 2:
t = [1]
- Call
dfs(3, 4)
(skip 2, try 3 with sum still 4)
Step 4: dfs(3, 4) with t = [1]
- Check conditions:
s ≠ 0
,i = 3 ≤ 9
,i = 3 ≤ s = 4
,len(t) = 1 < k = 2
✓ - Choice 1: Include 3
- Add 3 to
t
:t = [1, 3]
- Call
dfs(4, 1)
(next number is 4, remaining sum is 4-3=1)
- Add 3 to
Step 5: dfs(4, 1) with t = [1, 3]
- Check conditions:
s ≠ 0
, buti = 4 > s = 1
✗ - Return (4 is too large for remaining sum)
Back to Step 4: Try excluding 3
- Remove 3:
t = [1]
- Call
dfs(4, 4)
(skip 3, try 4 with sum still 4)
Step 6: dfs(4, 4) with t = [1]
- Check conditions:
s ≠ 0
,i = 4 ≤ 9
,i = 4 ≤ s = 4
,len(t) = 1 < k = 2
✓ - Choice 1: Include 4
- Add 4 to
t
:t = [1, 4]
- Call
dfs(5, 0)
(next number is 5, remaining sum is 4-4=0)
- Add 4 to
Step 7: dfs(5, 0) with t = [1, 4]
- Check:
s = 0
andlen(t) = 2 = k
✓ - Found valid combination! Add
[1, 4]
toans
- Return
The algorithm continues this pattern, eventually backtracking to try starting with 2 instead of 1:
- When starting with
dfs(2, 5)
after excluding 1, it finds[2, 3]
Final Result: ans = [[1, 4], [2, 3]]
The backtracking ensures we explore all paths systematically, while pruning conditions prevent us from wasting time on invalid branches.
Solution Implementation
1class Solution:
2 def combinationSum3(self, k: int, n: int) -> List[List[int]]:
3 """
4 Find all valid combinations of k numbers that sum up to n.
5 Only numbers 1 through 9 are used, and each combination should be unique.
6
7 Args:
8 k: Number of elements in each combination
9 n: Target sum
10
11 Returns:
12 List of all valid combinations
13 """
14
15 def backtrack(start_num: int, remaining_sum: int):
16 """
17 Recursive helper function to generate combinations.
18
19 Args:
20 start_num: Current number to consider (1-9)
21 remaining_sum: Remaining sum needed to reach target
22 """
23 # Base case: found a valid combination
24 if remaining_sum == 0:
25 if len(current_combination) == k:
26 result.append(current_combination[:]) # Add a copy of current combination
27 return
28
29 # Pruning conditions: stop exploring if:
30 # 1. Current number exceeds 9 (only use digits 1-9)
31 # 2. Current number exceeds remaining sum (cannot form valid sum)
32 # 3. Already have k elements (cannot add more)
33 if start_num > 9 or start_num > remaining_sum or len(current_combination) >= k:
34 return
35
36 # Choice 1: Include current number in the combination
37 current_combination.append(start_num)
38 backtrack(start_num + 1, remaining_sum - start_num)
39
40 # Backtrack: Remove current number
41 current_combination.pop()
42
43 # Choice 2: Skip current number and move to next
44 backtrack(start_num + 1, remaining_sum)
45
46 # Initialize result list and temporary combination tracker
47 result = []
48 current_combination = []
49
50 # Start DFS from number 1 with target sum n
51 backtrack(1, n)
52
53 return result
54
1class Solution {
2 // List to store all valid combinations
3 private List<List<Integer>> result = new ArrayList<>();
4 // List to store current combination being built
5 private List<Integer> currentCombination = new ArrayList<>();
6 // Target size of each combination
7 private int targetSize;
8
9 /**
10 * Find all valid combinations of k numbers that add up to n.
11 * Numbers can only be used from 1 to 9, and each number can be used at most once.
12 *
13 * @param k the number of elements in each combination
14 * @param n the target sum for each combination
15 * @return list of all valid combinations
16 */
17 public List<List<Integer>> combinationSum3(int k, int n) {
18 this.targetSize = k;
19 // Start DFS from number 1 with target sum n
20 dfs(1, n);
21 return result;
22 }
23
24 /**
25 * Depth-first search to find all valid combinations.
26 *
27 * @param currentNumber the current number being considered (1-9)
28 * @param remainingSum the remaining sum needed to reach target
29 */
30 private void dfs(int currentNumber, int remainingSum) {
31 // Base case: if remaining sum is 0, check if we have exactly k numbers
32 if (remainingSum == 0) {
33 if (currentCombination.size() == targetSize) {
34 // Found a valid combination, add a copy to result
35 result.add(new ArrayList<>(currentCombination));
36 }
37 return;
38 }
39
40 // Pruning conditions:
41 // 1. Current number exceeds 9 (only digits 1-9 are allowed)
42 // 2. Current number exceeds remaining sum (impossible to reach target)
43 // 3. Current combination already has k numbers (cannot add more)
44 if (currentNumber > 9 || currentNumber > remainingSum || currentCombination.size() >= targetSize) {
45 return;
46 }
47
48 // Choice 1: Include current number in the combination
49 currentCombination.add(currentNumber);
50 dfs(currentNumber + 1, remainingSum - currentNumber);
51 currentCombination.remove(currentCombination.size() - 1); // Backtrack
52
53 // Choice 2: Skip current number and move to next
54 dfs(currentNumber + 1, remainingSum);
55 }
56}
57
1class Solution {
2public:
3 vector<vector<int>> combinationSum3(int k, int n) {
4 vector<vector<int>> result; // Store all valid combinations
5 vector<int> currentCombination; // Current combination being built
6
7 // Define recursive DFS function using lambda
8 function<void(int, int)> backtrack = [&](int startNum, int remainingSum) {
9 // Base case: found a valid combination
10 if (remainingSum == 0) {
11 if (currentCombination.size() == k) {
12 result.emplace_back(currentCombination);
13 }
14 return;
15 }
16
17 // Pruning conditions:
18 // 1. startNum > 9: exceeded valid digit range (1-9)
19 // 2. startNum > remainingSum: current number is too large
20 // 3. currentCombination.size() >= k: already have k numbers
21 if (startNum > 9 || startNum > remainingSum || currentCombination.size() >= k) {
22 return;
23 }
24
25 // Choice 1: Include current number in the combination
26 currentCombination.emplace_back(startNum);
27 backtrack(startNum + 1, remainingSum - startNum);
28 currentCombination.pop_back(); // Backtrack
29
30 // Choice 2: Skip current number and try next
31 backtrack(startNum + 1, remainingSum);
32 };
33
34 // Start DFS from digit 1 with target sum n
35 backtrack(1, n);
36
37 return result;
38 }
39};
40
1/**
2 * Find all valid combinations of k numbers that sum up to n
3 * Each number must be from 1 to 9 and used at most once
4 * @param k - The number of elements in each combination
5 * @param n - The target sum
6 * @returns All unique combinations that sum to n
7 */
8function combinationSum3(k: number, n: number): number[][] {
9 const result: number[][] = [];
10 const currentCombination: number[] = [];
11
12 /**
13 * Depth-first search to explore all possible combinations
14 * @param currentNumber - The current number being considered (1-9)
15 * @param remainingSum - The remaining sum needed to reach target
16 */
17 const exploreCombinations = (currentNumber: number, remainingSum: number): void => {
18 // Base case: found a valid combination
19 if (remainingSum === 0) {
20 if (currentCombination.length === k) {
21 // Add a copy of the current combination to results
22 result.push([...currentCombination]);
23 }
24 return;
25 }
26
27 // Pruning conditions:
28 // 1. currentNumber exceeds 9 (max allowed digit)
29 // 2. currentNumber exceeds remaining sum (impossible to reach target)
30 // 3. combination already has k elements (cannot add more)
31 if (currentNumber > 9 || currentNumber > remainingSum || currentCombination.length >= k) {
32 return;
33 }
34
35 // Option 1: Include current number in the combination
36 currentCombination.push(currentNumber);
37 exploreCombinations(currentNumber + 1, remainingSum - currentNumber);
38 currentCombination.pop();
39
40 // Option 2: Skip current number and move to next
41 exploreCombinations(currentNumber + 1, remainingSum);
42 };
43
44 // Start DFS from number 1 with target sum n
45 exploreCombinations(1, n);
46
47 return result;
48}
49
Time and Space Complexity
Time Complexity: O(2^9 * k)
or simplified to O(2^9)
since k ≤ 9
The algorithm explores a binary decision tree where at each number from 1 to 9, we make two choices: include it or exclude it. This creates a search space of 2^9
possible combinations. At each leaf node where a valid combination is found (when s == 0
and len(t) == k
), we create a copy of the current combination which takes O(k)
time. Since we can have at most C(9, k)
valid combinations and k ≤ 9, the copying operation adds a factor of k. However, since we're bounded by exploring all 2^9
possibilities regardless, the overall time complexity is O(2^9 * k)
.
Space Complexity: O(k)
The space complexity consists of:
- Recursion stack depth: The maximum depth of recursion is 9 (since we only consider digits 1-9), giving us
O(9)
=O(1)
for the call stack - The temporary list
t
: This stores at mostk
elements at any time, contributingO(k)
space - The answer list
ans
: While this stores all valid combinations, it's typically not counted in auxiliary space complexity as it's part of the output
Therefore, the auxiliary space complexity is O(k)
dominated by the temporary list used during the backtracking process.
Common Pitfalls
1. Forgetting to Create a Copy When Adding to Results
The Pitfall: A frequent mistake is directly appending the reference to the current combination list instead of creating a copy:
# WRONG - This adds a reference to the mutable list
if len(current_combination) == k:
result.append(current_combination) # Bug: adds reference, not a copy
This causes all combinations in the result to point to the same list object. As backtracking continues to modify current_combination
, all previously added "combinations" in the result will reflect these changes, ultimately leaving you with an array of identical (and likely empty) lists.
The Solution: Always create a copy of the current combination before adding it to results:
# CORRECT - Creates a new list with current values
if len(current_combination) == k:
result.append(current_combination[:]) # Creates a shallow copy
# Alternative: result.append(list(current_combination))
2. Incorrect Pruning Logic Order
The Pitfall: Checking conditions in the wrong order or using incorrect operators can cause valid combinations to be missed:
# WRONG - Using > instead of >= for length check
if len(current_combination) > k: # Bug: allows k+1 elements before stopping
return
# WRONG - Checking length after recursion
backtrack(start_num + 1, remaining_sum - start_num)
if len(current_combination) > k: # Too late to prune!
return
The Solution: Use the correct comparison operator and check conditions before making recursive calls:
# CORRECT - Prune when we already have k elements
if len(current_combination) >= k:
return
3. Not Handling the "Skip" Case in Backtracking
The Pitfall: Some implementations forget to explore the path where the current number is excluded:
# INCOMPLETE - Only considers including numbers
def backtrack(start_num, remaining_sum):
if remaining_sum == 0 and len(current_combination) == k:
result.append(current_combination[:])
return
for i in range(start_num, 10):
current_combination.append(i)
backtrack(i + 1, remaining_sum - i)
current_combination.pop()
# Bug: No explicit "skip" path - relies on loop termination
While this might work, it's less clear and can miss optimizations.
The Solution: Explicitly handle both inclusion and exclusion paths:
# CORRECT - Clear inclusion/exclusion pattern current_combination.append(start_num) backtrack(start_num + 1, remaining_sum - start_num) # Include current_combination.pop() backtrack(start_num + 1, remaining_sum) # Exclude
4. Off-by-One Errors with Number Range
The Pitfall: Using incorrect bounds for the available numbers (1-9):
# WRONG - Misses number 9
if start_num >= 9: # Bug: stops at 8
return
# WRONG - Includes 0 or 10
for i in range(0, 10): # Bug: 0 is not valid
# or
for i in range(1, 11): # Bug: 10 is not valid
The Solution: Ensure the bounds correctly reflect numbers 1 through 9:
# CORRECT
if start_num > 9:
return
# Or in a loop:
for i in range(start_num, 10): # range(start, 10) gives numbers up to 9
Which type of traversal does breadth first search do?
Recommended Readings
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!