Previous | Next | Table of Contents | Index | Program List | Copyright

13.5 Problem Solving: More Case Studies in Recursion

This section presents two more case studies in recursion: Tower of Hanoi and picture processing with recursion.


Case Study: Towers of Hanoi

The Towers of Hanoi problem is a representation of an old Asian puzzle. It involves moving a specified number of disks from one tower (or peg) to another. The disks are arranged on the first tower in order of increasing size, with the largest disk on the bottom. Legend has it that the world will come to an end when the problem is solved for 64 disks. In the version of the problem shown in Figure 13.5 there are five disks (numbered 1 through 5) and three towers or pegs (lettered A, B, C). The goal is to move the five disks from peg A to peg C subject to the following rules:

  1. Only one disk may be moved at a time, and this disk must be the top disk on a peg.
  2. A larger disk can never be placed on top of a smaller disk.

Figure 13.5
Towers of Hanoi

Figure 13.5

PROBLEM SPECIFICATION

Solve the Towers of Hanoi Problem for N disks, where N is a parameter.

ANALYSIS

The solution to the Towers of Hanoi problem consists of a printed list of individual disk moves. We need a recursive procedure that can be used to move any number of disks from one peg to another, using the third peg as an auxiliary.

Data Requirements

Problem Inputs

the number of disks to be moved (N : Integer)

the from peg (FromPeg : 'A'..'C')

the to peg (ToPeg : 'A'..'C')

the auxiliary peg (AuxPeg : 'A'..'C')

Problem Outputs

a list of individual disk moves

DESIGN

The stopping cases of the problem involve moving one disk only (e.g., "move disk 2 from peg A to peg C"). A simpler problem than the original would be to move four disks subject to the conditions above, or three disks, and so on. Therefore, we want to split the original five-disk problem into one or more problems involving fewer disks. Let's consider splitting the original problem into the three problems below.

  1. Move four disks from peg A to peg B.
  2. Move disk 5 from peg A to peg C.
  3. Move four disks from peg B to peg C.
Step 1 moves all disks but the largest to tower B, an auxiliary tower not mentioned in the original problem. Step 2 moves the largest disk to the goal tower, tower C. Then step 3 moves the remaining disks from B to the goal tower where they will be placed on top of the largest disk. Let's assume that we will be able to perform step 1 and step 2 (a stopping case); Figure 13.6 shows the status of the three towers after completing these steps. At this point, it should be clear that we can solve the original five-disk problem if we can complete step 3.

Figure 13.6
Towers of Hanoi after Steps 1 and 2

Figure 13.6

Unfortunately, we still don't know how to perform step 1 or step 3. However, both these steps involve four disks instead of five so they are simpler than the original problem. We should be able to split them into even simpler problems. Step 3 involves moving four disks from tower B to tower C, so we can split it into two three-disk problems and a one-disk problem:

3.1. Move three disks from peg B to peg A.

3.2. Move disk 4 from peg B to peg C.

3.3. Move three disks from peg A to peg C.

Figure 13.7 shows the status of the towers after completing steps 3.1 and 3.2. We now have the two largest disks on peg C. Once we complete step 3.3, all five disks will be on peg C as required.

Figure 13.7
Towers of Hanoi after Steps 1, 2, 3.1, and 3.2

Figure 13.7

By splitting each N-disk problem into two problems involving N - 1 disks and a one-disk problem, we will eventually reach all cases of one disk, which we know how to solve.

INITIAL ALGORITHM

1. IF N is 1, THEN

     2. Move disk 1 from the from peg to the to peg

ELSE

     3. Move N-1 disks from the from peg to the auxiliary peg using the to peg

     4. Move disk N from the from peg to the to peg

     5. Move N-1 disks from the auxiliary peg to the to peg using the from peg

END IF;

If N is 1, a stopping case is reached. If N is greater than 1, the recursive step (following ELSE) splits the original problem into three smaller subproblems, one of which is a stopping case. Each stopping case displays a move instruction. Verify that the recursive step generates the three problems listed after Figure 13.5 when N is 5, the from peg is A, and the to peg is C.

IMPLEMENTATION

The implementation of this algorithm is shown as procedure Tower in Program 13.9. Procedure Tower has four parameters. The procedure call statement

    Tower ( N => 5, FromPeg => 'A',ToPeg => 'C',AuxPeg => 'B');
solves the problem posed earlier of moving five disks from tower A to tower C using B as an auxiliary. An auxiliary procedure MoveDisk is included.

Program 13.9
A Test of Towers of Hanoi

WITH Ada.Text_IO;
WITH Ada.Integer_Text_IO;
PROCEDURE Test_Tower IS
------------------------------------------------------------------------
--| Demonstration of the recursive procedure Tower,
--| which solves a 3-peg Towers of Hanoi problem
--| Author: Michael B. Feldman, The George Washington University 
--| Last Modified: July 1995                                     
------------------------------------------------------------------------

  SUBTYPE Pegs IS Character RANGE 'A'..'C';

  PROCEDURE MoveDisk (FromPeg, ToPeg: Pegs; Which: Natural) IS

  -- Auxiliary procedure implementing a 1-disk move

  BEGIN
    Ada.Text_IO.Put(Item => "Move disk ");
    Ada.Integer_Text_IO.Put(Item => Which, Width => 1);
    Ada.Text_IO.Put(Item => " from peg ");
    Ada.Text_IO.Put(Item => FromPeg);
    Ada.Text_IO.Put(Item => " to peg ");
    Ada.Text_IO.Put(Item => ToPeg);
    Ada.Text_IO.New_Line;
  END MoveDisk;

  PROCEDURE Tower (FromPeg, ToPeg, AuxPeg: Pegs; N: Natural) IS

  -- Moves N disks from FromPeg to ToPeg
  -- using AuxPeg as an auxiliary.   

  BEGIN  -- Tower  

    IF N = 1 THEN
      -- stopping case
      MoveDisk(FromPeg, ToPeg, 1);
    ELSE
      -- recursive step   
      Tower (FromPeg, AuxPeg, ToPeg, N-1);
      MoveDisk(FromPeg, ToPeg, N);
      Tower (AuxPeg, ToPeg, FromPeg, N-1);
    END IF;

  END Tower;

BEGIN -- Test_Tower

  Tower (FromPeg => 'A', ToPeg => 'B', AuxPeg => 'C', N => 5);

END Test_Tower;
Sample Run
Move disk 1 from peg A to peg B
Move disk 2 from peg A to peg C
Move disk 1 from peg B to peg C
Move disk 3 from peg A to peg B
Move disk 1 from peg C to peg A
Move disk 2 from peg C to peg B
Move disk 1 from peg A to peg B
Move disk 4 from peg A to peg C
Move disk 1 from peg B to peg C
Move disk 2 from peg B to peg A
Move disk 1 from peg C to peg A
Move disk 3 from peg B to peg C
Move disk 1 from peg A to peg B
Move disk 2 from peg A to peg C
Move disk 1 from peg B to peg C
Move disk 5 from peg A to peg B
Move disk 1 from peg C to peg A
Move disk 2 from peg C to peg B
Move disk 1 from peg A to peg B
Move disk 3 from peg C to peg A
Move disk 1 from peg B to peg C
Move disk 2 from peg B to peg A
Move disk 1 from peg C to peg A
Move disk 4 from peg C to peg B
Move disk 1 from peg A to peg B
Move disk 2 from peg A to peg C
Move disk 1 from peg B to peg C
Move disk 3 from peg A to peg B
Move disk 1 from peg C to peg A
Move disk 2 from peg C to peg B
Move disk 1 from peg A to peg B
In Program 13.9, the stopping case (move disk 1) is implemented as a call to procedure MoveDisk. Each recursive step consists of two recursive calls to Tower with a call to MoveDisk sandwiched between them. The first recursive call solves the problem of moving N-1 disks to the auxiliary peg. The call to MoveDisk displays a message to move disk N to the to peg.The second recursive call solves the problem of moving the N-1 disks back from the auxiliary peg to the to peg.

TESTING

The procedure call statement

    Tower (FromPeg => 'A',ToPeg => 'C',AuxPeg => 'B',N => 3);
solves a simpler three-disk problem: Move three disks from peg A to peg C. Its execution is traced in Figure 13.8. Verify for yourself that this list of steps does indeed solve the three-disk problem.

Figure 13.8
Trace of Tower ('A', 'B', 'C', 3)

Figure 13.8


Comparison of Iteration and Recursive Procedures

It is interesting to consider that procedure Tower in Program 13.9 will solve the Tower of Hanoi Problem for any number of disks.The three-disk problem results in a total of seven calls to procedure Tower and is solved by seven disk moves. The five-disk problem results in a total of 31 calls to procedure Tower and is solved in 31 moves. In general, the number of moves required to solve the n-disk problem is 2n - 1: We say that it is a O(2n) problem. Because each procedure call requires the allocation and initialization of a local data area in memory, the computer time increases exponentially with the problem size. For this reason, be careful about running this program with a value of N that is larger than 10.

The dramatic increase in processing time for larger towers is a function of this problem, not recursion. In general, however, if there are recursive and iterative solutions to the same problem, the recursive solution requires more time and space because of the extra procedure calls.

Although recursion was not really needed to solve the simpler problems in this chapter, it was extremely useful in formulating an algorithm for Towers of Hanoi. For certain problems, recursion leads naturally to solutions that are much easier to read and understand than their iterative counterparts. In those cases, the benefits gained from increased clarity far outweigh the extra cost in time and memory of running a recursive program.

Many would argue that the recursive programs are esthetically more pleasing. They are indeed often more compact. Once you are accustomed to thinking recursively, the recursive form is somewhat easier to read and understand than the iterative form.

Some programmers like to use recursion as a conceptual tool. Once they have written the recursive form of a function or procedure, they can translate it into an iterative version if run-time efficiency is a major concern.


Case Study: Picture Processing with Recursion

The next problem is a good illustration of the power of recursion. As for the Towers of Hanoi problem, its solution is relatively easy to write recursively; however, the problem would be much more difficult without using recursion. Unlike Towers of Hanoi, which is a cute and popular exercise, picture-processing algorithms have real application.

PROBLEM SPECIFICATION

We have a two-dimensional grid G of cells, each of which may be empty or filled. The filled cells that are connected form a blob. There may be several blobs on the grid. We would like a function that accepts as input the coordinates of a particular cell and returns the size of the blob containing the cell.

There are three blobs in the sample grid in Figure 13.9. If the function parameters represent the X and Y coordinates of a cell, the result of BlobSize(G,3,4) is 5, the result of BlobSize(G,1,2) is 2, the result of BlobSize(G,5,5) is 0, and the result of BlobSize(G,5,1) is 4.

Figure 13.9
Grid with Three Blobs

    1   2   3   4   5
  ---------------------------
1 |   | X |   |   | X |
  ---------------------------
2 | X |   |   | X | X |
  ---------------------------
3 |   |   | X | X |   |
  ---------------------------
4 | X |   |   |   |   |
  ---------------------------
5 | X | X | X |   |   |
  ---------------------------

ANALYSIS

Function BlobSize must test the cell specified by its arguments to see whether it is filled. There are two stopping cases: The cell (X, Y) is not on the grid, or the cell (X, Y) is empty. In either case, the value returned by BlobSize is 0. If the cell is on the grid and filled, the value returned is 1 plus the size of the blobs containing each of its eight neighbors. To avoid counting a filled cell more than once, we will mark it as empty once we have visited it.

Data Requirements

Problem Inputs

the grid (Grid: BlobArray)

the X and Y coordinates of the point being visited (X, Y : Integer)

Problem Outputs

the number of the cells in the blob containing point (X, Y)

DESIGN

Algorithm

1. IF cell (X, Y) is not in the array THEN

     2. Return a count of 0

ELSIF cell (X, Y) is empty, then

     3. Return a count of 0

ELSE

     4. Mark cell (X, Y) as empty

     5. Add 1 and see whether the blob contains any of the eight neighbors of cell (X, Y)

END IF;

Function BlobSize is shown in Program 13.10, assuming the declarations below. The array type BlobArray has element values Filled or Empty. The array G has, as usual, bounds G'Range(1) for the rows and G'Range(2) for the columns.

Program 13.10
Computing the Size of Blobs in a Grid

WITH Ada.Text_IO;
WITH Ada.Integer_Text_IO;
PROCEDURE Test_Blob_Size IS
------------------------------------------------------------------------
--| Illustrates the recursive function BlobSize, which computes
--| size of a "blob" or group of filled cells on a grid.
--| Author: Michael B. Feldman, The George Washington University 
--| Last Modified: July 1995                                     
------------------------------------------------------------------------
  
  TYPE Fill IS (Empty, Filled);
  TYPE BlobArray IS ARRAY (Integer RANGE <>,Integer RANGE <>) OF Fill;

  Test: BlobArray(1..5,1..5);

  PROCEDURE DisplayGrid(Grid: BlobArray) IS
  -- Pre:  Grid is defined
  -- Post: displays Grid on the screen

  BEGIN -- DisplayGrid

    FOR Column IN Grid'Range(2) LOOP -- top border
      Ada.Text_IO.Put(Item => "----");
    END LOOP;
    Ada.Text_IO.Put(Item => '-');
    Ada.Text_IO.New_Line;

    FOR Row IN Grid'Range(1) LOOP
      FOR Column IN Grid'Range(2) LOOP
        IF Grid(Row, Column) = Filled THEN
          Ada.Text_IO.Put(Item => "| X ");
        ELSE
          Ada.Text_IO.Put(Item => "|   ");
        END IF;
      END LOOP;
      Ada.Text_IO.Put(Item => '|');
      Ada.Text_IO.New_Line;

      FOR Column IN Grid'Range(2) LOOP -- after each row
        Ada.Text_IO.Put(Item => "----");
      END LOOP;
      Ada.Text_IO.Put(Item => '-');
      Ada.Text_IO.New_Line;
    END LOOP;

  END DisplayGrid;

  FUNCTION BlobSize(Grid : BlobArray; X, Y: Integer) RETURN Natural IS
  -- Counts the number of filled cells in the blob containing
  -- point (X, Y).
  -- Pre : Blob array Grid and point (X,Y) are defined.
  -- Post: Returns the size of the blob containing point (X, Y).
  --       Resets the status of each cell in the blob to Empty.

    CopyOfGrid : BlobArray(Grid'Range(1),Grid'Range(2)); 
                             -- because functions can't modify
                             -- their parameters, in Ada

    FUNCTION Blob (X, Y : Integer) RETURN Natural IS
      
    -- Inner function that performs the counting operation for BlobSize
    -- Pre : Global array CopyOfGrid and point (X,Y) are defined.
    -- Post: Returns the size of the blob containing point (X, Y).
    --       Resets the status of each cell in the blob to Empty.
      
    Result: Natural;

    BEGIN -- Blob   

      IF (X NOT IN CopyOfGrid'Range(1)) OR
         (Y NOT IN CopyOfGrid'Range(2)) THEN
        Result := 0;                        -- cell not in grid   
      ELSIF CopyOfGrid(X, Y) = Empty THEN
        Result := 0;                        -- cell is empty   
      ELSE -- cell is filled   
        -- recursive step   
        CopyOfGrid(X, Y) := Empty;
        Result := 1 + Blob(X-1, Y+1) + Blob(X, Y+1) +
                      Blob(X+1, Y+1) + Blob(X+1, Y) +
                      Blob(X+1, Y-1) + Blob(X, Y-1) +
                      Blob(X-1, Y-1) + Blob(X-1, Y);
      END IF;

      RETURN Result;

    END Blob;

  BEGIN

    CopyOfGrid := Grid;
    RETURN Blob(X,Y);

  END BlobSize;

BEGIN -- Test_Blob_Size

  Test := ((Empty,  Filled, Empty,  Empty,  Filled),
           (Filled, Empty,  Empty,  Filled, Filled),
           (Empty,  Empty,  Filled, Filled, Empty ),
           (Filled, Empty,  Empty,  Empty,  Empty ),
           (Filled, Filled, Filled, Empty,  Empty ));

  DisplayGrid (Grid => Test);

  Ada.Text_IO.Put(Item => "BlobSize(3,4) is ");
  Ada.Integer_Text_IO.Put(Item => BlobSize(Test,3,4), Width => 1);
  Ada.Text_IO.New_Line;

  Ada.Text_IO.Put(Item => "BlobSize(1,2) is ");
  Ada.Integer_Text_IO.Put(Item => BlobSize(Test,1,2), Width => 1);
  Ada.Text_IO.New_Line;

  Ada.Text_IO.Put(Item => "BlobSize(5,5) is ");
  Ada.Integer_Text_IO.Put(Item => BlobSize(Test,5,5), Width => 1);
  Ada.Text_IO.New_Line;

  Ada.Text_IO.Put(Item => "BlobSize(5,1) is ");
  Ada.Integer_Text_IO.Put(Item => BlobSize(Test,5,1), Width => 1);
  Ada.Text_IO.New_Line;

END Test_Blob_Size;
Sample Run
---------------------------
|   | X |   |   | X |
---------------------------
| X |   |   | X | X |
---------------------------
|   |   | X | X |   |
---------------------------
| X |   |   |   |   |
---------------------------
| X | X | X |   |   |
---------------------------
BlobSize(3,4) is 5
BlobSize(1,2) is 2
BlobSize(5,5) is 0
BlobSize(5,1) is 4

The auxiliary function Blob in Program 13.10, declared within BlobSize, implements the counting algorithm; function BlobSize simply calls the recursive function Blob, passing on its arguments, and returns the count computed by function Blob as its own result. The purpose of the auxiliary function is to protect the actual array from being modified when filled cells are reset to empty by function Blob. We will come back to this point shortly.

If the cell being visited is off the grid or is empty, a value of zero is returned immediately. Otherwise, the recursive step executes, causing function Blob to call itself eight times; each time, a different neighbor of the current cell is visited. The cells are visited in a clockwise manner, starting with the neighbor above and to the left. The function result is defined as the sum of all values returned from these recursive calls plus 1 (for the current cell).

The sequence of operations performed in function Blob is important. The IF statement tests whether the cell (X, Y) is on the grid before testing whether (X, Y) is empty. If the order were reversed, Constraint_Error would be raised whenever (X, Y) was off the grid.

Also, the recursive step resets Grid(X,Y) to Empty before visiting the neighbors of cell (X, Y). If this were not done first, cell (X, Y) would be counted more than once because it is a neighbor of all its neighbors. A worse problem is that the recursion would not terminate. When each neighbor of the current cell is visited, Blob is called again with the coordinates of the current cell as arguments. If the current cell is Empty, an immediate return occurs. If the current cell is still Filled, the recursive step would be executed erroneously. Eventually the program will run out of time or memory space; the latter is signaled in Ada by the raising of Storage_Error.

A side effect of the execution of function Blob is that all cells that are part of the blob being processed are reset to Empty. This is the reason for using two functions. Because the array is passed as a parameter to function BlobSize, a local copy CopyOfGrid is saved when BlobSize is first called. Only this local array is changed by function Blob, not the actual array. If the counting operation were performed in function BlobSize instead of in function Blob, eight copies of this array would be made each time the recursive step was executed. Using the function Blob and the array that is global to all recursive calls of Blob (but still local to BlobSize) prevents the unnecessary copying.


Exercises for Section 13.5

Self-Check

  1. How many moves are needed to solve the six-disk problem?
  2. Estimate the size of the largest Towers problem that could be solved in less than one day. Assume that one disk can be moved each second.
  3. Estimate the size of the largest Towers problem that could be solved in less than one year. Assume that one disk can be moved each second.
  4. Trace the execution of function BlobSize for the coordinate pairs (1, 1) and (1, 2) in the sample grid.
  5. Is the order of the two tests performed in function BlobSize critical? What happens if we reverse them or combine them into a single condition?

Programming

  1. Modify TestTower to read in a data value for N (the number of disks).


Previous | Next | Table of Contents | Index | Program List | Copyright

Copyright © 1996 by Addison-Wesley Publishing Company, Inc.