Start learning free

LESSON 357 OF 541 · BACKTRACKING

Combination Sum in Swift: 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 13
    Ctrl + Enter to check

    SWIFT · BACKTRACKING

    Combination Sum in Swift, explained

    Problem overview

    The method combinationSum(_ candidates: [Int], _ target: Int) -> [[Int]] receives distinct integers and a target. It returns every combination of candidates that adds up to target. A candidate can be used any number of times, and two combinations are the same if they use the same numbers the same number of times, in any order. Each combination is written in ascending order, and the combinations are returned in lexicographic order: compare number by number, and the first difference decides.

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

    Approach: build combinations one number at a time

    Brute force would generate every multiset of candidates and test its sum, which wastes almost all of its work. Backtracking builds a combination number by number and abandons it as soon as it can't work.

    Keep a list current and a number remaining, the amount still needed. If remaining is 0, current is a complete combination. Otherwise, try each candidate as the next number: add it, search deeper with a smaller remaining, then remove it so the next candidate can take its place.

    Two choices keep the output right:

    • Sort the candidates first. Then a combination built left to right is in ascending order, and trying candidates from smallest to largest produces the combinations in lexicographic order, which is the order the answer needs.
    • Only move forward. Each call receives an index i and may use only candidates from i on, including i itself, so a number can repeat. A combination is then built in exactly one order: [2, 2, 3] is found, and [3, 2, 2] never is. No duplicates arise, and there is nothing to filter out.

    Sorting also allows an early exit. When a candidate is bigger than remaining, every later one is bigger too, so the loop can stop.

    Step by step

    1. Sort a copy. Swift parameters are constants, so let candidates = candidates.sorted() makes a new sorted array and hides the parameter.
    2. Create result as an empty [[Int]], which collects the combinations.
    3. Create current as an empty [Int], the combination being built. Nothing is chosen yet.
    4. Start the search: call backtrack with the sorted candidates, target as the amount still needed, 0 as the first index it may use, and &current and &result.
    5. Return result. The helper added to the same array.
    6. Declare backtrack with candidates, remaining, the start index i, and current and result as inout parameters so every call shares them.
    7. Test for a finished combination. If remaining == 0, the numbers in current add up to the target.
    8. Record it and return. result.append(current) stores a copy, and returning is right because every candidate is at least 2: adding more would overshoot.
    9. Loop over the next choices with for j in i..<candidates.count.
    10. Stop at a number that is too big. If candidates[j] > remaining, break.
    11. Choose it: append candidates[j] to current.
    12. Complete the choice: call backtrack with remaining - candidates[j] and j as the new start index. Passing j rather than j + 1 allows the same number again.
    13. Undo the choice with current.removeLast(), so current is what it was before this turn of the loop.

    The solution in Swift

    class Solution {
        func combinationSum(_ candidates: [Int], _ target: Int) -> [[Int]] {
            let candidates = candidates.sorted()
            var result = [[Int]]()
            var current = [Int]()
            backtrack(candidates, target, 0, &current, &result)
            return result
        }
    
        func backtrack(_ candidates: [Int], _ remaining: Int, _ i: Int, _ current: inout [Int], _ result: inout [[Int]]) {
            if remaining == 0 {
                result.append(current)
                return
            }
            for j in i..<candidates.count {
                if candidates[j] > remaining {
                    break
                }
                current.append(candidates[j])
                backtrack(candidates, remaining - candidates[j], j, &current, &result)
                current.removeLast()
            }
        }
    }
    

    The Swift details worth a closer look:

    • Arrays are values. result.append(current) stores a copy of the array as it is now. After current is changed again, the stored combination stays the same. In a language where lists are references, you would copy by hand. In Swift the copy is automatic, and it is cheap until one side changes.
    • inout parameters. current and result are passed with &, and the helper changes the caller's arrays directly. That is why the helper returns nothing.
    • sorted() versus sort(). sorted() returns a new array and leaves the original alone, which suits a constant parameter. sort() sorts in place and needs a var.
    • The range i..<candidates.count. It is empty when i equals the count, so the loop simply doesn't run. It would crash if i were larger than the count, but i is always an index of a candidate that was just used.

    Complexity

    Time: exponential. With n candidates, a target t and smallest candidate m, a combination has at most t / m numbers, and each is one of n candidates, so the bound is O(n^(t / m)). The break and the forward-only index cut this down a lot in practice, and the output itself can be large.

    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. [2] with target 1 returns []: 2 is bigger than 1, so the loop breaks at once.
    • Unsorted input. Skipping the sort breaks both the output order and the early break: with [5, 2, 3], after choosing 5 the amount left is 3, and the loop would stop at 5 at once, never trying 2 or 3, so [3, 5] would be missed.
    • Starting at 0 every time. Looping from 0 in each call builds [2, 3] and [3, 2] as different lists, which gives duplicates.
    • Passing j + 1. Then each number is used at most once, and [2, 2, 3] is lost.
    • Forgetting removeLast(). current keeps growing and every later combination begins with numbers it never chose.
    • Several candidates, one sum. For [7, 3, 5, 2] and target 10, the answer has five combinations, [[2, 2, 2, 2, 2], [2, 2, 3, 3], [2, 3, 5], [3, 7], [5, 5]], including [5, 5], which uses one candidate twice.

    Testing

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