Enumerating subsets with bitmasks
Go through every subset of n items by counting from 0 to 2^n - 1 and reading each number's bits as "in" or "out".
O(2^n × n)Space O(2^n)5 practice problemsStep-through pictureThe idea
A subset of n items can be written as n bits: bit i is 1 if item i is in the subset. For 3 items, 101 means items 0 and 2 are in. Such a number is called a bitmask, or just a mask.
Every whole number from 0 to 2^n - 1 is a different mask, and together they cover every subset exactly once. So a simple for loop over mask visits all subsets, with no recursion. Inside, mask & (1 << i) tells you whether item i is in.
Masks are also handy to store: a subset of up to 31 items fits in one number, and two subsets can be combined with | (union), & (intersection), or compared with ===. A related trick, sub = (sub - 1) & mask, walks through just the subsets of a given mask.
An everyday example
Counting with fingers raised
Hold up your fingers to count in binary: each finger is up (1) or down (0). Counting from 0 to 7 with three fingers raises every possible combination of those fingers, each exactly once. If each finger stands for an item, counting up visits every group of items.
| In the example | In the code |
|---|---|
| Your fingers | the n items |
| A combination of raised fingers | mask |
| Counting up one | mask++ |
Is finger i raised? | mask & (1 << i) |
| All fingers raised | (1 << n) - 1 |
From the slow way to enumerating subsets with bitmasks
- Start with the usual way to visit every subset: a recursive function that, for each item, first skips it and then takes it. It works, but it needs a helper with extra parameters, a call stack, and some way to name the current subset, often an array that gets copied.
- Look at what a subset really is: for each of the
nitems, a yes or a no. That'snyes/no answers in a row, which is exactly whatnbits are. Write bitias 1 when itemiis in. For[4, 10, 3], the subset with items 0 and 2 is101. - Every pattern of
nbits is a whole number from0to2^n - 1, and every such number is a different pattern. So counting from0to2^n - 1walks through every subset exactly once, with no recursion and no copies. The subset is just the numbermask. - Inside the loop,
mask & (1 << i)is non-zero exactly when itemiis in, so an inner loop overican add up the subset or build it. A last trick,sub = (sub - 1) & mask, jumps straight to the next smaller subset of a given mask. The counting loop is subset enumeration with bitmasks.
How it works
- Let
nbe the number of items. The masks go from0to(1 << n) - 1. - Loop
maskfrom0whilemask < 1 << n. - For each
mask, loopifrom0ton - 1. Ifmask & (1 << i)is non-zero, itemiis in this subset. - Do the work for this subset, such as adding up
items[i]intosum. - To list only the subsets of a given
mask, start withsub = maskand step withsub = (sub - 1) & maskuntil it reaches 0, then add0itself.
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function subsetSums(items) {const n = items.length;const sums = [];for (let mask = 0; mask < 1 << n; mask++) {let sum = 0;for (let i = 0; i < n; i++) {if (mask & (1 << i)) sum += items[i];}sums.push(sum);}return sums;}function submasks(mask) {const result = [];for (let sub = mask; sub > 0; sub = (sub - 1) & mask) {result.push(sub);}result.push(0);return result;}
How fast it is
| Time | O(2^n × n) |
|---|---|
| Extra memory | O(2^n) |
There are 2^n masks and each one checks n bits. The sums array has one entry per mask.
- Best case
O(2^n × n)Every list of
nitems does the same work:2^nmasks, each checkingnbits. Onlynmatters, not the values. - Average case
O(2^n × n)For 20 items: 1,048,576 masks times 20 checks, about 21 million steps, fine in a browser.
- Worst case
O(2^n × n)Larger
n. Each extra item doubles the work, so 30 items is about 1,500 times slower than 20:2^10times more masks, each with more bits to check.submasks(mask)alone is cheaper:O(2^k)for a mask withkbits set.
Common mistakes
- Writing
mask & 1 << i === 0. Comparison runs before&, so add brackets:(mask & (1 << i)) === 0. - Using it for
nabove about 25.2^25is over 33 million subsets, which is slow, and1 << 31turns negative. - Treating bit 0 as the leftmost item. Bit 0 is the rightmost bit; just be consistent between building and reading masks.
- Forgetting the empty subset when walking submasks: the loop stops at 0 without visiting it.
When to use it
Look for these clues in a problem:
- The input has at most about 20 items and you need to check every subset.
- Each item is either chosen or not, and you need the best, a count, or all valid choices.
- Sets of up to 31 small things (skills, permissions, features) need to be compared or combined quickly.
- A dynamic programming state is "which items are used so far" (that's bitmask DP).
Check yourself
Answer each one in your head first, then open it.
Which mask stands for items 0 and 2 of [4, 10, 3], and what is its sum?
5, which is 101 in binary. Its sum is 4 + 3 = 7, stored at sums[5].
How many masks does a list of 20 items have?
2^20 = 1,048,576, one for each subset, including the empty one.
Why does subsetSums with 31 items return an empty array?
1 << 31 is negative in JavaScript, so mask < 1 << n is false at once and the loop never runs.
Why does submasks push 0 after the loop?
The loop condition is sub > 0, so it stops as soon as sub becomes 0, before pushing it. The empty subset is added separately.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 5 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingGo further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
Subsets and masks match one to one. Each subset gives exactly one pattern of n bits, and each pattern is exactly one number from 0 to 2^n - 1. The loop mask = 0, 1, 2, ... visits each of those numbers once, so it visits each subset once, with none missed and none repeated.
Inside, 1 << i has only bit i set, so mask & (1 << i) is non-zero exactly when bit i of mask is 1, which means item i is in the subset. So sum adds exactly the subset's items. Because subsets are visited in mask order, sums[mask] is that subset's sum, and any subset's answer can be looked up later by its mask.
For submasks, subtracting 1 from sub turns its lowest 1 into 0 and every 0 below it into 1. That's the next smaller number, but it may have bits outside mask; & mask removes them. The result is the largest subset of mask below sub, so the walk visits every subset of mask in decreasing order, each once. The loop stops at 0, so 0 is pushed after it.
Going deeper
3 more ideas
Why every subset appears once
Each subset gives exactly one pattern of n bits, and each pattern is exactly one number from 0 to 2^n - 1. So counting through those numbers visits each subset once, with no repeats and no gaps.
Walking through the subsets of a mask
Subtracting 1 from sub turns its lowest 1 into 0 and the 0s below into 1s; & mask then drops any bit that isn't in mask. The result is the next smaller subset of mask. For mask = 101: 101, then 100, then 001, then 000. Summed over all masks of n bits, this visits 3^n pairs, which is the basis of many bitmask DP solutions.
The cost and the limit
There are 2^n masks and each one checks n bits, so the time is O(2^n × n). That is about 20 million steps for n = 20, fine in a browser. For n = 30 it's 30 billion, far too slow. Also, 1 << 31 is negative in JavaScript, so this loop only works for n up to 30.
Work it by hand
Every step in a table, with why
List the sum of every subset of [4, 10, 3], then walk the subsets of the mask 110.
subsetSums([4, 10, 3]), then submasks(6)
| Step | mask | Bits | Items in | sum | What happens | Why |
|---|---|---|---|---|---|---|
| 1 | 0 | 000 | none | 0 | the empty subset | No bit is set, so the inner loop adds nothing. |
| 2 | 1 | 001 | 0 | 4 | only item 0 | Bit 0 is the rightmost bit. items[0] is 4. |
| 3 | 2 | 010 | 1 | 10 | only item 1 | 2 & (1 << 1) is non-zero, so items[1] is added. |
| 4 | 3 | 011 | 0, 1 | 14 | items 0 and 1 | 4 + 10. Counting up to 3 added item 0 back in. |
| 5 | 4 | 100 | 2 | 3 | only item 2 | Each time the count reaches a power of two, a new item appears on its own. |
| 6 | 5 | 101 | 0, 2 | 7 | items 0 and 2 | 4 + 3. |
| 7 | 6 | 110 | 1, 2 | 13 | items 1 and 2 | 10 + 3. |
| 8 | 7 | 111 | 0, 1, 2 | 17 | every item | mask + 1 is 8, which equals 1 << 3, so the loop ends after this. |
| 9 | sub = 6 | 110 | 1, 2 | - | push 6 | submasks(6) starts with the mask itself. |
| 10 | sub = 4 | 100 | 2 | - | (6 - 1) & 6 | 6 - 1 is 101; ANDing with 110 drops bit 0, which isn't in the mask, giving 100. |
| 11 | sub = 2 | 010 | 1 | - | (4 - 1) & 6 | 4 - 1 is 011; ANDing with 110 gives 010. |
| 12 | sub = 0 | 000 | none | - | loop ends, push 0 | (2 - 1) & 6 is 0, so the loop stops. The empty subset is pushed after the loop. |
subsetSums returns [0, 4, 10, 14, 3, 7, 13, 17], where sums[mask] is the sum of subset mask. submasks(6) returns [6, 4, 2, 0]: the 4 subsets of {1, 2}, from largest to smallest.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Enumerating subsets with bitmasks | O(2^n × n) | O(2^n) | Up to about 20-25 items when you want every subset with a simple loop and a number to name each one. |
| Recursive include or skip | O(2^n) | O(n) | You build each subset step by step, or want to cut off branches early (backtracking). |
| Reuse smaller subsets | O(2^n) | O(2^n) | You need a value for every subset and each one follows from a smaller subset. |
Extra memory: O(2^n): the sums array has one entry per subset. Just visiting subsets without storing them needs only O(1).
How the steps grow
- Enumerating subsets with bitmasks
O(2^n × n) - Recursive include or skip, Reuse smaller subsets
O(2^n)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Enumerating subsets with bitmasks | 10,240 | more than a quadrillion | more than a quadrillion |
| Recursive include or skip | 1,024 | more than a quadrillion | more than a quadrillion |
| Reuse smaller subsets | 1,024 | more than a quadrillion | more than a quadrillion |
Inputs that need care
5 tricky inputs
- No items:
subsetSums([]) 1 << 0is 1, so the loop runs once formask = 0and returns[0]: the empty subset, whose sum is 0.- 31 items
1 << 31is-2147483648, somask < 1 << nis false at once and it returns[]without warning. It would be far too slow anyway; keepnto about 25.- 32 items
1 << 32wraps to 1, so onlymask = 0runs and it returns[0]. Again silent; checknbefore looping.- Equal items:
[5, 5] - Returns
[0, 5, 5, 10]. Masks01and10are different subsets (by position) with the same sum. Put the sums in aSetif only distinct sums matter. submasks(0)- The loop doesn't run, since
sub > 0is false, and it returns[0]: the empty set is the only subset of the empty set.
Variations and follow-ups
Twists interviewers add
Reuse smaller subsets
sums[mask] equals sums[mask ^ low] plus the item at bit low, where low = mask & -mask is the lowest set bit. The item index is 31 - Math.clz32(low). Each mask then costs one step, so the whole table takes O(2^n) instead of O(2^n × n).
Bitmask dynamic programming
Use the mask as part of a DP state: "the best cost having visited exactly these cities", stored as dp[mask][last]. Problems with up to about 20 items, like assigning tasks to people or visiting every stop once, become O(2^n × n²) instead of O(n!).
Meet in the middle
For about 40 items, split them into two halves of 20, list each half's 2^20 subset sums, and combine the two lists with sorting and two pointers. That's two million subsets instead of a trillion.
Gray code order
Visiting masks in the order mask ^ (mask >> 1) changes exactly one item between neighbors: 000 001 011 010 110 111 101 100. When adding or removing one item is cheap, each step costs O(1).
In an interview
Spot it, explain it, answer follow-ups
- Spot it from a small
n("n <= 20"), "try every combination", "every subset", "choose any group of items", or a state that is "which items are used so far". - Explain it as: "Each of the
2^nnumbers below1 << nis one subset, where bitisays whether itemiis in. I loop over them and read the bits." Then state the cost,O(2^n × n). - Test
n = 0and a small case of 2 or 3 items by writing out the masks in binary. Mention the limit: past 30 items,1 << nbreaks in JavaScript. - A common follow-up is "list the subsets of a given mask". Show
sub = (sub - 1) & mask, and remember to add 0 at the end.
Where frontend code uses it
The same idea in real UI code
- Testing every on/off combination of a few feature flags or component props is a loop over masks.
- A form with up to ~16 optional checkboxes can store its state as one mask number, which is easy to put in a URL and compare.
Node.compareDocumentPositionreturns a bitmask such asDOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING, read with&checks like these.