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:
  - slug: backtracking
    is_optimal: true

function_signature: "def combination_sum3(k: int, n: int) -> list[list[int]]:"

test_cases:
  visible:
    - input: { k: 3, n: 7 }
      expected: [[1, 2, 4]]
    - input: { k: 3, n: 9 }
      expected: [[1, 2, 6], [1, 3, 5], [2, 3, 4]]
    - input: { k: 4, n: 1 }
      expected: []
  hidden:
    - input: { k: 2, n: 3 }
      expected: [[1, 2]]
    - input: { k: 2, n: 17 }
      expected: [[8, 9]]
    - input: { k: 2, n: 18 }
      expected: []
    - input: { k: 9, n: 45 }
      expected: [[1, 2, 3, 4, 5, 6, 7, 8, 9]]
    - input: { k: 3, n: 15 }
      expected: [[1, 5, 9], [1, 6, 8], [2, 4, 9], [2, 5, 8], [2, 6, 7], [3, 4, 8], [3, 5, 7], [4, 5, 6]]
    - input: { k: 5, n: 15 }
      expected: [[1, 2, 3, 4, 5]]

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

    &nbsp;

    **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

    &nbsp;

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

    &nbsp;

    **Step 4: Start the recursion**

    - Call `backtrack(1, n, [])` and return the collected results

    &nbsp;

    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.
