Subsets

For each element, branch on include vs exclude.

Approach

Backtrack over indices: at each step record the current partial subset, then for each remaining element, include it, recurse, and undo. Because every element is independently in or out, you enumerate all 2ⁿ subsets exactly once.

Time complexity

O(n · 2ⁿ)

Space complexity

O(n) recursion

Common mistake

Producing duplicate subsets by not advancing the start index, or mutating the shared list without undoing.

Interactive walkthrough — step through it below, free and without an account

Problem

Given an array of distinct integers, return every possible subset of its elements. The order of the subsets does not matter, and the empty subset must be included. No subset should appear more than once.

Examples

Input: nums = [1, 2, 3]

Output: [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]]

Each number is independently selected or skipped, producing 2³ = 8 subsets.

Input: nums = [0]

Output: [[], [0]]

With one element, there are two choices: exclude it or include it.

Problem

Problem

Given an array of distinct integers, return every possible subset of its elements. The order of the subsets does not matter, and the empty subset must be included. No subset should appear more than once.

Problem and Goal

Learning goal: Recognize why an array of n distinct elements has exactly 2^n subsets.

Core idea For every element, there are two independent decisions: include it in the current subset or leave it out. A complete sequence of these binary decisions identifies exactly one subset. The algorithm must also include the empty subset, which corresponds to excluding every element.

Algorithm plan

  • Start with an empty partial subset.
  • Make a decision for each element, either include or exclude.
  • Record every partial subset when the recursion reaches that decision point.
  • Return the collection of recorded subsets.

Micro-example For [0], excluding 0 gives []; including 0 gives [0]. Thus the answer contains two subsets.

Pitfalls

  • Forgetting to record [] loses a valid answer.
  • Treating the output order as significant can lead to unnecessary constraints.
  • The input values are distinct, so duplicate-handling logic is not needed.

Recap

  • Each element contributes two choices.
  • The empty subset is always part of the answer.
  • The total number of results is 2^n.

Problem and Goal

Every element creates a binary decision

For `nums = [1, 2, 3]`, each value is either included or excluded. Three independent binary choices create `2^3 = 8` distinct subsets, including `[]`.

Think of subset generation as walking through a binary decision tree.

Key Idea: Branch on Include vs Exclude

Learning goal: Understand how the include/exclude idea becomes a backtracking search.

Core idea A decision tree can represent the problem directly. At the node for an element, one branch skips it and the other branch adds it. The backtracking implementation uses a shared path to represent the current subset, then removes the most recently added value when returning from recursion.

Algorithm plan

  • At index i, consider nums[i].
  • Explore the exclude branch without changing path.
  • Explore the include branch after appending nums[i].
  • Undo the append before returning to the caller.

Key insight

The path is not a finished answer only at the leaves. Every state is a valid subset, so record it as soon as the state is reached.

Micro-example From path = [1], the next element 2 creates [1] when excluded and [1, 2] when included. Later, the same pattern applies to 3.

Pitfalls

  • Recording only at leaves misses shorter subsets such as [1].
  • Forgetting the undo step causes one branch's values to leak into another.
  • The recursion must preserve the input order while exploring choices.

Recap

  • Include and exclude are complementary branches.
  • Every recursion state is a valid subset.
  • Backtracking means choose, recurse, and undo.

Key Idea: Branch on Include vs Exclude

At every recursive call, `path` contains a subset of the elements chosen so far.

`[1, 3]` is valid after considering `1`, `2`, and `3`.

Main Approach: Index-Based Backtracking

Learning goal: Translate the search tree into a concise recursive procedure.

Core idea Use a function backtrack(start) where path stores the current subset. First copy path into the results; then loop from start through the remaining indices. For each candidate, append it, recurse from the next index, and pop it to restore the previous state.

Algorithm plan

Backtracking over indices
  1. Initialize results = [] and path = [].
  2. In backtrack(start), append a copy of path to results.
  3. For each i from start to n - 1, append nums[i].
  4. Call backtrack(i + 1) so future choices come strictly later.
  5. Pop nums[i] before the next loop iteration.
python
def backtrack(start):
    results.append(path.copy())
    for i in range(start, len(nums)):
        path.append(nums[i])
        backtrack(i + 1)
        path.pop()

Micro-example At start = 1 with path = [1], the loop tries 2, then undoes it, then tries 3. This produces [1, 2] and [1, 3] without duplicates.

Pitfalls

  • Pass i + 1, not start or i, to advance the search.
  • Append a copy of path, not the mutable list itself.
  • Always pop after the recursive call.

Recap

  • Record first, then choose each remaining element.
  • Recurse on the next index.
  • Undo exactly one choice after each recursive call.

Main Approach: Index-Based Backtracking

Choice after selecting 1What it exploresResult
Recurse with i + 1Only 2 and 3Each subset once
Recurse with i1 can be selected againRepeated elements or non-terminating recursion
Recurse with startEarlier candidates may repeatDuplicate subsets

Walkthrough: nums = [1, 2, 3]

Learning goal: Trace one complete depth-first execution and connect each state to an output subset.

Core idea The traversal records a subset when it enters a recursive call. It then tries each remaining element in order. The depth of the call reflects how many elements are currently in path; after a recursive branch finishes, pop() returns the path to its parent state.

Algorithm plan

  • Begin at start = 0 with path = []; record [].
  • Choose 1, then explore 2, then 3.
  • Backtrack from [1, 2, 3] to [1, 2], then to [1].
  • From [1], choose 3 to record [1, 3].
  • Return to the root and repeat the same process beginning with 2, then 3.

Micro-example The depth-first order is [], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]. These are all eight subsets, with no repetition.

Why it works

Each subset has a unique increasing sequence of selected indices, so the first selected element and every later extension identify one path through the tree.

Pitfalls

  • A visualization should show the undo step, not just the forward choices.
  • The current path must be copied when recorded.
  • A branch with no remaining choices still contributes its current subset.

Recap

  • Every visited state becomes an output.
  • Increasing indices prevent reuse and duplication.
  • Undoing choices lets one shared path represent many branches.

Walkthrough: nums = [1, 2, 3]

End-to-end backtracking trace for the concrete input [1, 2, 3].

Backtracking / recursion tree
1 / 19
Record [] at the root.

Path

empty

Choices

1
2
3
Depth: 0Solutions: 1

Recorded solution

Record the empty subset before choosing any element.

Action

record-solution
Recording before the loop includes the empty subset.Every recursive state is a valid subset.
1 / 19

Pitfalls and Edge Cases

Learning goal: Prevent duplicate outputs and incorrect shared-state behavior.

Core idea Backtracking is correct only when each recursive call owns a well-defined prefix of choices. The start index ensures that a branch moves forward through the input, while pop() ensures that the mutable path is restored before the next branch begins.

Common failure modes

MistakeSymptomFix
Recurse with the same start indexRepeated values or endless recursionCall backtrack(i + 1)
Never undo after recursionLater subsets contain unrelated earlier choicesExecute path.pop()
Store path directlyEvery result changes as path changesStore path.copy()
Record only at leavesMissing [] and shorter subsetsRecord on entry to every call

Micro-example After exploring [1, 2], the path must return to [1] before trying 3. If 2 remains in the shared list, the algorithm may incorrectly produce [1, 2, 3] again or distort a stored result.

Common pitfall

Advancing the start index and undoing the shared list are separate responsibilities: one prevents duplicate choices, the other prevents state leakage.

Pitfalls

  • Do not sort or deduplicate the result for this problem; distinct input values already guarantee uniqueness.
  • Be careful at start == n: record the current path, then return.
  • Copy mutable paths at the moment they are recorded.

Recap

  • Advance with i + 1.
  • Undo every append.
  • Record a snapshot, not the shared list.

Pitfalls and Edge Cases

After recursively exploring a candidate, which operation is required before trying the next candidate?

Complexity and Wrap-up

Learning goal: Justify the O(n · 2^n) time bound and O(n) recursion space.

Core idea There are 2^n subsets, so the algorithm must produce exponentially many results. Copying a current subset can cost up to O(n), making the total output-related work O(n · 2^n). The recursion path contains at most n selected elements, so the auxiliary call stack and path use O(n) space, excluding the returned output.

Algorithm plan

  • Record the current path.
  • Loop through candidates from start onward.
  • Choose one candidate and recurse with the next index.
  • Undo the choice and continue.
  • Stop when all branches have been explored.

Micro-example For n = 3, the algorithm records 8 subsets. The longest copied subset has length 3, and the deepest recursion also has depth 3.

Why it works

Every subset corresponds to one increasing sequence of selected indices. The algorithm explores every such sequence once, so it is complete and duplicate-free.

Pitfalls

  • Do not describe the space as only O(1); the recursion and current path grow with n.
  • If counting the output, storing all subsets requires O(n · 2^n) space.
  • The exponential time is unavoidable when the required output itself is exponential.

Recap

  • Time: O(n · 2^n) including path copies.
  • Auxiliary recursion space: O(n).
  • Include, recurse, undo, and record every state.

Complexity and Wrap-up

Summarizes the cost of enumerating every subset.

Complexity analysis
Time
O(n · 2^n)
Space
O(n)
Where the time goes
  • Visit every subsetThere are two choices for each of n elements.O(2^n)
  • Copy the current pathA recorded subset may contain n elements.O(n)
  • Recursive call stackAt most one call per selected element is active.O(n)
How it grows
input size n →opsO(n²)O(2ⁿ)O(n log n)O(n)O(log n)O(1)

The exponential cost matches the required output size; the extra recursion space is linear.

Now try it on your own problem

Paste any LeetCode problem — or one of your own — and codebrew builds a walkthrough like this one for it.

Build a lesson from any problem

Related problems