Problem Walkthrough

Combination Sum II LeetCode 40 Guide

Sort the candidates first, then backtrack with index+1 to prevent element reuse — and skip duplicates at each recursion level to guarantee unique combinations.

8 min read|

Combination Sum II

LeetCode 40

Problem Overview

Combination Sum II (LeetCode 40) gives you a candidates array that may contain duplicate values and a target integer. Your task is to find all unique combinations of candidates that sum exactly to the target, where each candidate may only be used once in a combination.

Unlike its predecessor Combination Sum I, the input array here can have duplicates — for example, two 1s in the candidates list. This changes the problem fundamentally: you must avoid producing the same combination twice even though the raw input contains repeated values.

The constraint that makes this problem tricky is dual: you cannot reuse an element (unlike Problem 39), and you cannot produce duplicate combinations even when the input has repeated values. Getting both constraints right simultaneously is the central challenge.

  • Input: candidates array with possible duplicates, integer target
  • Each element may be used at most once in a combination
  • No duplicate combinations allowed in the output
  • All numbers are positive integers
  • Return all unique combinations that sum to target

From Combination Sum I to II

In Combination Sum I (LeetCode 39), elements can be reused as many times as needed. The backtracking call passes the same index: recurse(i, remaining - nums[i]). Because you stay at index i, you can pick the same element again on the next level.

In Combination Sum II, each element is used at most once. The fix for reuse is straightforward: pass i+1 instead of i so the next level starts after the current element. But that alone is insufficient — the input can have duplicates, and naively picking nums[i] at position 0 and nums[i] at position 1 (when both equal 1) produces identical combinations.

Sorting the candidates is the enabling step. Once sorted, duplicate values are adjacent. You can then detect at the same recursion level whether you are about to explore a value that was already explored from this position — and skip it. This is the complete solution: sort, pass i+1, and skip adjacent duplicates at the same level.

💡

Key Difference from Combination Sum I

Pass start+1 instead of start to prevent element reuse. Skip nums[i] == nums[i-1] when i > start to avoid duplicate combinations at the same level. Two small changes, one fundamentally different problem.

Sort + Backtracking Algorithm

The algorithm begins by sorting candidates in non-decreasing order. Sorting serves two purposes: it enables the adjacent-duplicate skip condition, and it enables early termination — once a candidate exceeds the remaining target, all subsequent candidates will too, so you can break instead of continue.

The backtrack function takes three parameters: start (the index to begin choosing from), remaining (how much target is still needed), and path (the current combination being built). The base case is remaining == 0, meaning the current path sums to target and should be added to results.

For each index i from start to n-1, apply the skip condition first: if i > start and nums[i] == nums[i-1], skip this index. Then if nums[i] > remaining, break entirely. Otherwise, append nums[i] to path, recurse with (i+1, remaining-nums[i]), and backtrack by removing the last element.

  1. 1Sort candidates in non-decreasing order
  2. 2Call backtrack(start=0, remaining=target, path=[])
  3. 3Base case: if remaining == 0, append copy of path to results
  4. 4Loop i from start to n-1
  5. 5If i > start and nums[i] == nums[i-1]: continue (skip duplicate)
  6. 6If nums[i] > remaining: break (pruning — all further elements are too large)
  7. 7Append nums[i] to path, recurse with (i+1, remaining - nums[i], path)
  8. 8Remove last element from path (backtrack)

Why the Skip Condition Uses i > start

The condition i > start and nums[i] == nums[i-1] is precise. The i > start part means we are not at the first choice position for this recursion level. The nums[i] == nums[i-1] part means the current value equals the previous value. Together: we are at the same level and this exact value was already explored from this position.

When i == start, it is the first choice at this level — it is always valid to pick it regardless of what value nums[i] holds. The skip only fires for subsequent positions at the same level where the same value has already been tried.

Removing the i > start guard would incorrectly skip valid first choices. For example, if the first candidate at some level is 1 and the previous call also ended on 1, you would wrongly skip a legitimate starting point. The guard ensures correctness while still eliminating all true duplicates.

ℹ️

Pattern Transfer from Subsets II

This is the exact same skip condition as Subsets II (LeetCode 90). The pattern transfers directly: sort the input, loop with i from start, skip if i > start and nums[i] == nums[i-1]. Recognizing this family of problems is a significant interview advantage.

Walk-Through Example

Take candidates = [10, 1, 2, 7, 6, 1, 5] and target = 8. After sorting: [1, 1, 2, 5, 6, 7, 10]. The two 1s are now adjacent, setting up the duplicate skip.

Starting from index 0, the first branch picks nums[0] = 1 (remaining = 7). From there it picks nums[1] = 1 (remaining = 6), then tries nums[2] = 2, ..., eventually finds [1, 1, 6]. It also finds [1, 2, 5] and [1, 7] along this branch. When the loop reaches index 1 (the second 1) with i > 0 = start and nums[1] == nums[0], it skips — preventing a duplicate [1, ...] branch at the top level.

The final results are [1, 1, 6], [1, 2, 5], [1, 7], and [2, 6]. Notice there is no [1, 1, 6] appearing twice even though there are two 1s in the input — that is the skip condition doing its job correctly.

  1. 1Input: candidates=[10,1,2,7,6,1,5], target=8
  2. 2After sort: [1, 1, 2, 5, 6, 7, 10]
  3. 3Branch from index 0 (pick 1, remaining=7)
  4. 4 Branch from index 1 (pick 1, remaining=6) → finds [1,1,6]
  5. 5 Branch from index 2 (pick 2, remaining=5) → finds [1,2,5]
  6. 6 Branch from index 5 (pick 7, remaining=0) → finds [1,7]
  7. 7Skip index 1 at top level (i=1 > start=0 and nums[1]==nums[0]==1)
  8. 8Branch from index 2 (pick 2, remaining=6) → finds [2,6]
  9. 9Final results: [[1,1,6], [1,2,5], [1,7], [2,6]]

Code Walkthrough — Python and Java

In Python, sort candidates with candidates.sort(), define results = [] outside, and write backtrack(start, remaining, path). The loop is for i in range(start, len(candidates)). The skip is if i > start and candidates[i] == candidates[i-1]: continue. The prune is if candidates[i] > remaining: break. Otherwise: path.append, backtrack(i+1, remaining-candidates[i], path), path.pop().

In Java, Arrays.sort(candidates) first. Inside the recursive method, use a for loop with the same i > start duplicate check and the remaining < candidates[i] break. Use a LinkedList as the path and add/removeLast for O(1) operations at both ends. Add new ArrayList<>(path) to results when remaining == 0.

Time complexity is O(2^n) in the worst case — each element is either included or excluded. In practice, the sort+break pruning eliminates large portions of the search tree when candidates have many values larger than the remaining target. Space complexity is O(n) for the recursion stack depth, where n is the number of candidates.

⚠️

Break Requires Sorted Input

The break optimization — stopping the loop when nums[i] > remaining — only works because the input is sorted. Once one candidate exceeds the remaining target, all subsequent candidates are also too large. Without sorting, you would need continue instead of break, losing significant pruning power.

Ready to master algorithm patterns?

YeetCode flashcards help you build pattern recognition through active recall and spaced repetition.

Start practicing now