Start learning free

LESSON 352 OF 536 · BACKTRACKING

Combination Sum in Python: Backtracking on Sorted Candidates

Practice O(nt/m) Read the full explanation

Given a list 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 list.

 

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

    PYTHON · BACKTRACKING

    Combination Sum in Python, explained

    Problem overview

    The method combinationSum(candidates, target) receives a list of distinct integers and a target. It returns every combination of candidates that adds up to the target. A candidate may be used any number of times, and two combinations count as the same when they use the same numbers the same number of times, in any order, so [2, 2, 3] and [3, 2, 2] are one combination.

    Each combination is written 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 the target, the result is an empty list.

    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]]: the input isn't sorted, but the output still is. For [2] and 1 the answer is [].

    Approach: build combinations one number at a time

    This is a search problem, and the standard tool is backtracking. Build a combination by repeating one move: choose a candidate, add it to the combination, and try to finish the rest with what is left of the target. Then undo the choice and try the next candidate.

    Two ideas keep the search correct and tidy:

    • Only move forward through the sorted candidates. After sorting, each call may only use candidates from index i onward, where i is the position of the last number chosen. The next number can be the same one (index j again) or a larger one, never a smaller one. So [2, 2, 3] is built, but [3, 2, 2] never is, and every combination appears exactly once, already in ascending order. The combinations also come out in lexicographic order, because the loop tries smaller candidates first.
    • Stop early. The candidates are sorted, so once one is bigger than what remains, every later one is too. The loop can break there.

    The search is exponential. With n candidates, a target t and smallest candidate m, a combination has at most t / m numbers, each one of n candidates, so the time is O(n^(t/m)) in the worst case. Here the target is at most 40 and every candidate is at least 2, which keeps the search small.

    Step by step

    The outer method

    1. Sort candidates in place with sort(). This gives the ascending order inside each combination, the lexicographic order of the answer, and the early stop.
    2. Create result, an empty list that will hold the finished combinations, each of them a list.
    3. Start the search with self.backtrack(candidates, target, 0, [], result): all of the target is still needed, any candidate from index 0 may be used, and the combination so far is empty.
    4. Return result. backtrack fills this same list.

    The helper backtrack

    1. Declare it with candidates, remaining (how much is still needed), i (the first index it may use), current (the combination so far) and result.
    2. If remaining is 0, the numbers in current add up to exactly the target. Append current.copy() to result and return. The copy is essential: current keeps changing as the search goes on, so the result needs a snapshot. Returning here is also right because every candidate is at least 2, so adding more would overshoot.
    3. Loop j over range(i, len(candidates)). These are the numbers the combination may take next.
    4. Break if candidates[j] is bigger than remaining. Later candidates are even bigger.
    5. Choose: append candidates[j] to current.
    6. Recurse with remaining - candidates[j] and j as the start index. Passing j and not j + 1 is what allows the same number to be used again.
    7. Undo: current.pop() removes the number just chosen, so current is what it was before this turn of the loop.

    The solution in Python

    class Solution:
        def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
            candidates.sort()
            result = []
            self.backtrack(candidates, target, 0, [], result)
            return result
    
        def backtrack(self, candidates, remaining, i, current, result):
            if remaining == 0:
                result.append(current.copy())
                return
            for j in range(i, len(candidates)):
                if candidates[j] > remaining:
                    break
                current.append(candidates[j])
                self.backtrack(candidates, remaining - candidates[j], j, current, result)
                current.pop()
    

    Python details to know:

    • sort() sorts in place and returns None. That is why the code calls it on its own line and keeps using candidates, rather than writing candidates = candidates.sort(), which would set it to None. It also reorders the list the caller passed in.
    • Lists are shared, not copied, when passed around. Every call of backtrack works on the same current and the same result. Appending current itself, without .copy(), would store a reference to the one list that is later emptied. In this problem every stored entry would end up as [].
    • range(i, len(candidates)) is empty when i equals the length, so the loop simply does nothing in that case.

    Complexity

    The time is exponential, O(n^(t/m)) in the worst case, as explained above, plus the cost of copying each combination into the result. The extra space, besides the answer, is O(t / m): the combination being built holds at most t / m numbers, and the recursion is at most that many calls deep.

    Edge cases and common mistakes

    • No combination. [2] with target 1 returns [], since the loop breaks on the first candidate and nothing is ever added.
    • Repeating a number. [3] with target 9 returns [[3, 3, 3]]: one candidate can fill the whole sum.
    • Starting the loop at 0 every time. Each call would try all candidates, so the same combination would appear in several orders, such as [2, 2, 3], [2, 3, 2] and [3, 2, 2].
    • Passing j + 1 to the recursive call. That forbids reusing a number, so [2, 3] with target 6 would lose [2, 2, 2] and [3, 3].
    • Forgetting to undo. Without the pop(), current keeps growing and every later combination is wrong.
    • Unsorted input. [7, 3, 2] with target 7 returns [[2, 2, 3], [7]], in ascending order, because of the sort at the start.

    Testing

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