Combination Sum II
Problem Description
Given a collection of candidate numbers candidates and a target number target, find all unique combinations in candidates where the candidate numbers sum to target.
Each number in candidates may only be used once in the combination.
The solution set must not contain duplicate combinations.
Examples
Example 1:Input: candidates = [10,1,2,7,6,1,5], target = 8 Output: [[1,1,6],[1,2,5],[1,7],[2,6]]
Input: candidates = [2,5,2,1,2], target = 5 Output: [[1,2,2],[5]]
Constraints
1 ≤ candidates.length ≤ 1001 ≤ candidates[i] ≤ 501 ≤ target ≤ 30
The Duplicate Problem Is Structural, Not Accidental
The core tension in this problem is that the input can have duplicate values, but the output cannot have duplicate combinations. The brute-force instinct is to use a set to deduplicate results after generating everything, but that masks the actual insight the problem tests.
Sort the array first. Once sorted, duplicate values are adjacent. In your backtracking loop, if you’re about to pick a value that’s the same as the one you just tried at this recursion depth, skip it. That one check prevents you from ever generating a duplicate combination in the first place, which is far cleaner and faster than post-processing.
Solution: Backtracking with Duplicate Skip
import java.util.*;
class Solution {
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates); // sorting makes duplicate-skipping work
List<List<Integer>> result = new ArrayList<>();
backtrack(candidates, target, 0, new ArrayList<>(), result);
return result;
}
private void backtrack(int[] candidates, int remaining, int start,
List<Integer> current, List<List<Integer>> result) {
if (remaining == 0) {
result.add(new ArrayList<>(current));
return;
}
for (int i = start; i < candidates.length; i++) {
if (candidates[i] > remaining) break; // sorted, so nothing after helps
// skip duplicates at the same recursion depth
if (i > start && candidates[i] == candidates[i - 1]) continue;
current.add(candidates[i]);
backtrack(candidates, remaining - candidates[i], i + 1, current, result);
current.remove(current.size() - 1);
}
}
}class Solution:
def combinationSum2(self, candidates: list[int], target: int) -> list[list[int]]:
candidates.sort() # sorting makes duplicate-skipping work
result = []
def backtrack(start: int, remaining: int, current: list[int]):
if remaining == 0:
result.append(current[:])
return
for i in range(start, len(candidates)):
if candidates[i] > remaining:
break # sorted, so nothing after helps
# skip duplicates at the same recursion depth
if i > start and candidates[i] == candidates[i - 1]:
continue
current.append(candidates[i])
backtrack(i + 1, remaining - candidates[i], current)
current.pop()
backtrack(0, target, [])
return result#include <vector>
#include <algorithm>
class Solution {
public:
std::vector<std::vector<int>> combinationSum2(std::vector<int>& candidates, int target) {
std::sort(candidates.begin(), candidates.end());
std::vector<std::vector<int>> result;
std::vector<int> current;
backtrack(candidates, target, 0, current, result);
return result;
}
private:
void backtrack(std::vector<int>& candidates, int remaining, int start,
std::vector<int>& current, std::vector<std::vector<int>>& result) {
if (remaining == 0) {
result.push_back(current);
return;
}
for (int i = start; i < (int)candidates.size(); i++) {
if (candidates[i] > remaining) break;
// skip duplicates at the same recursion depth
if (i > start && candidates[i] == candidates[i - 1]) continue;
current.push_back(candidates[i]);
backtrack(candidates, remaining - candidates[i], i + 1, current, result);
current.pop_back();
}
}
};Complexity Analysis:
- Time Complexity: O(2^n) in the worst case, where every subset is explored. The duplicate-skip prune cuts this significantly in practice.
- Space Complexity: O(target / min_candidate) for the maximum recursion depth.
Where it breaks: the condition i > start is critical. If you write i > 0 instead, you’d skip valid combinations. For example, with candidates = [1,1,2] and target = 3, you need both 1s. The check i > start ensures you only skip a duplicate if you’re at the same choice point, not if it’s a genuinely new one deeper in the tree.
Why Not Just Use a Set to Deduplicate?
You can, and it works. But it generates far more subsets before filtering, which costs more time and memory. With target = 30 and small candidates, the set approach can generate thousands of duplicates that a sorted skip avoids entirely.
Common Mistakes
- Using
i > 0instead ofi > startfor the duplicate check. This incorrectly skips valid combinations that use the same value at different recursion levels. - Passing
iinstead ofi + 1to the recursive call. Each candidate can only be used once, so the next call must start after the current index. - Not sorting first. The duplicate-skip logic only works when equal values are adjacent.
Frequently Asked Questions
How is this different from Combination Sum?
In Combination Sum, each number can be reused unlimited times (you recurse with the same i). Here, each number is used at most once (you recurse with i + 1).
What if I need to count combinations instead of list them? That becomes a 2D DP problem (similar to Coin Change II). Backtracking is for when you need the actual combinations, not just the count.