Start learning free

LESSON 356 OF 540 · BACKTRACKING

Combination Sum in TypeScript: Backtracking over 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

    TYPESCRIPT · BACKTRACKING

    Combination Sum in TypeScript, explained

    Problem overview

    The method combinationSum(candidates, target) receives an array of distinct integers and a target. It returns every combination of the candidates that adds up to target. A candidate may be used any number of times. Two combinations are the same when they use the same numbers the same number of times, in any order, so [2, 2, 3] and [3, 2, 2] count once. Each combination is written in ascending order, and the combinations are returned in lexicographic order: the first position where two of them differ decides which comes first. If nothing adds up to the target, the result is an empty array.

    For candidates = [2,3,6,7] and target = 7 the answer is [[2,2,3],[7]]: 2 + 2 + 3 = 7, and 7 alone is 7. For candidates = [5,2,3] and target = 8 the answer is [[2,2,2,2],[2,3,3],[3,5]], even though the candidates aren't sorted. For candidates = [2] and target = 1 the answer is [].

    Approach: build combinations one number at a time

    There is no formula for this; the task is to list every answer, so the solution searches. It builds a combination one number at a time. At each point it knows how much is still needed (remaining), and it tries each candidate as the next number. After choosing one, it calls itself with the smaller remainder. When the remainder reaches exactly 0, the numbers chosen add up to the target, and the combination is saved. Then the last choice is undone and the next candidate is tried: that is backtracking.

    Two ideas keep the search tidy.

    • Never go backwards in the list. A combination is built from candidates in sorted order, so after choosing the candidate at index j, the next number comes from index j or later. Index j itself is allowed, which is how a number can repeat. Starting the loop at the last chosen index means [2, 2, 3] is found and [3, 2, 2] never is, so each combination appears once and already in ascending order.
    • Stop early. With the candidates sorted, once one candidate is bigger than remaining, all the later ones are too, so the loop can stop.

    Sorting also gives the output order. Trying candidates from the smallest up produces the combinations in lexicographic order without any extra sorting of the result.

    Step by step

    combinationSum

    1. Sort the candidates in place with candidates.sort((a, b) => a - b). Without the comparison function, sort compares numbers as text.
    2. Create result, an empty number[][], where finished combinations are collected.
    3. Start the search with this.backtrack(candidates, target, 0, [], result): all of the target is still needed, index 0 is the first usable candidate, and nothing is chosen yet.
    4. Return result, which the helper has filled.

    backtrack

    1. Declare the helper with the parameters candidates, remaining, i (the first index it may use), current (the combination so far) and result. It returns void: its output goes into result.
    2. Base case. If remaining === 0, the numbers in current add up to the target.
    3. Save a copy. Push [...current] onto result and return. A copy is needed because current keeps changing as the search goes on. Every candidate is at least 2, so adding more could only overshoot, and stopping here is right.
    4. Loop over the choices with j from i to the end of the candidates.
    5. Break when too big. If candidates[j] > remaining, leave the loop.
    6. Choose candidates[j] by pushing it onto current.
    7. Recurse with remaining - candidates[j] and j as the first usable index. Passing j, not j + 1, allows the same number again.
    8. Undo the choice with current.pop(), so the loop can try the next candidate from the same state.

    The solution in TypeScript

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

    Details worth knowing:

    • The comparison function. [10, 9, 2].sort() gives [10, 2, 9], because the default order compares the numbers as strings. [10, 9, 2].sort((a, b) => a - b) gives [2, 9, 10]. Note that sort changes the array it is called on, so the caller's candidates ends up sorted.
    • Why the copy matters. Pushing current itself would put the same array object into result many times. After the search finished and every number was popped, each entry would be the same empty array. [...current] copies the numbers into a new array at the moment the combination is complete.
    • Everything is shared. current and result are arrays, which JavaScript passes by reference. All the calls work on the same two arrays, which is why the helper doesn't need to return anything and why the undo step is needed.
    • Types are erased. number[][] and void help the compiler and disappear in the generated JavaScript.

    Complexity

    Time: 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 worst case is O(n^(t/m)). The sorted early exit cuts many branches in practice, and copying each answer costs time proportional to its length.

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

    Edge cases and common mistakes

    • No combination. [2] with target 1 gives [], and [3, 5] with target 7 also gives []. The loop breaks or ends and nothing reaches remaining === 0.
    • Repeated use. [2] with target 4 gives [[2,2]]: the recursion passes j, so 2 can be chosen twice.
    • Starting the inner loop at 0. That would find the same combination in every order, such as [2,2,3], [2,3,2] and [3,2,2], so the result would be full of duplicates.
    • Using j + 1 in the recursive call. That forbids repeats, so [2,2,3] would never be found.
    • Forgetting pop(). The combination keeps growing with numbers from earlier branches.
    • Forgetting to sort. Without sorting, the early break would skip candidates that fit, and the output would not be in ascending, lexicographic order.
    • Sorting without a comparison. Candidates such as 10 and 2 would be ordered as text, putting 10 first.

    Testing

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