Start learning free

LESSON 362 OF 569 · BACKTRACKING

Combination Sum II in Python: Backtracking Without Duplicates

Practice O(n · 2n) Read the full explanation

Given a list of integers candidates, which may contain duplicates, and an integer target, return every distinct combination of candidates that adds up to target.

Each candidate may be used at most once in a combination, so a number that appears twice in candidates can appear up to twice in one combination. Two combinations are the same when they hold the same numbers the same number of times, whichever copies of a repeated number they use: in [1,1,7] with target = 8, the first 1 with the 7 and the second 1 with the 7 are one combination, [1,7], 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. This is the order in which the search described in the note below finds them. If no combination adds up to target, return an empty list.

 

Example 1:

Input: candidates = [10,1,2,7,6,1,5], target = 8
Output: [[1,1,6],[1,2,5],[1,7],[2,6]]
Explanation: Sorted, the candidates are [1,1,2,5,6,7,10]. [1,1,6] uses both 1s.
[1,7] can be made with either 1, but it is listed once. [1,2,5] comes before [1,7] because at the second number, 2 is smaller than 7.

Example 2:

Input: candidates = [2,5,2,1,2], target = 5
Output: [[1,2,2],[5]]
Explanation: There are three 2s; [1,2,2] uses two of them and appears once. 5 alone is 5.

Example 3:

Input: candidates = [2,3], target = 4
Output: []
Explanation: 2 + 3 = 5 is too much, and the 2 can't be used twice, so no combination adds up to 4.

 

Constraints:

  • 1 <= candidates.length <= 100
  • 1 <= candidates[i] <= 50
  • 1 <= target <= 30

Note:

The tutorial uses backtracking, as in Combination Sum. Sort candidates. A call gets start, the first index it may still use, remaining, what the combination still needs, and current, the combination built so far. When remaining is 0, it saves a copy of current. Otherwise a loop tries each index i from start on as the next number: it adds candidates[i], calls itself with i + 1 and remaining - candidates[i], and removes the number again. Because the candidates are sorted, the loop can stop at the first one that is bigger than remaining.

Two things change from Combination Sum. First, the deeper call starts at i + 1, not at i: there, a number could be chosen again and again; here, each candidate is used at most once. Second, as in Subsets II, the loop skips candidates[i] when i > start and it equals candidates[i - 1]. Inside one call, every turn of the loop picks a different next number, and a second copy of the same value would build exactly the same combinations again. A copy is still taken when it is the first one this call tries, at i = start: that is how [1,1,6] gets its second 1.

The search is exponential: with n candidates, there are at most 2n ways to pick some of them, and copying a combination takes up to n steps, so the time is O(n · 2n) in the worst case. The sorted order keeps it far smaller in practice: the calls only build distinct combinations whose sum is at most target. Besides the answer, the extra space is O(n): the combination being built and one call per number in it.

STEP 1 OF 13
    Ctrl + Enter to check

    PYTHON · BACKTRACKING

    Combination Sum Ii in Python, explained

    Problem overview

    The function combinationSum2 receives a list of integers candidates, which may contain duplicates, and a target. It returns every distinct combination of candidates that adds up to target. Each candidate is used at most once, so a number that appears twice in the list can appear up to twice in one combination.

    Two combinations are the same when they hold the same numbers the same number of times, whichever copies they use. In [1, 1, 7] with target 8, the first 1 with the 7 and the second 1 with the 7 form one combination, [1, 7], and it is returned once. Each combination is written in ascending order, and the combinations are in lexicographic order. If nothing adds up to the target, the result is an empty list.

    For [10, 1, 2, 7, 6, 1, 5] and target 8, the answer is [[1, 1, 6], [1, 2, 5], [1, 7], [2, 6]]. For [2, 3] and target 4 it is []: 2 + 3 is 5, and the 2 can't be used twice.

    Approach: sorted backtracking with a skip rule

    Sort candidates. A search call receives start, the first index it may still use, remaining, what the combination still needs, and current, the combination built so far. When remaining is 0, current adds up to the target, and a copy is saved. Otherwise a loop tries each index i from start on as the next number: it adds candidates[i], calls itself with i + 1 and remaining - candidates[i], and removes the number again.

    Three details make this correct and fast:

    • The next call starts at i + 1. In the version of this problem where numbers can be reused, the deeper call starts at i. Here every candidate may be used once, so the next number must come from after this one.
    • Repeats are skipped. The loop skips candidates[i] when i > start and it equals candidates[i - 1]. Within one call, every turn picks a different next number, and a second copy of a value an earlier turn already picked would build the same combinations again. A copy is still taken at i == start, the first number the call tries, and that is how [1, 1, 6] gets its second 1.
    • The loop stops early. Because the list is sorted, the first candidate bigger than remaining means every later one is too big as well, so the loop uses break.

    Here is the search on [1, 1, 7] with target 8, with each line showing the depth of the call:

    0| take 1 -> remaining 7
    1| take 1 -> remaining 6
    2| break at i=2 (7 > 6)
    1| take 7 -> remaining 0
    2| found [1, 7]
    0| skip i=1 (1)
    0| take 7 -> remaining 1

    The first call takes the first 1. The next call takes the second 1, finds that 7 is too big for the 6 that remains, and stops. Back one level, that call takes 7 instead, and the combination [1, 7] is found. At the top, the second 1 is skipped, because the first 1 was already tried as the first number. The trace for [2, 5, 2, 1, 2] with target 5 has the same shape, and finds [1, 2, 2] and [5].

    Without the skip rule, [10, 1, 2, 7, 6, 1, 5] with target 8 returns [[1, 1, 6], [1, 2, 5], [1, 7], [1, 2, 5], [1, 7], [2, 6]], with [1, 2, 5] and [1, 7] listed twice.

    Step by step

    1. Sort candidates in place with sort(). Repeats become adjacent, the output has the required order, and the loop can stop at the first number that is too big.
    2. Create result, an empty list of combinations.
    3. Start the search with self.backtrack(candidates, 0, target, [], result): from index 0, all of the target still needed, nothing chosen.
    4. Return result. The helper adds to this same list.
    5. Declare the helper backtrack with candidates, start, remaining, current and result.
    6. Check for success. When remaining == 0, the numbers in current add up to exactly the target.
    7. Save a copy and return. current.copy() makes a snapshot, since current keeps changing. Every candidate is at least 1, so adding another number would go past the target and there is nothing more to try.
    8. Loop i over range(start, len(candidates)). Starting at start keeps each combination in index order, so in ascending order, and no choice of positions is built twice.
    9. Skip repeats with continue when i > start and the value equals the previous one.
    10. Stop when the number is too big. If candidates[i] > remaining, use break.
    11. Choose the number by appending it to current.
    12. Recurse with i + 1 and remaining - candidates[i].
    13. Undo the choice with current.pop(), so the loop can try the next candidate.

    The solution in Python

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

    The Python details worth a closer look:

    • copy() is needed. The same current list is shared by every call and changed by append and pop. Storing it directly would put the same list into result repeatedly.
    • sort() changes the caller's list. It sorts in place and returns None, so the list passed in comes back sorted. sorted(candidates) would give a new list and leave the original alone.
    • break versus continue. continue abandons one turn of the loop, and break abandons the whole loop. The skip for repeats is a continue, because later numbers may still work. The size test can be a break only because the list is sorted.
    • The skip test at the start of the list. In i > start and candidates[i] == candidates[i - 1], and stops at the first false test. At i == start the second test is never evaluated, so for start == 0 the code never reads candidates[-1], which in Python would silently read the last element.

    Complexity

    Time: O(n · 2^n) in the worst case. With n candidates there are at most 2^n ways to pick some of them, and copying a combination takes up to n steps. The sorted order keeps the search far smaller in practice: the calls only build distinct combinations whose sum is at most the target. With thirty 1s and target 30, for example, the helper is called 31 times, nowhere near the 2^30 ways of picking some of the thirty numbers.

    Space: besides the answer, O(n): the combination being built and one active call per number in it.

    Edge cases and common mistakes

    • No combination. [2, 3] with target 4 gives [], and so does [5] with target 3.
    • A single candidate. [1] with target 1 gives [[1]].
    • Only copies of one number. [3, 3, 3] with target 6 gives [[3, 3]], and [1, 1, 1, 1] with target 2 gives [[1, 1]]: one combination, however many pairs of copies could make it.
    • Using a candidate more than once. Recursing with i instead of i + 1 turns the problem into the reuse version: [2, 3] with target 4 would return [[2, 2]].
    • Forgetting the skip. The result then contains duplicates, as shown above.
    • Using i > 0 for the skip. That would also skip the second 1 of [1, 1, 6] when it is the first number a call tries, so [1, 1, 6] would be lost: for [10, 1, 2, 7, 6, 1, 5] with target 8 the result is [[1, 2, 5], [1, 7], [2, 6]].
    • Not sorting. The skip rule and the early break both depend on a sorted list.

    Testing

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