Return to the Lecture Notes Index

15-111 Lecture 11 (February 9, 2009)

The Eight Queens Problem

Last class, we played with recursion. But all of the problems that we played with were readily solved without recursive thinking. Today we'll look at two problems that are best solved using recursive thinking. The Eight Queens Problem is the first such problem. The next lab will also be such a challenge. We will use recursive thinking to solve a maze.

Backtracking is a typical application of recursion. Sometimes in trying to solve a problem, we speculate -- we take guesses. But guesses can be wrong, so we may want to back up and try again. Since recursion maintains a stack, it is very easy to backtrack using recursion. We can simply return from the current function, back to a previous state, and try again.

In the Eight Queens Problem the goal is to place 8 queens on a chessboard such that no queen can attack any other queen. Queens can attack other pieces on the same row, column, or diagonal.

We could try evey possibility -- but that could take 8! = 40,320 tries, even if we did the obvious thing and only placed one queen on each row and column.

Instead, we'll use recursion. We'll speculatively place a queen in each column, starting at the first row, and moving down until it is in a safe position. Then we'll try to place a queen in the next column. And charge forward until we're done (off the board on the other side), But what if we can't charge forward? What if we get to the bottom of the board and haven't found a safe row? Then, there is no safe row in the current column? This means that one of our previous guesses was wrong. So, we return back to the previous level and try the next position. Over the course of the execution, the algorithm may move backward and forward many times, as it discovers wrong guesses and is forced to backtrack.

But, how can we tell if we are returning because we got to the other side of the board and have placed all 8 queens or if we are returning becasue we got to the bottom of a column and couldn't place a queen? The answer is that the return value must be different.

The following PSEUDOCODE illustrates a solution to this problem:

  boolean addQueen (int col)
  {
    // If columns are number 0 - 7, if we
    // get to column 8, we've placed all 8 queens
    if (col > 7 )
      return true; // true indicates we're done

    for (row=0; row<8; row++)
    {
      // isSafe returns true if the queen would be safe if placed here
      if (isSafe(row, col)) 
      {
        placeQueen (row, col); // This adjusts the board object

        // If our successor, was successful, we were also!
        // Our predecessor, the instance that called us, will
        // see this return value and know that it was successful
        // This is also the ending position because if col == 0 and
        // col == 1 returned true then the original statement will have
        // returned true and everything is unwound and finished.
        if (addQueen (col+1)){
          return true;
        }
        else
        {
          unplaceQueen (row, col); // We guessed wrong, remove queen from board
        }
    }

    // If we got to the bottom and couldn't place a queen, we made a mistake
    // in a prior column. Returning false will signal the prior call
    // to try again.
    return false; 
  }
  
  

Backtracking

Our approach to the Queens Problem illustrates a problem solving technique known as backtracking. Consider the problems this way. At each stage, we are presented with a collection of options. As a result, we can view the problem as a tree. Our job is to find the path from our starting point, the root, to the solution. To do this, we charge forward along a particular path, until we get to the end, or determine that we cannot. Then, we move backward to the prior decision point and try again. After exporing all of the possibilities there, we back up again. And, if that doesn't work, we back up even farther. Basically, we have a tree. When we approach a collection of branches, we will charge down each in turn. We prefer to go deeper to broader, so this is known as a depth first search.

Regardless, using this approach to solve a problem is known as backtracking and is very naturally implemented using recursion. This is because the runtime stack keeps track of all prior decision points along the current path and which options have been explored. It also ensures that we return to each point in the right order -- the opposite of the order in which we visited them. It does this by returning us to the each function, exactly where we left off in the opposite order in which it was called (as is always the case when a function returns).