Find all unique combinations of candidates that sum to a target, where each candidate may be reused unlimited times. The key is a backtracking DFS that stays at the same index to allow reuse, and advances the start index at each loop iteration to prevent duplicate sets.
Given a list of distinct positive integers candidates and a target, return all unique combinations that sum to target. Each candidate may be used any number of times. Order within a combination doesn't matter, but no two combinations in the result may be the same multiset. candidates=[2,3,6,7], target=7 → [[2,2,3],[7]]. There are exactly two ways: three 2’s and a 3, or a single 7.
i (not i+1). To prevent generating the same multiset twice (e.g. both [2,3,2] and [2,2,3]), each recursive call only considers candidates at index start or later — you never go backwards. Sort first so you can prune entire branches early once a candidate exceeds the remaining target.[...path]) and return.start to end. The start parameter prevents going back to earlier candidates and thus avoids duplicate multisets.candidates[i] > remaining, break — no point continuing (sorted).candidates[i] onto path, then recurse with dfs(i, …) — same i, not i+1, so this candidate can be reused.i+1, exploring the branch that skips this candidate.i+1to the recursive call thinking “move forward so we don't repeat.” That prevents reuse — you'd never produce [2,2,3] at all. The correct rule: pass i to permit reuse; the outer loop handles the no-duplicates guarantee by never going backwards.Worst-case time is O(nt/m) where t is the target and m is the smallest candidate (maximum depth t/m, n branches at each level). Space is O(t/m) for the recursion stack plus output. Sorting is O(n log n) but dominates only for tiny inputs.
7.1function combinationSum(candidates: number[], target: number): number[][] {2▶ const result: number[][] = [];3▶ const sorted = [...candidates].sort((a, b) => a - b);45 function dfs(start: number, path: number[], remaining: number): void {6 if (remaining === 0) {7 result.push([...path]); // found a valid combination8 return;9 }10 for (let i = start; i < sorted.length; i++) {11 if (sorted[i] > remaining) break; // pruned: sorted, so all further also too large1213 path.push(sorted[i]);14 dfs(i, path, remaining - sorted[i]); // i not i+1: allow reuse of same candidate15 path.pop();16 }17 }1819▶ dfs(0, [], target);20 return result;21}
function combinationSum(candidates: number[], target: number): number[][] {
const result: number[][] = [];
const sorted = [...candidates].sort((a, b) => a - b);
function dfs(start: number, path: number[], remaining: number): void {
if (remaining === 0) {
result.push([...path]); // found a valid combination
return;
}
for (let i = start; i < sorted.length; i++) {
if (sorted[i] > remaining) break; // pruned: sorted, so all further also too large
path.push(sorted[i]);
dfs(i, path, remaining - sorted[i]); // i not i+1: allow reuse of same candidate
path.pop();
}
}
dfs(0, [], target);
return result;
}remaining reaches exactly 0, the path is a valid combination. We push a copy ([...path]) because path is mutated as the recursion unwinds.start, not 0, so we never pick a candidate that came before the current one in the sorted order. This guarantees no duplicate multisets. The break on line 11 exploits the sort: once sorted[i] > remaining, every subsequent candidate is even larger — safe to skip the entire tail.i (not i+1) is the reuse mechanism — the same candidate can appear multiple times. After the recursive call returns, path.pop() restores the path so the loop can explore the next candidate.if (i > start && candidates[i] === candidates[i-1]) continue and pass i+1 to the recursive call.dp[j] += dp[j - c].(path, start, remaining) tuples. Harder to read than the recursive version; only worth it if the interviewer asks.function combinationSum(candidates: number[], target: number): number[][] {
const result: number[][] = [];
const sorted = [...candidates].sort((a, b) => a - b);
function dfs(start: number, path: number[], remaining: number): void {
if (remaining === 0) {
result.push([...path]); // found a valid combination
return;
}
for (let i = start; i < sorted.length; i++) {
if (sorted[i] > remaining) break; // pruned: sorted, so all further also too large
path.push(sorted[i]);
dfs(i, path, remaining - sorted[i]); // i not i+1: allow reuse of same candidate
path.pop();
}
}
dfs(0, [], target);
return result;
}At every step try each candidate (reuse allowed) and recurse on the smaller remaining target. This generates the same multiset in different orders, so canonicalise by sorting each successful path and store it in a set to drop the duplicates.
function combinationSum(candidates: number[], target: number): number[][] {
const seen = new Set<string>();
const result: number[][] = [];
function dfs(path: number[], remaining: number): void {
if (remaining === 0) {
const key = [...path].sort((a, b) => a - b).join(',');
if (!seen.has(key)) { // collapse permutations of the same combo
seen.add(key);
result.push([...path].sort((a, b) => a - b));
}
return;
}
if (remaining < 0) return;
for (const c of candidates) { // no 'start' index — every candidate every time
path.push(c);
dfs(path, remaining - c);
path.pop();
}
}
dfs([], target);
return result;
}[2,3] and [3,2] — which a set then has to deduplicate. Threading a start index (the optimal) only ever moves forward, so each combination is generated exactly once with no set needed.| "combinations that sum to target, reuse allowed" | backtracking DFS with start index i |
| "unique combinations / no duplicate sets" | pass start index, never go backwards |
| "prune when candidate > remaining" | sort + break (not continue) |
| "count combinations (not list)" | unbounded knapsack DP — O(n·target) |
function combinationSum(candidates: number[], target: number): number[][] {
const result: number[][] = [];
candidates.sort((a, b) => a - b);
function dfs(start: number, path: number[], remaining: number): void {
if (remaining === 0) { result.push([...path]); return; }
for (let i = start; i < candidates.length; i++) {
if (candidates[i] > remaining) break; // prune
path.push(candidates[i]);
dfs(i, path, remaining - candidates[i]); // reuse: i, not i+1
path.pop();
}
}
dfs(0, [], target);
return result;
}candidates=[2,3,6,7], target=7, what does the function return?if (sorted[i] > remaining) break; — why break and not continue?