DSA Guide
Backtracking

Subsets

Generate every subset of a list, and meet the decision tree that all backtracking problems share

MediumBacktracking· take or skip
ArrayBacktrackingBit ManipulationRecursion
Problem #78

Problem Statement

Given an array of distinct integers, return all possible subsets (the power set). The answer may be in any order, and must not contain duplicates.

Input:  nums = [1, 2, 3]
Output: [[], [1], [2], [3], [1,2], [1,3], [2,3], [1,2,3]]

Input:  nums = [0]
Output: [[], [0]]

Intuition

There are 2³ = 8 subsets of a 3-element list, and that 2ⁿ is the clue.

Why exactly 2ⁿ? Because building a subset means walking the list once and making one yes/no decision per element: is this element in, or out? Three elements, two options each, 2 × 2 × 2 = 8.

The key insight

A subset is not something you search for. It is the record of a series of decisions — one per element.

So instead of hunting for subsets, walk through the elements making decisions, and every complete run of decisions is a subset.

The decision tree

                       []
                 /            \
            take 1            skip 1
            /    \            /    \
       take 2   skip 2   take 2   skip 2
        /  \     /  \     /  \      /  \
      t3  s3   t3  s3   t3  s3    t3  s3
       |   |    |   |    |   |     |   |
   [1,2,3][1,2][1,3][1] [2,3][2]  [3]  []

Eight leaves, eight subsets. Backtracking walks this tree depth-first.

Why every node is an answer, not just the leaves

In many backtracking problems only complete paths count. Here, every node on the way down is already a valid subset.

[1] is a subset. So is [1, 2]. So is the empty path at the root.

That is why the recording happens at the top of the function rather than inside an "is it complete?" check — a detail that makes this the simplest backtracking problem there is.

The start index, and why it exists

Without care you would produce [1, 2] and [2, 1] — the same subset twice, since order does not matter in a set.

The fix is a start index: when choosing the next element, only look at positions after the one just taken. That forces every subset into increasing order, so each one can be built exactly one way.

With start:     [1,2]  produced once
Without start:  [1,2]  and  [2,1]  — duplicates

Approach

Record the current path immediately

Every path is a valid subset, including the empty one. Record a copy — not the array itself.

Loop over the remaining candidates

Start at start, not at 0. Everything before start was already decided by an ancestor call.

Choose, explore, undo

Append nums[i], recurse with start = i + 1, then pop.

The i + 1 is what prevents reusing an element and what keeps the ordering fixed.

Watch it run

nums = [1, 2, 3] — a depth-first walk of the decision tree
0
1
1
2
2
3
path[]results1
The empty path is recorded straight away. That is the empty subset.
1 / 8
Each element is taken on some paths and skipped on others. 2³ = 8 combinations.

Solution

def subsets(nums: list[int]) -> list[list[int]]:
    """Return every subset of a list of distinct integers."""
    results: list[list[int]] = []
    path: list[int] = []

    def backtrack(start: int) -> None:
        # Every path is a valid subset — record a COPY, not the live array.
        results.append(path[:])

        for i in range(start, len(nums)):
            path.append(nums[i])      # choose
            backtrack(i + 1)          # explore, never looking back
            path.pop()                # undo

    backtrack(0)
    return results


def subsets_bitmask(nums: list[int]) -> list[list[int]]:
    """Alternative: the bits of 0..2^n - 1 spell out the include/exclude choices."""
    n = len(nums)
    results: list[list[int]] = []

    for mask in range(1 << n):  # 1 << n is 2^n
        # Bit i set means "include nums[i]".
        results.append([nums[i] for i in range(n) if mask & (1 << i)])

    return results

The bitmask version is worth knowing

Count from 0 to 2ⁿ - 1 and read each number's bits as the include/exclude decisions. For n = 3, the number 5 is binary 101, meaning "take index 0, skip index 1, take index 2" — the subset [1, 3].

No recursion and no undo. It only works when n ≤ 32 or so, and it is harder to adapt when a problem adds constraints — but for plain subsets it is short and fast.

Complexity Analysis

Time Complexity

O(n · 2ⁿ)

Space Complexity

O(n)

  • Time — there are 2ⁿ subsets, and copying each one into the results costs up to O(n). The recursion itself does O(2ⁿ) work; the copying is what adds the factor of n.
  • Space — excluding the output, only the path array and the recursion stack, both at most n deep. Including the output, it is O(n · 2ⁿ) because that is simply how big the answer is.

Why n is always small here

At n = 20 there are about a million subsets. At n = 30, a billion. Constraints for this problem are typically n ≤ 10, which is the author signalling that exponential is the intended answer.

Edge Cases

On this page