Algorithm

Combinations

Backtracking Pattern

Combinations

Given two integers n and k, return every possible combination of k distinct numbers drawn from the range 1 to n inclusive.

A combination is a set, so order carries no meaning: [1,2] and [2,1] are the same combination and exactly one of them must appear in the output. Each number may be used at most once within a combination.

The combinations themselves may be returned in any order. The output contains C(n, k) entries.

CONSTRAINTS
  • 1 <= n <= 20
  • 1 <= k <= n
  • Numbers are drawn from the range [1, n], never 0
  • Each number may appear at most once in a given combination
  • Output size is C(n, k), which peaks near k = n/2
EXAMPLE 1
Input: n = 4, k = 2
Output: [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]
Six combinations, matching C(4,2) = 6. Every entry is listed in increasing order, and no pair appears twice — [3,1] is absent because it describes the same set as [1,3].
EXAMPLE 2
Input: n = 4, k = 4
Output: [[1,2,3,4]]
When k equals n there is exactly one way to choose: take everything. The recursion has no real decisions to make, since skipping any number would leave too few remaining.
EXAMPLE 3
Input: n = 1, k = 1
Output: [[1]]
One number, choose one. The smallest possible input, and a useful check that the base case fires correctly rather than returning an empty list.
EXAMPLE 4
Input: n = 5, k = 1
Output: [[1],[2],[3],[4],[5]]
Choosing a single number gives one combination per candidate. Notice each is a one-element list rather than a bare number — the output shape stays consistent regardless of k.
Does [1,2] count as different from [2,1]?
No, they are the same combination and only one may appear. That is the entire difference between this problem and Permutations, and it is what makes the increasing-order trick both possible and necessary.
Can a number be used more than once within one combination?
No — the k numbers are distinct. Reuse is permitted in Combination Sum, and the code difference between the two is a single character, so it is worth being clear about which you are solving.
Do the numbers range from 0 to n-1 or 1 to n?
From 1 to n. The loop bound is therefore n+1 in a language with exclusive ranges, and an off-by-one here silently drops every combination containing n.
How large can the output be?
C(20,10) is 184,756, so the output is large but manageable. Since the answer itself is that size, no approach can be faster than producing it — the goal is to avoid generating anything that is not in the answer.

The natural first thought is that this is Subsets with a size filter — generate the power set, keep the entries of length k. That is correct, and looking at why it is wasteful leads directly to the real technique.

The cost of filtering

With n = 20 the power set has 2²⁰ ≈ 1,048,576 subsets. If k = 2, only C(20,2) = 190 of them survive. More than a million candidates are built to keep fewer than two hundred.

python
def combine(n, k):
    return [s for s in all_subsets(range(1, n + 1)) if len(s) == k]

The waste is not just the ratio. It is when the failure is detected. A subset of size 7 is only rejected after all seven decisions are made — even though it was doomed the moment it exceeded k. The filter runs at the leaves, and the search happily builds entire branches that could never contribute.

That observation generalises far beyond this problem: checking validity at the leaf is always worse than not generating the invalid branch at all. Everything in this section is an application of it.

Two things must be prevented

There are two separate ways a naive search goes wrong here, and they need different fixes.

First, wrong length. Paths shorter or longer than k are useless.

Second, and more interesting, duplicates. If at every level you may choose any of the n numbers, the search produces [1,2] and also [2,1]. Those are the same combination, so one must be suppressed. Filtering them afterwards means sorting each result and deduplicating — expensive, and it means generating k! copies of every answer just to throw away all but one.

The elegant fix handles the second problem structurally.

Choosing a canonical form

Every combination has exactly one representation in increasing order. The set {3,1,2} can be written six ways, but only [1,2,3] is sorted.

So decide in advance to build only sorted paths. Then each combination can be produced by exactly one route through the decision tree, and duplicates become impossible rather than merely detectable.

Enforcing it requires almost nothing: when the last number chosen was i, the next number must be greater than i. Pass a start index down the recursion and never look backwards.

That is the whole trick, and it is worth stating as a principle because it recurs constantly: to avoid generating duplicates, define a canonical ordering for each answer and only ever build answers in that form.

The mechanism

The skeleton is the choose-explore-un-choose rhythm from Subsets, with the choice space narrowed by the start index.

python
def combine(n, k):
    result = []
    path = []

    def build(start):
        if len(path) == k:
            result.append(list(path))     # complete: snapshot a copy
            return

        for num in range(start, n + 1):
            path.append(num)              # CHOOSE
            build(num + 1)                # EXPLORE — only larger numbers
            path.pop()                    # UN-CHOOSE

    build(1)
    return result
Crucial Notethe recursive call passes num + 1, not start + 1 and not num. Passing start + 1 would ignore which number was actually chosen and allow non-increasing paths. Passing num would permit choosing the same number again, which is Combination Sum's rule, not this one. This single expression is what distinguishes three different problems.
Crucial Noteresult.append(list(path)) copies the path. Appending path itself stores a reference to the one shared list, which continues mutating — the output would end up as many references to a single list, all showing the same final contents.
Pruning: stop when it is already hopeless

The version above still explores branches that cannot possibly succeed. With n = 5 and k = 4, starting a path at 4 leaves only the numbers 4 and 5 — two candidates for a combination needing four. The recursion dutifully explores and fails.

The check is arithmetic. If the path holds len(path) numbers, it still needs k - len(path) more, and the numbers from num to n provide n - num + 1 candidates. Once the supply falls short, every remaining iteration is doomed:

python
def build(start):
    if len(path) == k:
        result.append(list(path))
        return

    needed = k - len(path)
    # stop as soon as fewer than 'needed' numbers remain
    for num in range(start, n - needed + 2):
        path.append(num)
        build(num + 1)
        path.pop()

The upper bound n - needed + 2 is the largest starting number that still leaves needed values available. This does not change the complexity — the output size dominates — but it eliminates a great deal of pointless recursion on larger inputs, and recognising the opportunity is what interviewers are watching for.

Worked example:n = 4, k = 2
- build(1), path []. Loop runs over 1, 2, 3.
- Choose 1, path [1]. build(2): path has 1 element, not yet 2, so loop over 2, 3, 4.
- Choose 2, path [1,2]. Length is 2, record [1,2]. Pop back to [1].
- Choose 3, record [1,3]. Pop.
- Choose 4, record [1,4]. Pop.
- Pop back to [].
- Choose 2, path [2]. build(3) — note the loop now starts at 3, so 1 is unreachable and [2,1] can never be built.
- Choose 3, record [2,3]. Choose 4, record [2,4].
- Pop.
- Choose 3, path [3]. build(4): choose 4, record [3,4].
- Pop.
- Choose 4 is skipped by the pruning bound: one number is still needed and starting at 4 leaves only 4 itself, with nothing larger to follow. Without pruning, this branch would be entered and would fail.

Six combinations, matching C(4,2). Every one is increasing, and no set was produced twice — not because duplicates were filtered, but because the search could not construct them.

The pattern, extracted

Three problems in this section share one skeleton and differ only in the choice space at each level. Holding the comparison in mind is worth more than memorising any one of them:

- Subsets — decide take-or-skip for each position; every path length is valid.
- Combinations — choose from numbers after the last one taken; only paths of length k are recorded.
- Permutations — choose from every number not yet used; order matters, so no start index exists.

The start index is precisely the mechanism that says "order does not matter here". Its presence or absence is the first thing to decide when you meet an unfamiliar enumeration problem.

Recognition signals: choose k of n, how many ways to select, all groups of size k, all committees, all hands of cards — anything where selecting a group is asked for and rearranging that group does not produce a new answer.

Train the reflex: when order does not matter, pick a canonical ordering and generate only that. Deduplicating afterwards means doing k! times too much work and then throwing most of it away.

Interactive Strategy Visualization

Combinations (N=4, K=2)

Collected: 0 / 6
1
2
3
4
Current Combination
[ ]
Starting search for size 2 combinations from [1, 2, 3, 4].
MINDSET

To avoid duplicates, we only pick numbers strictly larger than our previous pick. This creates a natural ordering and shrinks the search space.

PERFORMANCE

The total number of calls is reduced compared to permutations. We only explore nCk paths where N is the pool size and K is the subset size.

O(N × 2ⁿ) Filter The Power Set
O(K × C(n,k)) Start-Index Backtracking
O(K × C(n,k)) With Insufficient-Remainder Pruning