Generate Combinations
Given n integers and an integer k, return all combinations of k elements — order does not matter. The standard backtracking solution uses a path and the start-index trick to only pick forward, avoiding duplicates like [2,1] when [1,2] is already counted. This lesson covers the algorithm, LeetCode 77/78, and implementations in C, Java, and Python with trace tables.
Overview
A combination selects k items from n without regard to order. Unlike permutations, [1,2] and [2,1] are the same combination. Backtracking builds subsets of size k by always moving forward in the array.
n = 4, k = 2, nums = [1, 2, 3, 4]
[]
/ | \ \
1 2 3 4
/|\ |\ |
2 3 4 3 4 4
Combinations: [1,2] [1,3] [1,4] [2,3] [2,4] [3,4]
Count: C(4,2) = 6
Problem Statement
Input: Integer n (elements 1 … n) and integer k.
Output: All combinations of k numbers chosen from 1 … n.
Example: n = 4, k = 2 → [[1,2], [1,3], [1,4], [2,3], [2,4], [3,4]]
Count formula: C(n, k) = n! / (k! × (n−k)!)
Backtracking Algorithm
- Maintain current
path(partial combination) and astartindex. - Base case: if
len(path) == k, append a copy toresult. - For each index
ifromstartton−1:- Append
nums[i]topath. - Recurse with
start = i + 1(only pick later elements). - Backtrack: remove last element from
path.
- Append
used[] array needed — the start index enforces increasing order and prevents duplicate combinations.Start-Index Trick
Always recurse from i + 1, never from earlier indices. This guarantees each combination appears once in sorted order internally.
- Without it, you'd generate both
[1,2]and[2,1]. - Pruning: if remaining elements
n − i < k − len(path), stop early — not enough left to fillk. - Same template extends to subsets (all sizes) by recording at every depth, not only when
len == k.
Code (C, Java, Python)
Python
def combine(n, k):
"""All k-combinations of 1..n — start-index backtracking."""
nums = list(range(1, n + 1))
path = []
result = []
def backtrack(start):
if len(path) == k:
result.append(path[:])
return
for i in range(start, n):
path.append(nums[i])
backtrack(i + 1)
path.pop()
backtrack(0)
return result
if __name__ == "__main__":
print(combine(4, 2)) # [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]
Java
import java.util.ArrayList;
import java.util.List;
public class Combinations {
public static void backtrack(int n, int k, int start, List<Integer> path,
List<List<Integer>> result) {
if (path.size() == k) {
result.add(new ArrayList<>(path));
return;
}
for (int i = start; i <= n; i++) {
path.add(i);
backtrack(n, k, i + 1, path, result);
path.remove(path.size() - 1);
}
}
public static List<List<Integer>> combine(int n, int k) {
List<List<Integer>> result = new ArrayList<>();
backtrack(n, k, 1, new ArrayList<>(), result);
return result;
}
public static void main(String[] args) {
System.out.println(combine(4, 2).size()); // 6
}
}
C
#include <stdio.h>
#include <stdlib.h>
int path[32], path_len = 0, count = 0;
void backtrack(int n, int k, int start) {
int i;
if (path_len == k) { count++; return; }
for (i = start; i <= n; i++) {
path[path_len++] = i;
backtrack(n, k, i + 1);
path_len--;
}
}
int main(void) {
backtrack(4, 2, 1);
printf("%d\n", count); /* 6 */
return 0;
}
Output & Trace
All combinations for n = 4, k = 2
| # | Combination |
|---|---|
| 1 | [1, 2] |
| 2 | [1, 3] |
| 3 | [1, 4] |
| 4 | [2, 3] |
| 5 | [2, 4] |
| 6 | [3, 4] |
Partial trace — start-index backtracking
| Step | start | Choice | path | Action |
|---|---|---|---|---|
| 1 | 0 | 1 | [1] | Append, recurse start=1 |
| 2 | 1 | 2 | [1,2] | Record combination #1 |
| 3 | — | undo 2 | [1] | pop, try next at start=1 |
| 4 | 1 | 3 | [1,3] | Record combination #2 |
| 5 | 1 | 4 | [1,4] | Record combination #3 |
| 6 | 0 | undo 1 | [] | pop, next root choice 2 |
| 7 | 1 | 3 | [2,3] | Record combination #4 … |
Combination counts C(n, k)
| n | k | C(n,k) |
|---|---|---|
| 4 | 2 | 6 |
| 5 | 2 | 10 |
| 5 | 3 | 10 |
| 6 | 3 | 20 |
LeetCode 77 & 78
| Problem | Task | Template change |
|---|---|---|
| 77 — Combinations | Choose k from 1..n | Base case when len(path) == k |
| 78 — Subsets | All subsets (any size) | Record path at every node; no fixed k |
| 39 — Combination Sum | Target sum, reuse allowed | Recurse from i (not i+1); track remaining sum |
| 40 — Combination Sum II | Target sum, no reuse, duplicates | Sort; skip duplicate values at same level |
Optimizations
- Early pruning: Skip loop when
n − i + 1 < k − path.size()— not enough elements left. - Iterative generation: Build combinations via bitmask from 0 to 2^n−1 (works for small n).
- Subsets shortcut: Same start-index loop; append path copy on every recursive call.
Approach Comparison
| Approach | Time | Space | Notes |
|---|---|---|---|
| Start-index backtrack | O(k · C(n,k)) | O(k) recursion | Standard interview solution |
| used[] like permutations | Same but more work | O(n) | Needs extra dedup — avoid |
| Bitmask enumeration | O(n · 2^n) | O(k) | Only for small n |
Real-Life Applications
| Domain | Scenario | Mapping |
|---|---|---|
| Team selection | Pick k members from n candidates | Each team = one combination |
| Lottery / sampling | Choose k numbers from a pool | C(n,k) possibilities |
| Feature subsets | ML feature selection trials | Subsets of fixed size k |
| Test suites | Pairwise / k-wise test cases | Combinatorial test design |
| Finance | Portfolio of k assets from n | Combination enumeration |
Interview Tips
- Clarify: order doesn't matter → use start index, not permutations template.
- Write
backtrack(start)and always recurse withi + 1. - At base case, copy path — never store the mutable list reference.
- Mention early pruning when
remaining < needed. - Link subsets (LC 78) as “combinations for every k.”
- Contrast with permutations — order matters there.
Key Takeaway
Generate combinations = backtrack with path and start index: pick forward only, record when len(path) == k, then undo. Count is C(n,k). Same skeleton powers subsets and combination-sum variants.
Next up: Rat in a maze · Generate permutations · Pruning techniques