LeetCode #77 Medium

Combinations

Combinations is LeetCode 77 (Medium). You are given two integers n and k, and must return every way to choose k distinct numbers from the range 1 to n.

  • Each number can appear at most once in a combination.
  • Order inside a combination does not matter, so the same set must not be returned twice in two different orders.
  • The combinations themselves can be returned in any order.

n is at most 20 and k at most n, so the output alone can run to six figures of lists. The work is dominated by producing them, which makes a search that never wastes a branch the goal.

Constraints
  • 1 <= n <= 20
  • 1 <= k <= n
backtrackingrecursioncombinatorics
Open on LeetCode ↗
02

Intuition

A combination is a set, so order does not matter. The simplest way to never produce the same set twice is to build every combination in increasing order: then each set has exactly one spelling, and its shuffled versions can never be built.

That turns the problem into a search tree with one number per level, where each level only picks numbers larger than the last. When the path is full it is a combination; the search then backtracks, removing the last number to try the next one. Pruning keeps it fast: a branch that cannot possibly collect k numbers before running out is never started.

How to spot this pattern

"Return all" subsets, orderings or selections is backtracking, and the combinations LeetCode problem is the plainest version: choose k of n, no reuse, order not mattering, so a start index only moves forward. Combination Sum (39) keeps start on the same number to allow reuse; Permutations (46) drops start and uses a used array instead.

03

Approach

Try it first

Before reading on, list all combinations for n = 4, k = 2 by hand, in order. Notice which first numbers you never needed to try, and why.

1

Keep one path and a start index

Keep path, the numbers chosen so far in increasing order, and start, the smallest number the next pick may use. Passing start down instead of always looping from 1 is what enforces increasing order, so no set is ever built twice.

2

Record when the path is full

If len(path) == k, append a copy of path to the result and return. In the combinations Python code that copy is path[:]; appending path itself stores a reference to the list that backtracking keeps emptying, so every saved answer ends up blank.

3

Otherwise try each allowed next number

  • need = k − len(path) numbers are still missing.
  • Loop num from start to n − need + 1.
  • Append num, recurse with start = num + 1, then pop num (backtrack).
4

Why the pruning bound is right

After picking num, the remaining need − 1 numbers must come from num + 1 … n, which has n − num numbers. That requires n − num ≥ need − 1, so num ≤ n − need + 1. Any larger pick starts a branch that can never finish.

04

Combinations solution in Python | C++ | Java

▶1class Solution:
▶2 def combine(self, n: int, k: int) -> List[List[int]]:
▶3 result, path = [], []
▶4 
▶5 def backtrack(start: int) -> None:
▶6 if len(path) == k:
▶7 result.append(path[:])
▶8 return
▶9 need = k - len(path)
▶10 for num in range(start, n - need + 2):
▶11 path.append(num)
▶12 backtrack(num + 1)
▶13 path.pop()
▶14 
▶15 backtrack(1)
▶16 return result
numbers1234may pickprunedpath0 / 2found0 / 6empty
n, k4, 2
expected6C(4, 2)
first pick1 to 3n − k + 1 = 3
Build each combination in increasing order, one number per level, so every set is produced exactly once: [1,2] is built, [2,1] never is. The first number can be at most 3, because 1 larger number must still fit after it.
numbers1234may pickpath1 / 21found0 / 6empty
path[1]1 more to pick
found0of 6
Pick 1. The next level may only use numbers above 1, which keeps every combination in increasing order, and only up to 4 so that the last slot is filled.
numbers1234may pickpath2 / 212found1 / 612
path[1, 2]k numbers: record a copy
found1of 6
Pick 2. The path now has 2 numbers, so [1, 2] is a combination. Record a copy, because the same path list keeps changing.
numbers1234may pickpath2 / 213found2 / 61213
path[1, 3]k numbers: record a copy
found2of 6
Backtrack: pop 2 and try the next number. Pick 3. The path now has 2 numbers, so [1, 3] is a combination. Record a copy, because the same path list keeps changing.
numbers1234may pickpath2 / 214found3 / 6121314
path[1, 4]k numbers: record a copy
found3of 6
Backtrack: pop 3 and try the next number. Pick 4. The path now has 2 numbers, so [1, 4] is a combination. Record a copy, because the same path list keeps changing.
numbers1234may pickpath1 / 22found3 / 6121314
path[2]1 more to pick
found3of 6
Backtrack: pop 1, 4 and try the next number. Pick 2. The next level may only use numbers above 2, which keeps every combination in increasing order, and only up to 4 so that the last slot is filled.
numbers1234may pickpath2 / 223found4 / 612131423
path[2, 3]k numbers: record a copy
found4of 6
Pick 3. The path now has 2 numbers, so [2, 3] is a combination. Record a copy, because the same path list keeps changing.
numbers1234may pickpath2 / 224found5 / 61213142324
path[2, 4]k numbers: record a copy
found5of 6
Backtrack: pop 3 and try the next number. Pick 4. The path now has 2 numbers, so [2, 4] is a combination. Record a copy, because the same path list keeps changing.
numbers1234may pickpath1 / 23found5 / 61213142324
path[3]1 more to pick
found5of 6
Backtrack: pop 2, 4 and try the next number. Pick 3. The next level may only use numbers above 3, which keeps every combination in increasing order, and only up to 4 so that the last slot is filled.
numbers1234may pickpath2 / 234found6 / 6121314232434
path[3, 4]k numbers: record a copy
found6of 6
Pick 4. The path now has 2 numbers, so [3, 4] is a combination. Record a copy, because the same path list keeps changing.
numbers1234path0 / 2found6 / 6121314232434
result6 combinations= C(4, 2)
All 6 combinations found, in lexicographic order. Pruning the loop at n − need + 1 meant no branch was ever started that could not reach 2 numbers, so every leaf of the search is an answer.
05

Pick / skip recursion

pick(i) decides number i: first it adds i and recurses on i + 1, then it removes i and recurses on i + 1 without it. A path is recorded as soon as it holds k numbers.

▶1class Solution:
▶2 def combine(self, n: int, k: int) -> List[List[int]]:
▶3 result, path = [], []
▶4 
▶5 def pick(i: int) -> None:
▶6 if len(path) == k:
▶7 result.append(path[:])
▶8 return
▶9 if i > n:
▶10 return
▶11 path.append(i)
▶12 pick(i + 1)
▶13 path.pop()
▶14 pick(i + 1)
▶15 
▶16 pick(1)
▶17 return result
06

Common pitfalls

Appending the path itself instead of a copy

✗ Wrong
result.append(path)
✓ Right
result.append(path[:])

Every entry would be the same list object. After the search it has been popped back to empty, so the result is a list of empty lists: [[], [], …].

Restarting the loop from 1

✗ Wrong
for num in range(1, n + 1):
    if num in path:
        continue
✓ Right
for num in range(start, n - need + 2):

That builds permutations, not combinations: [1, 2] and [2, 1] both appear. Moving start forward keeps every path increasing, so each set is built once.

Recursing with start instead of num + 1

✗ Wrong
backtrack(start + 1)
✓ Right
backtrack(num + 1)

The next pick must be larger than the number just chosen, not larger than where this level began. With start + 1, choosing 3 then allows 2 next, producing [3, 2] and duplicates.

07

Complexity

Time
O(k · C(n, k))
Space
O(k)
This combinations solution produces C(n, k) lists and each costs O(k) to copy. With pruning, the internal nodes of the search are bounded by the leaves times k. Extra space is the recursion depth and path, O(k), not counting the output.
08

Combinations FAQ

How do you get combinations in Python without writing the search?

Use itertools.combinations: list(combinations(range(1, n + 1), k)) from itertools returns tuples in lexicographic order. On LeetCode 77 convert each tuple with list(). Interviews usually still ask for the backtracking version, since itertools combinations hides the algorithm.

How many combinations are there?

C(n, k) = n! / (k! (n − k)!). For n = 4, k = 2 that is 6; for n = 5, k = 3 it is 10.

What is the difference between combinations and permutations?

Combinations ignore order: {1, 2} and {2, 1} are one combination. Permutations count each ordering separately, so there are k! times as many. In code, combinations move a start index forward; permutations try every unused number at every level.