40. Combination Sum II - Explanation
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 <= 1001 <= candidates[i] <= 501 <= 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.
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:
- Include the current number.
- 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
- Sort the array to keep combinations in consistent order.
- Use a recursive function
dfs(i, currentList, total):- If
total == target, add the tuple version ofcurrentListto a set. - If
total > targetori == len(candidates), stop exploring.
- If
- At each index
i:- Include the current number:
- Add it to
currentList, recurse withi + 1, then remove it.
- Add it to
- Exclude the current number:
- Recurse with
i + 1.
- Recurse with
- Include the current number:
- 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:
- Space complexity:
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:
- Sort the array so duplicates appear next to each other.
- Use backtracking to explore choices:
- Take the current number.
- Skip the current number.
- When skipping, we skip all duplicates in one jump to avoid creating duplicate combinations like
[1,2,2]multiple times. - 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
- Sort
candidates. - Use a recursive function
dfs(i, cur, total):- If
total == target, add a copy ofcurto the result. - If
total > targetori == len(candidates), stop exploring.
- If
- Include the current number:
- Add
candidates[i]tocur. - Recurse with next index
i + 1. - Remove the number (backtrack).
- Add
- Skip duplicates:
- Advance index
iforward while the next number is the same.
- Advance index
- Exclude the current number:
- Call
dfs(i + 1, cur, total)after skipping duplicates.
- Call
- 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 resTime & Space Complexity
- Time complexity:
- Space complexity:
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 combinationtarget < 0ori == len(nums)→ invalid path
This ensures we explore all valid combinations while preventing duplicates naturally.
Algorithm
- Build a frequency map (
count[num]++) for all numbers. - Build a list of unique numbers (
A). - Use backtracking function
backtrack(i, target, cur):- If
target == 0, addcurto the result. - If
target < 0oriis out of bounds, return.
- If
- 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
- Append number to
- Exclude
nums[i]:- Move to
i + 1
- Move to
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:
- Space complexity:
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:
Sort the array
This groups equal numbers together, which helps us skip duplicates easily.Use backtracking where at each index we decide:
- Take the number
- Skip the number
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.
- If
We stop early if
current_sum + candidates[i] > targetbecause the list is sorted.
This approach explores each number only once per combination path and guarantees no repeated results.
Algorithm
- Sort the candidates.
- Define a DFS function
dfs(idx, path, curSum):- If
curSum == target, add a copy ofpathto the result. - Loop
ifromidxto end:- If
i > idxand 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.
- If
- If
- 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 resTime & Space Complexity
- Time complexity:
- Space complexity:
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.
Sign in to join the discussion