Start learning free

LESSON 352 OF 536 · BACKTRACKING

Combination Sum in JavaScript: Backtracking with Sorted Candidates

Practice O(nt/m) Read the full explanation

Given an array of distinct integers candidates and an integer target, return every combination of numbers from candidates that adds up to target.

The same number may be chosen any number of times. Two combinations are the same when they use the same numbers the same number of times, whatever the order: [2, 2, 3] and [3, 2, 2] are one combination, 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. If no combination adds up to target, return an empty array.

 

Example 1:

Input: candidates = [2,3,6,7], target = 7
Output: [[2,2,3],[7]]
Explanation: 2 + 2 + 3 = 7, using the 2 twice, and 7 alone is 7.
No other combination adds up to 7. [2,2,3] comes first because its first number, 2, is smaller than 7.

Example 2:

Input: candidates = [5,2,3], target = 8
Output: [[2,2,2,2],[2,3,3],[3,5]]
Explanation: The candidates are not sorted, but every combination is written in ascending order.
[2,2,2,2] and [2,3,3] both start with 2; at their second number, 2 is smaller than 3. [3,5] starts with 3, so it comes last.

Example 3:

Input: candidates = [2], target = 1
Output: []
Explanation: The only candidate, 2, is already bigger than 1, so no combination adds up to 1.

 

Constraints:

  • 1 <= candidates.length <= 30
  • 2 <= candidates[i] <= 40
  • All the numbers in candidates are distinct.
  • 1 <= target <= 40

Note:

The tutorial uses backtracking. Sort candidates, then build a combination one number at a time: try every candidate from the position of the last number chosen onward (that same number included, since numbers can repeat), go deeper with what is left of target, then remove the number again and try the next one. Starting from the last chosen position builds each combination exactly once, already in ascending order, and the combinations come out in lexicographic order. Because the candidates are sorted, the loop can stop at the first one that is bigger than what is left.

The search is exponential: with n candidates, a target t and a smallest candidate m, a combination has at most t / m numbers, and each of them is one of n candidates, so the time is O(nt/m) in the worst case. Besides the answer, the extra space is O(t / m): the combination being built and one call per number in it.

STEP 1 OF 12
    Ctrl + Enter to check

    JAVASCRIPT · BACKTRACKING

    Combination Sum in JavaScript, explained

    Problem overview

    The method combinationSum(candidates, target) receives an array of distinct integers and a target. It returns every combination of candidates that adds up to target, where each candidate can be used any number of times. Order inside a combination doesn't matter, so [2, 2, 3] and [3, 2, 2] count as one. Each combination is written in ascending order, and the combinations are returned in lexicographic order. If none exists, the result is an empty array.

    For candidates = [2, 3, 6, 7] and target = 7 the answer is [[2, 2, 3], [7]]. For [5, 2, 3] and 8 it is [[2, 2, 2, 2], [2, 3, 3], [3, 5]], even though the input isn't sorted. For [2] and 1 it is [].

    Approach: build combinations one number at a time

    The search builds a combination step by step. At each step it picks a candidate, subtracts it from what is still needed, and continues. When nothing is needed any more, the combination is complete. When the next candidate is bigger than what remains, it can't be used.

    Two choices keep the answer free of duplicates and in the required order:

    • Sort the candidates first. Combinations then come out in ascending order, and the first position where two combinations differ decides their order in the result, exactly the lexicographic order the task asks for.
    • Only move forward in the list. A call may use candidates from index i onward, never earlier ones. The same candidate can repeat, because the next call starts at the same index, but [3, 2, 2] can never be built after a 3, since the 2s are behind it.

    Because the list is sorted, once one candidate is too big, every later one is too, so the loop can stop with break instead of skipping them one by one.

    After exploring a choice, the search removes the number again and tries the next one in its place. That undo is what makes it backtracking.

    Step by step

    combinationSum

    1. Sort candidates in place with the comparison (a, b) => a - b. Without a comparison, sort compares numbers as text.
    2. Create the empty array result, which will hold the combinations.
    3. Start the search with this.backtrack(candidates, target, 0, [], result): all of target is still needed, any candidate from index 0 may be used, and nothing is chosen yet.
    4. Return result, which backtrack has filled.

    backtrack

    1. If remaining is 0, the numbers in current add up to the target. Push a copy of current onto result and return. Every candidate is at least 2, so adding more would overshoot anyway.
    2. Loop j from i to the end of candidates.
    3. If candidates[j] is bigger than remaining, break.
    4. Push candidates[j] onto current.
    5. Call backtrack with remaining - candidates[j] and j as the first index. Passing j rather than j + 1 is what allows a number to be reused.
    6. Pop the last number off current, so the loop can try the next candidate from the same starting point.

    The solution in JavaScript

    class Solution {
        combinationSum(candidates, target) {
            candidates.sort((a, b) => a - b);
            const result = [];
            this.backtrack(candidates, target, 0, [], result);
            return result;
        }
    
        backtrack(candidates, remaining, i, current, result) {
            if (remaining === 0) {
                result.push([...current]);
                return;
            }
            for (let j = i; j < candidates.length; j++) {
                if (candidates[j] > remaining) {
                    break;
                }
                current.push(candidates[j]);
                this.backtrack(candidates, remaining - candidates[j], j, current, result);
                current.pop();
            }
        }
    }
    

    The JavaScript details worth knowing:

    • The comparison function. [10, 9, 1, 2].sort() gives [1, 10, 2, 9], because the default sort compares text. (a, b) => a - b returns a negative number when a should come first, so the numbers end up in numeric order. sort changes the array it is called on, so the caller's candidates comes back sorted.
    • Copying with [...current]. current is a single array that the search keeps changing. Pushing current itself would store the same array many times, and after the search it would be empty. The spread syntax builds a new array with the same numbers, a snapshot of this moment.
    • push and pop. Both change an array at its end in constant time, which is why the array is shared between calls instead of copied.
    • Shared arrays. result and current are passed by reference, so every call works on the same two arrays. This is why backtrack needs no return value.

    Complexity

    Time: exponential, O(n^(t/m)) in the worst case, where n is the number of candidates, t the target and m the smallest candidate. A combination has at most t / m numbers, and each one is one of n candidates. The sorted order and the break prune much of this in practice. Copying each finished combination adds a factor of its length.

    Space: O(t / m) besides the answer: the combination being built and one call per number in it.

    Edge cases and common mistakes

    • No combination. For [2] and 1 the first candidate is already bigger than what remains, so the loop breaks and the result stays [].
    • A candidate equal to the target. [7] with target 7 returns [[7]].
    • Reusing a number. [2, 3] with target 6 returns [[2, 2, 2], [3, 3]].
    • Starting every loop at 0. The search would then build [2, 2, 3], [2, 3, 2] and [3, 2, 2] as different combinations.
    • Passing j + 1. Each number would be used at most once, which solves a different problem.
    • Pushing current without copying. The result holds references to the same array, which is empty once the search finishes.
    • Forgetting pop(). Numbers pile up in current and later combinations contain leftovers.
    • Sorting without a comparison. Candidates such as [10, 2] sort as [10, 2], which breaks both the order and the break test. With the comparison, [10, 2] and target 12 returns [[2, 2, 2, 2, 2, 2], [2, 10]].

    Testing

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