Combination Sum
Difficulty: Medium Source: NeetCode
Problem
Given an array of distinct integers
candidatesand a target integertarget, return a list of all unique combinations ofcandidateswhere the chosen numbers sum totarget. You may return the combinations in any order.The same number may be chosen from
candidatesan unlimited number of times. Two combinations are unique if the frequency of at least one of the chosen numbers is different.Example 1: Input:
candidates = [2, 3, 6, 7],target = 7Output:[[2, 2, 3], [7]]Example 2: Input:
candidates = [2, 3],target = 4Output:[[2, 2], [3], [2, 3]]— wait,[3]sums to 3 not 4. Correct:[[2, 2]]… Actually:[[2, 2], [2, 2]]— no.2+2=4and3alone is 3 not 4. Output:[[2, 2]]Example 3: Input:
candidates = [2, 3, 5],target = 8Output:[[2, 2, 2, 2], [2, 3, 3], [3, 5]]Constraints:
1 <= candidates.length <= 302 <= candidates[i] <= 40- All elements of
candidatesare distinct.1 <= target <= 40
Prerequisites
Before attempting this problem, you should be comfortable with:
- Backtracking — exploring choices and undoing them when a path doesn’t lead to a solution
- Recursion — building combinations incrementally
1. Brute Force
Intuition
Generate all possible combinations of candidates (with repetition) up to some maximum length and check whether each one sums to target. The maximum number of elements in a valid combination is target // min(candidates). This is horribly inefficient but gets the point across.
Algorithm
- Compute
max_len = target // min(candidates). - Generate all combinations with repetition up to length
max_len. - Filter those that sum exactly to
target. - Deduplicate (since the same set in different orders counts once).
Solution
from itertools import combinations_with_replacement
def combinationSum_brute(candidates, target):
result = set()
max_len = target // min(candidates)
for length in range(1, max_len + 1):
for combo in combinations_with_replacement(sorted(candidates), length):
if sum(combo) == target:
result.add(combo)
return [list(c) for c in result]
print(combinationSum_brute([2, 3, 6, 7], 7)) # [[2, 2, 3], [7]]
print(combinationSum_brute([2, 3, 5], 8)) # [[2, 2, 2, 2], [2, 3, 3], [3, 5]]
Complexity
- Time:
O(n^(target/min))— exponential in the worst case - Space:
O(target/min)— recursion depth
2. Backtracking
Intuition
Walk through the candidates array from left to right. At each position i, you can either keep using candidates[i] (recurse with the same i, subtracting its value from the remaining target) or move on to candidates[i+1]. If the remaining target hits 0 you’ve found a valid combination; if it goes negative you prune that branch. Because we always recurse starting from the current index or later, we never produce duplicate combinations.
Algorithm
- Define
backtrack(start, current, remaining). - If
remaining == 0: append a copy ofcurrentto results and return. - If
remaining < 0: return (prune). - For each index
ifromstarttolen(candidates) - 1:- Append
candidates[i]tocurrent. - Recurse with
start = i(same element allowed again) andremaining - candidates[i]. - Pop
candidates[i]fromcurrent.
- Append
Solution
def combinationSum(candidates, target):
result = []
def backtrack(start, current, remaining):
if remaining == 0:
result.append(list(current))
return
if remaining < 0:
return
for i in range(start, len(candidates)):
current.append(candidates[i])
backtrack(i, current, remaining - candidates[i]) # i not i+1 — reuse allowed
current.pop()
backtrack(0, [], target)
return result
print(combinationSum([2, 3, 6, 7], 7)) # [[2, 2, 3], [7]]
print(combinationSum([2, 3, 5], 8)) # [[2, 2, 2, 2], [2, 3, 3], [3, 5]]
print(combinationSum([2], 1)) # []
Complexity
- Time:
O(n^(target/min))— in the worst case, explores deep trees; sorting + pruning helps in practice - Space:
O(target/min)— maximum recursion depth
Common Pitfalls
Passing i + 1 instead of i when recursing. Since each candidate can be reused unlimited times, you need to recurse with start = i, not start = i + 1. Using i + 1 would prevent reuse and miss combinations like [2, 2, 3].
Not making a copy when appending to results. result.append(current) stores a reference; always use result.append(list(current)).
Not pruning when remaining < 0. Without this check, you’d explore paths that can never reach the target. Sorting candidates first lets you add an early break instead of return for more aggressive pruning.