Start learning free

LESSON 356 OF 540 · BACKTRACKING

Combination Sum in Kotlin: Backtracking on 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

    KOTLIN · BACKTRACKING

    Combination Sum in Kotlin, explained

    Problem overview

    The function combinationSum receives an IntArray of distinct numbers, candidates, and a target. It returns every combination of candidates that adds up to target. Each number can be used as many times as you like, and two combinations are the same when they use the same numbers the same number of times, whatever the order. Each combination is written in ascending order, and the combinations are returned in lexicographic order.

    For candidates = [2, 3, 6, 7] and target = 7 the answer is [[2, 2, 3], [7]]. For candidates = [5, 2, 3] and target = 8 it is [[2, 2, 2, 2], [2, 3, 3], [3, 5]], even though the candidates arrive unsorted. For candidates = [2] and target = 1 there is no combination, so the result is an empty list.

    Approach: build a combination one number at a time

    This is a search over choices. At each point you hold a partial combination and the amount still needed, remaining. You pick the next number, subtract it from remaining, and continue. If remaining reaches 0, the partial combination is complete and gets recorded. After exploring a choice you remove the number again and try the next one: that is backtracking.

    Two decisions keep the answer free of duplicates and in the right order:

    • Sort the candidates first. Then every combination is built in ascending order, and trying candidates from smallest to largest produces the combinations in lexicographic order.
    • Only move forward. A call receives a starting index i and may use candidates from i on, never earlier ones. The same number can repeat, because the recursive call starts from j, not j + 1. [2, 2, 3] is found; [3, 2, 2] can't be, because after a 3 the search never goes back to a 2.

    Sorting also allows a cut: if candidates[j] is larger than remaining, every later candidate is larger still, so the loop can stop with break.

    The search is exponential. A combination has at most target / m numbers, where m is the smallest candidate, and each is one of n candidates, so the time is O(n^(target / m)) in the worst case.

    Step by step

    The outer function:

    1. Sort in place with candidates.sort().
    2. Create result as an empty mutableListOf<List<Int>>(). Each combination is a list of numbers, so the result is a list of lists.
    3. Start the search with backtrack(candidates, target, 0, mutableListOf(), result): nothing is chosen yet, all of target is still needed and every candidate may be used.
    4. Return result, which the helper filled.

    The helper, backtrack:

    1. Record a finished combination. When remaining == 0, the numbers in current add up to the target. Add a copy, current.toList(), and return. Every candidate is at least 2, so adding more would overshoot anyway.
    2. Loop over the allowed candidates with for (j in i until candidates.size).
    3. Stop at a number that is too big. If candidates[j] > remaining, break.
    4. Choose it by adding candidates[j] to current.
    5. Recurse with remaining - candidates[j] and the same index j, which lets the number be used again.
    6. Undo the choice with current.removeAt(current.size - 1), so the loop can try the next candidate from the same state.

    The solution in Kotlin

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

    The Kotlin details worth a closer look:

    • toList() makes a snapshot. current keeps changing during the search. If the code added current itself, every entry of result would be the same list object, and by the end it would show whatever current holds then, an empty list. toList() copies the numbers into a new list.
    • List versus MutableList. The return type List<List<Int>> is a read-only view. Inside, result and current are MutableLists, which have add and removeAt. The mutableListOf() in the first call has its element type inferred from the parameter type.
    • removeAt versus remove. On a MutableList<Int>, removeAt(index) removes by position. remove(x) removes the first element equal to x, which is a different operation, so removeAt is the right call for undoing the last choice.
    • i until candidates.size is a range that stops before the size, so j is always a valid index.
    • sort() on an IntArray sorts in place and returns nothing, so the caller's array is reordered. Use sorted() or copy the array first if the original order matters.

    Complexity

    Time: exponential, O(n^(target / m)) in the worst case, where n is the number of candidates and m the smallest one. The depth of the search is at most target / m, and each level may try up to n candidates. The break and the forward-only index prune much of that.

    Space: O(target / m) besides the answer: the combination being built and one call per number in it. The copies stored in result are the output itself.

    Edge cases and common mistakes

    • No combination. [2] with target 1 gives []: the loop breaks at once because 2 is bigger than 1.
    • Reusing a number. [2] with target 4 gives [[2, 2]]. Recursing with j + 1 would forbid it.
    • Duplicate combinations. Starting every loop at 0 instead of i would produce [2, 3] and [3, 2] as separate answers.
    • Forgetting the copy. Adding current instead of current.toList() fills result with references to one list that ends up empty.
    • Forgetting to undo. Without removeAt, the numbers pile up in current and later combinations contain leftovers.
    • Unsorted input. Without sorting, the output order is wrong and the break would cut off candidates that still fit.

    Testing

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