Backtracking
Intermediate 9 min read

Subsets

In one line

To list every subset, decide for each element whether it is in or out; a set of n elements has 2^n subsets, and backtracking generates them in O(n * 2^n).

What it is

A subset (also called the power set when you list all of them) is any selection of elements where order does not matter: {1, 3} and {3, 1} are the same subset. A set of n elements has 2^n subsets, including the empty set and the full set.

The backtracking version uses a start index: at each call you may add any element at or after start, then recurse with the next index. Because you only look to the right, each subset is produced exactly once, in one fixed order.

When the input has duplicate values, like [1, 2, 2], you sort first and skip a value if it equals the previous value at the same level of the tree, so {1, 2} is not produced twice.

Why interviewers ask

Subsets is the simplest backtracking problem and the base for combinations, combination sum and many 'choose some items' puzzles. Interviewers check that you avoid duplicates, know the 2^n count, and can also give the iterative or bitmask version.

How it works

  1. 1Keep one list path for the current subset and one list result.
  2. 2On every call, first record a copy of path: every node in the tree is a valid subset, not just the leaves.
  3. 3Loop i from start to n - 1: add nums[i], recurse with start = i + 1, then remove nums[i].
  4. 4Because the next call starts at i + 1, an element can never be picked twice and the order inside a subset always follows the input order, so no subset repeats.
  5. 5For inputs with duplicates, sort, and inside the loop skip i when i > start and nums[i] == nums[i - 1].

Worked example · subsets([1, 2, 3])

  1. Call with start 0, path []: record []. Pick 1.
  2. start 1, path [1]: record [1]. Pick 2 -> start 2, path [1, 2]: record [1, 2]. Pick 3 -> path [1, 2, 3]: record it. Loop ends, pop 3, pop 2.
  3. Back at path [1], i = 2: pick 3 -> path [1, 3]: record. Pop 3, pop 1.
  4. Back at the root, i = 1: pick 2 -> path [2]: record. Pick 3 -> [2, 3]: record. Pop back.
  5. Root i = 2: pick 3 -> [3]: record.
  6. Result in order: [], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]. That is 8 = 2^3 subsets.

Picture it

Packing for a trip from a pile of items: for each item you either put it in the bag or leave it out. Three items give 2 x 2 x 2 = 8 different bags, from empty to everything.

Reference code

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

    def backtrack(start):
        result.append(path[:])          # every node is a subset
        for i in range(start, len(nums)):
            path.append(nums[i])        # choose nums[i]
            backtrack(i + 1)            # only look to the right
            path.pop()                  # un-choose

    backtrack(0)
    return result

def subsets_with_dup(nums):
    nums = sorted(nums)
    result, path = [], []

    def backtrack(start):
        result.append(path[:])
        for i in range(start, len(nums)):
            if i > start and nums[i] == nums[i - 1]:
                continue                # same value at same level: skip
            path.append(nums[i])
            backtrack(i + 1)
            path.pop()

    backtrack(0)
    return result

Both functions record path at every call. The second sorts the input and skips a value that equals its left neighbour at the same level, so for [1, 2, 2] it returns [], [1], [1, 2], [1, 2, 2], [2], [2, 2] with no repeats.

Key patterns

Start index (pick from the right)

When: Order does not matter and each element is used at most once

How: Recurse with i + 1 so you never look back; record at every node.

Include / exclude binary tree

When: You want a simple two-branch recursion

How: At index i, recurse once without nums[i] and once with it; record at i == n.

Skip duplicates at the same level

When: Input has repeated values (Subsets II)

How: Sort, then skip nums[i] if i > start and nums[i] == nums[i - 1].

Bitmask enumeration

When: n is at most about 20 and you want an iterative version

How: Loop mask from 0 to 2^n - 1; bit i set means nums[i] is in the subset.

Real-world use

Feature flag and config testing

Test tools that check combinations of optional flags (for example pairwise testing tools) start from the idea of enumerating subsets of options, then cut the list down because 20 flags already give over a million subsets.

Market basket analysis

The Apriori algorithm used in retail analytics grows itemsets one item at a time and drops any itemset whose subsets are not frequent, which is subset generation with pruning.

Query optimizers

Database optimizers such as PostgreSQL's standard planner consider subsets of the tables in a join to find the cheapest join order, and switch to a genetic search (GEQO) when there are too many tables, by default 12 or more.

In a project

A food delivery app wants to suggest a combo of add-ons (up to 10 items) that fits a customer's remaining budget and maximises a popularity score.

  1. Generate subsets of the add-on list with backtracking, carrying the running price and score in the state.
  2. Prune any branch whose running price already exceeds the budget, since adding more items only raises it.
  3. Keep the best score seen so far and return that subset.
  4. Benchmark: 10 items is only 1,024 subsets, so it runs in well under a millisecond per request.

Impact: The suggestion is provably the best combo within budget, computed per request with no precomputation. Knowing the 2^n growth also tells the team to switch to a knapsack DP if the menu grows to 40 or more items.

Trade-offs

Use it when

  • You need every subset, or the best subset under constraints that allow pruning
  • n is small, about 20 or fewer
  • Combinations of options must each be inspected

Avoid it when

  • Only the best total value is needed and values are small integers: knapsack DP is polynomial in the value range
  • n is large: 2^30 is already about a billion subsets

Cost: There are 2^n subsets and copying each costs up to O(n), so time is O(n * 2^n). Extra space is O(n) for the path and recursion, plus O(n * 2^n) for the output.

Common mistakes

  • Recursing with start instead of i + 1, which reuses the same element and creates wrong or infinite results.
  • Recording only at leaves, which drops the smaller subsets.
  • Handling duplicates with a set of tuples at the end instead of skipping in the loop; it works but wastes time and misses the point.
  • Using the skip rule i > 0 instead of i > start, which wrongly removes valid subsets like [2, 2].
  • Saying the complexity is O(2^n) and forgetting the cost of copying each subset.

Interview questions

Quick revision

  • n elements -> 2^n subsets (including empty and full).
  • Record at every node; recurse with i + 1.
  • Duplicates: sort + skip if i > start and nums[i] == nums[i - 1].
  • Bitmask: subset of mask m contains nums[i] if (m >> i) & 1.
  • Iterative: for x in nums: result += [s + [x] for s in result].
  • Time O(n * 2^n), extra space O(n).

Check yourself

1. How many subsets does a set of 4 distinct elements have?

2. In the start-index template, why recurse with i + 1?

3. For nums = [1, 2, 2] sorted, which skip condition gives correct unique subsets?

4. Where does the subsets template record a result?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free