Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Generating subsets

Open In Colab

Generating subsets

In this post, we will discuss the problem of generating all subsets of a given set of elements. A subset is a collection of elements that are selected from a set. The table below shows all subsets of the set {1, 2, 3}:

Subsets of {1, 2, 3}
{}
{1}
{2}
{3}
{1, 2}
{1, 3}
{2, 3}
{1, 2, 3}

For a set of nn elements, there are 2n2^n subsets. The problem of generating all subsets of a set of elements is a common problem in computer science and has applications in various fields, such as combinatorial optimization, cryptography, and data analysis.

We use a small helper to print a subset in set notation.

1. Binary representation

Each subset of {a1,…,an}\{a_1, \ldots, a_n\} corresponds to a bit string b1b2…bnb_1 b_2 \ldots b_n of length nn, where bi=1b_i = 1 means aia_i is in the subset and bi=0b_i = 0 means it is not. To generate all subsets, we generate the binary numbers from 0 to 2n−12^n - 1 and translate each one into a subset.

000 ∅
001 {a3}
010 {a2}
011 {a2, a3}
100 {a1}
101 {a1, a3}
110 {a1, a2}
111 {a1, a2, a3}

This reproduces the table in the slides: bit b1b_1 (the leftmost) stands for a1a_1.

Squashed order

In squashed order, every subset that involves aja_j comes after all the subsets of {a1,…,aj−1}\{a_1, \ldots, a_{j-1}\}. The order above is not squashed: {a3}\{a_3\} comes before {a1}\{a_1\}. To get squashed order, we read the bits from right to left instead, so that the least significant bit stands for a1a_1. Bit ii of kk is (k >> i) & 1.

∅
{a1}
{a2}
{a1, a2}
{a3}
{a1, a3}
{a2, a3}
{a1, a2, a3}

2. Bottom-up

The bottom-up algorithm is a decrease-by-one algorithm. It relies on one observation: every subset of {a1,…,an}\{a_1, \ldots, a_n\} either contains ana_n or it does not, so

Subsets(n)=Subsets(n−1)  ∪  { S∪{an}∣S∈Subsets(n−1) }\text{Subsets}(n) = \text{Subsets}(n-1) \;\cup\; \{\, S \cup \{a_n\} \mid S \in \text{Subsets}(n-1) \,\}

The algorithm builds the 2n2^n subsets incrementally, adding one element at a time. Start with the only subset of the empty set, ∅\emptyset. Then, for each new element aia_i, keep all the subsets generated so far (those not containing aia_i) and append a copy of each of them with aia_i added (those containing aia_i). Each step doubles the list.

Printing the list after each step gives the table from the slides:

0 ∅
1 ∅  {a1}
2 ∅  {a1}  {a2}  {a1, a2}
3 ∅  {a1}  {a2}  {a1, a2}  {a3}  {a1, a3}  {a2, a3}  {a1, a2, a3}

The same idea can be written as a recursion that follows the recurrence directly: solve the problem for the first n−1n-1 elements, then add ana_n to a copy of each of those subsets.

['∅', '{1}', '{2}', '{1, 2}', '{3}', '{1, 3}', '{2, 3}', '{1, 2, 3}']

Both versions produce the subsets in squashed order, the same order as the binary method with the bits read from right to left:

All three agree on 32 subsets

3. The binary reflected Gray code algorithm

The binary reflected Gray code (BRGC) is a minimal-change algorithm: it generates the 2n2^n bit strings so that successive strings differ in exactly one bit. In terms of subsets, each subset is obtained from the previous one by adding or removing a single element.

The list for nn bits is built from the list for n−1n-1 bits. The second half of the list is the first half reflected, that is, in reverse order:

BRGC(n):
    if n = 1, make list L of the two bit strings 0 and 1
    else
        generate recursively list L1 of bit strings of length n-1
        copy list L1 in reverse order to get list L2
        add 0 in front of each bit string in list L1
        add 1 in front of each bit string in list L2
        append L2 to L1 to get list L
    return L

We can implement the algorithm using recursion as follows:

n = 2: 00 01 11 10
n = 3: 000 001 011 010 110 111 101 100
n = 4: 0000 0001 0011 0010 0110 0111 0101 0100 1100 1101 1111 1110 1010 1011 1001 1000

The function gray_code(n) generates the Gray code for nn bits. The base case is when n=1n = 1, in which case the function returns the list ['0', '1']. Otherwise, the function generates the Gray code for n−1n-1 bits and copies the list in reverse order. It then adds '0' as a prefix to each bit string in the original list and '1' as a prefix to each bit string in the reversed list. Finally, it appends the reversed list to the original list and returns the result.

We can avoid the recursion and the extra copies of the list by growing a single list in place. The following iterative implementation appends the reflected strings with a '1' prefix, then adds the '0' prefix to the first half:

000 001 011 010 110 111 101 100

We can check the minimal-change property, and translate the bit strings into subsets:

000 ∅
001 {a3}
011 {a2, a3}
010 {a2}
110 {a1, a2}
111 {a1, a2, a3}
101 {a1, a3}
100 {a1}

Application: the knapsack problem

The exhaustive-search solution to the knapsack problem examines every subset of items, computes its total weight and value, and keeps the most valuable subset that fits in the knapsack. If the subsets are generated independently, computing the totals of each one takes linear time.

With the Gray code order, consecutive subsets differ by one item, so the totals of the new subset are obtained from those of the previous one in constant time: add the item’s weight and value if it was just added, subtract them if it was just removed.

To find which bit changes at step kk without comparing strings, we use a property of the BRGC: the bit that flips at step kk (counting from the right, starting at 0) is the number of trailing zeros in the binary representation of kk. For example, from step 3 to step 4 (010 → 110), 4=10024 = 100_2 has two trailing zeros, so bit 2, the leftmost one, flips.

We use the instance from the brute-force chapter: four items and a knapsack of capacity 10.

ItemWeightValue
1742
2312
3425
4540
Best value: 65 with items {3, 4}