Return to the lecture notes index

Lecture 11 (May 4, 2003)

The Blob 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 such problems. The BlobProblem is the first such problem. This week's lab will also be such a challenge.

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).

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. If we were to ignore diagonal cells, this pseudocode might look as follows:

  int blobCount(Grid G, int row, int col)
  {
    // Cases that end our search -- cell can't be counted.

    if (!G.infected) return 0;
    if (row < 0) return 0;
    if (row > G.maxRow) return 0;
    if (col < 0) return 0;
    if (col > G.maxCol) return 0;

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

But there is a problem with this approach -- some infected cells might be adjacent to two other infected cells. Since they are reachable from two places, they might get counted twice -- or worse, our recursion might bounce around among the same cells forever. To solve this problem, we need to mark the cells when we count them. Then, when we do the counting, we should treat marked cells as uninfected cells:

  int blobCount(Grid G, int row, int col)
  {
    // Cases that end our search -- cell can't be counted.
    
    if (G.marked) return 0; // If we've been here -- don't come back

    if (!G.infected) return 0;
    if (row < 0) return 0;
    if (row > G.maxRow) return 0;
    if (col < 0) return 0;
    if (col > G.maxCol) return 0;

    G.marked = true; // Mark it, so we will know that we've been here.

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

That should give you the boost you need to get started on lab...

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 saem row, column, or diagnol.

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 discoves 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 (col=0; col<8; col++)
    {
      // Is safe 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 instacne that called us, will
        // see this return value and know that it was successful
        if (addQueen (row, col+1))
          return true;
      }
      else
      {
        removeQueen (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).