Problem
Given two integers n and k, return all possible ways to choose k distinct numbers from the range 1 to n (inclusive). The order of numbers within each combination does not matter โ [1, 2] and [2, 1] are the same selection.
- Input: n = 4, k = 2
- Output: [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]
- Explanation: Every unique pair that can be drawn from {1, 2, 3, 4}, each listed in ascending order.
Counter-example: [2, 1] is NOT a separate answer โ the output contains each unique selection exactly once.
Intuition
At each position you pick a number and recurse to fill the rest, always choosing from numbers strictly larger than the last pick so the same set is never generated twice. The key optimization is that if you've placed m numbers and still need k โ m more, starting at any number greater than n โ (k โ m) is pointless โ there wouldn't be enough numbers left to complete the combination. This upper-bound prune cuts many dead branches before they're explored.
Solution โ Backtracking with Pruning
Build combinations one number at a time, always picking from numbers larger than the last chosen. Prune branches early when too few numbers remain to reach size k.
- Start with an empty
currentlist and smallest available numberstart = 1. - When
currentreaches lengthk, record a copy and return. - Compute
remaining = k โ len(current)โ how many more numbers are needed. - Loop
numfromstartton โ remaining + 1(inclusive) โ the pruned upper bound. - Append
num, recurse withstart = num + 1, then popnumto undo the choice.
1def combine(n: int, k: int) -> list[list[int]]:
2 results = []
3 current = []
4
5 def backtrack(start: int) -> None:
6 if len(current) == k:
7 results.append(current[:]) # snapshot before backtracking modifies current
8 return
9 remaining = k - len(current)
10 # no point starting at num if fewer than remaining numbers exist after it
11 for num in range(start, n - remaining + 2):
12 current.append(num)
13 backtrack(num + 1)
14 current.pop() # undo choice so next iteration starts with a clean slate
15
16 backtrack(1)
17 return resultsTime: O(k ร C(n, k)) โ there are C(n, k) combinations and copying each takes O(k).
Space: O(k) for the recursion stack depth (output storage is not counted).
Complexity Summary
| Approach | Time | Space | When to use |
|---|---|---|---|
| Backtracking with Pruning | O(k ร C(n, k)) | O(k) | The standard approach whenever you need to enumerate all k-element subsets |
Common Mistakes
range(start, n)instead ofrange(start, n + 1)โ an off-by-one that silently dropsnfrom every possible combination.- Appending
currentinstead ofcurrent[:]โ all entries inresultspoint to the same list object, which ends up empty once backtracking finishes. - Skipping the pruning upper bound โ iterating all the way to
nwastes time on recursive calls that can never produce a full combination of sizek. - Calling
backtrack(num)instead ofbacktrack(num + 1)โ allowing the same number to be picked again produces multisets (e.g.,[1, 1]) instead of combinations. - Forgetting
current.pop()after the recursive call โ without undoing the choice, the list keeps growing across all branches and the results are corrupted.
Related Problems
permutationsโ same backtracking skeleton but order matters, so every remaining element is eligible at each step rather than only larger onessubsetsโ generate combinations of every size from 0 to n, not just a fixed kcombination-sum-iiโ combinations from a list with duplicate values that must reach a target sum, adding duplicate-skipping logicsubsets-iiโ subsets from a list with repeated elements, same deduplication technique applied during backtrackingletter-combinations-of-a-phone-numberโ backtracking over a character alphabet rather than an integer range