Today we discussed how to use recursion to perform an exhaustive exploration of a search space. The examples I like to first introduce are generating all sequences from a set of choices, generating all permutations, and generating all subsets. We worked on sequences and permutations today and will continue with subsets on Wednesday.

Generating sequences

Let’s say you flip a coin twice. What are the possible sequences that might result? A little human brainstorming can come up with the four possibilities:

H H 
H T
T H
T T

What about a sequence of three coin flips or four? If we try to manually enumerate these longer sequences, how can we be sure we got them all and didn’t repeat any? It will help to take a systematic approach.

Consider: how does a length N sequence build upon the length N-1 sequences? If we take each of the above length 2 sequences and paste an H or T on the front, we will generate all the length 3 sequences. We can do something similar to extend from length 3 to length 4 and so on. This indicates a self-similarity in the problem that should lend itself nicely to being solved recursively.

H H H 
H H T
H T H
H T T
T H H 
T H T
T T H
T T T

Next we created a visualization of the search space. We model the choices and where each leads using a diagram called a decision tree.

Each decision is one flip, and the possibilities are heads or tails. A sequence of N flips has N such choices.

The top of the tree is the first choice with its possibilities underneath:

                                 |
                        +--------+--------+
                        |                 |   
                        H                 T 

Each choice we make leads to a different part of the search space. From there, we consider the remaining choices. Below is a decision tree for the first two flips:

                                 |
                        +--------+---------+
                        |                  |   
                        H                  T 
                        |                  |
                 +------+------+     +-----+------+
                 |             |     |            | 
                HH            HT    TH           TT

The height of the tree corresponds to the number of decisions we have to make. The width at each decision point corresponds to the number of options. To exhaustively explore the entire search space, we must try every possible option for every possible decision. That can be a lot of paths to walk!

Next we wrote a function to generate these sequences. This code traverses the entire decision tree. By using recursion, we are able to capitalize on the self-similar structure.

void generateSequences(int length, string sofar)
{
    if (length == 0) {
        cout << sofar << endl;
    } else {
        generateSequences(length - 1, sofar + " H");
        generateSequences(length - 1, sofar + " T");
    }
}

Note that the recursive case makes two recursive calls. The first call recursively explores the left arm of the decision tree (having chosen “H”), the second explores the right arm (having chosen “T”).

This function prints all possible 2^N sequences. If you extend the sequence length from N to N + 1, there are now twice as many sequences printed. It first prints the complete set of the sequences of length N each with an “H” pasted in front, followed by a repeat of the sequences of length N now with an “T” pasted in front.

Next we changed the search space. In place of flipping a coin, we will choose letter from the alphabet. The code changes only slightly:

void generateSequences(int length, string sofar)
{
    if (length == 0) {
        cout << sofar << endl;
    } else {
        for (char ch = 'A'; ch <= 'Z'; ch++) {
            generateSequences(length - 1, sofar + ch);
        }
    }
}

We started to write out 26 recursive calls, one per letter, but that was tedious. A loop is just the thing to compactly express iterating over the alphabet.

Using both iteration and recursion in combo may be a bit puzzling. In some of our previous examples, recursion was used as an alternative to iteration, but in this case, both are working together. The iteration is used to enumerate the options for a given single choice. Once an option is picked in the loop, recursion is used to explore subsequent choices from there. In terms of the decision tree, iteration is traversing horizontally and recursion is traversing vertically.

Permutations

Permutations, e.g. generating all possible re-orderings, is another task that fits this exhaustive recursive pattern.

Consider: what are the choices you make when forming a permutation? At each step, you must choose the next letter to add to the sequence so far. These will be the decision points of the tree. Now consider what options are available for choosing a letter. This dictates the branching structure at each decision point. We sketched a partial decision tree for permutations.

Once we have a decision tree, we translated it into code thusly:

void listPermutations(string input, string sofar = "")
{
    if (input.empty()) {
        cout << sofar << endl;
    } else {
        for (int i = 0; i < input.length(); i++) {
            string rest = input.substr(0, i) + input.substr(i+1);
            listPermutations(rest, sofar + input[i]);
        }
    }
}

Each call to the recursive function chooses the next letter and removes it from the options to further consider. Once a selection is made, the recursive call moves deeper into the decision tree. After fully exploring that option, it returns to this choice and un-chooses it to allow trying another option. The loop repeats for every option.

This choose-recur-unchoose structure is a classic pattern for using recursion to exhaustively explore a search space. Here it is summarized in pseudocode:

if (no more choices to make) {
    // base case, you're done
} else {
    // recursive case
    for (each available option) {
        choose that option
        recur from here
        unchoose (if necessary to make different choice)
    }
}

We will do more with this in Wednesday’s lecture.

The inspiration for preparing these notes was the work of my colleague Stuart Reges at University of Washington.