Start learning free

LESSON 359 OF 543 · BACKTRACKING

Combination Sum in Rust: 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

    RUST · BACKTRACKING

    Combination Sum in Rust, explained

    Problem overview

    The function combination_sum receives a vector of distinct integers, candidates, and a target. It returns every combination of candidates that adds up to target. A candidate can be used as many times as you like. Two combinations are the same when they use the same numbers the same number of times, so [2, 2, 3] and [3, 2, 2] are one combination and must appear once.

    The answer has a fixed shape: each combination is in ascending order, and the combinations are in lexicographic order, which means the first position where two combinations differ decides which comes first. If nothing adds up to target, the result is an empty vector.

    With candidates = [5, 2, 3] and target = 8, the answer is [[2, 2, 2, 2], [2, 3, 3], [3, 5]]. The candidates aren't sorted, but every combination is. [2, 2, 2, 2] comes before [2, 3, 3] because their second numbers are 2 and 3.

    Approach: build combinations one number at a time

    The search builds a combination by choosing a number, then asking what is still needed. After choosing 2 for a target of 7, 5 remains, and the same question is asked again with 5. When the remainder is exactly 0, the numbers chosen so far are a complete combination. When the next candidate is larger than the remainder, it can't be used.

    Two decisions make the answer correct without any cleanup:

    • Sort the candidates first. Then every combination is built in ascending order, and the loop can stop at the first candidate that is too big, because all the later ones are bigger still.
    • Only try candidates from the last chosen position onward. The next number is either the same as the last one (allowed, since numbers repeat) or a later, larger one. That way [2, 2, 3] is built and [3, 2, 2] never is, so each combination appears exactly once. Trying all candidates at every step would produce every ordering: for [2, 3, 6, 7] and target 7, it gives [[2, 2, 3], [2, 3, 2], [3, 2, 2], [7]].

    The sorted order also gives the lexicographic order of the output for free. The search tries smaller numbers first at every position, so combinations are found in the order the answer asks for.

    This trace shows the search for [2, 3, 6, 7] with target 7. Each line is a call, indented by depth, and a "stop" line is a loop ended early because of a candidate that is too big:

    current=[] remaining=7 i=0
      current=[2] remaining=5 i=0
        current=[2, 2] remaining=3 i=0
          current=[2, 2, 2] remaining=1 i=0
          stop at 2
          current=[2, 2, 3] remaining=0 i=1
        stop at 6
        current=[2, 3] remaining=2 i=1
        stop at 3
      stop at 6
      current=[3] remaining=4 i=1
        current=[3, 3] remaining=1 i=1
        stop at 3
      stop at 6
      current=[6] remaining=1 i=2
      stop at 6
      current=[7] remaining=0 i=3
    

    Two calls end with remaining=0: [2, 2, 3] and [7], so the answer is [[2, 2, 3], [7]]. The call with [2, 2, 2] has 1 left, but the smallest candidate is 2, so it ends at once. The 6 appears as a dead end for the same reason.

    Step by step

    1. Sort the candidates in place. The parameter is declared mut, so candidates.sort() can reorder it.
    2. Create result with Vec::new(). Rust works out that it is a Vec<Vec<i32>> from how it is used.
    3. Start the search. Call Self::backtrack with the candidates borrowed, the full target as the amount still needed, 0 as the first index that may be used, a new empty vector for the combination being built, and &mut result.
    4. Return result. backtrack added to the same vector, so it now holds everything.
    5. Write the helper's signature. remaining is how much the combination still needs, i is the first index it may use, current is the combination so far, and result collects the finished ones.
    6. Check for a finished combination. If remaining == 0, the numbers in current add up to the target.
    7. Save a copy and return. current keeps changing as the search continues, so result gets a snapshot made with clone().
    8. Loop over the allowed candidates. j goes over i..candidates.len(). Starting at i instead of 0 is what builds each combination once.
    9. Stop at a candidate that is too big. If candidates[j] > remaining, break: the later candidates are even bigger.
    10. Choose the candidate by pushing it onto current.
    11. Complete the choice with a recursive call: remaining - candidates[j] is what is left, and passing j, not j + 1, lets the same number be chosen again.
    12. Undo the choice with current.pop(), so the loop can try the next candidate in its place.

    The solution in Rust

    impl Solution {
        fn combination_sum(mut candidates: Vec<i32>, target: i32) -> Vec<Vec<i32>> {
            candidates.sort();
            let mut result = Vec::new();
            Self::backtrack(&candidates, target, 0, &mut Vec::new(), &mut result);
            result
        }
    
        fn backtrack(candidates: &Vec<i32>, remaining: i32, i: usize, current: &mut Vec<i32>, result: &mut Vec<Vec<i32>>) {
            if remaining == 0 {
                result.push(current.clone());
                return;
            }
            for j in i..candidates.len() {
                if candidates[j] > remaining {
                    break;
                }
                current.push(candidates[j]);
                Self::backtrack(candidates, remaining - candidates[j], j, current, result);
                current.pop();
            }
        }
    }
    

    Rust details worth knowing:

    • &mut Vec::new() as an argument. The temporary empty vector lives until the end of the statement, which is long enough for the whole search. Nothing else needs it afterwards, because every finished combination is copied into result.
    • Why clone(). current is a mutable borrow of one vector that the search keeps pushing to and popping from. Pushing it directly into result isn't possible, since result needs to own its vectors, and clone() gives it a separate copy.
    • Passing borrows on. candidates, current and result are already references inside backtrack, so the recursive call passes them as they are. Rust reborrows the mutable ones for the length of the call.
    • Index types. i and j are usize, because they index a vector, while remaining and the candidates are i32. The range i..candidates.len() excludes its end, so j is always a valid index.
    • A function that returns nothing. backtrack has no return type, so it returns (), and the bare return; exits it early.

    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 time is O(n^(t/m)) in the worst case. Sorting adds O(n log n), which doesn't change that. Each saved combination is also copied, but its length is at most t / m.

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

    Edge cases and common mistakes

    • No combination. [2] with target 1 gives []: the only candidate is bigger than the target, so the loop breaks on its first step and nothing is saved. [7, 3, 2] with target 1 gives [] as well.
    • One number repeated. [3] with target 9 gives [[3, 3, 3]]. The recursive call must pass j for that to happen.
    • Passing j + 1. Each number could then be used at most once, and [2, 2, 3] would be missed.
    • Passing 0 instead of j. Every ordering of a combination would be found, so the answer would have duplicates.
    • Forgetting to sort. Unsorted input would give combinations in the wrong order, and break would stop too early, since a smaller candidate could come after a larger one.
    • Pushing current without copying. A shared vector would be emptied again by the pop calls, and the saved combinations would change after the fact. This is why the lesson clones.
    • Using continue instead of break. The answer stays the same, but the loop wastes time on candidates that are known to be too big.

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