Start learning free

LESSON 366 OF 573 · BACKTRACKING

Combination Sum II in Kotlin: Backtracking Without Repeats

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

Given an array 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 array.

 

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

    KOTLIN · BACKTRACKING

    Combination Sum Ii in Kotlin, explained

    Problem overview

    The method combinationSum2 receives candidates, an IntArray that may contain duplicates, and an integer target. It returns every distinct combination of candidates that adds up to target. Each candidate can be used at most once, but a value that appears twice in the array can appear twice in one combination.

    Two combinations are the same when they hold the same numbers the same number of times, whichever copies were used. With [1,1,7] and a target of 8, the first 1 plus the 7 and the second 1 plus the 7 are the single combination [1, 7], listed once. Each combination is written in ascending order, and the combinations are in lexicographic order: the first position where two of them differ decides which comes first.

    For candidates = [10,1,2,7,6,1,5] and target = 8, the answer is [[1, 1, 6], [1, 2, 5], [1, 7], [2, 6]]. When no combination works, as with [2,3] and a target of 4, the method returns an empty list.

    Approach: sorted backtracking that skips repeated values

    Sort candidates first. Then a recursive helper builds a combination one number at a time. A call knows start, the first index it may still use, remaining, how much the combination still needs, and current, the combination 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: add candidates[i], recurse with i + 1 and the smaller remaining, and remove the number again.

    Three details turn this into the right answer:

    • Recursing with i + 1. The deeper call starts after the number just chosen, so each candidate is used at most once. Starting at i would allow a number to be chosen again and again. For [2,3] with target 4, that mistake returns [[2, 2]] where the right answer is empty.
    • Skipping repeated values. Inside one call, every turn of the loop picks a different candidate for the next number. If candidates[i] equals candidates[i - 1] and i > start, that value was already tried in this call, and trying it again would build the same combinations a second time. At i == start the number is the first one the call tries, so it is taken even when it equals its left neighbour. That is how [1, 1, 6] gets its second 1.
    • Stopping early. Because the array is sorted, once candidates[i] is bigger than remaining, every later candidate is bigger too, and the loop can stop with break.

    Sorting also gives the required order: the loop tries smaller numbers first, so the combinations come out in lexicographic order.

    Step by step

    1. Sort candidates in place with sort(). Equal values become neighbours.
    2. Create result as a mutableListOf<List<Int>>().
    3. Start the search with backtrack(candidates, 0, target, mutableListOf(), result): from index 0, needing all of target, with an empty combination.
    4. Return result.
    5. Declare backtrack with candidates, start, remaining, current and result.
    6. Check remaining == 0: nothing is needed any more.
    7. Save a copy with current.toList() and return. Every candidate is at least 1, so adding another one would pass the target.
    8. Loop i over start until candidates.size. Starting at start makes a combination take its numbers in index order.
    9. Skip repeats: if i > start and candidates[i] == candidates[i - 1], continue.
    10. Break when too big: if candidates[i] > remaining, leave the loop.
    11. Choose the number by adding it to current.
    12. Recurse with i + 1 and remaining - candidates[i].
    13. Undo the choice with removeAt(current.size - 1).

    The solution in Kotlin

    class Solution {
        fun combinationSum2(candidates: IntArray, target: Int): List<List<Int>> {
            candidates.sort()
            val result = mutableListOf<List<Int>>()
            backtrack(candidates, 0, target, mutableListOf(), result)
            return result
        }
    
        fun backtrack(candidates: IntArray, start: Int, remaining: Int, current: MutableList<Int>, result: MutableList<List<Int>>) {
            if (remaining == 0) {
                result.add(current.toList())
                return
            }
            for (i in start until candidates.size) {
                if (i > start && candidates[i] == candidates[i - 1]) {
                    continue
                }
                if (candidates[i] > remaining) {
                    break
                }
                current.add(candidates[i])
                backtrack(candidates, i + 1, remaining - candidates[i], current, result)
                current.removeAt(current.size - 1)
            }
        }
    }
    

    Kotlin details worth knowing:

    • sort() on an IntArray works in place and returns nothing, so the caller's array ends up sorted.
    • The copy matters. current is one list that keeps changing. result.add(current.toList()) stores a snapshot with the numbers it holds right now, instead of a reference to the list that is emptied again as the search backs out.
    • continue and break both apply to the nearest for loop: continue goes on with the next i, and break leaves the loop.
    • The return type is the read-only List<List<Int>>, while the object returned is a MutableList of lists, which is allowed.

    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. In practice the search is far smaller, because it only builds distinct combinations whose sum so far does not pass target.

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

    Edge cases and common mistakes

    • No combination. [2,3] with target 4 gives [], and [5] with target 3 also gives [].
    • Single candidate. [1] with target 1 gives [[1]].
    • Several equal values. [2,5,2,1,2] with target 5 gives [[1, 2, 2], [5]]: there are three 2s, and the combination with two of them appears once. [1,1,1,1] with target 2 gives [[1, 1]].
    • Duplicates of the pair. [1,1,7] with target 8 gives [[1, 7]]. Without the skip, the result is [[1, 7], [1, 7]], and on the main example it is [[1, 1, 6], [1, 2, 5], [1, 7], [1, 2, 5], [1, 7], [2, 6]], which also breaks the order.
    • Reusing a number. Recursing with i instead of i + 1 lets the main example produce [1, 1, 1, 1, 1, 1, 1, 1], a combination that uses the same 1 over and over, and many other wrong ones.
    • Many equal values. With one hundred 1s and a target of 30, the answer is a single combination of thirty 1s.

    Testing

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