Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Coding Interview Patterns  Module 16 – Backtracking

Generating subsets three ways

List every subset of a list by recursion, by iteration and with bitmasks, drop repeats when values repeat, and keep one fixed order on every run.

  • Beginner
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, quickjs 0.32.0, Java 25, Clang 21.0.0 and Go 1.27.1
  • By MySmartCoPilot
Free core, Pro depth: 10 of 30 minutes free
Free: The problem: every combo from a list of add-ons · The brute force: every subset, then drop the repeats · What to notice · 3 more sections. With a pass: 3 in-depth parts and 3 interview answers, about 20 minutes.

What you will learn

  • Generate every subset of a list by recursion, by iteration and with bitmasks
  • Avoid repeated subsets when values repeat, by sorting and skipping equal neighbours at one depth
  • Order the subsets the same way on every run, and name the order each method gives
  • Prove that the skip rule lists every distinct subset exactly once
  • Find the k-th subset in sorted order without listing the ones before it

Before you start

What you’ll learn · Before you start

What you will learn

  • Generate every subset of a list by recursion, by iteration and with bitmasks
  • Avoid repeated subsets when values repeat, by sorting and skipping equal neighbours at one depth
  • Order the subsets the same way on every run, and name the order each method gives
  • Prove that the skip rule lists every distinct subset exactly once
  • Find the k-th subset in sorted order without listing the ones before it

Before you start

On this page

Generating subsets means listing every way to pick some of a list’s items, from none to all of them: n different items have 2ⁿ subsets. Three methods produce them. Recursion uses the backtracking template: each call records the subset built so far, then tries every later item as the next one. Iteration starts from the empty subset and, for each item, adds a copy of every subset found so far with that item put in. Bitmasks count from 0 to 2ⁿ − 1 and read the bits of each number as choices: bit i set means item i is in. All three do work in proportion to what they print. When values repeat, all three build some subsets twice, and one more rule fixes it: sort the items, and never try two equal values at the same depth.

The problem: every combo from a list of add-ons

The problem

A café sells cold coffee with add-ons, and its till keeps them as a list of names. A name can appear more than once: two shots of the same syrup are two entries. A combo is any choice of entries, from none to all, and two combos are the same when they hold the same names the same number of times. Write combos(addons), which returns every distinct combo once, each as a sorted list of names, with the combos in sorted order. Limits: 0 ≤ n ≤ 20.

Three examples:

  1. ["mint", "lime", "soda"] gives all 8 combos, from [] to ["lime", "mint", "soda"].
  2. ["shot", "milk", "shot"] gives 6, not 8: taking the first shot or the second makes the same combo.
  3. [] gives [[]], the one combo with nothing in it.

Sorted order compares two combos name by name, the way Python compares lists, so each combo comes just before the combos that extend it.

The brute force: every subset, then drop the repeats

Build all 2ⁿ subsets of the positions with bitmasks, sort the names in each, and let a set of tuples drop the repeats. It is short and plainly right, which makes it the reference that faster code is tested against:

Every subset of the positions, then a set, timed Python · brute/brute_force.py
"""Every distinct combo by brute force: all 2^n subsets as bitmasks, each one sorted, repeats dropped with a set."""
import time


def combos_brute(addons):
    """Distinct combos of the add-ons, each as a sorted list, the whole list sorted."""
    n = len(addons)
    found = set()
    for mask in range(1 << n):  # bit i of mask says whether addons[i] is in this subset
        found.add(tuple(sorted(addons[i] for i in range(n) if mask >> i & 1)))
    return [list(c) for c in sorted(found)]


print(combos_brute(["mint", "lime", "soda"]))
print(combos_brute(["shot", "milk", "shot"]))
print(combos_brute([]))

print("add-ons                     subsets built  distinct combos  time")
for label, addons in [
    ("14 shots of one syrup", ["shot"] * 14),
    ("16 shots of one syrup", ["shot"] * 16),
    ("18 shots of one syrup", ["shot"] * 18),
    ("16 different add-ons", [f"addon{i:02}" for i in range(16)]),
]:
    start = time.perf_counter()
    combos = combos_brute(addons)
    seconds = time.perf_counter() - start
    print(f"{label:26} {1 << len(addons):>14,} {len(combos):>16,}  {seconds:.2f} s")
print(f"20 shots of one syrup: {1 << 20:,} subsets built to keep 21 combos")

Output

[[], ['lime'], ['lime', 'mint'], ['lime', 'mint', 'soda'], ['lime', 'soda'], ['mint'], ['mint', 'soda'], ['soda']]
[[], ['milk'], ['milk', 'shot'], ['milk', 'shot', 'shot'], ['shot'], ['shot', 'shot']]
[[]]
add-ons                     subsets built  distinct combos  time
14 shots of one syrup              16,384               15  0.02 s
16 shots of one syrup              65,536               17  0.17 s
18 shots of one syrup             262,144               19  1.65 s
16 different add-ons               65,536           65,536  0.92 s
20 shots of one syrup: 1,048,576 subsets built to keep 21 combos

This output changes from run to run: the times depend on the computer and on what else it is doing

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brute_force.py

The brute force builds every subset of the positions, so its work grows like n · 2ⁿ whatever the values are. The answer can be tiny all the same: 18 shots of one syrup cost 262,144 subsets to keep 19 combos, and 20 shots would cost 1,048,576 to keep 21. With 16 different add-ons every one of the 65,536 subsets is a combo of its own, so no method can do much better there. The waste comes only from repeated values.

What to notice

A repeat is born at one step, when two choices of the next entry hold equal values. From the empty combo of ["milk", "shot", "shot"], the next entry may be either shot, and everything built after the one is built again after the other. Sorting puts equal values side by side, which makes those choices easy to spot: they are neighbours.

Three ways to list subsets

For n different items each method builds the same 2ⁿ subsets, in its own order:

  • Recursion with a start index. Each call records its subset, then extends it with each later item in turn, so every subset gets one call.
  • Iteration. The list starts as [[]], and each item appends a copy of every subset in the list with the item added, which doubles it.
  • Bitmasks. Each number m from 0 to 2ⁿ − 1 is one subset: item i is in it when bit i of m is 1.
What differs Recursion with a start index Iteration Bitmasks
Order on a, b, c {} a ab abc ac b bc c {} a b ab c ac bc abc {} a b ab c ac bc abc
Memory besides the output the recursion, n + 1 calls deep none one integer
Cutting a branch short yes, as in any backtracking search no no
Repeated values the skip rule below a change to which subsets an item extends only a set afterwards

Recursion lists sorted order. Iteration and bitmasks list counting order, because counting in binary takes the subsets of the first items before any subset that holds a later one.

Repeated values: sort, then skip

The rule for the recursive search fits in one line: sort the items, and in each call’s loop skip position j when j > start and items[j] equals items[j − 1]. The first choice of a loop is never skipped, so one path can still take a value twice, as in [2, 2]. What the rule stops is trying an equal value twice as the next item from one state, which is exactly where repeats are born. Each call still records one subset, so the search makes one call per distinct subset: 21 calls for 20 shots of one syrup, where the brute force built 1,048,576 subsets.

The search tree for the sorted items 1, 2, 2: six calls, one per distinct subset, and two choices skipped as repeats.sorted items [1, 2, 2], positions 0, 1, 2pos 0pos 1pos 2pos 1pos 2pos 2pos 2[ ]start 0[1]start 1[2]start 2[2] againskipped[1, 2]start 2[1, 2] againskipped[2, 2]start 3[1, 2, 2]start 3dashed: the same value as the choicejust before it, at the same depth6 calls, 6 distinct subsets, 2 skips

The search on the sorted items [1, 2, 2]; dashed boxes are choices the skip rule leaves out

Text description of the diagram

The diagram shows the recursive search with a start index on the sorted items 1, 2 and 2, at positions 0, 1 and 2, drawn from the root at the top. Each solid box is one call: it shows the subset recorded by that call and the first position the call may still add, its start. Each line is a choice of the next position.

  • The root records the empty subset, with start 0. It may add position 0 (the value 1), position 1 (the first 2) or position 2 (the second 2). The choice of position 2 is dashed: its value equals the value at position 1, already tried at this depth, so it would only build the subset [2] again.
  • The call for [1] has start 1. It adds position 1 to make [1, 2]; the choice of position 2 is dashed, because it would build [1, 2] again.
  • The call for [1, 2] has start 2 and adds position 2, giving [1, 2, 2]. Here position 2 is the first choice of its loop, so it is not skipped.
  • The call for [2] has start 2 and adds position 2, giving [2, 2].

There are six solid boxes, one for each distinct subset: [ ], [1], [1, 2], [1, 2, 2], [2] and [2, 2]. The two dashed boxes are the repeats that a search without the rule would build: eight subsets in all.

Exercise

Exercise · Easy · Python, JavaScript

Every distinct bundle, once each

Write unique_bundles(items) in bundles.py (JavaScript: uniqueBundles(items) in bundles.mjs). items lists the things a bundle may contain, and a thing can be listed more than once: two copies of the same sticker are two entries. A bundle takes any of the entries, from none to all of them. Two bundles are the same when they hold the same things the same number of times, whichever copies they took. Return every distinct bundle once, as a list of things in sorted order, with the bundles themselves in sorted order.

unique_bundles(["pen", "cap", "pen"])
# [[], ["cap"], ["cap", "pen"], ["cap", "pen", "pen"], ["pen"], ["pen", "pen"]]

Sorted order for bundles means comparing them item by item, the way Python compares lists: a bundle comes before every bundle that extends it, so [] is first and ["cap"] comes before ["cap", "pen"]. A search that sorts the items first and adds them in that order produces exactly this order. An empty items has one bundle, the empty one, [[]]. All entries are of one type: all strings or all integers. In JavaScript, sort with a comparator that uses < and >: the default sort() compares numbers as text, so it would put 10 before 9.

One sample test lists 16 entries made of 4 values, 4 copies each: 65,536 ways to pick entries, but only 625 distinct bundles. Another checks that changing one returned bundle changes no other and leaves items as it was.

Python · Starter code · bundles.py

def unique_bundles(items):
    """Every distinct bundle of the items once, each sorted, the bundles in sorted order."""
    # Replace this line with your code.
    return []
The sample tests · test_bundles.py
import random
from itertools import combinations

from bundles import unique_bundles


def trusted(items):
    """The answer the tests trust: every combination of entries, sorted, repeats dropped, then sorted."""
    return [list(t) for t in sorted({tuple(sorted(c)) for k in range(len(items) + 1) for c in combinations(items, k)})]


def test_prompt_example():
    """the example of the prompt: two pens and a cap"""
    assert unique_bundles(["pen", "cap", "pen"]) == [[], ["cap"], ["cap", "pen"], ["cap", "pen", "pen"], ["pen"], ["pen", "pen"]]


def test_all_different():
    """three different things give all 8 bundles, in sorted order"""
    assert unique_bundles(["b", "a", "c"]) == [[], ["a"], ["a", "b"], ["a", "b", "c"], ["a", "c"], ["b"], ["b", "c"], ["c"]]


def test_one_value_repeated():
    """three copies of one value: only 4 distinct bundles"""
    assert unique_bundles([7, 7, 7]) == [[], [7], [7, 7], [7, 7, 7]]


def test_empty():
    """no items: one bundle, the empty one"""
    assert unique_bundles([]) == [[]]


def test_numbers_in_numeric_order():
    """integers are sorted as numbers: 9 comes before 10"""
    assert unique_bundles([10, 9, 10]) == [[], [9], [9, 10], [9, 10, 10], [10], [10, 10]]


def test_bundles_are_separate_lists():
    """changing one returned bundle changes no other, and items is left as it was"""
    items = ["pen", "cap", "pen"]
    found = unique_bundles(items)
    found[1].append("ink")
    assert found[2:] == [["cap", "pen"], ["cap", "pen", "pen"], ["pen"], ["pen", "pen"]]
    assert items == ["pen", "cap", "pen"]


def test_sixteen_entries_four_values():
    """4 values, 4 copies each: 65,536 ways to pick entries, 625 distinct bundles"""
    items = [v for v in ["ink", "cap", "pen", "nib"] for _ in range(4)]
    found = unique_bundles(items)
    assert len(found) == 625
    assert found == trusted(items)


def test_random_lists():
    """300 random lists with many repeats, checked against every combination"""
    rng = random.Random(21)
    for _ in range(300):
        items = [rng.randint(1, 4) for _ in range(rng.randint(0, 9))]
        assert unique_bundles(items) == trusted(items), items

JavaScript · Starter code · bundles.mjs

/** Every distinct bundle of the items once, each sorted, the bundles in sorted order. */
export function uniqueBundles(items) {
  // Replace this line with your code.
  return [];
}
The sample tests · bundles.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { uniqueBundles } from './bundles.mjs';

let seed = 21;
const random = () => (seed = (seed * 48271) % 2147483647) / 2147483647;
const randInt = (lo, hi) => lo + Math.floor(random() * (hi - lo + 1));
const less = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
/** Compares two sorted bundles item by item: a bundle comes before every bundle that extends it. */
function compareBundles(a, b) {
  for (let i = 0; i < Math.min(a.length, b.length); i++) if (a[i] !== b[i]) return less(a[i], b[i]);
  return a.length - b.length;
}

/** The answer the tests trust: every pick of entries by bitmask, sorted, repeats dropped, then sorted. */
function trusted(items) {
  const seen = new Map();
  for (let m = 0; m < 2 ** items.length; m++) {
    const bundle = items.filter((_, i) => Math.floor(m / 2 ** i) % 2 === 1).sort(less);
    seen.set(JSON.stringify(bundle), bundle);
  }
  return [...seen.values()].sort(compareBundles);
}

test('the example of the prompt: two pens and a cap', () => {
  assert.deepEqual(uniqueBundles(['pen', 'cap', 'pen']), [[], ['cap'], ['cap', 'pen'], ['cap', 'pen', 'pen'], ['pen'], ['pen', 'pen']]);
});

test('three different things give all 8 bundles, in sorted order', () => {
  assert.deepEqual(uniqueBundles(['b', 'a', 'c']), [[], ['a'], ['a', 'b'], ['a', 'b', 'c'], ['a', 'c'], ['b'], ['b', 'c'], ['c']]);
});

test('three copies of one value give 4 bundles; no items give one, the empty one', () => {
  assert.deepEqual(uniqueBundles([7, 7, 7]), [[], [7], [7, 7], [7, 7, 7]]);
  assert.deepEqual(uniqueBundles([]), [[]]);
});

test('numbers are sorted as numbers, not as text: 9 comes before 10', () => {
  assert.deepEqual(uniqueBundles([10, 9, 10]), [[], [9], [9, 10], [9, 10, 10], [10], [10, 10]]);
});

test('changing one returned bundle changes no other, and items is left as it was', () => {
  const items = ['pen', 'cap', 'pen'];
  const found = uniqueBundles(items);
  found[1].push('ink');
  assert.deepEqual(found.slice(2), [['cap', 'pen'], ['cap', 'pen', 'pen'], ['pen'], ['pen', 'pen']]);
  assert.deepEqual(items, ['pen', 'cap', 'pen']);
});

test('4 values, 4 copies each: 65,536 ways to pick entries, 625 distinct bundles', () => {
  const items = ['ink', 'cap', 'pen', 'nib'].flatMap((v) => [v, v, v, v]);
  const found = uniqueBundles(items);
  assert.equal(found.length, 625);
  assert.deepEqual(found, trusted(items));
});

test('300 random lists with many repeats, checked against every pick of entries', () => {
  for (let trial = 0; trial < 300; trial++) {
    const items = Array.from({ length: randInt(0, 9) }, () => randInt(1, 4));
    assert.deepEqual(uniqueBundles(items), trusted(items), JSON.stringify(items));
  }
});
A hint

Sort a copy of the items, so that equal values sit side by side, and use the backtracking search with a start index: every call records a copy of the bundle so far, then tries each later position as the next item. Two choices at the same depth that hold the same value lead to the same bundles, so in the loop skip position j when j > start and items[j] equals items[j - 1].

The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Number Base Converter Type a mask such as 11 to see its bits, then read off which items it selects.

Check yourself

3 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 3 How many distinct subsets does the list [3, 3, 3, 8] have, the empty one included?

    Type a number.

    Show the answer to question 1

    Answer: 8

    A distinct subset is fixed by how many copies of each value it takes: 0 to 3 copies of 3, four ways, and 0 or 1 copy of 8, two ways. That makes 4 × 2 = 8 distinct subsets, although there are 2⁴ = 16 ways to pick positions.

  2. Question 2 of 3 Bit i of a mask selects items[i]. Which subset of ["a", "b", "c", "d"] does the mask 11, binary 1011, select?

    Choose one answer.

    Show the answer to question 2

    Answer: ["a", "b", "d"]

    Binary 1011 has bits 0, 1 and 3 set, reading from the right, so it selects items 0, 1 and 3: a, b and d. Reading the bits from the left instead would give a, c and d, which is a common slip.

  3. Question 3 of 3 The search runs on the sorted items [1, 2, 2] and the call for [1] has start 1. Which choice of its loop does the skip rule leave out?

    Choose one answer.

    Show the answer to question 3

    Answer: Position 2, because it holds the same value as position 1, which this loop already tried

    The loop tries position 1 first, which makes [1, 2] and then [1, 2, 2]. Position 2 holds another 2, so it would only build [1, 2] again, and the rule skips it. The first choice of a loop is never skipped, which is why [1, 2, 2] is still found one level down.

The three ways in five languages, and where a bitmask breaks Pro: opens with a Pro, Premium or Ultimate pass

All three methods fit in a few lines each. Here they run side by side in Python and JavaScript, then as Java, C++ and Go programs, and the bitmask is pushed past what its integer can hold.

In this part:

  • Recursion, iteration and bitmasks, side by side
  • Why the recursive search builds each subset exactly once
  • Java, C++ and Go, built and run
  • Where a bitmask breaks: shifts at 31, 32 and 64 bits

3 runnable examples (C++, Go, Java, JavaScript, Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Repeated values: the skip rule proved, tested and broken Pro: opens with a Pro, Premium or Ultimate pass

The skip rule is one line, and each plausible variation of it breaks in a different way. This part proves the rule, adapts it to the doubling list, tests both versions against the brute force and reproduces four bugs on the smallest inputs that show them.

In this part:

  • The invariant: every distinct subset is built once
  • The skip rule in the search, and in the doubling list
  • Tested against the brute force on 2,000 random lists
  • Four bugs and the smallest inputs that break them

3 runnable examples (Python) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Fixed orders, the k-th subset, and a practice set Pro: opens with a Pro, Premium or Ultimate pass

A fixed order is part of the answer whenever output is compared, paged or resumed. This part measures four orders, computes the k-th subset in sorted order directly, and ends with a practice set with answers.

In this part:

  • Four orders, and how much changes between neighbours, measured
  • The k-th subset in sorted order, without listing
  • A practice set with answers

1 runnable example (Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher · Warm-up

    How many subsets does a list of n different items have, and how many distinct ones when some values repeat?

    A list of n different items has 2ⁿ subsets, the empty one and the whole list included, because each item is either in or out independently of the others: two choices, n times. When values repeat, two subsets that take different copies of the same values are the same subset, so count by value instead: a value with c copies can be taken 0, 1, … or c times, which is c + 1 choices. The number of distinct subsets is the product of (c + 1) over the values. For [1, 2, 2] that is 2 × 3 = 6, not 2³ = 8, and for twenty copies of one value it is 21, against 1,048,576 ways to pick positions. Listing every subset of 20 different items writes 10,485,760 items in all, n · 2ⁿ⁻¹, since each item is in half of the subsets.

  2. Mid to senior

    A service lists the distinct bundles that can be made from a stock list of up to 40 entries, most of which repeat. The code builds every subset and drops repeats with a set, and it times out. What do you change, how many bundles can there be, and how do you show that nothing is lost or listed twice?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Fresher to mid

    A JavaScript function lists subsets with a bitmask loop and passes every test. A caller passes 31 items and gets an empty list back, with no error. Explain what happened, which other languages behave the same way, and how you would make the function fail loudly instead.

    Model answer, Pro. First step and follow-ups

  4. Senior to staff

    An API returns the subsets of up to 30 sorted items in sorted order, 100 to a page, and clients ask for page p directly. Listing from the start for each request is far too slow for high page numbers. How do you serve page p, and what does a page cost?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • n different items have 2ⁿ subsets holding n · 2ⁿ⁻¹ items in all, so no method lists them faster than that.
  • Recursion with a start index lists sorted order; iteration and bitmasks list counting order.
  • With repeated values, sort the items and skip a value equal to the one just tried at the same depth: one call per distinct subset.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.