Combination Sum II

Combination Sum II mixes two tricky rules at once. Each number may be used only once, and the array can have duplicates. So you must move forward each step and also dodge repeated combinations. This pairs the no-reuse idea with the sort-then-skip idea. Get both right together and you have shown real backtracking control.

🎯 The Problem

You get an array that may contain duplicate numbers and a target, and you find every combination that adds up to the target. Here are the rules.

  • Find every combination of numbers that adds up to the target.
  • Each number at each position may be used only once.
  • No combination may appear twice in the answer.
  • The order inside a combination does not matter.

Let us say the array is [10, 1, 2, 7, 6, 1, 5] and the target is 8. Now 1 + 7 works. So does 1 + 2 + 5. So does 2 + 6. And 1 + 1 + 6 works using both of the 1 values. Each value is used as many times as it appears, no more.

Input: candidates = [10, 1, 2, 7, 6, 1, 5], target = 8
Output: [[1,1,6], [1,2,5], [1,7], [2,6]]
Explanation: each number used at most as many times as it appears, no duplicate combinations.

Here is the danger. The two 1 values can each start a 1 + 7 branch. Both give the same combination, so one must be skipped.

target 8

use first 1, then 7 [1,7]

use second 1, then 7 [1,7] DUPLICATE

valid, keep

duplicate, skip at this level

🐒 Approach 1: Build Every Subset Then Filter (Brute Force)

The idea in one line: make every possible subset, then keep the ones that sum to the target.

How it works:

  • List every subset of the array, like the power set.
  • Add up each subset.
  • Keep the subsets whose sum equals the target.
  • Sort each kept subset and drop repeats so no combination appears twice.

Why it is weak:

  • There are 2 to the power n subsets, and you build all of them.
  • You also spend extra work removing duplicate combinations at the end.
  • So it is slow and wasteful. The pruning ideas below are never used.

Here is the brute-force subset code for that idea:

combination_sum_ii_brute_force.py
def combination_sum2(candidates, target):
result = set()
def dfs(index, path):
if index == len(candidates):
if sum(path) == target:
result.add(tuple(sorted(path)))
return
dfs(index + 1, path)
dfs(index + 1, path + [candidates[index]])
dfs(0, [])
return [list(item) for item in sorted(result)]

⚑ Approach 2: Sort, Move Forward, Skip Equal Siblings (Best)

The idea in one line: sort, never reuse a position, and skip a repeated value at the same level.

Move forward (no reuse):

  • Each number may be used only once.
  • Pass i + 1 to the recursion, not i, so the current position is not picked again.
  • That is the one difference from Combination Sum, where we passed i to allow reuse.

Sort and skip (no duplicate combinations):

  • Sort the array so equal values sit together.
  • At one level, the first copy of a value already explores every combination starting with it.
  • A second equal copy at the same level would only repeat those, so skip it.
  • The test is β€œi greater than start and this value equals the previous value.”
  • Sorting is the only reason a simple previous-value check works.

Siblings versus children:

  • Same i > start guard as Subsets II.
  • We skip an equal sibling but still allow an equal value deeper in the path.
  • So [1, 1, 6] is valid. The second 1 is a child of the first, not a sibling.
  • But a second [1, 7] started by the sibling 1 is blocked.

Why it is fast:

  • The early break on a too-big value and the duplicate skip prune whole branches.
  • So real runs are usually far below the worst case.

This dry run on the sorted array shows the forward move and the skip together.

sorted [1,1,2,5,6,7,10] target 8 start=0

i=0 use first 1, remain 7

i=1 nums[1]==nums[0] and i>start: SKIP

i=1 use second 1, remain 6

i=4 use 6, remain 0 RECORD [1,1,6]

i=2 use 2, remain 5 -> use 5 RECORD [1,2,5]

Steps to Solve

  1. Sort the array so equal numbers sit next to each other.
  2. Create a result list and a path for the current combination.
  3. Start a recursive helper with the full target and start index 0.
  4. If the remaining target is 0, record a copy of the path and return.
  5. Loop from the start index. If the current value is bigger than the remaining target, stop the loop early.
  6. If i > start and the current value equals the previous value, skip it with continue.
  7. Add the value to the path, then recurse with i + 1 so the value is not reused.
  8. Remove the value you just added. This is the un-choose step.

This Python version sorts first, moves forward with i + 1 so no number repeats, and skips equal siblings.

combination_sum_ii.py
def combination_sum2(candidates, target):
candidates.sort() # duplicates become neighbours
result = []
path = []
def backtrack(start, remain):
if remain == 0: # exact match found
result.append(path[:])
return
for i in range(start, len(candidates)):
if candidates[i] > remain: # sorted, so nothing further fits
break
if i > start and candidates[i] == candidates[i - 1]: # skip equal sibling
continue
path.append(candidates[i]) # choose
backtrack(i + 1, remain - candidates[i]) # i+1 = no reuse
path.pop() # un-choose
backtrack(0, target)
return result
print(combination_sum2([10, 1, 2, 7, 6, 1, 5], 8))

The output of the above code will be:

[[1, 1, 6], [1, 2, 5], [1, 7], [2, 6]]

Let us walk through the Python version line by line, because two rules run side by side here.

candidates.sort() puts equal values next to each other. Both the early-stop and the duplicate-skip depend on the array being sorted.

if remain == 0 records a copy of the path and returns when the picks hit the target exactly.

for i in range(start, len(candidates)) loops from the start index forward. We never look before start, which keeps combinations in one order.

if candidates[i] > remain: break is an early stop. Because the array is sorted, once a value is bigger than what is left, every value after it is also too big. So we break out of the loop entirely, which saves work.

if i > start and candidates[i] == candidates[i - 1]: continue is the duplicate skip. The i > start part means we are past the first choice at this level. The equality part means this value matches its left neighbour. Together they catch a repeated sibling and skip it, so the same combination is not built twice. The first copy at a level still runs, because there i equals start.

path.append(candidates[i]) chooses the value.

backtrack(i + 1, remain - candidates[i]) explores deeper. Here we pass i + 1, not i. That moves us forward, so the current position is never reused. This is the no-reuse rule.

path.pop() un-chooses, resetting the path for the next sibling.

⏱️ Time and Space Complexity

In the worst case the search explores many subsets, so the time is exponential, around O(2^n) over n candidates. The sort adds O(n log n), which is small. The duplicate skip and the early break prune branches, so real runs are usually much faster than the worst case. The extra space is O(n) for the recursion depth and the path.

Approach Time Complexity Space Complexity
Build every subset then filter O(2^n Γ— n) O(2^n)
Backtracking, no reuse, sort and skip O(2^n) O(n) depth

Tip

Combination Sum uses i for reuse. Combination Sum II uses i + 1 for no reuse, plus the sort-and-skip from Subsets II. Same backtracking skeleton, two small changes.

🧩 Key Takeaways

  • βœ… Each number may be used only once, so recurse with i + 1, not i.
  • βœ… The array can have duplicates, so sort first to make equal values neighbours.
  • βœ… Skip an equal sibling with the i > start and equal-previous test.
  • βœ… The early break on a too-big value prunes the search once the array is sorted.
  • βœ… An equal value deeper in the path is fine, like the second 1 in [1,1,6].

Check Your Knowledge

4 questions Show quiz Hide quiz

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    How does Combination Sum II differ from Combination Sum in the recursion call?

    Why: Passing i + 1 moves past the current position, so a number is never reused.

  2. 2

    Why must the array be sorted first?

    Why: Sorting groups equal values together, which is what the i > start and equal-previous skip relies on.

  3. 3

    What does the early `break` on a too-big value rely on?

    Why: Because the array is sorted, once a value exceeds the remaining target, all following values do too, so we can break.

  4. 4

    Is the combination [1,1,6] allowed when the input has two 1 values?

    Why: The skip blocks equal siblings at the same level, not an equal value used deeper as a child of the first.

πŸš€ What’s Next?