Combinations. — Cracked Java
// Data Structures & Algorithms · Recursion & Backtracking
MidCoding

Combinations.

Problem. Given two integers n and k, return all combinations of k numbers chosen from 1..n. For n = 4, k = 2: [1,2], [1,3], [1,4], [2,3], [2,4], [3,4]. Order within a combination doesn't matter, so [2,1] is the same as [1,2].

Brute force

Generating all 2ⁿ subsets and keeping those of size k works but wastes effort on every wrong-sized subset. We can instead enumerate only size-k combinations directly — there are C(n,k) of them.

Optimal — backtracking with a start index

Because order is irrelevant, we impose a canonical one: each combination is built in increasing value. The start index enforces this — after choosing value i, the next choice may only come from i+1..n, which guarantees we never produce [2,1] after [1,2] and never reuse an element.

List<List<Integer>> combine(int n, int k) {
    List<List<Integer>> result = new ArrayList<>();
    backtrack(1, n, k, new ArrayList<>(), result);
    return result;
}

void backtrack(int start, int n, int k, List<Integer> path, List<List<Integer>> result) {
    if (path.size() == k) {                      // complete combination
        result.add(new ArrayList<>(path));
        return;
    }
    // Pruning: need (k - path.size()) more; stop when not enough numbers remain.
    int need = k - path.size();
    for (int i = start; i <= n - need + 1; i++) {
        path.add(i);                             // choose
        backtrack(i + 1, n, k, path, result);    // explore — note i + 1, not start
        path.remove(path.size() - 1);            // unchoose
    }
}

The loop upper bound n - need + 1 is the key prune: if fewer than need numbers remain, no branch from here can reach size k, so we cut it. Passing i + 1 (not start + 1) is what advances past the just-chosen element.

There are C(n,k) leaves and each costs O(k) to copy, so the cost is O(k · C(n,k)) time, O(k) auxiliary space plus recursion depth k.

Mark your status