Problem Statement in English

You’re given an array. Your task is to return the powerset of that array, i.e., every possible subset of that array.


Approach

Modelling this as a decision tree makes it so much simpler. Think about it. Say you’re given an array, [1,2,3,4,5], and you want to find every subset. Let’s work through it by hand:

  • We can start at index 0 - we’ve 2 options, either include, or exclude it. This generates 2 possibilities. Let’s build our next solution on each of these 2 options.
  • In both of those 2 options we can either include, or exclude index 1. Since we have 2 options per solution, that’s $2*2=4$ generated arrays.
  • We continue this process until we’ve exhausted all indices in the original array that we can either include, or exclude.

Here’s what the decision tree would look like for the array [1,2,3]

    graph TD
    A(<>) -->|don't pick 1| B(<>)
    A -->|pick 1| C(<1>)

    B -->|don't pick 2| D(<>)
    B -->|pick 2| E(<2>)

    C -->|don't pick 2| F(<1>)
    C -->|pick 2| G(<1,2>)

    D -->|don't pick 3| H(<>)
    D -->|pick 3| I(<3>)

    E -->|don't pick 3| J(<2>)
    E -->|pick 3| K(<2,3>)
    
    F -->|don't pick 3| L(<1>)
    F -->|pick 3| M(<1,3>)
    
    G -->|don't pick 3| N(<1,2>)
    G -->|pick 3| O(<1,2,3>)

As you can see, the leaf nodes contain all the subsets. It’s important to note that this approach doesn’t generate duplicates when the array has distinct elements.


Solution in Python


class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:

        # we'll store all unique possbilities in this
        ans = []
        
        # i'm using this to have access to the `ans` variable without having to pass it around
        def recursiveCall(l :List, i:int):
            # reached leaf node status
            if i == len(nums)-1:
                # create a copy of the previous result,
                #  and append the value at this index to it
                yes = l.copy()
                yes.append(nums[i])

                # copy the previous result
                no = l.copy()

                # since we've reached the leaf node, 
                # we don't need to perform any further appends,
                # and this forms part of our asnwer.
                # So we save both options
                ans.append(yes)
                ans.append(no)
            # we've gone out of bounds
            # let go of this path
            elif i > len(nums):
                pass
            # not a leaf node, and further indices to include or exclude exist
            else:
                yes = l.copy()
                yes.append(nums[i])
                # pass this possibility into the recursive call
                # to form the base of subsequent answers
                recursiveCall(yes, i+1)

                # pass this possibility into the recursive call
                # to form the base of subsequent answers
                recursiveCall(l.copy(), i+1)
        
        # initial call
        recursiveCall([], 0)

        # our final answer
        return ans

Complexity

  • Time: $O(n \times{2^n})$
    This is because we generate $2^n$ possibilities per choice, and since we have $n$ choices, that’s $n*2^n$.

  • Space: $O(n \times 2^n)$
    This is because our final answer list contains $2^n$ possible answers, with each answer containing $n$ elements at most. Hence $n \times 2^n$.


Mistakes I Made

The usual fumbling whilst building a recursive solution. It all came together once I could put my finger on the base case.


And we are done.