Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Backtracking & State Search
P2MediumMental model~4 min speedrun

Subsets (Power Set)

Grows into
CombinationsOne Choice per SlotPermutationsSubsets II
Unlocks
Bitmask SubsetsMeet in the Middle
The problem

Subsets (LC 78)(opens leetcode.com)

You are given an array nums of different integers. Return every subset of it: the empty subset, every subset in between, and the whole array. No subset may appear twice. You may return the subsets in any order.

Example 1
Input:nums = [1,2,3]
Output:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
Three numbers give 2^3 = 8 subsets. Each number is either in a subset or not.

Subsets

elements ➔ every decision tree node is valid ➔ snapshot deep copy
Spot it when: You must list all subsets, subsequences or on/off choices of the input.
Subsets
Python
1from typing import List
2 
3def subsets(nums: List[int]) -> List[List[int]]:
4 """Subsets (LC 78): every subset of nums, saved once at the node of the decision tree that builds it.
5 N = len(nums) and all values are different.
6 Time: O(N * 2^N) | Space: O(N) path + recursion stack, output not counted
7 """
8 results: List[List[int]] = []
9 path: List[int] = [] # the choices from the root to the current node, in order
10 
11 def backtrack(start: int) -> None:
12 # 1. Save This Node:
13 # Every node of the decision tree is a valid subset, so copy path into results
14 # as soon as the call starts, before trying any further choice.
15 results.append(list(path))
16 
17 for i in range(start, len(nums)):
18 # 2. Choose, Explore, Un-choose:
19 # Add nums[i] to path, recurse forward so the next call only sees later
20 # indices, then remove nums[i] so the loop's next i tries its own branch.
21 path.append(nums[i])
22 backtrack(i + 1)
23 path.pop()
24 
25 backtrack(0)
26 return results
Invariant: path always holds exactly the choices from the root to the current node, and every node is saved exactly once.

Every node of the decision tree is a valid subset, so save a copy of path as soon as you enter backtrack. The loop only picks indices from start onward, so each subset is built in one order and saved once. The trap is saving path itself: path keeps changing with append and pop, so write results.append(list(path)), or every saved subset ends up empty.

See it run

Subsets, step by step

start 0 · path [] · saved 0

start 0 · path [] · saved 1

start 0 · path [1] · saved 1

start 1 · path [1] · saved 2

start 1 · path [1, 2] · saved 2

start 2 · path [1, 2] · saved 3

start 2 · path [1, 2, 3] · saved 3

start 3 · path [1, 2, 3] · saved 4

start 2 · path [1, 2] · saved 4

start 1 · path [1] · saved 4

start 1 · path [1, 3] · saved 4

start 3 · path [1, 3] · saved 5

start 1 · path [1] · saved 5

start 0 · path [] · saved 5

start 0 · path [2] · saved 5

start 2 · path [2] · saved 6

start 2 · path [2, 3] · saved 6

start 3 · path [2, 3] · saved 7

start 2 · path [2] · saved 7

start 0 · path [] · saved 7

start 0 · path [3] · saved 7

start 3 · path [3] · saved 8

start 0 · path [] · saved 8

start 0 · path [] · saved 8 · answer [[],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3]]

The top row is nums. The middle row is 1 when the item is in path. The bottom row is path, filled from the left.

  • i the index the loop is looking at
  • start the first index this call may use
  • i = start the loop index is the first index of this call
  • path the slot just added to path
  • undo the slot just removed from path

nums is [1, 2, 3]. path is empty, and backtrack(0) is about to run.

backtrack(0) starts and saves a copy of path = []. results now holds 1 subset.

Choose nums[0] = 1: path becomes [1]. Then backtrack(1) runs.

backtrack(1) starts and saves a copy of path = [1]. results now holds 2 subsets.

Choose nums[1] = 2: path becomes [1, 2]. Then backtrack(2) runs.

backtrack(2) starts and saves a copy of path = [1, 2]. results now holds 3 subsets.

Choose nums[2] = 3: path becomes [1, 2, 3]. Then backtrack(3) runs.

backtrack(3) starts and saves a copy of path = [1, 2, 3]. results now holds 4 subsets.

Undo: path.pop() removes 3, so path is [1, 2] again. The loop moves to the next i.

Undo: path.pop() removes 2, so path is [1] again. The loop moves to the next i.

Choose nums[2] = 3: path becomes [1, 3]. Then backtrack(3) runs.

backtrack(3) starts and saves a copy of path = [1, 3]. results now holds 5 subsets.

Undo: path.pop() removes 3, so path is [1] again. The loop moves to the next i.

Undo: path.pop() removes 1, so path is [] again. The loop moves to the next i.

Choose nums[1] = 2: path becomes [2]. Then backtrack(2) runs.

backtrack(2) starts and saves a copy of path = [2]. results now holds 6 subsets.

Choose nums[2] = 3: path becomes [2, 3]. Then backtrack(3) runs.

backtrack(3) starts and saves a copy of path = [2, 3]. results now holds 7 subsets.

Undo: path.pop() removes 3, so path is [2] again. The loop moves to the next i.

Undo: path.pop() removes 2, so path is [] again. The loop moves to the next i.

Choose nums[2] = 3: path becomes [3]. Then backtrack(3) runs.

backtrack(3) starts and saves a copy of path = [3]. results now holds 8 subsets.

Undo: path.pop() removes 3, so path is [] again. The loop moves to the next i.

backtrack(0) returns. results holds 8 subsets: [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]].

nums
01start
12
23
in path
0
0
0
path
1 / 24
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Saving path instead of a copy list(path)

    Mistake
    results.append(path)
    Fix
    results.append(list(path))

    What happens: results.append(path) stores the same list object every time. The code keeps changing that one list with append and pop. At the end it is empty, so every saved subset is empty.

    Example: nums = [1, 2]
    Mistake: returns [[], [], [], []]Fix: returns [[], [1], [1, 2], [2]]
  2. 2

    Gotcha 2: Recursing with start + 1 instead of i + 1

    Mistake
    backtrack(start + 1)
    Fix
    backtrack(i + 1)

    What happens: The next call must start after the index that was just picked, which is i. With start + 1, the next call can pick the same index i again, so one value appears twice in a subset.

    Example: nums = [1, 2]
    Mistake: returns [[], [1], [1, 2], [2], [2, 2]]Fix: returns [[], [1], [1, 2], [2]]
  3. 3

    Gotcha 3: Forgetting to remove the choice with path.pop()

    Mistake
    pass
    Fix
    path.pop()

    What happens: path keeps the choice after the call returns. The next value of i then starts from a path that already holds the old choice, so every later subset contains a wrong item.

    Example: nums = [1, 2]
    Mistake: returns [[], [1], [1, 2], [1, 2, 2]]Fix: returns [[], [1], [1, 2], [2]]
  4. 4

    Gotcha 4: Saving a subset only when the loop reaches the end

    Mistake
    if start == len(nums): results.append(list(path))
    Fix
    results.append(list(path))

    What happens: A save at the end happens only when start equals len(nums), which means the last index was just picked. Every subset that does not use the last index is missed, including the empty subset.

    Example: nums = [1, 2]
    Mistake: returns [[1, 2], [2]]Fix: returns [[], [1], [1, 2], [2]]
Not this one if…
  • The input has repeated values and the same subset must not be saved twice, as in Subsets II (LC 90): the loop also needs a sort and a skip test. See Subsets II
  • Only subsets of one exact size (or one target sum) count, as in Combinations (LC 77): saving at every node would return every size. See Combinations
  • You only test or count on/off patterns of a few fixed bits, as in Binary Watch (LC 401), instead of building each subset as a list. See Bitmask Subsets

Deep Dive

Why saving at every call finds every subset once

Backtracking walks a decision tree. Each call of backtrack is one node. The node's path is the list of choices made from the first call down to this call.

A subset is any group of items, so every path is already a valid subset. That is why the code saves list(path) as soon as backtrack starts, and does not wait for a leaf.

Each subset has exactly one path, because the choices are made in index order (see the next section). So each subset is saved once. For three different numbers, the tree has 8 nodes and the result has 8 subsets.

Why start removes repeated orderings

The loop in backtrack(start) tries only the indices from start to the end. After it picks i, it calls backtrack(i + 1). So the next pick always has a larger index than the last pick.

A group of indices has only one increasing order. For example, the numbers at indices 0 and 1 can be reached as [nums[0], nums[1]] only. The order [nums[1], nums[0]] would need a pick that goes back, and the loop never goes back.

This is the difference from Permutations (LC 46), where the order matters and the loop restarts at 0. A subset ignores the order, so one order is enough.

Choose, explore, un-choose: why path.pop() is needed

path is one list that every call shares. The call adds nums[i] with append, runs backtrack(i + 1), and then removes nums[i] with pop.

Without pop, the next value of i would start from a path that still holds the old choice. Every later subset would then contain a wrong item.

With pop, path is the same list after each branch as it was before the branch. This is the invariant: path always holds exactly the choices from the first call to the current call.

The cost: nodes times the copy

The tree has one node per subset, so N different numbers give 2^N nodes. Each node copies path, which costs up to O(N). The time is O(N * 2^N), and the call stack is O(N) deep. The saved lists are the output and are not counted.

For N = 20 different numbers, backtrack runs 1,048,576 times. A size of about 20 is the limit that the statement usually gives for this kind of problem.

If the input has repeated values, the same subset can be built twice. The card Subsets II shows how to skip the repeats.

60-Second Pitch

"Every node of the decision tree is a valid subset, so I save a copy of path when backtrack starts. The loop picks only indices from start onward, and the recursion goes to i + 1, so each subset is built in one order and saved once. I add nums[i] to path, recurse, and then pop it, so path is the same after each branch as before it. There are 2^N nodes, and each copies path in O(N). So the time is O(N * 2^N), and the stack is O(N) deep."

Common Questions

Why do you save path at every call and not only when the index reaches the end?

Every path is already a valid subset, including the empty list at the first call. A save at the end would find only the subsets that use the last index. Saving at every node finds all of them, and each exactly once.

Why do you save list(path) and not path?

path is one list that the code keeps changing with append and pop. Saving path stores a reference to that list, and at the end it is empty. list(path) stores a copy of the choices at that moment.

What is the time and space complexity?

O(N * 2^N) time, because there are 2^N nodes and each one copies path in up to O(N). The space is O(N) for path and the recursion stack. The output is not counted.

How does the answer change if the order inside a subset matters?

Then the problem is about orderings, not subsets. The loop restarts at 0 in every call and uses a used array, as in Permutations (LC 46). Both use the same choose, explore, un-choose steps.

How can you make the answer without recursion?

Start with a list that holds the empty subset. For each number, add a copy of every existing subset with that number appended. After N numbers the list holds all 2^N subsets. The cost is the same, and the recursion is not needed.

SWE Reasoning

Spot it

  • The statement asks for all subsets, all subsequences, or all on/off choices. The answer is a list of lists, not one number.
  • Every group is valid, including the empty group, so you save at every node and not only at a leaf.
  • The order inside a group does not matter. [1, 2] and [2, 1] count as one answer.
  • The input is small, usually 20 items or fewer, because the answer has up to 2^N groups.

The invariant, step by step

  1. backtrack(start) is called with path holding the choices from the first call to this call. The first thing it does is results.append(list(path)).
  2. The loop runs i from start to the end. path.append(nums[i]) makes the choice, and backtrack(i + 1) explores every subset that extends this path with later indices.
  3. path.pop() removes the choice. path is now exactly what it was before step 2, so the next i starts from a correct path.
  4. Each subset has one increasing order of indices, so each subset is saved once.

Traps

  • Saving path and not list(path). The code saves one list object and keeps changing it with append and pop. For [1, 2], the result is four empty lists.
  • backtrack(start + 1) instead of backtrack(i + 1). The next call then starts at the wrong index, and a value is picked twice. For [1, 2], the result contains [2, 2].
  • Forgetting path.pop(). path keeps the old choice, and every later subset contains a wrong item.
  • Saving only at a leaf. The code then misses every subset that does not use the last index. For [1, 2], it returns only [1, 2] and [2].

Practice

Canonical LeetCode:Subsets (LC 78)Count Number of Maximum Bitwise-OR Subsets (LC 2044)Generalized Abbreviation (LC 320)Sum of All Subset XOR Totals (LC 1863)
Theory Context•Backtracking & State Search
P2MediumMental model~4 min speedrun

Subsets (Power Set)

Grows into
CombinationsOne Choice per SlotPermutationsSubsets II
Unlocks
Bitmask SubsetsMeet in the Middle
The problem

Subsets (LC 78)(opens leetcode.com)

You are given an array nums of different integers. Return every subset of it: the empty subset, every subset in between, and the whole array. No subset may appear twice. You may return the subsets in any order.

Example 1
Input:nums = [1,2,3]
Output:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
Three numbers give 2^3 = 8 subsets. Each number is either in a subset or not.

Subsets

elements ➔ every decision tree node is valid ➔ snapshot deep copy
Spot it when: You must list all subsets, subsequences or on/off choices of the input.
Subsets
Python
1from typing import List
2 
3def subsets(nums: List[int]) -> List[List[int]]:
4 """Subsets (LC 78): every subset of nums, saved once at the node of the decision tree that builds it.
5 N = len(nums) and all values are different.
6 Time: O(N * 2^N) | Space: O(N) path + recursion stack, output not counted
7 """
8 results: List[List[int]] = []
9 path: List[int] = [] # the choices from the root to the current node, in order
10 
11 def backtrack(start: int) -> None:
12 # 1. Save This Node:
13 # Every node of the decision tree is a valid subset, so copy path into results
14 # as soon as the call starts, before trying any further choice.
15 results.append(list(path))
16 
17 for i in range(start, len(nums)):
18 # 2. Choose, Explore, Un-choose:
19 # Add nums[i] to path, recurse forward so the next call only sees later
20 # indices, then remove nums[i] so the loop's next i tries its own branch.
21 path.append(nums[i])
22 backtrack(i + 1)
23 path.pop()
24 
25 backtrack(0)
26 return results
Invariant: path always holds exactly the choices from the root to the current node, and every node is saved exactly once.

Every node of the decision tree is a valid subset, so save a copy of path as soon as you enter backtrack. The loop only picks indices from start onward, so each subset is built in one order and saved once. The trap is saving path itself: path keeps changing with append and pop, so write results.append(list(path)), or every saved subset ends up empty.

See it run

Subsets, step by step

start 0 · path [] · saved 0

start 0 · path [] · saved 1

start 0 · path [1] · saved 1

start 1 · path [1] · saved 2

start 1 · path [1, 2] · saved 2

start 2 · path [1, 2] · saved 3

start 2 · path [1, 2, 3] · saved 3

start 3 · path [1, 2, 3] · saved 4

start 2 · path [1, 2] · saved 4

start 1 · path [1] · saved 4

start 1 · path [1, 3] · saved 4

start 3 · path [1, 3] · saved 5

start 1 · path [1] · saved 5

start 0 · path [] · saved 5

start 0 · path [2] · saved 5

start 2 · path [2] · saved 6

start 2 · path [2, 3] · saved 6

start 3 · path [2, 3] · saved 7

start 2 · path [2] · saved 7

start 0 · path [] · saved 7

start 0 · path [3] · saved 7

start 3 · path [3] · saved 8

start 0 · path [] · saved 8

start 0 · path [] · saved 8 · answer [[],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3]]

The top row is nums. The middle row is 1 when the item is in path. The bottom row is path, filled from the left.

  • i the index the loop is looking at
  • start the first index this call may use
  • i = start the loop index is the first index of this call
  • path the slot just added to path
  • undo the slot just removed from path

nums is [1, 2, 3]. path is empty, and backtrack(0) is about to run.

backtrack(0) starts and saves a copy of path = []. results now holds 1 subset.

Choose nums[0] = 1: path becomes [1]. Then backtrack(1) runs.

backtrack(1) starts and saves a copy of path = [1]. results now holds 2 subsets.

Choose nums[1] = 2: path becomes [1, 2]. Then backtrack(2) runs.

backtrack(2) starts and saves a copy of path = [1, 2]. results now holds 3 subsets.

Choose nums[2] = 3: path becomes [1, 2, 3]. Then backtrack(3) runs.

backtrack(3) starts and saves a copy of path = [1, 2, 3]. results now holds 4 subsets.

Undo: path.pop() removes 3, so path is [1, 2] again. The loop moves to the next i.

Undo: path.pop() removes 2, so path is [1] again. The loop moves to the next i.

Choose nums[2] = 3: path becomes [1, 3]. Then backtrack(3) runs.

backtrack(3) starts and saves a copy of path = [1, 3]. results now holds 5 subsets.

Undo: path.pop() removes 3, so path is [1] again. The loop moves to the next i.

Undo: path.pop() removes 1, so path is [] again. The loop moves to the next i.

Choose nums[1] = 2: path becomes [2]. Then backtrack(2) runs.

backtrack(2) starts and saves a copy of path = [2]. results now holds 6 subsets.

Choose nums[2] = 3: path becomes [2, 3]. Then backtrack(3) runs.

backtrack(3) starts and saves a copy of path = [2, 3]. results now holds 7 subsets.

Undo: path.pop() removes 3, so path is [2] again. The loop moves to the next i.

Undo: path.pop() removes 2, so path is [] again. The loop moves to the next i.

Choose nums[2] = 3: path becomes [3]. Then backtrack(3) runs.

backtrack(3) starts and saves a copy of path = [3]. results now holds 8 subsets.

Undo: path.pop() removes 3, so path is [] again. The loop moves to the next i.

backtrack(0) returns. results holds 8 subsets: [[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]].

nums
01start
12
23
in path
0
0
0
path
1 / 24
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Saving path instead of a copy list(path)

    Mistake
    results.append(path)
    Fix
    results.append(list(path))

    What happens: results.append(path) stores the same list object every time. The code keeps changing that one list with append and pop. At the end it is empty, so every saved subset is empty.

    Example: nums = [1, 2]
    Mistake: returns [[], [], [], []]Fix: returns [[], [1], [1, 2], [2]]
  2. 2

    Gotcha 2: Recursing with start + 1 instead of i + 1

    Mistake
    backtrack(start + 1)
    Fix
    backtrack(i + 1)

    What happens: The next call must start after the index that was just picked, which is i. With start + 1, the next call can pick the same index i again, so one value appears twice in a subset.

    Example: nums = [1, 2]
    Mistake: returns [[], [1], [1, 2], [2], [2, 2]]Fix: returns [[], [1], [1, 2], [2]]
  3. 3

    Gotcha 3: Forgetting to remove the choice with path.pop()

    Mistake
    pass
    Fix
    path.pop()

    What happens: path keeps the choice after the call returns. The next value of i then starts from a path that already holds the old choice, so every later subset contains a wrong item.

    Example: nums = [1, 2]
    Mistake: returns [[], [1], [1, 2], [1, 2, 2]]Fix: returns [[], [1], [1, 2], [2]]
  4. 4

    Gotcha 4: Saving a subset only when the loop reaches the end

    Mistake
    if start == len(nums): results.append(list(path))
    Fix
    results.append(list(path))

    What happens: A save at the end happens only when start equals len(nums), which means the last index was just picked. Every subset that does not use the last index is missed, including the empty subset.

    Example: nums = [1, 2]
    Mistake: returns [[1, 2], [2]]Fix: returns [[], [1], [1, 2], [2]]
Not this one if…
  • The input has repeated values and the same subset must not be saved twice, as in Subsets II (LC 90): the loop also needs a sort and a skip test. See Subsets II
  • Only subsets of one exact size (or one target sum) count, as in Combinations (LC 77): saving at every node would return every size. See Combinations
  • You only test or count on/off patterns of a few fixed bits, as in Binary Watch (LC 401), instead of building each subset as a list. See Bitmask Subsets

Deep Dive

Why saving at every call finds every subset once

Backtracking walks a decision tree. Each call of backtrack is one node. The node's path is the list of choices made from the first call down to this call.

A subset is any group of items, so every path is already a valid subset. That is why the code saves list(path) as soon as backtrack starts, and does not wait for a leaf.

Each subset has exactly one path, because the choices are made in index order (see the next section). So each subset is saved once. For three different numbers, the tree has 8 nodes and the result has 8 subsets.

Why start removes repeated orderings

The loop in backtrack(start) tries only the indices from start to the end. After it picks i, it calls backtrack(i + 1). So the next pick always has a larger index than the last pick.

A group of indices has only one increasing order. For example, the numbers at indices 0 and 1 can be reached as [nums[0], nums[1]] only. The order [nums[1], nums[0]] would need a pick that goes back, and the loop never goes back.

This is the difference from Permutations (LC 46), where the order matters and the loop restarts at 0. A subset ignores the order, so one order is enough.

Choose, explore, un-choose: why path.pop() is needed

path is one list that every call shares. The call adds nums[i] with append, runs backtrack(i + 1), and then removes nums[i] with pop.

Without pop, the next value of i would start from a path that still holds the old choice. Every later subset would then contain a wrong item.

With pop, path is the same list after each branch as it was before the branch. This is the invariant: path always holds exactly the choices from the first call to the current call.

The cost: nodes times the copy

The tree has one node per subset, so N different numbers give 2^N nodes. Each node copies path, which costs up to O(N). The time is O(N * 2^N), and the call stack is O(N) deep. The saved lists are the output and are not counted.

For N = 20 different numbers, backtrack runs 1,048,576 times. A size of about 20 is the limit that the statement usually gives for this kind of problem.

If the input has repeated values, the same subset can be built twice. The card Subsets II shows how to skip the repeats.

60-Second Pitch

"Every node of the decision tree is a valid subset, so I save a copy of path when backtrack starts. The loop picks only indices from start onward, and the recursion goes to i + 1, so each subset is built in one order and saved once. I add nums[i] to path, recurse, and then pop it, so path is the same after each branch as before it. There are 2^N nodes, and each copies path in O(N). So the time is O(N * 2^N), and the stack is O(N) deep."

Common Questions

Why do you save path at every call and not only when the index reaches the end?

Every path is already a valid subset, including the empty list at the first call. A save at the end would find only the subsets that use the last index. Saving at every node finds all of them, and each exactly once.

Why do you save list(path) and not path?

path is one list that the code keeps changing with append and pop. Saving path stores a reference to that list, and at the end it is empty. list(path) stores a copy of the choices at that moment.

What is the time and space complexity?

O(N * 2^N) time, because there are 2^N nodes and each one copies path in up to O(N). The space is O(N) for path and the recursion stack. The output is not counted.

How does the answer change if the order inside a subset matters?

Then the problem is about orderings, not subsets. The loop restarts at 0 in every call and uses a used array, as in Permutations (LC 46). Both use the same choose, explore, un-choose steps.

How can you make the answer without recursion?

Start with a list that holds the empty subset. For each number, add a copy of every existing subset with that number appended. After N numbers the list holds all 2^N subsets. The cost is the same, and the recursion is not needed.

SWE Reasoning

Spot it

  • The statement asks for all subsets, all subsequences, or all on/off choices. The answer is a list of lists, not one number.
  • Every group is valid, including the empty group, so you save at every node and not only at a leaf.
  • The order inside a group does not matter. [1, 2] and [2, 1] count as one answer.
  • The input is small, usually 20 items or fewer, because the answer has up to 2^N groups.

The invariant, step by step

  1. backtrack(start) is called with path holding the choices from the first call to this call. The first thing it does is results.append(list(path)).
  2. The loop runs i from start to the end. path.append(nums[i]) makes the choice, and backtrack(i + 1) explores every subset that extends this path with later indices.
  3. path.pop() removes the choice. path is now exactly what it was before step 2, so the next i starts from a correct path.
  4. Each subset has one increasing order of indices, so each subset is saved once.

Traps

  • Saving path and not list(path). The code saves one list object and keeps changing it with append and pop. For [1, 2], the result is four empty lists.
  • backtrack(start + 1) instead of backtrack(i + 1). The next call then starts at the wrong index, and a value is picked twice. For [1, 2], the result contains [2, 2].
  • Forgetting path.pop(). path keeps the old choice, and every later subset contains a wrong item.
  • Saving only at a leaf. The code then misses every subset that does not use the last index. For [1, 2], it returns only [1, 2] and [2].

Practice

Canonical LeetCode:Subsets (LC 78)Count Number of Maximum Bitwise-OR Subsets (LC 2044)Generalized Abbreviation (LC 320)Sum of All Subset XOR Totals (LC 1863)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•