Last time we saw that producing all the subsequences of a given size k from a given sequence of size n is essentially the same problem as computing all the sequences of n Booleans where exactly k of them are true. How do we do that?

An approach that I often see is “enumerate all sequences of n Booleans, count the number of on bits, and discard those which have too many or too few”. Though that works, it’s not ideal. Suppose we are seeking to enumerate the combinations of 16 items chosen from a set of 32. There are over four billion possible combinations of 32 bits, and of those over three billion of them have more or fewer than 16 true bits, so that’s a lot of counting and discarding to do. We can do better! To do so, we’ll use a combination of all my favourite techniques:

Immutable data structures

Abstract classes with derived nested classes

Recursion

Long-time readers of this blog will have seen me use these techniques before, but for new readers, a quick introduction is in order.

The idea of an immutable collection is that the collection does not change when you add or remove something from it. That seems contradictory, but really it makes a lot of sense. The number 3 does not change into 7 when you add 4 to it; the number 3 is eternal and unchanging. Rather, adding 4 to it produces a new, entirely different number called 7. Similarly, adding an item to a collection does not change the original collection; it produces a new collection.

That might sound inefficient, but actually it is very efficient: because the original collection is immutable, we can use all or some of the original collection data structure in the new data structure.

The data structure I need to solve this problem is an immutable stack of Booleans, so let’s whip one of those up. (Of course we could use data structures in the BCL immutable collections, but for pedagogic and amusement purposes, let’s just make our own.) The operations I’m going to need are just pushing and enumerating, but for completeness let’s add public members for popping and emptiness checking as well. I’ll use one of my favourite techniques of an abstract base class whose derived classes are nested within it:

using System; using System.Collections; using System.Collections.Generic; abstract class ImmutableStack<T>: IEnumerable<T> { public static readonly ImmutableStack<T> Empty = new EmptyStack(); private ImmutableStack() {} public abstract ImmutableStack<T> Pop(); public abstract T Top { get; } public abstract bool IsEmpty { get; } public IEnumerator<T> GetEnumerator() { var current = this; while(!current.IsEmpty) { yield return current.Top; current = current.Pop(); } } IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } public ImmutableStack<T> Push(T value) { return new NonEmptyStack(value, this); } private class EmptyStack: ImmutableStack<T> { public override ImmutableStack<T> Pop() { throw new InvalidOperationException(); } public override T Top { get { throw new InvalidOperationException(); } } public override bool IsEmpty { get { return true; } } } private class NonEmptyStack : ImmutableStack<T> { private readonly T head; private readonly ImmutableStack<T> tail; public NonEmptyStack(T head, ImmutableStack<T> tail) { this.head = head; this.tail = tail; } public override ImmutableStack<T> Pop() { return this.tail; } public override T Top { get { return this.head; } } public override bool IsEmpty { get { return false; } } } }

We can create one of these like this:

ImmutableStack<bool> myStack = ImmutableStack<bool>.Empty .Push(true) .Push(false) .Push(false);

And hey, now we’ve got the stack false, false, true . Again, remember, pushing on Empty did not change Empty at all. It’s the same as it ever was.

Our technique is going to be to recursively build longer bit sequences out of shorter bit sequences. Since pushing onto an immutable stack is cheap, and we don’t ever have to worry about the stack being changed by someone else, we’ll see that the algorithm is pretty straightforward.

Next time: Now that we have our helper class, we’ll actually enumerate the combinations.