0077. Combinations¶
Table of Contents¶
- Problem
- Problem Breakdown
- Approach 1: Backtracking
- Approach 2: Backtracking with Pruning
- Approach 3: Iterative (Lex-Order Combinations)
- Complexity Comparison
- Edge Cases
- Common Mistakes
- Related Problems
- Visual Animation
Problem¶
| Leetcode | 77. Combinations |
| Difficulty | |
| Tags | Backtracking |
Description¶
Given two integers
nandk, return all possible combinations ofknumbers chosen from the range[1, n].You may return the answer in any order.
Examples¶
Example 1:
Input: n = 4, k = 2
Output: [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]
Example 2:
Input: n = 1, k = 1
Output: [[1]]
Constraints¶
1 <= n <= 201 <= k <= n
Problem Breakdown¶
1. What is being asked?¶
Enumerate every k-element subset of {1, 2, ..., n}. There are C(n, k) such subsets.
2. What is the input?¶
| Parameter | Type | Description |
|---|---|---|
n | int | Upper bound of range |
k | int | Size of each combination |
3. What is the output?¶
A list of C(n, k) lists, each with k distinct ascending numbers.
4. Constraints analysis¶
| Constraint | Significance |
|---|---|
n <= 20, k <= n | C(20, 10) = 184,756. Backtracking is fine |
5. Step-by-step example analysis¶
n = 4, k = 2¶
Backtrack(start=1, current=[]):
pick 1 โ Backtrack(start=2, current=[1]):
pick 2 โ emit [1,2]
pick 3 โ emit [1,3]
pick 4 โ emit [1,4]
pick 2 โ Backtrack(start=3, current=[2]):
pick 3 โ emit [2,3]
pick 4 โ emit [2,4]
pick 3 โ Backtrack(start=4, current=[3]):
pick 4 โ emit [3,4]
pick 4 โ Backtrack(start=5, current=[4]):
can't pick more (start > n) โ return
Result: 6 combinations.
6. Key Observations¶
- Ascending order avoids duplicates -- We always pick a value greater than the previous, eliminating reordered duplicates.
- Pruning -- We need
k - len(current)more values; the smallest validstartisstart <= n - (k - len(current)) + 1. Beyond that, we cannot fillcurrent.
7. Pattern identification¶
| Pattern | Why it fits |
|---|---|
| Backtracking | Tree of choices, prune via ordering |
Chosen pattern: Backtracking with Pruning.
Approach 1: Backtracking¶
Algorithm (step-by-step)¶
backtrack(start, current):- If
len(current) == k: append a copy ofcurrentto result, return. - For
vfromstartton:- Append
vtocurrent. - Recurse on
(v + 1, current). - Pop.
- Append
Complexity¶
| Complexity | |
|---|---|
| Time | O(C(n, k) * k) |
| Space | O(k) recursion |
Implementation¶
Python¶
class Solution:
def combineNoPrune(self, n: int, k: int) -> List[List[int]]:
result = []
cur = []
def bt(start: int):
if len(cur) == k:
result.append(cur.copy())
return
for v in range(start, n + 1):
cur.append(v)
bt(v + 1)
cur.pop()
bt(1)
return result
Approach 2: Backtracking with Pruning¶
Idea¶
If we have already chosen
len(current)items, we still needk - len(current)more. The largest possiblestartwe can take isn - (k - len(current)) + 1because after pickingstartwe still needk - len(current) - 1items, all fromstart+1..n.
Algorithm¶
def bt(start):
if len(cur) == k:
emit and return
need = k - len(cur)
for v in start .. n - need + 1:
cur.push(v); bt(v + 1); cur.pop()
Complexity¶
| Complexity | |
|---|---|
| Time | O(C(n, k) * k) |
| Space | O(k) |
Implementation¶
Go¶
func combine(n int, k int) [][]int {
result := [][]int{}
cur := []int{}
var bt func(start int)
bt = func(start int) {
if len(cur) == k {
cp := make([]int, k)
copy(cp, cur)
result = append(result, cp)
return
}
need := k - len(cur)
for v := start; v <= n-need+1; v++ {
cur = append(cur, v)
bt(v + 1)
cur = cur[:len(cur)-1]
}
}
bt(1)
return result
}
Java¶
class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> result = new ArrayList<>();
List<Integer> cur = new ArrayList<>();
bt(1, n, k, cur, result);
return result;
}
private void bt(int start, int n, int k, List<Integer> cur, List<List<Integer>> result) {
if (cur.size() == k) {
result.add(new ArrayList<>(cur));
return;
}
int need = k - cur.size();
for (int v = start; v <= n - need + 1; v++) {
cur.add(v);
bt(v + 1, n, k, cur, result);
cur.remove(cur.size() - 1);
}
}
}
Python¶
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
result = []
cur = []
def bt(start: int):
if len(cur) == k:
result.append(cur.copy())
return
need = k - len(cur)
for v in range(start, n - need + 2):
cur.append(v)
bt(v + 1)
cur.pop()
bt(1)
return result
Dry Run¶
n = 4, k = 2
bt(start=1):
need = 2, range = 1..3 (inclusive of 3 because n - need + 1 = 3)
v = 1: bt(2):
need = 1, range = 2..4
v = 2 โ emit [1,2]
v = 3 โ emit [1,3]
v = 4 โ emit [1,4]
v = 2: bt(3):
range = 3..4
v = 3 โ [2,3]; v = 4 โ [2,4]
v = 3: bt(4):
range = 4..4
v = 4 โ [3,4]
Approach 3: Iterative (Lex-Order Combinations)¶
Idea¶
Maintain an array
cur = [1, 2, ..., k]. To advance to the next combination in lexicographic order, find the rightmost indexisuch thatcur[i] < n - k + 1 + i, increment it, and reset everything to its right.Beautiful but harder to derive than backtracking.
Implementation¶
Python¶
class Solution:
def combineIter(self, n: int, k: int) -> List[List[int]]:
cur = list(range(1, k + 1))
result = [cur.copy()]
while True:
i = k - 1
while i >= 0 and cur[i] == n - k + 1 + i:
i -= 1
if i < 0: break
cur[i] += 1
for j in range(i + 1, k):
cur[j] = cur[j - 1] + 1
result.append(cur.copy())
return result
Complexity Comparison¶
| # | Approach | Time | Space | Pros | Cons |
|---|---|---|---|---|---|
| 1 | Backtracking | O(C(n,k) * k) | O(k) | Simple | No early stop |
| 2 | Backtracking + Pruning | O(C(n,k) * k) | O(k) | Faster in practice | -- |
| 3 | Iterative | O(C(n,k) * k) | O(k) | No recursion | Harder to grok |
Which solution to choose?¶
Approach 2 in interviews; Approach 1 if simplicity is preferred.
Edge Cases¶
| # | Case | Reason |
|---|---|---|
| 1 | k == n | Single combination = all numbers |
| 2 | k == 1 | n single-element lists |
| 3 | n == 1, k == 1 | [[1]] |
| 4 | Large n and k = n/2 | Maximum number of combinations |
Common Mistakes¶
Mistake 1: Forgetting to copy cur when emitting¶
# WRONG โ reference shared, all entries become equal at the end
result.append(cur)
# CORRECT โ emit a snapshot
result.append(cur.copy())
Reason: cur mutates throughout backtracking; we need its current snapshot.
Mistake 2: Wrong upper bound when pruning¶
# WRONG โ loop goes one step too far
for v in range(start, n - need + 1): # exclusive end skips n - need + 1
# CORRECT โ Python range upper exclusive: use n - need + 2
for v in range(start, n - need + 2):
Reason: Python's range(a, b) excludes b; we need to include n - need + 1.
Related Problems¶
| # | Problem | Difficulty | Similarity |
|---|---|---|---|
| 1 | 78. Subsets | All subsets | |
| 2 | 39. Combination Sum | Sum target | |
| 3 | 46. Permutations | Order matters |
Visual Animation¶
Interactive animation: animation.html
The animation includes: - Number row with chosen elements highlighted - Backtracking tree depth indicator - Live list of generated combinations