CSE143 Notes 5/10/06

Recursive Backtracking

Here's a simple problem: program a computer to play chess.

Well, at least it's simple to state the problem. Let's be a bit more specific: program a computer to play chess interactively against a person. The person gets to move first. How would we do this?

First, we let the user make a move. Then we'll look at the board and, for each possible, legal move that we could make, calculate how "good" that move would be. After checking all possible moves, we can pick the best one:

   public void move(Board b) {
     for each possible move on b {
       calculate how good that move is
       if that's better than anything we've seen before, remember that move
     }
     make the best move
   }

Now how do we figure out how good a move is? Well, we need to do roughly the same calculation. Given the updated board after the proposed move has been made, how good is the outcome after the next move by the opponent? How do we figure that out? For each of the possible countermoves, we need to see how good it is by seeing how good all the possible next moves are.

Hmmmm, sounds a lot like recursion. Also a lot of computation.

This sort of process leads to a combinatorial explosion of choices to evaluate. One way to look at it as a decision tree. At the top level, we have to evaluate several possible choices. At the next level we have to evaluate a set of choices for each of the original choices, and so on. We can draw a diagram to illustrate what's going on.

 

 

 

 

 

 

 

 

 

How do we turn this into a (recursive) program? One issue: what is the base case? Easy choices for a game are when we win or lose. In a complex game, we may also want to abandon a possible path if it's clear that there are too many possible choices and this particular path is unlikely to be the best one. (Take an AI course to explore this topic in depth.)

A more interesting issue if we want to turn this into a program is how do we deal with the multiple choices at each level? Conceptually we'd like to just look at "all" of them, but we need to figure out how to describe this as a computational process that does one thing at a time (assuming that we don't have access to some sort of massivly parallel computer). The basic idea is to make one possible choice at one level, then make a choice at the next level, then the next, and so on until we reach some sort of base case. If we hit a successful base case (e.g., we win the game), then we can report success and we're done.

But if we reach a base case that does not represent success, or if we simply want to consider other possibilities as well, we need to "unwind" the most recent choice we tried, go back, and try something else. This process is known as backtracking. We explore a path, but then if we want to explore further, we first need to back out of the most recent choice we made on that path before trying the next one. In some sense we need to leave "electronic breadcrumbs" so we can retrace our path back to the previous point where there were unexplored options. We also need to somehow arrange the computation so we don't go back and retrace previously explored paths. Sometimes this is easy because of the structure of the problem; at other times we need to deliberately do something to mark paths that have been explored already and shouldn't be tried again (think of trying to find your way out of a maze).

8 Queens

Rather than exploring the full details of playing chess, let's look at a simpler problem. The 8 Queens problem is a classic example of recursive backtracking. The earliest reference I know of is in Edsger Dijkstra's Notes on Structured Programming (published 1972, written a bit earlier), and he credits it to Niklaus Wirth (the inventer of the Pascal programming language, among many other accomplishments).

The problem is easily stated: find a configuration where 8 queens can be placed on a chessboard so that no queen threatens any other with capture. A queen can move horizontally, vertically, or diagonally, so the problem is to find a configuration of 8 queens where no two queens are in the same row, or the same column, or on the same diagonal.

A brute force approach would be to try placing a queen on one of the 64 possible squares, then try all of the remaining 63 squares for the second piece, then, given the positions of the first two pieces, try the remaining 62 squares for the third piece, and so forth.

But the structure of the problem suggests that we don't need to consider so many possibilities. Once we've placed a queen in a particular row or column, we can't place any others in that row or column. So instead of searching all the possible squares at each level, we can start by placing a queen in one column or row, then explore the next column or row. Because the problem is symmetric, it doesn't really matter whether we proceed by rows or columns. Since we have to pick one (and since the sample program does it this way), we'll proceed column by column.

So we wind up with the following decision tree. First we make a choice for column 1. (e.g., which row do we try?). Then for each choice in column 1, we make a choice in column 2, and so on.

 

 

 

 

 

 

 

This process repeats until either of two things happen: we reach the final column and are able to place a queen there succsessfully (we win!), or at some point we are unable to place a queen successfully in a column. In that case we backtrack to the previous column and try the next possible choice there, if any. If there are no remaining possibilities in the previous column, we back up and try the next choice in the column before that.

Now we want to design a program to solve the problem. A good design principle is to see if there's some way to break things down into smaller parts that can be developed relatively independently. In this case there is: one task is to keep track of the board configuration (which squares have a queen, add and remove a queen from the board, etc.). The other task is the more interesting one: what strategy do we use to find a configuration? Where do we place the queens? How do we decide if we have a solution?

The board is reasonably straightforward. We'll define a class Board to represent the board. Obviously we'll need some way to create a board of a particular size, so we'll define a constructor:

   // initialize a new board with n rows and n columns
   pubic Board(int n)

(A detail we'll skim over here is the exact representation. We could, of course, use an 8x8 array. But since for this problem there is at most one piece in a particular column, it suffices to have a 1-dimensional array with 8 entries, one for each column. The entry for each column contains either the row where a queen is placed in that column, or some "not possible" row number to indicate that no queen has been placed in that column yet.)

Now, what other methods do we need? What sort of manipulations are needed on the board in the course of solving the problem?

 

 

 

 

 

 

 

 

So we'll assume that we have a Board object that can manage the contents of the board for us and provides these methods. Now, how do we solve the problem? How about

   // Store a solution to the 8 queens problem in board b
   pubic void solve(Board b)

Sounds great! But that only defers the work to a recursive helper method that solves the problem. Our basic operation is, given a board that has some queens placed on it already, try to find a solution by placing queens in the next available column and seeing if there is a solution from that point. Let's call that method explore (since that's what's already in the code).

   // return true if there is a solution to the 8 queens problem
   public boolean explore(...) { ... }

What parameters does this method need? What sort of preconditions would make sense (i.e., we only want to call this method when there is a chance that it might discover a solution)?

When should we stop? What is the base case if the search succeeds? What do we return if we get there? What is the base case if we fail? Then what?

OK, now what is the general, recursive case? Assuming we are asked to place a queen in a particular column, how do we do this? How do we explore from there? What do we do if this succeeds? (Declare victory of course!) But what if this search fails or turns out to be a dead end? In that case we need to be careful to unwind what we've done so we can explore different choices. Then we report failure, return, and leave it to the previous invocation of the method to figure out what to do next.