Find all unique combinations in a candidate list that sum to a target, where each number may be used at most once. Sort first, then backtrack — advancing to i + 1 to enforce single-use and skipping duplicate siblings at the same depth to kill duplicate results.
Given a list of candidates (may contain duplicates) and a target integer, return all unique combinations where the chosen numbers sum to the target. Each candidate may only be used once per combination. For example, candidates = [10,1,2,7,6,1,5], target = 8 should return [[1,1,6],[1,2,5],[1,7],[2,6]] — four unique combos, despite the duplicate 1s in the input.
candidates[i] === candidates[i-1] and i > start, this sibling would produce the exact same sub-tree as the previous sibling — skip it. The i > start guard preserves the first occurrence so we can still use each value in a child call. Advancing to i + 1 (not i) prevents reuse of the same index.candidates.sort() so duplicates are adjacent and so the array is monotone for the pruning step.remaining === 0, snapshot the current path and return — we found a valid combination.start. Try each index i from start onward as the next element to include.i > start && candidates[i] === candidates[i-1], this choice is identical to one already explored at the same depth — skip to avoid duplicate results.candidates[i] > remaining, all future candidates are also too large — break.candidates[i], call backtrack(i + 1, remaining - candidates[i], path) (note i + 1, not i), then pop.i > start, not i > 0. Using i > 0 incorrectly blocks using the same value in a child call (e.g., the two 1s in [1,1,6] are from the same sorted list but at different depths, which is legal).Space is O(target / min) for the recursion depth (at most that many elements can fit). The sort is O(n log n) and typically dominates the useful work.
candidates[i] === candidates[i-1] && !used[i-1]), and Palindrome Partitioning. Any backtracking problem with duplicates in the input reaches for this pattern.[1, 1, 2, 5, 6, 7, 10]. Target: 8. Starting backtrack from index 0.1function combinationSum2(candidates: number[], target: number): number[][] {2▶ candidates.sort((a, b) => a - b);3▶ const result: number[][] = [];45 function backtrack(start: number, remaining: number, path: number[]): void {6 if (remaining === 0) {7 result.push([...path]);8 return;9 }10 for (let i = start; i < candidates.length; i++) {11 // Skip duplicate siblings at the same recursion depth12 if (i > start && candidates[i] === candidates[i - 1]) continue;13 // Prune: sorted array means further candidates only grow14 if (candidates[i] > remaining) break;15 path.push(candidates[i]);16 backtrack(i + 1, remaining - candidates[i], path);17 path.pop();18 }19 }2021 backtrack(0, target, []);22 return result;23}
function combinationSum2(candidates: number[], target: number): number[][] {
candidates.sort((a, b) => a - b);
const result: number[][] = [];
function backtrack(start: number, remaining: number, path: number[]): void {
if (remaining === 0) {
result.push([...path]);
return;
}
for (let i = start; i < candidates.length; i++) {
// Skip duplicate siblings at the same recursion depth
if (i > start && candidates[i] === candidates[i - 1]) continue;
// Prune: sorted array means further candidates only grow
if (candidates[i] > remaining) break;
path.push(candidates[i]);
backtrack(i + 1, remaining - candidates[i], path);
path.pop();
}
}
backtrack(0, target, []);
return result;
}break early.remaining hits zero the current path sums exactly to target. We spread-copy the path before pushing because the same array is mutated throughout the recursion.i > start means this is not the first option at this depth. If it equals the previous option, the sub-tree it would produce is identical to one we just finished — skip it. The guard must be i > start, not i > 0, so that a parent call can still pick this value.candidates[i] > remaining, every subsequent candidate is also too large. A break (not continue) cuts the entire remaining loop.i + 1 (single-use — never revisit the same index), then pop to restore state for the next sibling.O(2ⁿ) worst case (all subsets), but pruning and deduplication make the practical cost far lower. Space: O(target / min(candidates)) for the call stack depth.i instead of i + 1 — that's Combination Sum I (LC 39).O(n · target) time without enumerating them.used[] array and skip when candidates[i] === candidates[i-1] && !used[i-1] to handle duplicates in full-permutation context.function combinationSum2(candidates: number[], target: number): number[][] {
candidates.sort((a, b) => a - b);
const result: number[][] = [];
function backtrack(start: number, remaining: number, path: number[]): void {
if (remaining === 0) {
result.push([...path]);
return;
}
for (let i = start; i < candidates.length; i++) {
// Skip duplicate siblings at the same recursion depth
if (i > start && candidates[i] === candidates[i - 1]) continue;
// Prune: sorted array means further candidates only grow
if (candidates[i] > remaining) break;
path.push(candidates[i]);
backtrack(i + 1, remaining - candidates[i], path);
path.pop();
}
}
backtrack(0, target, []);
return result;
}O(2ⁿ) worst case (all subsets), but pruning and deduplication make the practical cost far lower. Space: O(target / min(candidates)) for the call stack depth.Each candidate is either in or out, so walk all 2^n subsets, keep the ones whose sum equals target, and drop duplicates by stringifying each sorted combination into a set.
function combinationSum2(candidates: number[], target: number): number[][] {
const sorted = [...candidates].sort((a, b) => a - b);
const seen = new Set<string>();
const result: number[][] = [];
function subsets(i: number, path: number[], sum: number): void {
if (sum === target) {
const key = path.join(','); // path stays sorted as we go
if (!seen.has(key)) {
seen.add(key);
result.push([...path]);
}
return;
}
if (i === sorted.length || sum > target) return;
// Include candidate i (used once).
path.push(sorted[i]);
subsets(i + 1, path, sum + sorted[i]);
path.pop();
// Exclude candidate i.
subsets(i + 1, path, sum);
}
subsets(0, [], 0);
return result;
}2^n subsets even when sums have already shot past target, and leans on a set to undo the duplicate combinations it generates. Pruning with if (candidates[i] > remaining) break and skipping equal siblings at the same depth (the optimal) avoids the wasted branches and the dedup set entirely.| "combinations / subsets, no reuse, duplicates in input" | sort + backtrack(i+1) + skip-sibling |
| backtracking produces duplicate results | sort + if (i > start && arr[i] === arr[i-1]) continue |
| "all combinations that sum to target" | backtracking with remaining budget |
| large input, many duplicates | prune with break when candidates[i] > remaining |
candidates.sort((a, b) => a - b);
const result: number[][] = [];
function backtrack(start: number, remaining: number, path: number[]): void {
if (remaining === 0) { result.push([...path]); return; }
for (let i = start; i < candidates.length; i++) {
if (i > start && candidates[i] === candidates[i - 1]) continue; // skip dup siblings
if (candidates[i] > remaining) break; // prune
path.push(candidates[i]);
backtrack(i + 1, remaining - candidates[i], path);
path.pop();
}
}
backtrack(0, target, []);
return result;candidates = [10,1,2,7,6,1,5], target = 8, how many unique valid combinations exist?break instead of continue in the candidate loop?candidates = [2,5,2,1,2], target = 5. After sorting, the array is [1,2,2,2,5]. How many sibling-skip events happen at the top depth (start=0)?