Skip to content

40. Combination Sum II (Medium)

Problem

Given a collection of integers candidates (may contain duplicates) and a target, find all unique combinations that sum to target. Each number may be used at most once; the result must not contain duplicate combinations.

Example

  • candidates = [10,1,2,7,6,1,5], target = 8[[1,1,6], [1,2,5], [1,7], [2,6]]
  • candidates = [2,5,2,1,2], target = 5[[1,2,2], [5]]

LeetCode 40 · Link · Medium

Try it yourself

idle

Starter code: this editor begins with intentional TODOs. Fill the function, run the embedded tests, then compare your solution with the worked approaches below.

Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 1: Brute force, all subsets, filter by sum, dedup

Generate the power set; keep subsets summing to target; dedup via canonical tuples.

def combination_sum2(candidates, target):
from itertools import chain, combinations
found = set()
for r in range(1, len(candidates) + 1):
for combo in combinations(candidates, r):
if sum(combo) == target:
found.add(tuple(sorted(combo)))
return [list(t) for t in found]

Complexity

  • Time: O(2nnlogn)O(2^n · n log n).
  • Space: same.

Exponential and wasteful.

final class Solution {
private func normalized(_ values: [[Int]]) -> [[Int]] {
values.sorted { $0.lexicographicallyPrecedes($1) }
}
func combinationSum2(_ candidates: [Int], _ target: Int) -> [[Int]] {
let count = candidates.count; var unique = Set<[Int]>()
for mask in 0..<(1 << count) { var sum = 0, values: [Int] = []; for index in 0..<count where mask & (1 << index) != 0 { sum += candidates[index]; values.append(candidates[index]) }; if sum == target { unique.insert(values.sorted()) } }
return normalized(Array(unique))
}
}

Approach 2: Backtracking with start index (no skip)

Standard start-based backtracking, but without duplicate handling, this produces duplicate combinations when the input has repeated values.

def combination_sum2(candidates, target):
candidates.sort()
result = []
path = []
def backtrack(start, remaining):
if remaining == 0:
result.append(path[:])
return
for i in range(start, len(candidates)):
if candidates[i] > remaining:
break
path.append(candidates[i])
backtrack(i + 1, remaining - candidates[i])
path.pop()
backtrack(0, target)
# Dedup the output
return list({tuple(x) for x in result}) # fix

Complexity

  • Time: exponential + O(mlogm)O(m log m) for dedup.
  • Space: exponential storage.

The dedup step is a symptom, fix it at the source.

final class Solution {
private func normalized(_ values: [[Int]]) -> [[Int]] {
values.sorted { $0.lexicographicallyPrecedes($1) }
}
func combinationSum2(_ candidates: [Int], _ target: Int) -> [[Int]] {
let values = candidates.sorted(); var unique = Set<[Int]>()
func search(_ start: Int, _ remaining: Int, _ current: [Int]) { if remaining == 0 { unique.insert(current); return }; if remaining < 0 { return }; for index in start..<values.count { search(index + 1, remaining - values[index], current + [values[index]]) } }
search(0, target, []); return normalized(Array(unique))
}
}

Approach 3: Sort + skip same-level duplicates (canonical)

Sort, then at each level skip indices whose value equals the previous one at the same level. Same template as Subsets II.

def combination_sum2(candidates, target):
candidates.sort() # L1: O(n log n) sort
result = []
path = []
def backtrack(start, remaining):
if remaining == 0:
result.append(path[:]) # L2: O(k) copy
return
for i in range(start, len(candidates)):
if candidates[i] > remaining:
break # L3: O(1) prune
if i > start and candidates[i] == candidates[i - 1]:
continue # L4: O(1) skip same-level duplicate
path.append(candidates[i]) # L5: O(1) push
backtrack(i + 1, remaining - candidates[i]) # L6: recurse (i+1, no reuse)
path.pop() # L7: O(1) pop
backtrack(0, target)
return result

Where the time goes, line by line

Variables: n = len(candidates), k = average solution length.

LinePer-call costTimes executedContribution
L1 (sort)O(nlogn)O(n log n)1O(nlogn)O(n log n)
L3 (prune)O(1)O(1)pruned nodesO(pruned)O(pruned)
L4 (skip dup)O(1)O(1)dup nodesO(dup)O(dup)
L6 (recurse)O(1)O(1) dispatchsurviving nodesO(2n)O(2^n) ← dominates

Sorting enables both L3 (early termination) and L4 (same-level duplicate skip). The i > start guard is critical: it allows the same value to be chosen at different recursion levels while forbidding it at the same level.

Complexity

  • Time: O(2n)O(2^n) worst case; pruning and skip make it much faster in practice.
  • Space: O(k)O(k) recursion, where k = average solution length.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).
final class Solution {
private func normalized(_ values: [[Int]]) -> [[Int]] {
values.sorted { $0.lexicographicallyPrecedes($1) }
}
func combinationSum2(_ candidates: [Int], _ target: Int) -> [[Int]] {
let values = candidates.sorted(); var result: [[Int]] = []
func search(_ start: Int, _ remaining: Int, _ current: [Int]) { if remaining == 0 { result.append(current); return }; var previous: Int?; for index in start..<values.count { let value = values[index]; if value > remaining { break }; if value == previous { continue }; previous = value; search(index + 1, remaining - value, current + [value]) } }
search(0, target, []); return normalized(result)
}
}

Summary

ApproachTimeSpace
Powerset + filter + dedupO(2nnlogn)O(2^n · n log n)O(2n)O(2^n)
Backtracking + post-dedupexponentialexponential
Sort + skip same-levelO(2n)O(2^n) with pruningO(k)O(k)

The skip-same-level template (shared with Subsets II) is the right abstraction for “no duplicates allowed when input has duplicates.”

Test cases

def combination_sum2(candidates, target):
candidates.sort()
result = []
path = []
def backtrack(start, remaining):
if remaining == 0:
result.append(path[:])
return
for i in range(start, len(candidates)):
if candidates[i] > remaining:
break
if i > start and candidates[i] == candidates[i - 1]:
continue
path.append(candidates[i])
backtrack(i + 1, remaining - candidates[i])
path.pop()
backtrack(0, target)
return result
def _run_tests():
r = combination_sum2([10, 1, 2, 7, 6, 1, 5], 8)
assert sorted(map(tuple, r)) == sorted([tuple([1,1,6]), tuple([1,2,5]), tuple([1,7]), tuple([2,6])])
r2 = combination_sum2([2, 5, 2, 1, 2], 5)
assert sorted(map(tuple, r2)) == sorted([tuple([1,2,2]), tuple([5])])
# no solution
assert combination_sum2([1, 2], 10) == []
# single element solution
assert combination_sum2([3, 3], 3) == [[3]]
print("all tests pass")
if __name__ == "__main__":
_run_tests()
  • Arrays, sorted for pruning and dedup