Start learning free

LESSON 362 OF 569 · BACKTRACKING

Combination Sum II in JavaScript: Backtracking Without Duplicates

Practice O(n · 2n) Read the full explanation

Given an array of integers candidates, which may contain duplicates, and an integer target, return every distinct combination of candidates that adds up to target.

Each candidate may be used at most once in a combination, so a number that appears twice in candidates can appear up to twice in one combination. Two combinations are the same when they hold the same numbers the same number of times, whichever copies of a repeated number they use: in [1,1,7] with target = 8, the first 1 with the 7 and the second 1 with the 7 are one combination, [1,7], and it must appear only once.

Write the numbers of each combination in ascending order, and return the combinations in lexicographic order: compare two combinations number by number, and the first position where they differ decides which one comes first. This is the order in which the search described in the note below finds them. If no combination adds up to target, return an empty array.

 

Example 1:

Input: candidates = [10,1,2,7,6,1,5], target = 8
Output: [[1,1,6],[1,2,5],[1,7],[2,6]]
Explanation: Sorted, the candidates are [1,1,2,5,6,7,10]. [1,1,6] uses both 1s.
[1,7] can be made with either 1, but it is listed once. [1,2,5] comes before [1,7] because at the second number, 2 is smaller than 7.

Example 2:

Input: candidates = [2,5,2,1,2], target = 5
Output: [[1,2,2],[5]]
Explanation: There are three 2s; [1,2,2] uses two of them and appears once. 5 alone is 5.

Example 3:

Input: candidates = [2,3], target = 4
Output: []
Explanation: 2 + 3 = 5 is too much, and the 2 can't be used twice, so no combination adds up to 4.

 

Constraints:

  • 1 <= candidates.length <= 100
  • 1 <= candidates[i] <= 50
  • 1 <= target <= 30

Note:

The tutorial uses backtracking, as in Combination Sum. Sort candidates. A call gets start, the first index it may still use, remaining, what the combination still needs, and current, the combination built so far. When remaining is 0, it saves a copy of current. Otherwise a loop tries each index i from start on as the next number: it adds candidates[i], calls itself with i + 1 and remaining - candidates[i], and removes the number again. Because the candidates are sorted, the loop can stop at the first one that is bigger than remaining.

Two things change from Combination Sum. First, the deeper call starts at i + 1, not at i: there, a number could be chosen again and again; here, each candidate is used at most once. Second, as in Subsets II, the loop skips candidates[i] when i > start and it equals candidates[i - 1]. Inside one call, every turn of the loop picks a different next number, and a second copy of the same value would build exactly the same combinations again. A copy is still taken when it is the first one this call tries, at i = start: that is how [1,1,6] gets its second 1.

The search is exponential: with n candidates, there are at most 2n ways to pick some of them, and copying a combination takes up to n steps, so the time is O(n · 2n) in the worst case. The sorted order keeps it far smaller in practice: the calls only build distinct combinations whose sum is at most target. Besides the answer, the extra space is O(n): the combination being built and one call per number in it.

STEP 1 OF 13
    Ctrl + Enter to check

    JAVASCRIPT · BACKTRACKING

    Combination Sum Ii in JavaScript, explained

    Problem overview

    combinationSum2(candidates, target) receives an array of integers that may repeat and a target. It returns every distinct combination of candidates that adds up to target. Each candidate may be used at most once, so a value that appears twice in candidates can appear up to twice in one combination. Two combinations are the same when they hold the same numbers the same number of times, whichever copies they use.

    The numbers of each combination are in ascending order, and the combinations are returned in lexicographic order: compare number by number, and the first difference decides. If nothing adds up to target, the result is an empty array.

    For candidates = [10,1,2,7,6,1,5] and target = 8 the answer is [[1,1,6],[1,2,5],[1,7],[2,6]]. The combination [1,7] can be built with either 1, but it is listed once. For [2,3] and target 4 the answer is []: the 2 can't be used twice.

    Approach: sorted backtracking

    Sort the candidates first. That gives three things at once: equal values sit side by side, the numbers of each combination come out in ascending order, and once a candidate is too big, every later one is too big as well.

    A call of the search receives start, the first index it may still use, remaining, how much the combination still needs, and current, the combination built so far. When remaining is 0, current adds up to the target and a copy is saved. Otherwise a loop tries each index i from start on as the next number: it adds candidates[i], recurses with i + 1 and remaining - candidates[i], then removes the number again.

    Two details handle the rules of this problem:

    • The recursive call starts at i + 1, so a candidate can't be picked twice. (If a variant allowed unlimited reuse, the call would pass i.)
    • The loop skips candidates[i] when i > start and it equals candidates[i - 1]. In one call, each turn of the loop picks a different next number, and a second copy of the same value would rebuild combinations that the first copy already produced. At i = start the copy is taken, because it is the first one this call tries. That is how [1,1,6] gets its second 1.

    The brute-force idea is to try every subset of the candidates and keep those with the right sum, which is far more work than needed. Here the search never builds a combination that goes over the target.

    Step by step

    1. Sort candidates in place with the comparison (a, b) => a - b. Without a comparison, sort compares numbers as text.
    2. Create result, an empty array of combinations.
    3. Start the search: this.backtrack(candidates, 0, target, [], result). Every index may be used and all of the target is still needed.
    4. Return result.
    5. Declare the helper backtrack(candidates, start, remaining, current, result) in the class.
    6. Stop at a full combination: if remaining === 0...
    7. ...save a copy with [...current] and return. Every candidate is at least 1, so adding anything more would pass the target.
    8. Loop i from start while i < candidates.length. Starting at start, never at 0, means a combination takes its numbers in index order, and no choice of indexes is built twice.
    9. Skip repeats: if i > start and candidates[i] === candidates[i - 1], use continue.
    10. Stop at a number that is too big: if candidates[i] > remaining, use break. The array is sorted, so every later candidate is bigger still.
    11. Choose candidates[i] by pushing it onto current.
    12. Recurse with i + 1 and remaining - candidates[i].
    13. Undo with current.pop() so the loop can try the next candidate.

    The solution in JavaScript

    class Solution {
        combinationSum2(candidates, target) {
            candidates.sort((a, b) => a - b);
            const result = [];
            this.backtrack(candidates, 0, target, [], result);
            return result;
        }
        backtrack(candidates, start, remaining, current, result) {
            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();
            }
        }
    }
    

    JavaScript details to watch:

    • The comparator is required. [10, 2].sort() compares "10" and "2" as text and puts 10 first. (a, b) => a - b sorts numerically. Note that sort reorders the caller's array.
    • Copy before saving. current is one array reused by the whole search. Pushing current itself would leave result full of references to that array, which ends up empty. [...current] saves a snapshot.
    • continue against break. continue skips one value and goes on to the next i. break leaves the whole loop, which is right only because the array is sorted.

    Trace on [10,1,2,7,6,1,5], target 8

    After sorting, the candidates are [1,1,2,5,6,7,10]. Printing the arguments at the start of each call gives:

    start=0 remaining=8 current=[]
    start=1 remaining=7 current=[1]
    start=2 remaining=6 current=[1,1]
    start=3 remaining=4 current=[1,1,2]
    start=4 remaining=1 current=[1,1,5]
    start=5 remaining=0 current=[1,1,6]
    start=3 remaining=5 current=[1,2]
    start=4 remaining=0 current=[1,2,5]
    start=4 remaining=2 current=[1,5]
    start=5 remaining=1 current=[1,6]
    start=6 remaining=0 current=[1,7]
    start=3 remaining=6 current=[2]
    start=4 remaining=1 current=[2,5]
    start=5 remaining=0 current=[2,6]
    start=4 remaining=3 current=[5]
    start=5 remaining=2 current=[6]
    start=6 remaining=1 current=[7]
    

    The calls with remaining=0 are the four answers, in the required order. The 10 is never chosen: it is bigger than every remaining the loop meets, so break stops before it. At the top level, the second 1 is skipped because i > start and it equals the one before, so [1,7] is found only once.

    Complexity

    Time: O(n · 2^n) in the worst case, for 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 much smaller, because the search only builds distinct combinations whose sum is at most the target.

    Extra space: O(n), besides the answer: the combination being built and one call per number in it.

    Edge cases and common mistakes

    • No duplicate check. For [10,1,2,7,6,1,5] and target 8, a version without it returns [[1,1,6],[1,2,5],[1,7],[1,2,5],[1,7],[2,6]], with [1,2,5] and [1,7] twice.
    • Skipping with i > 0 instead of i > start. The second 1 can then never follow the first, so [1,1,6] disappears and the result is [[1,2,5],[1,7],[2,6]].
    • Recursing with i instead of i + 1. This lets a candidate be used again: for [2,3] and target 4 it returns [[2,2]] instead of [].
    • No combination. [2,3] with target 4 gives [], and so does [10,20] with target 5.
    • Many copies. [1,1,1,1] with target 2 gives [[1,1]] once, although there are six ways to choose two of the four 1s.
    • Forgetting the sort. Equal values might not be adjacent, so the skip test fails, and the break would cut off candidates that still fit.

    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,1,1], target 2 gives [[1,1]].
    • [1], target 1 gives [[1]].
    • [3,1,3,5,1,1], target 8 gives [[1,1,1,5],[1,1,3,3],[3,5]].
    Keep going with JavaScript500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free