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.
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:
["mint", "lime", "soda"]gives all 8 combos, from[]to["lime", "mint", "soda"].["shot", "milk", "shot"]gives 6, not 8: taking the first shot or the second makes the same combo.[]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 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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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 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].
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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.
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.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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
-
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.
-
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?
-
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.
-
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?
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
- itertools: combinations() and the powerset() recipe (The Python Standard Library) (Python Software Foundation)
- Comparing sequences and other types (The Python Tutorial) (Python Software Foundation)
- int.bit_count() (The Python Standard Library: built-in types) (Python Software Foundation)
- NIST Dictionary of Algorithms and Data Structures: Gray code (National Institute of Standards and Technology (NIST))
- The Java Language Specification, Java SE 25: 15.19 Shift operators (Oracle)
- ECMAScript Language Specification: Number::leftShift (Ecma International (TC39))
- Arithmetic operators: bitwise shift operators (cppreference.com) (cppreference.com)
- The Go Programming Language Specification: arithmetic operators (shifts) and constant expressions (The Go Authors)
- UndefinedBehaviorSanitizer (Clang documentation) (The LLVM Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress