Combinations and Combination Sum — The Start-Index Backtracking Pattern
Advertisement
Problem Statement
LC 77 — Combinations. Given two integers
nandk, return all possible combinations ofknumbers chosen from the range[1, n]. Order within a combination does not matter.LC 39 — Combination Sum. Given an array of distinct positive integers
candidatesand atarget, return all unique combinations where the chosen numbers sum totarget. The same number may be chosen unlimited times.LC 40 — Combination Sum II. Same as LC 39 but each candidate may be used at most once and the array may contain duplicates. Each combination must be unique.
Constraints (LC 77): 1 <= k <= n <= 20. Constraints (LC 39 / 40): 1 <= candidates.length <= 30, 2 <= candidates[i] <= 50, target up to 500.
Example (LC 77):
Input: n = 4, k = 2
Output: [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]Example (LC 39):
Input: candidates = [2,3,6,7], target = 7
Output: [[2,2,3],[7]]Example (LC 40):
Input: candidates = [10,1,2,7,6,1,5], target = 8
Output: [[1,1,6],[1,2,5],[1,7],[2,6]]Why This Problem Matters
Combination problems are the second pillar of backtracking interviews after subsets. The combinations explained here power real-world systems: building product bundles within a price target, picking a tournament squad of size k, generating quorum subsets in distributed consensus, and selecting features in machine learning. FAANG companies including Amazon, Google, Meta, and Microsoft regularly post LC 39 and LC 40 in onsite loops because they expose three skills at once: recursion fluency, deduplication reasoning, and pruning intuition.
What makes this problem set so instructive is the contrast between the three variants. LC 77 teaches the pure start-index template. LC 39 introduces unlimited reuse by recursing with the same index. LC 40 layers in deduplication for repeated candidates. By the time you can write all three from memory, you own the start-index pattern forever and you can recognize it the moment a future problem asks for "all combinations" or "all selections."
The Core Insight
Combinations differ from permutations in one crucial way: order does not matter, so [1, 2, 3] and [3, 2, 1] are the same combination. To avoid emitting both, you only ever extend a partial combination by elements that come AFTER the last one you picked. This is the start-index pattern: each recursive call receives a start index, and the for-loop iterates i from start to n. After choosing candidates[i], you recurse with either i + 1 (no reuse) or i (reuse allowed).
The decision tree at each node has up to n - start children, where each child commits to a specific element at the current position. The path from root to any node represents a partial combination, and you append a copy to the result when the size hits k (LC 77) or when the remaining target hits zero (LC 39 / 40).
For LC 40 with repeated candidates, the deduplication rule is the same trick used in Subsets II and 3Sum: sort the array, then within a single recursive call, skip candidates[i] when i greater than start and candidates[i] == candidates[i-1]. The condition i greater than start is the load-bearing piece — it allows the FIRST occurrence at this level to be picked but skips subsequent equal-value siblings that would generate identical subtrees.
Pruning is what turns a brute-force backtrack into an interview-worthy solution. Sort candidates ascending and break out of the for-loop the moment candidates[i] greater than remaining — any later element is even larger, so no valid combination exists down that branch. This single break can shrink the search space by orders of magnitude on adversarial inputs.
Visual Dry Run
LC 39 with candidates = [2, 3, 6, 7], target = 7. Tree (each node shows the chosen element and remaining target).
bt(start=0, current=[], remaining=7)
i=0: pick 2 -> bt(0, [2], 5)
i=0: pick 2 -> bt(0, [2,2], 3)
i=0: pick 2 -> bt(0, [2,2,2], 1)
i=0: 2 greater than 1, break (prune)
i=1: pick 3 -> bt(1, [2,2,3], 0) -> RECORD [2,2,3]
i=2: 6 greater than 3, break
i=1: pick 3 -> bt(1, [2,3], 2)
i=1: 3 greater than 2, break
i=2: 6 greater than 5, break
i=1: pick 3 -> bt(1, [3], 4)
i=1: pick 3 -> bt(1, [3,3], 1)
i=1: 3 greater than 1, break
i=2: 6 greater than 4, break
i=2: pick 6 -> bt(2, [6], 1)
i=2: 6 greater than 1, break
i=3: pick 7 -> bt(3, [7], 0) -> RECORD [7]Two combinations recorded: [[2,2,3], [7]]. Notice how the start index pinned to i (not i+1) on the recursive call enables 2 + 2 + 3 — element 2 was reused twice without revisiting 2 again from a higher level.
Solution (Optimal)
Python — full backtracking template
# LC 77 — Combinations of size k from [1..n]
def combine(n: int, k: int) -> list[list[int]]:
result = []
def bt(start: int, current: list[int]) -> None:
# Goal: combination of exactly k elements
if len(current) == k:
result.append(current[:]) # snapshot copy
return
# Pruning: stop when remaining slots can't be filled
# Need (k - len(current)) more elements; only n - i + 1 available from i
remaining_slots = k - len(current)
max_start = n - remaining_slots + 1
for i in range(start, max_start + 1):
current.append(i) # choose
bt(i + 1, current) # explore: i+1 = no reuse
current.pop() # unchoose (backtrack)
bt(1, [])
return result
# LC 39 — Combination Sum (unlimited reuse)
def combinationSum(candidates: list[int], target: int) -> list[list[int]]:
candidates.sort() # enable pruning by ascending order
result = []
def bt(start: int, current: list[int], remaining: int) -> None:
if remaining == 0:
result.append(current[:])
return
for i in range(start, len(candidates)):
if candidates[i] > remaining:
break # pruning: sorted, all later are larger
current.append(candidates[i])
bt(i, current, remaining - candidates[i]) # i, not i+1 -> reuse
current.pop()
bt(0, [], target)
return result
# LC 40 — Combination Sum II (no reuse, candidates may repeat)
def combinationSum2(candidates: list[int], target: int) -> list[list[int]]:
candidates.sort()
result = []
def bt(start: int, current: list[int], remaining: int) -> None:
if remaining == 0:
result.append(current[:])
return
for i in range(start, len(candidates)):
# Deduplicate at this recursion level only
if i > start and candidates[i] == candidates[i - 1]:
continue
if candidates[i] > remaining:
break
current.append(candidates[i])
bt(i + 1, current, remaining - candidates[i]) # i+1 -> no reuse
current.pop()
bt(0, [], target)
return resultJavaScript
// LC 77
function combine(n, k) {
const result = [];
const current = [];
function bt(start) {
if (current.length === k) {
result.push([...current]);
return;
}
const need = k - current.length;
const maxStart = n - need + 1;
for (let i = start; i <= maxStart; i++) {
current.push(i); // choose
bt(i + 1); // explore: no reuse
current.pop(); // unchoose
}
}
bt(1);
return result;
}
// LC 39
function combinationSum(candidates, target) {
candidates.sort((a, b) => a - b);
const result = [];
const current = [];
function bt(start, remaining) {
if (remaining === 0) {
result.push([...current]);
return;
}
for (let i = start; i < candidates.length; i++) {
if (candidates[i] > remaining) break; // prune
current.push(candidates[i]);
bt(i, remaining - candidates[i]); // reuse: stay at i
current.pop();
}
}
bt(0, target);
return result;
}
// LC 40
function combinationSum2(candidates, target) {
candidates.sort((a, b) => a - b);
const result = [];
const current = [];
function bt(start, remaining) {
if (remaining === 0) {
result.push([...current]);
return;
}
for (let i = start; i < candidates.length; i++) {
if (i > start && candidates[i] === candidates[i - 1]) continue;
if (candidates[i] > remaining) break;
current.push(candidates[i]);
bt(i + 1, remaining - candidates[i]); // no reuse
current.pop();
}
}
bt(0, target);
return result;
}Complexity
| Variant | Time | Space |
|---|---|---|
| LC 77 Combinations | O(C(n,k) * k) | O(k) recursion depth |
| LC 39 Combination Sum | O(N^(target/min)) loose bound | O(target/min) depth |
| LC 40 Combination Sum II | O(2^N * N) | O(N) depth |
Time bounds are pessimistic — pruning typically slashes them by huge constant factors in practice.
Common Mistakes
- Using
i + 1when reuse is allowed (LC 39). This emits[7]but never[2, 2, 3]because the same element can never be picked twice in one combination. Useito allow reuse. - Using
iinstead ofi + 1for LC 40. This produces duplicates like[1, 1, 1, ...]from a single1candidate. Each LC 40 candidate is consumed once, so always advance. - Wrong dedup boundary
i greater than 0instead ofi greater than start. The condition must be relative to the current recursive level — outer levels have already chosen the prior duplicate legitimately. - Forgetting to sort before pruning. The break-on-too-large optimization assumes ascending order. Without sorting, only the dedup-skip rule still works; the break is unsafe.
- Returning the same list reference instead of a copy.
result.append(current)shares the mutable list — every recorded combination ends up empty after backtracking. Always copy withcurrent[:]or[...current].
Interview Tips
- State the pattern out loud: "This is the start-index pattern. Order does not matter, so I only extend by elements after the last chosen index."
- Name the recurrence parameters:
start(next index to consider),current(path so far), and eitherremaining(target left) orlen(current)(depth). - Show pruning explicitly. Sorting plus the
breakline is a 1-second add that earns major points and demonstrates production-quality thinking. - Demo on a tiny input. Walk through
n=3, k=2ortarget=5, candidates=[2,3]to prove the trace is rock solid before scaling up. - Mention space. Stack depth is bounded by
k(LC 77) ortarget / min(candidates)(LC 39).
Follow-up Questions
- Combinations with duplicates allowed in input (LC 40 with target up to 10000) — same algorithm; pruning becomes critical and a memoized version is sometimes asked.
- Count instead of enumerate — switch to DP. LC 377 Combination Sum IV (which is actually permutations, despite the name) is a classic DP follow-up.
- All combinations of size at most k — change the base case to
len(current) less-than-or-equal kand record at every level. - Non-decreasing factor decomposition — same start-index pattern with a multiplicative target.
Key Takeaways
- The start-index pattern enumerates combinations without duplicates by only ever extending forward in the input array.
i + 1means "no reuse" andimeans "reuse allowed" on the recursive call — a one-character switch between LC 39 and LC 77.- Sort plus break-on-too-large is the single biggest pruning win and converts brute force into an interview-strength solution.
- For repeated candidates, dedup with
i greater than start and candidates[i] == candidates[i-1]to skip equal-value siblings at the SAME level. - Always snapshot
current[:]when recording — backtracking will mutate the path on the way back up. - Mastering LC 77, LC 39, and LC 40 unlocks dozens of related problems including factor combinations, change-making, expression building, and bundle generation.
Advertisement