Combination Sum II
Difficulty: Medium Source: NeetCode
Problem
Given a collection of candidate numbers (
candidates) and a target number (target), find all unique combinations incandidateswhere the candidate numbers sum totarget.Each number in
candidatesmay only be used once in the combination. The solution set must not contain duplicate combinations.Example 1: Input:
candidates = [10, 1, 2, 7, 6, 1, 5],target = 8Output:[[1, 1, 6], [1, 2, 5], [1, 7], [2, 6]]Example 2: Input:
candidates = [2, 5, 2, 1, 2],target = 5Output:[[1, 2, 2], [5]]Constraints:
1 <= candidates.length <= 1001 <= candidates[i] <= 501 <= target <= 30
Prerequisites
Before attempting this problem, you should be comfortable with:
- Combination Sum I — the version where reuse is allowed and all candidates are distinct
- Backtracking — exploring and pruning a decision tree
- Duplicate handling — sorting to group duplicates and skipping at the same recursion level
1. Brute Force
Intuition
Sort the candidates. Generate every possible subset of candidates (each element used at most once). For each subset that sums to target, convert it to a sorted tuple and add to a set to avoid duplicate results. Return the deduplicated list. This is wasteful because we build many subsets that can never sum to target, but it’s conceptually simple.
Algorithm
- Sort
candidates. - Use backtracking to enumerate all subsets.
- If a subset sums to
target, add its tuple to a result set. - Convert the set back to a list of lists.
Solution
def combinationSum2_brute(candidates, target):
candidates.sort()
result_set = set()
def backtrack(start, current, remaining):
if remaining == 0:
result_set.add(tuple(current))
return
for i in range(start, len(candidates)):
if candidates[i] > remaining:
break
current.append(candidates[i])
backtrack(i + 1, current, remaining - candidates[i])
current.pop()
backtrack(0, [], target)
return [list(t) for t in result_set]
print(sorted(combinationSum2_brute([10, 1, 2, 7, 6, 1, 5], 8)))
# [[1, 1, 6], [1, 2, 5], [1, 7], [2, 6]]
print(sorted(combinationSum2_brute([2, 5, 2, 1, 2], 5)))
# [[1, 2, 2], [5]]
Complexity
- Time:
O(2^n * n)— exponential subsets, each costs O(n) to hash - Space:
O(n)— recursion depth
2. Backtracking with Duplicate Skipping
Intuition
Sort candidates first so duplicates are adjacent. During backtracking, after picking candidates[i] and recursing, if the next element candidates[i+1] has the same value as candidates[i], skip it entirely. This prevents starting the same subtree twice from the same position in the recursion, which is exactly what causes duplicate results. Each element is still used at most once (we pass i + 1 when recursing).
Algorithm
- Sort
candidates. - Define
backtrack(start, current, remaining). - If
remaining == 0: recordcurrentand return. - For
ifromstartto end:- Skip if
i > startandcandidates[i] == candidates[i - 1](same value at same level). - Skip if
candidates[i] > remaining(prune — sorted order makes this a break, not continue). - Append
candidates[i], recurse withi + 1, pop.
- Skip if
Solution
def combinationSum2(candidates, target):
candidates.sort()
result = []
def backtrack(start, current, remaining):
if remaining == 0:
result.append(list(current))
return
for i in range(start, len(candidates)):
# skip duplicates at the same recursion level
if i > start and candidates[i] == candidates[i - 1]:
continue
# prune: sorted array, so all remaining are too large
if candidates[i] > remaining:
break
current.append(candidates[i])
backtrack(i + 1, current, remaining - candidates[i])
current.pop()
backtrack(0, [], target)
return result
print(combinationSum2([10, 1, 2, 7, 6, 1, 5], 8))
# [[1, 1, 6], [1, 2, 5], [1, 7], [2, 6]]
print(combinationSum2([2, 5, 2, 1, 2], 5))
# [[1, 2, 2], [5]]
print(combinationSum2([1, 1, 1, 1], 2))
# [[1, 1]]
Complexity
- Time:
O(2^n)— in the worst case (all distinct), explores every subset - Space:
O(n)— recursion depth
Common Pitfalls
Using i > 0 instead of i > start for the duplicate check. The condition i > start and candidates[i] == candidates[i-1] means “skip if we’re not the first element at this recursion level and the previous element had the same value.” Using i > 0 instead is wrong — it would skip valid combinations like [1, 1, 6] where you legitimately pick the second 1 at a deeper level.
Passing i instead of i + 1 when recursing. Unlike Combination Sum I, each number can only be used once. Pass i + 1 to move past the current element.
Forgetting to sort. Without sorting, duplicates aren’t adjacent and the skip condition candidates[i] == candidates[i-1] doesn’t catch all duplicates. Always sort first.