We reviewed the general pattern of choose-explore-unchoose and played with several variations on the theme.

Q: How does recursive backtracking “remember” where to go back to?

On the board I had a decomposition tree for a non-recursive program such as MadLibs. Perhaps main calls the function doMadLib which has a loop that repeatedly calls doOnePlaceholder which then calls findBracket and promptUser. When a function returns, control goes back to the function that called it and execution picks up where it left off. If the loop in doMadLib just finished processing the third placeholder, when control returns to doMadLib, it will advance to the fourth iteration. The “memory” of what was happening in a previous function is stored on the call stack (the very same call stack we can view in the debugger). Also note that function calls are entered and exited in LIFO (last-in-first-out) order.

The same procedure is followed for recursive calls. You might worry that many copies of the same function on the call stack would confuse things. How does the computer know which is which? Consider the situation where fact(3) called fact(2) which called fact(1) which called fact(0). What that base case fact(0) returns, which version of fact does it go back to? The function underneath on the call stack is fact(1) so that’s where control resumes. LIFO ordering dictates that we unwind in reverse order of how we arrived. It’s also important to note that each invocation of a function call has its own private state (e.g. copies of parameters and variables) so as to not be confused with others.

For exhaustive recursion such as our explore function, the tree can get crazy-bushy when we draw it out the full exploration, but the same LIFO principle is used. If we look at the call stack, the stack frames indicate exactly what functions were called on the way here and where we will return on our way out.

Playing with choose-explore-unchoose

We started with the code that we had written last time. Given a set of options (in our example, the options were state postal codes), this recursive function prints all words that can be formed out of those options.

void explore(Lexicon& lex, Set<string>& options, string soFar = "")
{
    if (!lex.containsPrefix(soFar)) {
        return;
    }
    if (lex.contains(soFar)) {
        cout << soFar << endl;
    }
    for (string choice : options) {
       explore(lex, options, soFar + choice);
    }
}

Count words

We duplicated the above function and made some tweaks to create the count function. It follows the same exploration but rather than print each words it counts it. The results from the inner recursive calls are accumulated to pass back out of this call.

int count(Lexicon& lex, Set<string>& options, string soFar = "")
{
    int result = 0;

    if (!lex.containsPrefix(soFar)) {
        return 0;
    }
    if (lex.contains(soFar)) {
        result = 1;       // what is consequence of return 1 here?
    }
    for (string choice : options) {
                        // what if result = count instead of result += count
       result += count(lex, options, soFar + choice);  
    }
    return result;
}

We ran the program and counted 45 words.

Find any word

Now we change the goal to find a single word, any word. Given we don’t care which word, we might as well take the first one we find. Furthermore, once one is found, no need to look further. We should not make any additional recursive calls and instead begin unwinding. Let’s see what changes are required to make that work:

string findWord(Lexicon& lex, Set<string>& options, string soFar = "")
{
    if (!lex.containsPrefix(soFar)) {
        return "";
    }
    if (lex.contains(soFar)) {
        return soFar;   // if we find a word, take it & run!
    }
    for (string choice : options) {
        string cur = findWord(lex, options, soFar + choice))
        if (!cur.empty()) { // what if return cur without checking non-empty?
            return cur;
        }
    }
    return "";      // this subtree was a bust, back up to look elsewhere
}

We ran this and got “al” as the first word found. (Side note: our dictionary is the Official Scrabble Player’s Dictionary which is loaded with obscure 2-letter words revered by crossword fans).

If you trace on the code above, you can verify that at the point a word is found, no further recursive calls are initiated. A successful result triggers backtracking right away. The found result is passed back without further searching.

Longest word

The final version we created was to report the longest word. To determine the longest, we have to go back to exhaustively searching the entire tree rather than stopping short as we did for findWord. The longest function must try each subtree and compare the result from each to find the winner, which is returned as the overall best.

string longest(Lexicon& lex, Set<string>& options, string soFar = "")
{
    string best = "";

    if (!lex.containsPrefix(soFar)) {
        return "";
    }
    if (lex.contains(soFar)) {
        best = soFar;
    }
    for (string choice : options) {
            // below originally had a bug, findWord instead of longest
        string cur = longest(lex, options, soFar + choice)); 
        if (cur.length() > best.length())
            best = cur;
        }
    }
    return best;  
}

When I ran my first version, it reported that “caca” was the longest word. But our earlier exhaustive search printed plenty of longer words, so it was clear we had a bug. I asked for some help from the class and they quickly narrowed in on the problem. Instead of making a recursive call on longest, I mistakenly called findWord. So it was getting the first word from the subtrees, not the longest. Oops!

When fixed, it now properly reported that the longest was “mainland”. A student asked about the other words of length 8 and I mentioned that the function was breaking ties somewhat arbitrarily; preferring the first word of a given length. We changed the > to a >=" which caused it to return the last word found of that length. We ran with that change and got the word “moorland”.

8 Queens

In chess, the all-powerful queen can attack in any straight line (horizontally, vertically, or diagonally). This puzzle is presented as asking how to place 8 queens on an 8x8 chessboard such that no queen threatens another.

We started by trying to place 4 queens on a 4x4 board. I started the board off with a queen in the upper left corner and asked the students to place the rest. We got as far as this, but had no where safe to put the last queen:

-----------------
| Q |   |   |   |
-----------------
|   |   |   |   |
-----------------
|   |   |   | Q |
-----------------
|   | Q |   |   |
-----------------

If instead I place the first queen in second column of the first row, we are able to place all remaining queens in safe positions.

-----------------
|   | Q |   |   |
-----------------
|   |   |   | Q |
-----------------
| Q |   |   |   |
-----------------
|   |   | Q |   |
-----------------

An approach to solving the 8 queens puzzle is to use a brute-force recursive exploration that tries all the rearrangements until it finds a successful one. The function below organizes the search by having each recursive call be responsible for safely placing a queen in a particular row.

bool solveQueens(Board& board, int row)
{
    if (row == board.size()) {
        return true;            // base case: we have a solution!
    } else {
        // available options to try are columns in this row
        for (int col = 0; col < board.size(); col++) {
            if (board.isSafe(row, col)) {
                board.place(row, col);             // choose
                if (solveQueens(board, row + 1)) { // explore
                   return true;
                }
                board.remove(row, col);            // unchoose
            }
        }

        return false; // tried all rows, none work, backtrack
    }
}

We single-stepped through this algorithm solving an 4x4 board, then let the animation run at top speed solving the 8x8 board. Brute force can be an effective tool in the hands of a patient and relentless systematic computer!

Boggle

I spent the last few minutes of class giving a quick demo of the Boggle game, your next assignment. We have found this program to be a fun way to put your new backtracking skills to work and hope you enjoy building and playing it!

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