top of page

Combination Sum II

  • 2 days ago
  • 7 min read

The Duplicate Problem

Combination Sum II has the same goal as Combination Sum - find all combinations that sum to a target - with one crucial difference. Each number can only be used once, and the input may contain duplicates. That single change forces you to be much more deliberate about how you explore the search space. Get the duplicate handling wrong and you'll produce repeated combinations. Get it too aggressive and you'll miss valid ones.


Problem Statement

You are given a collection of candidate numbers candidates and a target number target.


Return all unique combinations in candidates where the chosen numbers sum to target.

  • Each number in candidates may be used at most once.

  • The input array may contain duplicate numbers.

  • The solution set must not contain duplicate combinations.


Examples:

  • 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]]

  • candidates = [1,1,1,1], target = 2 → [[1,1]]


1. Clarify Requirements Before Jumping Into Code

Before coding, lock down the constraints carefully, as this problem has more of them than Combination Sum.

Input: An array of positive integers (may contain duplicates) and a target integer.

Output: All unique combinations that sum exactly to target, where each element is used at most once.


Key rules to confirm:

  • [1, 2, 5] and [2, 1, 5] are the same combination - ordering in paths doesn't matter.

  • Is candidates sorted? Not necessarily.

  • Duplicate numbers in the input can produce duplicate combinations if you're careless.

  • All numbers are positive - sums only grow, so pruning is safe.


This is where paying close attention to the problem statement pays off. Combination Sum allowed reuse and had no duplicates. This problem disallows reuse and introduces duplicates. Both changes need to be handled, and they need to be handled independently.


2. Identify the Category of the Problem

This is backtracking - the same choose → explore → un-choose structure from Combination Sum, Subsets, and Permutations. But this problem has the most constrained version of the pattern so far:

  • No reuse: advance to i + 1 after choosing element i.

  • No duplicate combinations: skip candidates at the same recursion depth that have already been tried.


The second constraint is the new one, and it's where most candidates go wrong. Understanding why the deduplication logic works - not just what it is - is the real test.


3. Start With Brute Force to Build Intuition

The naive approach generates every possible subset of candidates, filters the ones that sum to target, then deduplicates the results.


The problems here are compounding. First, there are 2ⁿ subsets - expensive to enumerate. Second, deduplicating afterward requires sorting each combination and comparing it to every other - even more expense. Third, and most importantly, it completely sidesteps the interesting part of the problem: learning to structure the search so duplicates never appear in the first place.


The brute force solution does provide some insights around what we need to focus on for our final solution: a way to avoid generating the same combination twice, without doing extra work after the fact.


4. Brainstorm More Solutions

Step 1: Start From What You Already Know

Compare this problem directly to Combination Sum. In that problem:

  • Numbers could be reused → we passed i (not i + 1) in the recursive call.

  • No duplicate inputs → no duplicate combinations could arise.


Here, both of those change. Let's handle them one at a time.

Fixing reuse: Easy. Pass i + 1 instead of i in the recursive call. That moves past the current element, so it can't be picked again in the same combination.

Fixing duplicates: This is harder. Looks dive deeper on this requirement.


Step 2: Understand Why Duplicates Arise

Suppose candidates = [1, 1, 2] and target = 3.


After sorting, the array is [1, 1, 2]. Let's label them 1a, 1b, and 2 to tell them apart. Without any deduplication logic, backtracking would find:

[1a, 2]
[1b, 2]
[1a, 1b]

But [1a, 2] and [1b, 2] are the same combination - both are [1, 2]. We've generated a duplicate because we used the two identical 1s as if they were distinguishable.


The root cause: at the same level of the recursion tree, we tried 1a as our choice and also tried 1b as our choice, but since they have the same value, both choices lead to the same set of combinations downstream.


Step 3: The Fix - Skip Duplicates at the Same Depth

If we're at a given recursion depth and we've already tried a candidate with value v, there's really no point trying other candidates with that same value at our current recursion depth. It will generate exactly the same combinations.


What kind of conditional clause do we need to tell us if the current candidate has the same value as the previous one?

Something like: candidates[i] == candidates[i - 1]


Now, we know we only want to skip duplicates at the same depth, and not across different depths. Otherwise we risk eliminating valid combinations like [1, 1] from [1, 1, 2]. How do we turn that into a conditional clause?

Something like i > start should work, to ensure that when we're looping through our candidates, we don't look back at candidates that we've already passed


The condition that captures this:

if (i > start && candidates[i] == candidates[i - 1]) {
    continue;
}

Together: "if this value is a duplicate of something we already tried at this recursion level, skip it."


So to summarize our solution:

  • Backtracking to explore all possible combinations

  • Sorting the candidates to enable early pruning

  • Skipping duplicate candidates at same recursion levels to ensure no duplicate combinations


5. Trade-Offs at a Glance

Approach

Time

Space

Notes

Brute-force subsets + dedup

O(2ⁿ × n)

Large

Conceptually simple; wasteful

Backtracking

O(2ⁿ)

O(n)

Clean, pruned, correct

Backtracking + sort + early break

O(2ⁿ)

O(n)

Faster in practice

The time complexity is exponential for the same reason as Combination Sum - in the worst case, many combinations may be valid and must be enumerated. Sorting enables the early break when candidates[i] > remaining, which prunes dead-end branches before they're explored.


6. Pseudocode

sort candidates

result = empty list

backtrack(start, remaining, path):
    if remaining == 0:
        add copy of path to result
        return

    for i from start to candidates.length - 1:
        if i > start AND candidates[i] == candidates[i-1]:
            continue    ← skip duplicate values at the same depth

        if candidates[i] > remaining:
            break       ← prune: no point continuing (array is sorted)

        add candidates[i] to path
        backtrack(i + 1, remaining - candidates[i], path)   ← i+1: no reuse
        remove last element from path

Compared to Combination Sum's pseudocode, there are exactly two differences: i + 1 instead of i in the recursive call, and the duplicate-skip condition at the top of the loop. Everything else is identical.


7. Edge Cases

  • No valid combination: The recursion prunes or exhausts all paths without hitting remaining == 0. Returns [].

  • All duplicates: [1,1,1,1], target = 2 → [[1,1]]. Only one combination despite four identical elements — the skip condition collapses the duplicates correctly.

  • Target smaller than all candidates: [3,4,5], target = 2 → []. The break triggers on the first iteration of every call.

  • Single element: [1], target = 1 → [[1]]. Straightforward - no duplicates to manage.


8. Full Solution

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class CombinationSumII {

    public static List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);  // required for both duplicate skipping and early break
        List<List<Integer>> result = new ArrayList<>();
        backtrack(candidates, 0, target, new ArrayList<>(), result);
        return result;
    }

    private static void backtrack(int[] candidates, int start,
                                   int remaining,
                                   List<Integer> path,
                                   List<List<Integer>> result) {

        if (remaining == 0) {
            // copy — not a reference
            result.add(new ArrayList<>(path));  
            return;
        }

        for (int i = start; i < candidates.length; i++) {
            // Skip duplicate values at the same recursion depth
            if (i > start && candidates[i] == candidates[i - 1])
                continue;

            // Prune subsequent candidates  that are too large
            if (candidates[i] > remaining) 
                break;

            path.add(candidates[i]);
            backtrack(candidates, i + 1, remaining - candidates[i], path, result);
            path.remove(path.size() - 1);
        }
    }
}

9. Test Your Solution

// Standard case with duplicates in input
System.out.println(combinationSum2(new int[]{10,1,2,7,6,1,5}, 8));
// [[1,1,6], [1,2,5], [1,7], [2,6]]

// Multiple duplicates
System.out.println(combinationSum2(new int[]{2,5,2,1,2}, 5));
// [[1,2,2], [5]]

// All identical elements
System.out.println(combinationSum2(new int[]{1,1,1,1}, 2));
// [[1,1]]  ← not [[1,1], [1,1], [1,1], ...] — dedup is working

// No valid combination
System.out.println(combinationSum2(new int[]{3,4,5}, 2));
// []

// Single element
System.out.println(combinationSum2(new int[]{1}, 1));
// [[1]]

The [1,1,1,1] test is the most important one to examine closely. If your deduplication logic is right, you get exactly one [1,1]. If you get multiple copies, the i > start guard is missing or wrong. If you get nothing, the guard is too aggressive and is skipping valid combinations across depths.


10. Key Lessons for Future Problems

  • Sorting can sometimes be a prerequisite, not just an optimization. The duplicate-skip logic requires that equal values are adjacent. Without sorting, candidates[i] == candidates[i-1] means nothing reliable. Sort first when the input may contain duplicates.

  • Skip duplicates at the same depth, not across depths. The i > start condition is the heart of this problem. Understand it mechanically: i > start means "we've already tried at least one candidate at this recursion level." If the current candidate has the same value as the previous one, trying it will produce the same results - skip it. But if we're at a new recursion depth (i == start), we haven't tried anything yet at this level, so even a duplicate value gets a fresh chance.

  • "Use once" and "no duplicate combinations" are different constraints. "Use once" is handled by advancing to i + 1. "No duplicate combinations" is handled by the skip condition. These solve different problems and must both be present. Conflating them is a common source of bugs.


Understand the whole backtracking family together. You've now seen four variations of the same pattern:

Problem

Reuse?

Duplicate inputs?

Record at?

No

No

Only when path is full

No

No

Every level

Yes

No

Only at remaining == 0

Combination Sum II

No

Yes

Only at remaining == 0

Seeing them as a family, rather than four separate problems to memorize, means you can reconstruct any of them from first principles. When you encounter a new backtracking problem in an interview, you can ask yourself: does it allow reuse? Does the input have duplicates? When do I record a result? The answers tell you exactly how to adapt the skeleton.


Combination Sum II is a precision test. The algorithm isn't complex - it's one condition added to code you've already written. What it tests is whether you understand that condition deeply enough to get it exactly right under pressure.


Good Luck and Happy Coding!

Recent Posts

See All
Combination Sum

Learn how to solve the Combination Sum coding problem to prepare for your next technical interview! Combination Sum is a structured exploration problem where you need to build valid combinations while

 
 
 

Comments


Drop Me a Line, Let Me Know What You Think

Thanks for submitting!

© 2023 by Train of Thoughts. Proudly created with Wix.com

bottom of page