Facebook Pixel

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 to 9
  • 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 target
  • t: the current combination being built
  • ans: stores all valid combinations found

The algorithm makes decisions at each step:

  1. Base case: If s = 0 and we've used exactly k numbers, we've found a valid combination
  2. 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
  3. 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)

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 of k 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Choose: Decide whether to include the current number
  2. Explore: Recursively solve the smaller problem with updated parameters
  3. 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 found
  • t: temporary list that tracks the current combination being built
  • dfs(1, n): starts the search from number 1 with target sum n

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 needed
  • len(t) >= k: We've already selected k 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:

  1. Include it: Add i to the current combination t, then recursively search with the next number i + 1 and reduced sum s - i
  2. Exclude it: After backtracking (removing i with t.pop()), try the path without including i by calling dfs(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 Evaluator

Example 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)

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)

Step 3: dfs(3, 2) with t = [1, 2]

  • Check conditions: s ≠ 0, but len(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)

Step 5: dfs(4, 1) with t = [1, 3]

  • Check conditions: s ≠ 0, but i = 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)

Step 7: dfs(5, 0) with t = [1, 4]

  • Check: s = 0 and len(t) = 2 = k
  • Found valid combination! Add [1, 4] to ans
  • 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 most k elements at any time, contributing O(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
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which type of traversal does breadth first search do?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More