Return to the Lecture Notes Index

15-111 Lecture 14 (February 23, 2007)

The Blobs 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 BlobProblem is the first such problem. Next week's lab will also be such a challenge. We will use recursive thinking to solve a maze.

Assume we have a two-demensional grid of cells. Each cell may be empty or filled. Any group of cells that are connected (horizontally, vertically, or diagonally) constitutes a "blob." The goal is to count the number of cells in a blob, given the location of the blob. You might imagine that the cells have been created by scanning a microscope slide of a bacterial culture, and that the purpose is the estimate the degree of infection. (This problem comes from McCraken's classic textbook -- see the assignment handout for the full citation). This has also been refered to as a forest problem, what part of the forest was burned by the fire. Kesden also gave a third example but if you missed class then you will have to ask a friend what the example was. I am not putting it here.

So, how should you go about solving this problem? The basic idea is this. The recursive method is invoked to determine the number of cells in its blob that are infected. If the cell is not infected or is not in the grid, it should return 0 -- these are the cases that will break the recursion and allow the count to unwind. Otherwise, it should return the sum of 1 for itself, plus whatever is counted by a recursive search of the cells around it. To accomplish this, it should call itself on those cells. We also have to account for the fact that once we count a cell it should have a marker that is changed so that we don't double count cells.
REMEMBER THIS IS PSEUDO CODE AND NOT ACTUAL CODE

  int blobCount( int row, int col)
  {


    // Cases that end our search -- cell can't be counted.

    if ( (row<0)|| (col<0) || (row >= marked.length) || (col >= marked.length) )
	 return 0;

    if (counted[row][col])
        return 0;
    else
        counted[row][col] = true;

    if( !marked[row][col])
        return 0;

    return blobCount (row-1, col-1) + blobCount (row-1, col) +  
      + blobCount (row-1, col+1) + 1 + blobCount (row , col+1)  
      + blobCount (row, col-1) + blobCount (row+1, col+1) 
      + blobCount (row+1, col) + blobCount( row+1, col-1);
  }
  
  

The Eight Queens Problem

Backtracking is another 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).