Module 9: Recursion, Part II


Supplemental material


The Tower of Hanoi

In the Tower of Hanoi puzzle:

In-Class Exercise 1: Work out the moves for the ToH problem with three disks. You can either cut out pieces of paper or find an interactive applet on the web. Write down the moves on paper as in "Step 1: Move disk 0 from tower 0 to tower 1 ..."

We will solve the problem using (of course) recursion:

Here's the program:
public class TowerOfHanoi {

    public static void main (String[] argv)
    {
        // A 3-disk puzzle:
        System.out.println ("3-Disk solution: ");
	solveHanoi (2, 0, 1);

        // A 4-disk puzzle:
        System.out.println ("4-Disk solution: ");
	solveHanoi (3, 0, 1);
    }


    static void solveHanoi (int n, int i, int j)
    {
	// Bottom-out.
	if (n == 0) {
            // The smallest disk.
	    move (0, i, j);
	    return;
	}

	int k = other (i, j);
	solveHanoi (n-1, i, k);    // Step 1.
	move (n, i, j);            // Step 2.
	solveHanoi (n-1, k, j);    // Step 3.
    }


    static void move (int n, int i, int j)
    {
        // For now, we'll merely print out the move.
        System.out.println ("⇒ Move disk# " + n + " from tower " + i + " to tower " + j);
    }


    static int other (int i, int j)
    {
        // Given two towers, return the third.
        if ( (i == 0) && (j == 1) ) {
            return 2;
        }
        else if ( (i == 1) && (j == 0) ) {
            return 2;
        }
        else if ( (i == 1) && (j == 2) ) {
            return 0;
        }
        else if ( (i == 2) && (j == 1) ) {
            return 0;
        }
        else if ( (i == 0) && (j == 2) ) {
            return 1;
        }
        else if ( (i == 2) && (j == 0) ) {
            return 1;
        }

        // We shouldn't reach here.
        return -1;
    }

}
Note:

In-Class Exercise 2: Compile and execute the above program to verify that your "moves" from Exercise 1 were correct.

In-Class Exercise 3: The other() method now has above 10-12 lines of code. Can you rewrite it to have only one line, a very short line, of code? Hint: think of a mathematical property you could exploit.

Next, suppose we want to maintain the state of each tower:

Here's the program:

import java.util.*;

public class TowerOfHanoi2 {

    // towers[0], towers[1] and towers[2] are the three stacks.
    static Stack<Integer>[] towers;


    public static void main (String[] argv)
    {
        // A 3-disk puzzle:
        System.out.println ("3-Disk solution: ");
	solveHanoi (2, 0, 1);

        // A 4-disk puzzle:
        System.out.println ("4-Disk solution: ");
	solveHanoi (3, 0, 1);
    }


    static void solveHanoi (int n, int i, int j)
    {
        // Create the three stacks.
	towers = new Stack [3];
	for (int k=0; k<3; k++) {
	    towers[k] = new Stack<Integer>();
	}

        // Put disks 0,...,n on stack 0.
	for (int k=n; k>=0; k--) {
	    towers[0].add (k);
	}

        // Print the initial stack.
	printTowers ();

        // Now solve recursively. Note: this is the method below.
	solveHanoiRecursive (n, i, j);
    }
    

    static void solveHanoiRecursive (int n, int i, int j)
    {
	// Bottom-out.
	if (n == 0) {
	    move (0, i, j);
	    return;
	}
	int k = other (i, j);
	solveHanoiRecursive (n-1, i, k);   // Step 1.
	move (n, i, j);                    // Step 2.
	solveHanoiRecursive (n-1, k, j);   // Step 3.
    }


    static void move (int n, int i, int j)
    {
        // Pull out the top disk on stack i.
	int topVal = towers[i].pop();
        // Put it on stack j.
	towers[j].push (topVal);
        // Print status.
        System.out.println ("Towers after moving " + n + " from tower " + i + " to tower " + j);
	printTowers ();
    }


    static int other (int i, int j)
    {
        // ... 
    }


    static void printTowers ()
    {
	for (int i=0; i<towers.length; i++) {
	    System.out.print ("Tower " + i + ": ");
	    if ( ! towers[i].isEmpty() ) {
		for (Integer I: towers[i]) {
		    System.out.print (" " + I);
		}
	    }
	    System.out.println ();
	}
    }

}
Note:

In-Class Exercise 4: Modify TowerOfHanoi2.java to count the total number of moves. How many moves are needed for a puzzle with N disks? Is this a polynomial or exponential algorithm?

An application: disk backup schedules

In-Class Exercise 5: Modify TowerOfHanoi3.java to print out the disk-backup schedule for disks A, ..., E (five disks).


Recursion: a review

First, let's review a simple example from Module 4:

In-Class Exercise 6: Draw the stack picture at each step (call) for the first TowerOfHanoi example, using three disks. How many stack snapshots are in the drawing?

Let's now review another example from Module 4:

Two types of recursion:

In-Class Exercise 7: Which kind of recursion is TowerOfHanoi.java?


Maze construction and traversal: a (long) example of recursion with backtracking

We will now examine a maze application in detail to illustrate recursion with backtracking:

To start with, let's examine the methods in the class Maze:

  • The constructor: we create an instance by specifying the number of rows and columns:
         Maze maze = new Maze (5, 5);
         
    Our examples will use square mazes.

  • display(): call this method to display the maze:
         Maze maze = new Maze (5, 5);
    
         // Bring up the GUI:
         maze.display ();              
         

  • breakWall():
    • Initially, the maze consists of complete cells. To actually create a walkable maze, we will "break walls" between neighboring cells.
    • For example, we can break the wall between cells (3,4) and (2,4) as follows:
              Maze maze = new Maze (5, 5);
      
              // Note the use of the Coord class:
              Coord c1 = new Coord (3,4);
              Coord c2 = new Coord (2,4);
              maze.breakWall (c1, c2);
      
              maze.display ();              
            
      Or, alternatively, with shorter code:
              Maze maze = new Maze (5, 5);
      
              // Create an instance directly in the method argument list:
              maze.breakWall (new Coord(3,4),  new Coord (2,4));
      
              maze.display ();              
            
    • The Coord class looks like this:
      public class Coord {
      
          public int row=-1, col=-1;
      
          public Coord (int r, int c)
          {
              row = r;
              col = c;
          }
          
          public String toString ()
          {
              return "[" + row + "," + col + "]";
          }
          
      }
            
    • Thus, to access the stored coordinates:
            Coord c = new Coord (3,4);
            System.out.println ("Row=" + c.row + " Col=" + c.col);
            // Or, to print both using toString():
            System.out.println (c);
            

  • A number of methods (in the class Maze) allow us to mark and un-mark cells as visited:
    • markVisited (Coord c): mark cell c as visited.
    • markUnVisited (Coord c): mark cell c as not visited.
    • markAllUnvisited(): mark all cells as unvisited.
    • isVisited (Coord c): see whether cell c has been visited.

  • A number of methods related to fetching a list (array) of a cell's neighbors:
    • Coord[] getUnvisitedClosedNeighbors (Coord c): get cell c's neighbors that are closed off from c (there's a wall between) and haven't been visited yet.
    • Coord[] getClosedNeighbors (Coord c): get neighbors of c that share a wall (unbroken) with c whether visited or not.
    • Coord[] getUnvisitedOpenNeighbors (Coord c): get those neighbors of c for which we can walk through to the neighbor (no wall) and which haven't been visited.
    Each of these methods return an array of Coord instances.

  • A few additional useful methods:
    • copy(): make a full copy of the maze, as in:
             Maze m = new Maze (5,5);
             // ... do stuff: break walls etc ...
             Maze m2 = m.copy();
             // Now m2 is a copy of m. Making changes to m won't affect m2.
             
    • setSolutionPath (LinkedList<Coord> solutionPath): we will call this method once we have constructed a solution.

In-Class Exercise 8: Download Maze.java (which you don't have to read), Coord.java, and modify MazeByHand.java to draw a maze path from (3,4) to (1,1). Do this by breaking walls. You can type in each wall-break separately using calls to breakWall(). Then, when that's working, create a solution path and display it.

We'll now write our first attempt:

  • We will try to generate a maze path of a given path length.
  • The key idea:
         Algorithm: recursiveGenerate (Coord c)
         1.   if path-length has been reached
         2.     return
              // Otherwise ...
         3.   c' = Pick a random neighbor of cell c
         4.   recursiveGenerate (c')
         
Here's the program:
public class MazeMaker {

    // These variables will be used across methods:
    static int desiredPathLength;    
    static Maze maze;


    public static void main (String[] argv)
    {
        generateMaze (5, 5);
    }


    public static void generateMaze (int numRows, int numCols)
    {
        maze = new Maze (numRows, numCols);

        // We want to generate a path of this length:
	desiredPathLength = numRows * numCols;

	// Initially, we'll start with the top left cell and mark that as visited.
        Coord start = new Coord (0, 0);
	maze.markVisited (start);

        // Generate the maze path recursively.
	boolean found = recursiveGenerate (start, 1);

        if (! found) {
            System.out.println ("Could not create the whole maze");
        }
        maze.display();
    }


    static boolean recursiveGenerate (Coord c, int pathLength)
    {
        // Bottom out condition 1:
	if (pathLength == desiredPathLength) {
	    // Done. We've create a maze path of the desired length.
	    return true;
	}

        // Bottom out condition 2: see if there's a neighbor to visit.
	Coord[] validNeighbors = maze.getUnvisitedClosedNeighbors (c);
	if ((validNeighbors == null) || (validNeighbors.length == 0)) {
            // There's no neighbor whose wall we can break. So quit trying.
	    return false;
	}

        // Otherwise, pick a random neighbor and proceed.
        int i = UniformRandom.uniform (0, validNeighbors.length-1);

        // Break the wall and mark the neighbor as visited.
        maze.breakWall (c, validNeighbors[i]);
        maze.markVisited (validNeighbors[i]);
            
        // Recurse.
        return recursiveGenerate (validNeighbors[i], pathLength+1);
    }

}

In-Class Exercise 9: Download Maze.java and and MazeMaker.java. You will also need UniformRandom.java. Then compile and execute. Why doesn't it produce a path of the desired length?

Next attempt:

  • Simple tail recursion can get stuck.
         ⇒ We need a way to "un-do" paths and try different neighbors.

  • We will pick a neighbor to explore recursively
         ⇒ If that doesn't work out, we'll try another.
Here's the program:
public class MazeMaker2 {

    static int desiredPathLength;
    static Maze maze;


    public static void main (String[] argv)
    {
        generateMaze (5, 5);
    }


    public static void generateMaze (int numRows, int numCols)
    {
        maze = new Maze (numRows, numCols);

	desiredPathLength = numRows * numCols;

	// Initially, we'll start with the top left cell.
        Coord start = new Coord (0, 0);
	maze.markVisited (start);

        // Generate the maze path recursively.
	boolean found = recursiveGenerate (start, 1);

        if (! found) {
            System.out.println ("Could not create the whole maze");
        }
        maze.display();
    }


    static boolean recursiveGenerate (Coord c, int pathLength)
    {
        // Bottom out condition 1:
	if (pathLength == desiredPathLength) {
	    return true;
	}

        // Bottom out condition 1: see if we're stuck.
	Coord[] validNeighbors = maze.getUnvisitedClosedNeighbors (c);
	if ((validNeighbors == null) || (validNeighbors.length == 0)) {
	    return false;
	}


	// Otherwise, we have some neighbors to explore.
	// Permute the directions randomly.
	permute (validNeighbors);

	for (int i=0; i < validNeighbors.length; i++) {

            // Try neighbor i.
            maze.breakWall (c, validNeighbors[i]);
            maze.markVisited (validNeighbors[i]);
            
	    boolean ok = recursiveGenerate (validNeighbors[i], pathLength+1);
	    if (ok) {
                // If neighbor i worked out, great.
		return true;
	    }

	    // Otherwise, undo assignment to i.
	    maze.fixWall (c, validNeighbors[i]);
	    maze.markUnvisited (validNeighbors[i]);

	} // end-for

	// Couldn't make it work.
	return false;
    }


    static void permute (Coord[] coords)
    {
	for (int i=0; i < coords.length; i++) {
	    // Find a random element to place into i-th place.
	    int k = (int) UniformRandom.uniform (i, coords.length-1);
	    Coord temp = coords[i];
	    coords[i] = coords[k];
	    coords[k] = temp;
	}
    }

}
Note:
  • The key idea is to try as many neighbors as needed:
         Algorithm: recursiveGenerate (Coord c)
         1.   if path-length has been reached
         2.     return true
         3.   endif
              // Otherwise ...
         4.   for each neighbor c' of c
         5.     found = recursiveGenerate (c')
         6.     if found
         7.       return true
         8.     endif
         9.   endfor
              // We reach here only if every neighbor did not work out.
         10.  return false
         

  • Note how we un-do a broken wall:
    	for (int i=0; i < validNeighbors.length; i++) {
                // Try neighbor i.
                maze.breakWall (c, validNeighbors[i]);
                maze.markVisited (validNeighbors[i]);
                
                // ... recursion here ...
    
    	    // To un-do: repair the wall and mark as UNvisited.
    	    maze.fixWall (c, validNeighbors[i]);
    	    maze.markUnvisited (validNeighbors[i]);
    	} // end-for
        

In-Class Exercise 10: Download MazeMaker2.java, compile and execute. Does it produce a complete path covering all cells? What do you notice about the execution time? Try making this a 6x6 maze with desired path-length 36.

A maze with choices:

  • The above maze path has no choices
         ⇒ There's only one way from the topleft to the end of the path.

  • We will build upon this to create a maze with choices
         ⇒ After generating the long path, we'll break some random walls.
Here's the program:

public class MazeMaker3 {

    // ... variables ...

    public static void main (String[] argv)
    {
        generateMaze (5, 5);
    }


    public static void generateMaze (int numRows, int numCols)
    {
        // ... create and generate single-path maze ...

        // Break a few more walls, randomly.
        breakRandomWalls (maze, 10);

        maze.display();
    }


    static boolean recursiveGenerate (Coord c, int pathLength)
    {
        // ... same as before ...
    }

 
    static void breakRandomWalls (Maze maze, int numWalls)
    {
	for (int k=0; k < numWalls; k++) {
            // Create random coordinates, i.e., identify a random cell.
	    int x = UniformRandom.uniform (0, maze.numRows-1);
	    int y = UniformRandom.uniform (0, maze.numCols-1);
	    Coord c = new Coord (x,y);

            // Get its neighbors that are separated by a wall.
	    Coord[] validNeighbors = maze.getClosedNeighbors (c);
	    if (validNeighbors != null) {
                // Pick one and break the wall.
                int m = UniformRandom.uniform (0, validNeighbors.length-1);
		maze.breakWall (c, validNeighbors[m]);
	    }
	}
    }

}

In-Class Exercise 11: Download MazeMaker3.java, compile and execute. Solve by hand the maze generated: start at the top-left corner and find a path to the bottom-right corner.

Let's turn our attention to solving the maze:

  • We'll first try a simple idea:
         Algorithm: recursivelyFindPath (Coord c)
         1.   if c is the end
         2.     return true
         3.   endif
         4.   // Otherwise search further ...
         5.   c' = pick a random "open" neighbor 
         6.   if c' is null
                // Stuck: couldn't find a path.
         7.     return false
         8.   endif
         9.   return recursivelyFindPath (c')
         
Here's the program:
import java.util.*;

public class MazeMaker4 {

    static int desiredPathLength;
    static Maze maze;


    public static void main (String[] argv)
    {
        Maze maze = generateMaze (5, 5);
	if (maze != null) {
            // We will seek a path from the top-left to the bottom-right corner.
	    Coord start = new Coord (0,0);
	    Coord end = new Coord (4,4);
	    solveMaze (maze, start, end);
	    maze.display ();
	}
	else {
	    System.out.println ("Maze creation did not work");
	}
    }


    // A path is a list of cells, i.e., a list of Coord instances.
    static LinkedList<Coord> solutionPath;


    static void solveMaze (Maze maze, Coord start, Coord end)
    {
        // We'll mark visited cells as we go along our path. Initially:
	maze.markAllUnvisited ();

        // Mark the start cell as visited.
	maze.markVisited (start);

        // Create the list.
	solutionPath = new LinkedList<Coord> ();

        // Recursively find the path and fill in coord's into the list.
	recursivelyFindPath (maze, start, end);

        // Pass the path into the GUI.
	maze.setSolutionPath (solutionPath);
    }


    static boolean recursivelyFindPath (Maze maze, Coord c, Coord end)
    {
        // First add the current cell into the path.
	solutionPath.add (c);

        // If we've reached the end, we're done.
	if ( (c.row == end.row) && (c.col == end.col) ) {
	    return true;
	}

        // Otherwise, let's find a neighbor to explore.
	Coord[] validNeighbors = maze.getUnvisitedOpenNeighbors (c);

	if (validNeighbors == null) {
            // If there aren't any, we're done, but couldn't find a path.
	    return false;
	}

	// Take the first one and explore from there.
	maze.markVisited (validNeighbors[0]);

	return recursivelyFindPath (maze, validNeighbors[0], end);
    }


    // ...  Maze generation code ... same as before ...

}

In-Class Exercise 12: Download MazeMaker4.java, compile and execute. Does it find a solution? Why not?

A solution that works:

  • When exploring a neighbor, we need a way to un-do (backtrack) if it doesn't lead to a solution.

  • The idea is to try all possible neighbors, as many as needed:
         Algorithm: recursivelyFindPath (Coord c)
         1.   if c is the end
         2.     return true
         3.   endif
         4.   // Otherwise search further ...
         5.   for each "open" neighbor c'
         6.      found = recursivelyFindPath (c')
         7.      if found
         8.        add c' to path
         9.        return true
         10.     endif
         10.  endfor
         11.  return false
         
Here's the program:
import java.util.*;

public class MazeMaker5 {

    // ... variables ... same as before (for generation) ...


    public static void main (String[] argv)
    {
        // ... same ...
    }


    // A path is a list of cells, i.e., a list of Coord instances.
    static LinkedList<Coord> solutionPath;


    static void solveMaze (Maze maze, Coord start, Coord end)
    {
        // We'll mark visited cells as we go along our path. Initially:
	maze.markAllUnvisited ();

        // Mark the start cell as visited.
	maze.markVisited (start);

        // Create the list.
	solutionPath = new LinkedList<Coord> ();

        // Recursively find the path and fill in coord's into the list.
	recursivelyFindPath (maze, start, end);

        // The start node gets added last. Why?
        solutionPath.addFirst (start);

        // Pass the path into the GUI.
	maze.setSolutionPath (solutionPath);
    }


    static boolean recursivelyFindPath (Maze maze, Coord c, Coord end)
    {
        // If we've reached the end, we're done.
	if ( (c.row == end.row) && (c.col == end.col) ) {
	    return true;
	}

        // Otherwise, let's find a neighbor to explore.
	Coord[] validNeighbors = maze.getUnvisitedOpenNeighbors (c);
	if (validNeighbors == null) {
            // If we couldn't find any neighbors to explore, we're stuck.
	    return false;
	}

        // Try each neighbor, as many as needed.

        for (int i=0; i < validNeighbors.length; i++) {

            // Try neighbor i.
	    maze.markVisited (validNeighbors[i]);
	    boolean found = recursivelyFindPath (maze, validNeighbors[i], end);

	    if (found) {
                // Notice that we add this to the front of the list, and only
                // after the solution has been found up to here.
		solutionPath.addFirst (validNeighbors[i]);
		return true;
	    }
            
            // If neighbor i didn't work out, un-do the visit and continue.
	    maze.markUnvisited (validNeighbors[i]);
	}

        // If we reached here, then none of the neighbors worked out.
	return false;
    }

}

In-Class Exercise 13: Download MazeMaker5.java, compile and execute. Does it find a solution? Then add code to answer these questions:

  • How many dead-end's are reached in trying to find a path? Add code to count such dead-end's.
  • What does a trace look like? Add copious println's to see how the recursion works. Use the indented printing we studied in Module 4.


The N-Queens problem

In this problem:

  • We are given an N x N chessboard and M queens (where M <= N).

  • The goal is to place the queens on the board so that no queen "attacks" another.

  • For example (N=8):

  • Recall that a queen can attack any square in its row, column or diagonal, e.g.

We will of course solve this recursively:

  • This is another example of recursion with backtracking.

  • Key ideas:
    • We'll proceed columm-by-column (left to right).
    • When solving for the current column, try to place a queen on each possible row, and solve the sub-problem (starting with the next column) recursively.

  • Pseudocode:
        Algorithm: solve (n, c)
        Input: n = the number of queens to assign, c = the column to start with
     
                // ... Bottom-out conditions ... (we'll do this later) 
    
           1.   for each row r
           2.     if [r,c] is a non-attacked square
           3.        place queen n on cell [r,c]
           4.        found = solve (n-1, c+1)               // Assign remaining n-1 recursively
           5.        if (found)
           6.          return true
           7.        endif
           8.     endif
           9.   endfor
                // We reach here if none of the rows in column c worked out.
           10.  return false
        

  • Thus, we try every row for a queen and try to solve the sub-problem with fewer queens.
         ⇒ If if that gets solved, we can place the queen there and recurse back up

  • The bottom-out conditions:
    • Check if there are no more queens to assign
           ⇒ Problem gets solved.
    • Check if there are no more columns to try
           ⇒ No solution exists.

  • We will build a class called ChessBoard with methods that hide most of the messy board-manipulation details:
    • addQueen (row,col): add a queen in square [row,col].
    • removeQueen (row,col): remove the queen in square [row,col].
    • boolean isForbidden (row,col): see if [row,col] is an attacked square.
    • display(): to bring up a GUI with the board displayed.
Here's the program:
import java.awt.*;
import java.util.*;

public class NQueens {

    static ChessBoard board;


    public static void main (String[] argv)
    {
        solveQueens (8, 8);
    }


    static void solveQueens (int numQueens, int size)
    {
        board = new ChessBoard (size);

        boolean solutionExists = solve (numQueens, 0);

        if (! solutionExists) {
            System.out.println ("No solution for " + numQueens + " on a " + size + " x " + size + " board");
            return;
        }

        System.out.println ("Solution found for " + numQueens + " on a " + size + " x " + size + " board");

        System.out.println (board);
        board.display();
    }
    

    static boolean solve (int numQueens, int whichCol)
    {
        // Bottom-out condition 1:
        if (numQueens == 0) {
            // None to assign - done.
            return true;
        }
        

        // Bottom-out condition 2:
        if (whichCol >= board.size()) {
            // No columns left to try - done.
            return false;
        }
        
        // Try every un-forbidden spot in each row.
        for (int row=0; row < board.size(); row++) {
            if ( ! board.isForbidden(row,whichCol) ) {

                // Try this location.
                board.addQueen (row, whichCol);
                
                boolean solutionExists = solve (numQueens-1, whichCol+1);

                if (solutionExists) {
                    return true;
                }

                // Else, un-do
                board.removeQueen (row, whichCol);
            }

        }
        
        // Couldn't find a solution.
        return false;
    }

}

In-Class Exercise 14: Can 3 queens be placed on a 3x3 board? Can 3 queens be placed on a 4x4 board? Download and modify NQueens.java and ChessBoard.java to find out. You will also need ImageTool.java and queen.jpg.


© 2006-2020, Rahul Simha & James Taylor (revised 2020)