Recursion

All Subsets

Backtracking

Given an array of unique numbers nums, return all possible subsets of those numbers.

You can return the subsets in any order.

Example:

nums = [1,2]subsets = [ [], [1], [2], [1,2] ]\notag \begin{align*} \texttt{nums} &\texttt{ = [1,2]} \\ \texttt{subsets} &\texttt{ = }\bigg[ \texttt{ [], [1], [2], [1,2] } \bigg] \end{align*}
First Few Test Cases:
generateSubsets([])
# [[]]
generateSubsets([1, 2])
# [[], [2], [1], [1, 2]]
generateSubsets([1, 2, 3])
# [[], [3], [2], [2, 3], [1], [1, 3], [1, 2], [1, 2, 3]]

You could solve this problem using regular old recursion, where you make a function called subsets(nums), and then find a recursion for it. But this is slow because the Data Structure input nums changes, and there's no way to apply Backtracking to that to fix the problem. The function generates solutions all at once, so there's nothing to "undo" when you try applying Backtracking by making the array global.

The regular old recursion solution is inefficient, so we'll solve this problem by generating solutions one at a time instead. That way, we'll be able to optimize using Backtracking.

We can get all the subsets by starting with an empty array [], and then considering adding every possible element to it.

Let's find the solutions for nums = [1,2,3].

We're allowed to either add the first element or not add it, like this:

All Subsets - image

We can then consider adding and not adding the second element:

All Subsets - image

We can then consider adding and not adding the third element:

All Subsets - image

Now we've considered adding all of the elements. We now have the solution, which is all the subsets at the very end - [], [3], [2], [2,3], [1], [1,3], [1,2], [1,2,3].

Tying everything together, what we're really doing is building this tree:

All Subsets - image

We can really just solve this problem by traversing this tree.

We'll write a function visitAll that traverses the tree. When it gets to the bottom of the tree it should add the subset that it's on to the solutions. When the function is called, it needs to know the location in the tree that it's standing at. You can know where in the tree you're standing if you know the current subset subset. You also need to know the index of the element that you should add next idx.

visitAll(subset, idx)

1. Recursion

The function is called on a particular subset subset and index idx. The function should consider adding the current number it's on nums[idx] to the subset, and also not adding it. It should then continue traversing on the next number. Here's the recursion for this:

visitAll(subset, idx) = (
    visitAll(subset + [nums[idx]], idx + 1) # add element
    visitAll(subset, idx + 1)               # don't add element
)

2. Base case

When the function gets to the bottom of the tree, it should add the current subset to the solution and stop. This happens when idx == len(nums).

if idx == len(nums):
    solns.append(subset)
    return

3. Code

Combining the recursion and base case, we can easily get the solution.

Here's the recursive code:

Open SpoilerClose Spoiler
def generateSubsets(nums):
    solns = []

    def visitAll(subset, idx):
        # base case
        if idx == len(nums):
            solns.append(subset)
            return
        # recursion
        visitAll(subset + [nums[idx]], idx + 1)
        visitAll(subset, idx + 1)
        
    visitAll([], 0)
    return solns

Time Complexity O(3n⋅2n)O( 3n\cdot 2^n ). We need to build all 2n2^n possibilities, and each has up to n\texttt{n} entries in it. We included the factor of 3 to show it's worse in practice than Backtracking, even though it's theoretically the same time complexity.

Space Complexity O(n⋅2n)O(n\cdot 2^n )

Call Stack Complexity O(n2)O(n^2). There are nn calls at a time, which all store their own O(n)O(n) sized array.

This code is very inefficient though. This is because we're using subset + [nums[idx]]\small \texttt{subset + [nums[idx]]} which creates a whole new array, wasting time and space. This is where Backtracking comes in. Backtracking is where we make the inputs to the function like subset a global variable so we never create a new array. We also decided to make idx global to go full force with Backtracking, although this doesn't change the time or space complexity since it's just a number.

Here's the full code using Backtracking:


def generateSubsets(nums):
    # global parameters
    subset = []
    idx = 0

    solution = []

    def visitAll():
        # without `nonlocal` we'd just be setting local variables
        nonlocal idx, subset, solution

        # base case
        if idx == len(nums):
            # `subset` is global and will change if we don't copy it
            solution.append(subset.copy())
            return

        # recursion
        
        idx += 1 # add 1
        visitAll()
        idx -= 1 # undo

        subset.append(nums[idx]) # add nums[idx]
        idx += 1 # add 1
        visitAll()
        idx -= 1 # undo 
        subset.pop() # undo

    visitAll()
    return solution

Time Complexity O(n⋅2n)O(n\cdot 2^n ) . We need to build all 2n2^n possibilities, and each has up to n\texttt{n} entries in it.

Space Complexity O(n⋅2n)O(n\cdot 2^n ) for the output array. The Call Stack and global parameters also take up some space, but the output array's space complexity beats them by far.

Call Stack Complexity O(n)O(n). The Call Stack gets nn calls deep, and each call uses O(1)O(1) space.

Mark as Completed:
Submits:
generateSubsets
Test your code to get an output here!
generateSubsets(
)
Test your code to get an output here!