Combination Sum is the gateway to backtracking — building every valid combination by making a choice, recursing, then undoing it. The twist here is that you may reuse a candidate as many times as you want.
Problem. Given a list of distinct positive integers candidates and a target, return all unique
combinations where the chosen numbers sum to target. The same number may be used unlimited times.
Example: candidates = [2, 3, 6, 7], target = 7 → [[2, 2, 3], [7]] (because 2 + 2 + 3 = 7 and 7 = 7).
The slow way first
You might try generating every possible multiset of numbers and checking which ones sum to the target. But the number of multisets is enormous, and most of them blow past the target. Brute force has no way to stop early, so it explores a gigantic space pointlessly.
The fix is to build combinations incrementally and abandon a path the instant it can no longer succeed. That is backtracking.
The idea: choose, recurse, undo
We keep a running total and a path (the numbers chosen so far). At each step we loop over the candidates and try adding one:
- If
total + vovershoots the target, skip it — prune that branch. - Otherwise add
vto the path and recurse. Crucially, we recurse with start indexi, noti + 1, so the same number can be picked again. - When
total == target, we found a valid combination — collect a copy of the path. - After exploring, we pop
vback off the path so the next choice starts clean.
Passing start (and only ever increasing it across the loop, never resetting it for the recursion) is what stops us from producing the same combination in a different order — we only ever extend with candidates at index i or later.
Walk through it
Step through the animation. Each tree node shows the running sum; each edge adds a candidate. We dive 2 → 2 → 2 to reach sum 6, then adding another 2 would make 8, so that branch turns red and is pruned. We backtrack and add 3 instead: 2 + 2 + 3 = 7, a hit — collect [2, 2, 3]. Unwinding to the root, picking 7 lands on the target directly: collect [7]. The result is [[2, 2, 3], [7]].
Pseudocode
result = []
backtrack(start, total, path):
if total == target:
add a COPY of path to result
return
for i from start to end of candidates:
v = candidates[i]
if total + v > target:
continue # prune: this choice overshoots
path.append(v)
backtrack(i, total + v, path) # i (not i+1) -> v can repeat
path.pop() # undo the choice
backtrack(0, 0, [])The Python solution
def combination_sum(candidates, target):
result = []
def backtrack(start, total, path):
if total == target:
result.append(path[:])
return
for i in range(start, len(candidates)):
v = candidates[i]
if total + v > target:
continue # prune: overshoot
path.append(v)
backtrack(i, total + v, path) # i, not i + 1 -> reuse
path.pop() # undo (backtrack)
backtrack(0, 0, path=[])
return resultpath[:]stores a copy —pathkeeps mutating, so we must snapshot it when we collect.- The
if total + v > target: continueline is the prune; because candidates are positive, once you overshoot, adding more can never come back. backtrack(i, ...)passesi, noti + 1— that single character is what allows reusing a candidate.path.pop()is the undo that makes this backtracking: after exploring a choice fully, we remove it so the next loop iteration starts from the same state.
Complexity
| Case | Time | Notes |
|---|---|---|
| Branching | O(N^(T/M)) (moderate) | N candidates, T target, M the smallest candidate |
| Per solution | O(T/M) (moderate) | copying a path of up to T/M numbers |
O(T/M) (moderate)The bound is exponential in the worst case — backtracking explores a tree — but pruning overshoots keeps the real work far smaller. Space is the recursion depth plus the current path, both at most T/M.
When this pattern shows up
Whenever a problem asks for all combinations / subsets / arrangements that satisfy a rule, reach for
backtracking: choose, recurse, undo. The start index controls duplicates, and whether you recurse with
i or i + 1 decides reuse versus no-reuse. Subsets, Permutations, Combination Sum II, and Palindrome
Partitioning are all the same skeleton.
Two classic bugs: recursing with i + 1 (which silently forbids reuse), and appending path instead of
path[:] (every stored result then points at the same list, which ends up empty after all the pops).
Practice
We are at sum 6 via the path [2, 2, 2]. Why do we not add another 2, and what do we do instead?
1. Why does the recursive call pass i instead of i + 1?
2. What does the line if total + v > target: continue do?
3. Why do we append path[:] instead of path?
4. What is the role of path.pop() after the recursive call?