40. Combination Sum II - Explanation

Problem Link

Description

You are given an array of integers candidates, which may contain duplicates, and a target integer target. Your task is to return a list of all unique combinations of candidates where the chosen numbers sum to target.

Each element from candidates may be chosen at most once within a combination. The solution set must not contain duplicate combinations.

You may return the combinations in any order and the order of the numbers in each combination can be in any order.

Example 1:

Input: candidates = [9,2,2,4,6,1,5], target = 8

Output: [
  [1,2,5],
  [2,2,4],
  [2,6]
]

Example 2:

Input: candidates = [1,2,3,4,5], target = 7

Output: [
  [1,2,4],
  [2,5],
  [3,4]
]

Constraints:

  • 1 <= candidates.length <= 100
  • 1 <= candidates[i] <= 50
  • 1 <= target <= 30


Topics

Recommended Time & Space Complexity

You should aim for a solution with O(n * (2^n)) time and O(n) space, where n is the size of the input array.


Hint 1

A brute-force solution would be to create a hash set, which is used to detect duplicates, to get combinations without duplicates. Can you think of a better way without using a hash set? Maybe you should sort the input array and observe the recursive calls that are responsible for duplicate combinations.


Hint 2

We can use backtracking to generate all combinations whose sum equals the given target. When the input array contains duplicate elements, it may result in duplicate combinations. To avoid this, we can sort the array. Why does sorting help? Because as we traverse the array from left to right, we form combinations with the current element. By skipping duplicate elements, we ensure that the same combinations are not repeated for identical elements. How can you implement this?


Hint 3

We recursively traverse the given array starting at index i, with a variable sum representing the sum of the picked elements. We explore elements from i to the end of the array and extend the recursive path if adding the current element results in sum <= target. When we processed an element, we backtrack and proceed to the next distinct element, skipping any similar elements in the middle to avoid duplicate combinations.


Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Backtracking - Exploring choices by including/excluding elements and undoing decisions when needed
  • Recursion - Building combinations by making decisions at each index in the array
  • Sorting - Arranging elements to group duplicates together for efficient skip logic
  • Handling duplicates - Skipping repeated elements at the same recursion level to avoid duplicate combinations

1. Brute Force

Intuition

The brute-force approach tries every possible subset of the candidate numbers.

  • We sort the array so duplicate combinations appear in the same order.
  • At each index, we have two choices:
    1. Include the current number.
    2. Skip the current number.
  • This produces all subsets (like a binary tree of choices).
  • Whenever a subset’s sum equals the target, we store it.
  • To avoid duplicate combinations, we store each result as a tuple in a set.

This method is easy to understand but slow because it explores all subsets, even invalid or duplicate ones.

Algorithm

  1. Sort the array to keep combinations in consistent order.
  2. Use a recursive function
    dfs(i, currentList, total):
    • If total == target, add the tuple version of currentList to a set.
    • If total > target or i == len(candidates), stop exploring.
  3. At each index i:
    • Include the current number:
      • Add it to currentList, recurse with i + 1, then remove it.
    • Exclude the current number:
      • Recurse with i + 1.
  4. After recursion finishes, convert all unique tuples in the set into lists and return them.
class Solution:
    def combinationSum2(self, candidates, target):
        res = set()
        candidates.sort()

        def generate_subsets(i, cur, total):
            if total == target:
                res.add(tuple(cur))
                return
            if total > target or i == len(candidates):
                return

            cur.append(candidates[i])
            generate_subsets(i + 1, cur, total + candidates[i])
            cur.pop()

            generate_subsets(i + 1, cur, total)

        generate_subsets(0, [], 0)
        return [list(combination) for combination in res]

Time & Space Complexity

  • Time complexity: O(n2n)O(n * 2 ^ n)
  • Space complexity: O(n2n)O(n * 2 ^ n)

2. Backtracking

Intuition

The goal is to choose numbers that sum to the target, but each number can be used once, and the list may contain duplicates.
To avoid generating duplicate combinations, we:

  1. Sort the array so duplicates appear next to each other.
  2. Use backtracking to explore choices:
    • Take the current number.
    • Skip the current number.
  3. When skipping, we skip all duplicates in one jump to avoid creating duplicate combinations like [1,2,2] multiple times.
  4. If the running total exceeds the target, we stop exploring the current path early.

Sorting + skipping duplicates + backtracking ensures we only build valid and unique combinations.

Algorithm

  1. Sort candidates.
  2. Use a recursive function dfs(i, cur, total):
    • If total == target, add a copy of cur to the result.
    • If total > target or i == len(candidates), stop exploring.
  3. Include the current number:
    • Add candidates[i] to cur.
    • Recurse with next index i + 1.
    • Remove the number (backtrack).
  4. Skip duplicates:
    • Advance index i forward while the next number is the same.
  5. Exclude the current number:
    • Call dfs(i + 1, cur, total) after skipping duplicates.
  6. Return the result list.
class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        res = []
        candidates.sort()

        def dfs(i, cur, total):
            if total == target:
                res.append(cur.copy())
                return
            if total > target or i == len(candidates):
                return

            cur.append(candidates[i])
            dfs(i + 1, cur, total + candidates[i])
            cur.pop()


            while i + 1 < len(candidates) and candidates[i] == candidates[i+1]:
                i += 1
            dfs(i + 1, cur, total)

        dfs(0, [], 0)
        return res

Time & Space Complexity

  • Time complexity: O(n2n)O(n * 2 ^n)
  • Space complexity: O(n)O(n)

3. Backtracking (Hash Map)

Intuition

Instead of sorting and skipping duplicates, this method uses a frequency map that stores how many times each number appears.
Example:
If input is [1,1,2,2,2,3], we convert it into:

  • Unique list: [1, 2, 3]
  • Count map: {1: 2, 2: 3, 3: 1}

Now each number can be chosen up to its allowed count, and we explore combinations using backtracking.
This avoids duplicates because we never pick the same number more times than it appears.

At each index i (pointing to unique numbers):

  • Option 1: Take the number
    If its count is still > 0, we include it and reduce the count.
  • Option 2: Skip the number
    Move to next index.

We stop exploring a path when:

  • target == 0 → we found a valid combination
  • target < 0 or i == len(nums) → invalid path

This ensures we explore all valid combinations while preventing duplicates naturally.

Algorithm

  1. Build a frequency map (count[num]++) for all numbers.
  2. Build a list of unique numbers (A).
  3. Use backtracking function backtrack(i, target, cur):
    • If target == 0, add cur to the result.
    • If target < 0 or i is out of bounds, return.
  4. Include nums[i] if available in frequency map:
    • Append number to cur
    • Decrease count
    • Recurse with same index i (because duplicates allowed up to frequency)
    • Backtrack by restoring count and removing number
  5. Exclude nums[i]:
    • Move to i + 1
class Solution:
    def combinationSum2(self, nums, target):
        self.res = []
        self.count = defaultdict(int)
        cur = []
        A = []

        for num in nums:
            if self.count[num] == 0:
                A.append(num)
            self.count[num] += 1
        self.backtrack(A, target, cur, 0)
        return self.res

    def backtrack(self, nums, target, cur, i):
        if target == 0:
            self.res.append(cur.copy())
            return
        if target < 0 or i >= len(nums):
            return

        if self.count[nums[i]] > 0:
            cur.append(nums[i])
            self.count[nums[i]] -= 1
            self.backtrack(nums, target - nums[i], cur, i)
            self.count[nums[i]] += 1
            cur.pop()

        self.backtrack(nums, target, cur, i + 1)

Time & Space Complexity

  • Time complexity: O(n2n)O(n * 2 ^ n)
  • Space complexity: O(n)O(n)

4. Backtracking (Optimal)

Intuition

We need all unique combinations where each number can be used at most once, and duplicates in the input should not create duplicate combinations.

To handle duplicates safely, we:

  1. Sort the array
    This groups equal numbers together, which helps us skip duplicates easily.

  2. Use backtracking where at each index we decide:

    • Take the number
    • Skip the number
  3. To avoid duplicate combinations:

    • If candidates[i] == candidates[i - 1] and we are still in the same level of recursion (i > idx),
      we skip that number.
  4. We stop early if current_sum + candidates[i] > target because the list is sorted.

This approach explores each number only once per combination path and guarantees no repeated results.

Algorithm

  1. Sort the candidates.
  2. Define a DFS function dfs(idx, path, curSum):
    • If curSum == target, add a copy of path to the result.
    • Loop i from idx to end:
      • If i > idx and the current number equals the previous → skip (duplicate control).
      • If adding this number exceeds target → break (pruning).
      • Include the number and recurse with i + 1 (cannot reuse same element).
      • Backtrack by removing the last number.
  3. Call dfs(0, [], 0) and return the result.
class Solution:
    def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
        res = []
        candidates.sort()

        def dfs(idx, path, cur):
            if cur == target:
                res.append(path.copy())
                return
            for i in range(idx, len(candidates)):
                if i > idx and candidates[i] == candidates[i - 1]:
                    continue
                if cur + candidates[i] > target:
                    break

                path.append(candidates[i])
                dfs(i + 1, path, cur + candidates[i])
                path.pop()

        dfs(0, [], 0)
        return res

Time & Space Complexity

  • Time complexity: O(n2n)O(n * 2 ^ n)
  • Space complexity: O(n)O(n)

Common Pitfalls

Generating Duplicate Combinations

The input contains duplicates, and each element can only be used once. Without proper duplicate handling, [1,1,2] with target 3 might produce [1,2] twice. Always sort first and skip consecutive duplicates at the same recursion level.

# Wrong: generates duplicates
for i in range(idx, len(candidates)):
    dfs(i + 1, ...)
# Correct: skip duplicates at same level
for i in range(idx, len(candidates)):
    if i > idx and candidates[i] == candidates[i - 1]:
        continue
    dfs(i + 1, ...)

Reusing Elements (Using i Instead of i + 1)

Unlike Combination Sum I where elements can be reused, this problem requires each element to be used at most once. Recursing with the same index allows reuse.

# Wrong: allows reusing same element
dfs(i, cur, total + candidates[i])
# Correct: move to next element
dfs(i + 1, cur, total + candidates[i])

Forgetting to Sort Before Skipping Duplicates

The duplicate-skipping logic candidates[i] == candidates[i-1] only works on a sorted array. Without sorting, duplicates won't be adjacent and the skip logic fails silently, producing duplicate combinations.