Problem overview
The method combinationSum2 receives candidates, an array of positive integers that may repeat, and a target. It returns every distinct combination of candidates that adds up to target. Each candidate is used at most once, so a value that appears twice in the array can appear up to twice in one combination. Two combinations are the same when they hold the same values the same number of times, whichever copies they use.
Each combination is written in ascending order, and the combinations come in lexicographic order: compare number by number, and the first difference decides. When nothing adds up to target the answer is an empty array.
For candidates = [10, 1, 2, 7, 6, 1, 5] and target = 8, the sorted candidates are [1, 1, 2, 5, 6, 7, 10] and the answer is [[1, 1, 6], [1, 2, 5], [1, 7], [2, 6]]. The combination [1, 7] can be made with either 1, yet it is listed once. For [2, 3] with target 4 the answer is []: 2 + 3 is 5, and the single 2 can't be used twice.
Approach: sort, choose the next number, skip repeated values
The search builds a combination one number at a time. A call receives start, the first index it may still use, remaining, the amount the combination still needs, and current, the combination so far. When remaining is 0, current adds up to the target, so the call saves a copy. Otherwise a loop tries each index i from start as the next number: add candidates[i], call itself with i + 1 and the smaller remaining, then remove the number again.
Three details make it correct and fast.
- Sorting. Sorted candidates give ascending combinations, the required order, and make equal values neighbours.
- The deeper call starts at
i + 1. A call that started atiwould be allowed to pick the same candidate again and again, which is a different problem. Starting afteriuses each candidate at most once. - Skipping repeated values inside one loop. Every turn of a loop picks a different next number. If
candidates[i]equalscandidates[i - 1]andi > start, that value was already tried as the next number in this call, and trying it again would build the same combinations a second time. Without this rule, the first example returns[[1, 1, 6], [1, 2, 5], [1, 7], [1, 2, 5], [1, 7], [2, 6]], with[1, 2, 5]and[1, 7]twice.
The condition is i > start, not i > 0. At i = start the number is the first this call tries, so it is taken even when it equals its left neighbour. That is how [1, 1, 6] gets its second 1. With i > 0 the same input returns [[1, 2, 5], [1, 7], [2, 6]], and [1, 1, 6] is lost.
The sorted order has one more use. Candidates are at least 1, and once candidates[i] is bigger than remaining, every later candidate is bigger still. The loop can stop there with break.
Step by step
- Sort
candidatesin place withsort((a, b) => a - b). Without the comparison,sortcompares numbers as text and puts 10 before 2. - Create
resultas an emptynumber[][]. - Start the search at index 0 with
targetstill needed and an empty combination. - Return
result, filled in place by the search. - Write the helper
backtrack(candidates, start, remaining, current, result), returningvoid. - Check for a finished combination: if
remaining === 0, the numbers incurrentadd up to the target. - Save a copy with
[...current]and return. Adding anything more would pass the target, since every candidate is at least 1. - Loop from
startto the end of the array. - Skip repeats: if
i > startandcandidates[i] === candidates[i - 1],continue. - Stop when too big: if
candidates[i] > remaining,break. - Choose the number: push it onto
current. - Recurse with
i + 1andremaining - candidates[i]. - Undo with
current.pop(), so the loop can try the next candidate.
The solution in TypeScript
class Solution {
combinationSum2(candidates: number[], target: number): number[][] {
candidates.sort((a, b) => a - b);
const result: number[][] = [];
this.backtrack(candidates, 0, target, [], result);
return result;
}
backtrack(candidates: number[], start: number, remaining: number, current: number[], result: number[][]): void {
if (remaining === 0) {
result.push([...current]);
return;
}
for (let i = start; i < candidates.length; i++) {
if (i > start && candidates[i] === candidates[i - 1]) {
continue;
}
if (candidates[i] > remaining) {
break;
}
current.push(candidates[i]);
this.backtrack(candidates, i + 1, remaining - candidates[i], current, result);
current.pop();
}
}
}
TypeScript details:
- Copying with
[...current]. Arrays are objects, andcurrentis the same array in every call. Pushing it directly would store a reference that is emptied again by the laterpop()calls. The spread builds a new array with the current numbers. continueandbreak.continueends this turn of the loop and moves to the nexti.breakleaves the loop completely. Usingcontinuefor the too-big check would still be correct but would keep looking at numbers that can't fit.sortmutates its argument. The array the caller passed ends up sorted.- Erased types. The annotations are checked by the compiler and removed from the JavaScript, so they never convert anything at run time.
- Method calls need
this. Inside the class the helper is called asthis.backtrack(...).
Complexity
Time: O(n · 2^n) in the worst case. With n candidates there are at most 2^n ways to pick some of them, and copying a combination takes up to n steps. In practice it is far less: sorting and the break mean the search only extends combinations whose sum is still at most the target, and the repeat rule means each distinct combination is reached once. For example, 100 candidates made of two copies of each of 1 to 50, with target 30, take 7,330 calls and return 1,225 combinations.
Space: O(n) besides the answer. The combination under construction and the recursion both hold at most n numbers.
Edge cases and common mistakes
- No combination.
[2, 3]with target 4 gives[], because the lone 2 can't be reused. - Several copies.
[2, 5, 2, 1, 2]with target 5 gives[[1, 2, 2], [5]]. Three 2s are available, and the combination uses two of them, once. - Copies of the same value only.
[1, 1, 1, 1]with target 2 gives[[1, 1]], a single combination, though there are six ways to choose two of the four 1s. - Another mix.
[3, 1, 3, 5, 1, 1]with target 8 gives[[1, 1, 1, 5], [1, 1, 3, 3], [3, 5]]. - Reusing a candidate. Passing
iinstead ofi + 1in the recursive call lets one candidate repeat. For[2, 3]with target 4 it would return[[2, 2]], which is wrong here. - Using
i > 0in the skip rule. It removes combinations that use equal values, as shown above. - Dropping the skip rule. It returns duplicate combinations.
- Sorting as text. The default
sort()would order 10 before 2 and break both the ascending order and thebreaktest.
Testing
[10, 1, 2, 7, 6, 1, 5], target 8, gives[[1, 1, 6], [1, 2, 5], [1, 7], [2, 6]].[2, 5, 2, 1, 2], target 5, gives[[1, 2, 2], [5]].[2, 3], target 4, gives[].[1, 1, 7], target 8, gives[[1, 7]].[1, 1, 1, 1], target 2, gives[[1, 1]].[3, 1, 3, 5, 1, 1], target 8, gives[[1, 1, 1, 5], [1, 1, 3, 3], [3, 5]].
