Start learning free

LESSON 355 OF 540 · BACKTRACKING

Subsets in Kotlin: Backtracking, O(n · 2^n)

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

Given an array nums of distinct integers, return every subset of nums: every way to pick some of its numbers, from none of them (the empty subset) to all of them.

Write the numbers of each subset in the order they have in nums. Return the subsets in the order you get by deciding about the numbers one at a time, skipping before taking: every subset without nums[0] comes before every subset with it; inside each of those two groups, the subsets without nums[1] come before the ones with it, then the same for nums[2], and so on. So the empty subset always comes first, and all of nums comes last.

 

Example 1:

Input: nums = [1,2,3]
Output: [[],[3],[2],[2,3],[1],[1,3],[1,2],[1,2,3]]
Explanation: The first four subsets skip 1, the last four take it.
Among the first four, [] and [3] skip 2, while [2] and [2,3] take it; in each pair, the one without 3 comes first.
The last four are the same four with 1 in front.

Example 2:

Input: nums = [0]
Output: [[],[0]]
Explanation: With one number there are two subsets: without it, then with it.

Example 3:

Input: nums = [3,-1]
Output: [[],[-1],[3],[3,-1]]
Explanation: The subsets without 3 come first. The last subset is written [3,-1], in the order of nums, not sorted.

 

Constraints:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • All the numbers in nums are distinct.

Note:

The tutorial uses backtracking. Walk through nums one index at a time, keeping the numbers taken so far in a list current. At index i, first skip nums[i]: go on to index i + 1 with current as it is. Then take it: add it to current, go on to index i + 1, and remove it again. When the index reaches the end of nums, every number has been skipped or taken: save a copy of current. Trying skip before take gives exactly the order above. It is the same choice as when you generate binary strings: a subset is a string of n bits, 0 for skip and 1 for take, and the subsets come out in the order of those strings.

There are 2n subsets and copying one takes up to n steps, so the time is O(n · 2n). Besides the answer, the extra space is O(n): the subset being built and one call per index.

STEP 1 OF 10
    Ctrl + Enter to check

    KOTLIN · BACKTRACKING

    Subsets in Kotlin, explained

    Problem overview

    The function subsets(nums: IntArray): List<List<Int>> receives an array of distinct integers and returns every subset: every way to pick some of the numbers, from none (the empty subset) to all of them. Each subset lists its numbers in the order they have in nums.

    The order of the subsets is fixed too. You decide about the numbers one at a time, skipping before taking: every subset without nums[0] comes before every subset with it, and inside each group the same rule applies to nums[1], then nums[2], and so on. For nums = [1, 2, 3] the result is:

    [[], [3], [2], [2, 3], [1], [1, 3], [1, 2], [1, 2, 3]]
    

    The first four subsets skip 1 and the last four take it. For [0] the result is [[], [0]], and for [3, -1] it is [[], [-1], [3], [3, -1]]: the last subset keeps the order of nums, so it isn't sorted.

    Approach: one skip-or-take choice per number

    A subset is the answer to one yes/no question per number: is it in or out? With n numbers that makes 2^n subsets, so any solution has to produce exponentially many lists. The goal is to produce each exactly once, in the required order.

    Backtracking does this by walking through nums by index and keeping a list current of the numbers taken so far. At index i it makes two recursive calls, each moving on to i + 1:

    • Skip nums[i]: leave current as it is.
    • Take nums[i]: add it to current, recurse, and then remove it again.

    When i reaches the end of nums, every number has been skipped or taken, and current is a finished subset. Doing the skip call first gives exactly the order the lesson wants. It is the same pattern as writing binary strings: a subset is a string of n bits, 0 for skip and 1 for take, and the subsets come out in the order of those strings.

    The removal after the "take" call is what makes the pattern work with one shared list. Each call gets current in some state and must hand it back in that same state, so the caller can go on with its next choice. That undo is the "back" in backtracking.

    Here is the recursion on nums = [1, 2], with deeper calls indented:

    i=0 skip 1, current=[]
      i=1 skip 2, current=[]
        i=2 save []
      i=1 take 2, current=[2]
        i=2 save [2]
    i=0 take 1, current=[1]
      i=1 skip 2, current=[1]
        i=2 save [1]
      i=1 take 2, current=[1, 2]
        i=2 save [1, 2]
    

    The saved subsets are [], [2], [1], [1, 2], in the required order.

    Step by step

    1. Create result as an empty mutableListOf<List<Int>>(). It collects the subsets, each of which is itself a list.
    2. Start the search by calling backtrack with nums, index 0, an empty mutableListOf() for the subset in progress, and result. At the start nothing is decided yet.
    3. Return result. The helper has added to this same list, so it holds every subset after the call.
    4. Write the helper's signature. It takes the array, the index i to decide about, the current list and result, and returns nothing.
    5. Check for the end. When i == nums.size, every number has been decided.
    6. Save a snapshot and return. current.toList() makes a new list with the same numbers, because current keeps changing as the search goes on.
    7. Skip first. Recurse to i + 1 without touching current. This finds every subset that leaves out nums[i].
    8. Take. Add nums[i] to the end of current. Numbers are added in index order, so each subset keeps the order of nums.
    9. Recurse again to i + 1, now with nums[i] in current. This finds every subset that contains it.
    10. Undo the choice with removeAt(current.size - 1), which removes the last element, so current is what this call received.

    The solution in Kotlin

    class Solution {
        fun subsets(nums: IntArray): List<List<Int>> {
            val result = mutableListOf<List<Int>>()
            backtrack(nums, 0, mutableListOf(), result)
            return result
        }
    
        fun backtrack(nums: IntArray, i: Int, current: MutableList<Int>, result: MutableList<List<Int>>) {
            if (i == nums.size) {
                result.add(current.toList())
                return
            }
            backtrack(nums, i + 1, current, result)
            current.add(nums[i])
            backtrack(nums, i + 1, current, result)
            current.removeAt(current.size - 1)
        }
    }
    

    Kotlin details worth a look:

    • Read-only and mutable lists. List<Int> is a read-only view, while MutableList<Int> has add and removeAt. The helper takes mutable lists because it changes them. A MutableList is also a List, so result.add(current.toList()) fits a MutableList<List<Int>>, and returning result as List<List<Int>> needs no conversion.
    • Why toList() matters. Without it, result.add(current) stores the very list the search keeps changing. By the end of the search, current is empty again, so every saved entry shows the same empty list: for [1, 2, 3] the output becomes eight copies of []. A copy freezes the subset at the moment it was complete.
    • Type inference. mutableListOf() in the first call needs no type argument, because the parameter type MutableList<Int> tells the compiler it holds Ints.
    • removeAt versus remove. On a MutableList<Int>, removeAt(index) removes by position and remove(element) removes by value. With [10, 20, 30], remove(30) gives [10, 20] while removeAt(0) gives [20, 30]. Here the code wants the last position, so it uses removeAt.
    • Reading the array. nums is never changed, so every call can share it.

    Complexity

    Time: O(n · 2^n). The recursion makes a binary tree of depth n with 2^n leaves, and the whole tree has fewer than 2^(n+1) calls. Each leaf copies current, which takes up to n steps. Adding and removing a number is constant time.

    Space: O(n) extra, not counting the answer. The list current holds at most n numbers, and the recursion is at most n + 1 calls deep. The answer itself holds 2^n lists, which is unavoidable.

    Edge cases and common mistakes

    • The empty subset. It is always first, because skipping every number is the first leaf the search reaches. Dropping it from the result is a mistake.
    • One number. [0] gives [[], [0]]: two subsets, without the number and then with it.
    • Taking before skipping. The subsets are all there, but in a different order from the one the lesson requires, so the tests fail.
    • Forgetting to undo. If removeAt is missing, numbers pile up in current and later subsets contain numbers they shouldn't.
    • Saving current without copying it. The saved entries are all the same list and end up empty.
    • Not keeping the order of nums. For [3, -1] the last subset is [3, -1], not [-1, 3]. Adding numbers as the index grows keeps that order without any sorting.
    • Size. A ten-number input produces 1024 subsets. The count doubles with every extra number.

    Testing

    • nums = [1, 2, 3] gives [[], [3], [2], [2, 3], [1], [1, 3], [1, 2], [1, 2, 3]].
    • nums = [0] gives [[], [0]].
    • nums = [3, -1] gives [[], [-1], [3], [3, -1]].
    • An array with the numbers 1 to 10 gives a list of 1024 subsets.
    Keep going with Kotlin500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free