Algorithm

Subsets (Power Set)

Backtracking Pattern

Subsets

Given an array of distinct integers, return every possible subset — the power set.

A subset is formed by choosing, independently for each element, whether to include it. That includes taking none of them (the empty subset) and taking all of them, both of which must appear in the output.

Subsets are sets, so order within a subset carries no meaning and no subset may be listed twice. The order of the subsets themselves does not matter.

CONSTRAINTS
  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • All integers in nums are distinct
  • The output contains exactly 2ⁿ subsets, including the empty one
  • Subsets may be returned in any order
EXAMPLE 1
Input: nums = [1,2,3]
Output: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
Eight subsets for three elements — one for every combination of independent include-or-exclude decisions. Note that [1,2] appears but [2,1] does not: they describe the same set, so only one representative is listed.
EXAMPLE 2
Input: nums = [0]
Output: [[],[0]]
A single element yields two subsets: the one omitting it and the one taking it. The empty subset is a genuine answer, not an edge case to skip — leaving it out is the most common mistake here.
EXAMPLE 3
Input: nums = [1,2]
Output: [[],[1],[2],[1,2]]
Two elements give four subsets. Each additional element doubles the count, since every existing subset can either take the newcomer or not — which is exactly why the total is a power of two.
EXAMPLE 4
Input: nums = [-1,5]
Output: [[],[-1],[5],[-1,5]]
Values are irrelevant to the structure of the answer. Negatives, zero and duplicat-free ordering change nothing: only the positions matter, because each element is independently taken or not.
Should the empty subset be included?
Yes. Excluding every element is a legitimate set of choices, and the power set of any collection always contains it. An output of size 2ⁿ is only achievable with it present.
Are the input elements guaranteed distinct?
Here, yes, and it matters enormously. With duplicates, [2,2] would produce identical subsets by different routes and you would need to sort the input and skip repeated values at each level — that variant is Subsets II.
Does the order of elements within a subset matter, or the order of subsets in the output?
Neither. [1,2] and [2,1] are the same subset and only one should appear. The subsets themselves may be returned in any order, which means no sorting step is required at the end.
How large can the output get?
2ⁿ subsets, which at n = 10 is 1024. That the answer itself is exponential is worth saying out loud: no algorithm can beat exponential time here, because merely writing the output takes that long.

This is the first problem in the backtracking section, and it is the right place to build the technique from nothing — because the structure here is as simple as it ever gets, which makes the machinery visible.

What the question is really asking

Look at what defines a subset. For each element you make one decision: take it, or leave it. Nothing else. The subset is entirely determined by those decisions.

That immediately tells you how many subsets exist. Three elements means three independent yes-or-no decisions, and independent binary choices multiply: 2 × 2 × 2 = 8. In general 2ⁿ.

It also tells you the output is exponential, so no clever algorithm will make this fast. The work is unavoidable — you are being asked to produce 2ⁿ things. The only question is how to generate them systematically, without missing any and without producing any twice.

The decision tree

Draw the decisions as a tree. Start at the root with nothing chosen. The first branching is over element 0: one branch takes it, the other skips it. From each of those, branch on element 1. Then element 2.

After n levels of branching, every path from root to leaf has made a decision about every element — so each leaf is exactly one subset, and there are 2ⁿ leaves. Generating all subsets means walking every root-to-leaf path.

That reframing is the important move, and it is worth stating in general terms because every problem in this section uses it: a problem asking for "all possible X" is a problem about walking a tree of decisions, where each level is one decision and each leaf is one complete answer.

Walking the tree without wasting memory

The obvious way to walk it is recursion. At each node you know which element you are deciding about and what you have chosen so far.

The naive implementation passes a fresh copy of the current subset down each branch:

python
def build(i, path):
    if i == len(nums):
        result.append(path)
        return
    build(i + 1, path + [nums[i]])    # take it — makes a new list
    build(i + 1, path)                # skip it

That works and is genuinely fine here. But it allocates a new list at every node of the tree, and the pattern does not survive into harder problems where the state being carried is large — a chessboard, a Sudoku grid, a marked-up input.

So the standard technique keeps one shared path and mutates it. Before recursing down a branch, modify the path to reflect that choice. After the recursion returns, undo the modification so the path is exactly as it was before, ready for the next branch.

python
def subsets(nums):
    result = []
    path = []

    def build(i):
        if i == len(nums):
            result.append(list(path))     # snapshot: copy, do not append path itself
            return

        path.append(nums[i])              # CHOOSE: take element i
        build(i + 1)                      # EXPLORE
        path.pop()                        # UN-CHOOSE: restore the shared state

        build(i + 1)                      # EXPLORE the skip branch

    build(0)
    return result

That three-beat rhythm — choose, explore, un-choose — is the entire pattern, and every remaining problem in this section is a variation on it. The name backtracking refers to the third beat: you back out of a decision to try a different one.

Crucial Noteresult.append(list(path)) copies. Appending path directly would store a reference to the single shared list, which keeps changing as the search continues — the result would end up holding many references to the same list, all showing whatever the path contained at the very end (usually empty). This is the single most common backtracking bug, and it produces output that looks bizarre rather than merely wrong.
Crucial Notethe pop() must sit between the two recursive calls, not after both. Its job is to restore the state so the second branch starts from the same point the first one did. Placing it at the end would leave element i in the path while the skip branch runs, which is precisely the branch that must not contain it.
Why the undo is necessary at all

It is worth being explicit about this, because it looks like bookkeeping and it is not.

The shared path represents the decisions made on the route from the root to where you are now. When recursion returns from a branch, you are conceptually back at the parent node — so the path must again describe the parent's state. If the child's modification is left in place, the sibling branch inherits a decision it never made, and every subset generated beneath it is wrong.

So the invariant is: when a call returns, the shared state is exactly as it was when the call began. Every backtracking function must preserve it, and every bug in this section comes from breaking it.

Worked example:nums = [1,2]

Tracing every call, with the path shown at each point:

- build(0), path is []. Take 1: path becomes [1].
- build(1), path is [1]. Take 2: path becomes [1,2].
- build(2). Index equals length, so record a copy of [1,2]. Return.
- Pop, path back to [1]. Now the skip branch.
- build(2). Record [1]. Return.
- Return.
- Pop, path back to []. Now the skip branch for element 0.
- build(1), path is []. Take 2: path becomes [2].
- build(2). Record [2]. Return.
- Pop, path back to []. Skip branch.
- build(2). Record [] — the empty subset, produced by the path that declined everything. Return.

Result: [[1,2], [1], [2], []]. Four subsets for two elements, and note the path is [] again at the end — the invariant held all the way back to the root.

The alternative worth knowing

There is a much shorter solution, and it is the one to mention if asked for another approach.

Start with a list containing only the empty subset. For each element, take every subset already built and produce a copy with the element added.

python
def subsets(nums):
    result = [[]]
    for num in nums:
        result += [curr + [num] for curr in result]
    return result

Same complexity, three lines. It is a direct transcription of the counting argument: each new element doubles the answer, because every existing subset either takes it or does not.

There is also a bit-manipulation version — the numbers 0 to 2ⁿ-1 each encode one subset, with bit i deciding element i — which is the most literal expression of "n independent binary choices" and worth a sentence in an interview.

So why learn the backtracking form for a problem with a three-line solution? Because the doubling trick is specific to this problem, and the choose-explore-un-choose skeleton is not. Combinations adds a constraint on which choices are available. Permutations changes what counts as a choice. Combination Sum allows reuse and prunes on a running total. N-Queens and Sudoku prune on constraints at every level. All of them are this same function with different slots filled.

Train the reflex: when a problem asks for all of something, define one decision, define the choices available at each decision, and walk the tree — choosing, exploring, and always undoing on the way back out.

Interactive Strategy Visualization

The Recursion Tree

Visualizing how the algorithm branches for each decision.

Root[]+1-1+2-2+2-2
Active Path
Found Subset
Start at the Root (Empty Set). We have 2 choices for number '1'.
TREE STRUCTURE

The tree grows from left to right. Each level corresponds to one number in the input. For each number, we branch Up (+ Include) or Down (- Exclude).

DFS TRAVERSAL

Backtracking is simply a Depth First Search. We go as deep as possible into one reality, then step back to the last fork and try the other path.

O(N × 2ⁿ) Enumerate All Binary Masks
O(N × 2ⁿ) Iterative Doubling
O(N × 2ⁿ) Backtracking With O(N) Working Space