Recitation for Week 3: Solving a Maze

In this recitation, you will experiment with the use of a Stack and a Queue for searching a maze. During this recitation, you will

  • discuss the implementation of the doubly-linked list in the class Deque,
  • discuss the implementation of a Stack and a Queue class using Deque,
  • discuss the implementation of the Maze and Position classes,
  • discuss the solution of a maze using the SolveMazeStackclass,
  • write a new class named SolveMazeQueue that uses a queue instead of a stack,

The Deque Class

First, we will need our usual Node class.

Node.java
class Node<T> {
   T item;
   Node<T> next;
   Node<T> previous;
 
   Node() {
      item = null;
      next = null;
      previous = null;
   }
 
   Node(T newItem, Node<T> previousArg, Node<T> nextArg) {
      item = newItem;
      previous = previousArg;
      next = nextArg;
   }
 
   public String toString() {
      return item.toString();
   }
 
}

Notice, no member variables or methods are declared publici or private or protected. This means they can be used by any classes in this package.

Now for the Deque.

Deque.java
public class Deque<T> {
 
   Node<T> head;
   int size;
 
   public Deque() {
      head = new Node<T>();
      head.next = head;
      head.previous = head;
      size = 0;
   }
 
   public boolean isEmpty() {
      return size == 0;
   }
 
   public int size() {
      return size;
   }
 
   public void addFirst(T newItem) {
      Node<T> newNode = new Node<T>(newItem, head, head.next);
      head.next.previous = newNode;
      head.next = newNode;
      size++;
    }
 
   public void addLast(T newItem) {
      Node<T> newNode = new Node<T>(newItem, head.previous, head);
      head.previous.next = newNode;
      head.previous = newNode;
      size++;
    }
 
   public T peekFirst() throws DequeException {
      if (isEmpty())
         throw new DequeException("DequeException: peekFirst() attempt on empty deque");
      else
         return head.next.item;
   }
 
   public T removeFirst() throws DequeException {
      if (isEmpty())
         throw new DequeException("DequeException: removeFirst() attempt on empty deque");
      else {
         T item = peekFirst();
         head.next = head.next.next;
         head.next.previous = head;
         return item;
      }
   }
 
   public T peekLast() throws DequeException {
      if (isEmpty())
         throw new DequeException("DequeException: peekLast() attempt on empty deque");
      else 
         return head.previous.item;
   }
 
   public T removeLast() throws DequeException {
      if (isEmpty())
         throw new DequeException("DequeException: removeLast() attempt on empty deque");
      else {
         T item = peekLast();
         head.previous = head.previous.previous;
         head.previous.next = head;
         return item;
      }
   }
 
    public String toString() {
       String result = "";
       for (Node<T> current = head.next; current != head; current = current.next)
          result += current.item + " ";
       return result;
    }
 
    // Test
    public static void main(String[] args){
 
       Deque<String> ds = new Deque<String>();
       ds.addFirst("front1");
       ds.addFirst("front2");
       ds.addLast("last1");
       System.out.println(ds);
       String a = ds.removeLast();
       System.out.println("removeLast = " + a);
       System.out.println(ds);
       a = ds.removeFirst();
       System.out.println("removeFirst = " + a);
       System.out.println(ds);
 
       Deque<Integer> di = new Deque<Integer>();
       di.addLast(10);
       di.addFirst(20);
       System.out.println(di);
    }       
}

Looks like we need a DequeException class.

DequeException.java
public class DequeException extends java.lang.RuntimeException {
 
    public DequeException(String s) {
        super(s);
    }
}

Download these, compile them, and run the Deque class to test it.

Stack

Stack.java
public class Stack<T> {
 
   Deque<T> deque;
 
   public Stack() {
      deque = new Deque<T>();
   }
 
   public boolean isEmpty() {
      return deque.isEmpty();
   }
 
   public void push(T newItem) {
      deque.addFirst(newItem);
    }
 
    public T pop() throws StackException {
       try {
          T item = deque.removeFirst();
          return item;
       } 
       catch (DequeException e) {
          throw new StackException("StackException: pop() attempt on empty stack");
       }
    }
 
    public T peek() throws StackException {
       try {
          T item = deque.peekFirst();
          return item;
       } 
       catch (DequeException e) {
          throw new StackException("StackException: peek() attempt on empty stack");
       }
    }
 
    public int size() {
       return deque.size();
    }
 
    public String toString() {
       String result = "Stack: ";
       result += deque;
       return result;
    }
 
    // Test
    public static void main(String[] args) {
       Stack<String> ss = new Stack<String>();
       ss.push("one");
       ss.push("two");
       System.out.println(ss);
       String a = ss.pop();
       System.out.println("popped " + a);
       System.out.println(ss);
 
       Stack<Double> sd = new Stack<Double>();
       sd.push(42.33);
       sd.push(-132.2333);
       System.out.println(sd);
       double b = sd.pop();
       System.out.println("popped " + b);
       System.out.println(sd);
    }
 
}

And the accompanying exception class

StackException.java
public class StackException extends java.lang.RuntimeException {
 
    public StackException(String s) {
        super(s);
    }
}

Download and test the Stack.

Queue

Now the Queue implementation and its exception class.

Queue.java
public class Queue<T> {
 
   Deque<T> deque;
 
   public Queue() {
      deque = new Deque<T>();
   }
 
   public boolean isEmpty() {
      return deque.isEmpty();
   }
 
   public void enqueue(T newItem) {
      deque.addLast(newItem);
    }
 
    public T dequeue() throws QueueException {
       try {
          T item = deque.removeFirst();
          return item;
       } 
       catch (DequeException e) {
          throw new QueueException("QueueException: dequeue() attempt on empty stack");
       }
    }
 
    public T peek() throws QueueException {
       try {
          T item = deque.peekFirst();
          return item;
       } 
       catch (DequeException e) {
          throw new QueueException("QueueException: peek() attempt on empty stack");
       }
    }
 
    public int size() {
       return deque.size();
    }
 
    public String toString() {
       String result = "Queue: ";
       result += deque;
       return result;
    }
 
    // Test
    public static void main(String[] args) {
       Queue<String> ss = new Queue<String>();
       ss.enqueue("one");
       ss.enqueue("two");
       System.out.println(ss);
       String a = ss.dequeue();
       System.out.println("dequeueed " + a);
       System.out.println(ss);
 
       Queue<Double> sd = new Queue<Double>();
       sd.enqueue(42.33);
       sd.enqueue(-132.2333);
       System.out.println(sd);
       double b = sd.dequeue();
       System.out.println("dequeueed " + b);
       System.out.println(sd);
    }
 
}
QueueException.java
public class QueueException extends java.lang.RuntimeException {
 
    public QueueException(String s) {
        super(s);
    }
}

Maze and Position

Finally, we can talk about how to represent a maze, and a position in a maze. Here is an example of the kind of mazes we will be searching.

maze1
12 10
##########
#      G #
#     #  #
#     #  #
#     #  #
#     #  #
#     #  #
#   ###  #
#        #
#    S   #
#        #
##########

S is the starting position, and G is the goal. Walls are marked by #.

The maze can be stored as a two-dimensional character array.

A Position is just a row and column index.

The Maze class constructor will read a maze like this from a text file whose name is given as a command line argument.

Position.java
public class Position {
 
   protected int row;
   protected int column;
 
   public Position(int rowArg, int columnArg) {
      row = rowArg;
      column = columnArg;
   }
 
   public String toString() {
      return "(" + row + "," + column + ")";
   }
 
   public int getRow() {
      return row;
   }
   public int getColumn() {
      return column;
   }
 
   public Position up() {
      return new Position(row-1,column);
   }
   public Position down() {
      return new Position(row+1,column);
   }
 
   public Position right() {
      return new Position(row,column+1);
   }
   public Position left() {
      return new Position(row,column-1);
   }
 
   // Test it.
   public static void main(String[] args) {
      Position a = new Position(0,0);
      Position b = new Position(10,20);
 
      System.out.println(a + " " + b);
   }
}
Maze.java
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.StringTokenizer;
 
public class Maze {
   protected char [][] maze;
   protected int nRows;
   protected int nCols;
   protected String filename;
   final char WALL = '#';
   final char GOAL = 'G';
   final char START = 'S';
   final char MARK = '*';
   final char OPEN = ' ';
 
   Maze(String filenameArg) {
      filename = filenameArg;
      BufferedReader input;
      String line;
      StringTokenizer tokenizer;
 
      nRows = 0;
      try {
         /* File is expected to look like this.
            Here is maze with 12 rows and 10 columns.
 
            12 10
            ##########
            #  G     #
            #     #  #
            #     #  #
            #   ###  #
            #        #
            #        #
            #    S   #
            #        #
            ##########
         */
 
         int currentRow = 0;
         input = new BufferedReader(new FileReader(filename));
         while ((line = input.readLine()) != null) {
            if (nRows == 0) {
               // Assume first line of file is  nRows nCols
               tokenizer = new StringTokenizer(line);
               nRows = Integer.parseInt(tokenizer.nextToken());
               nCols = Integer.parseInt(tokenizer.nextToken());
               maze = new char[nRows][nCols];
            } else {
               // Other rows look like
               // #   G     #
               for (int c = 0; c < nCols; c++)
                  maze[currentRow][c] = line.charAt(c);
               currentRow ++;
            }
         }
      }
      catch (IOException e) {
         e.printStackTrace();
         System.exit(1);
      }
   }
 
   public void clear() {
      for (int r=0; r < nRows; r++)
         for (int c=0; c < nCols; c++)
            if (maze[r][c] == MARK)
               maze[r][c] = OPEN;
   }
 
   public Position findStart() {
      for (int r = 0; r < nRows; r++) {
         for (int c = 0; c < nCols; c++) {
            if (maze[r][c] == START) {
               return new Position(r,c);
            }
         }
      }
      return null;
   }
 
   public boolean isOpen(Position p) {
      char here = maze[p.row][p.column];
      return here == OPEN; // || here == GOAL; // so goal position will be tried, and found!
   }
 
   public boolean isGoal(Position p) {
      return maze[p.row][p.column] == GOAL;
   }
 
   public void mark(Position p) {
      if (maze[p.row][p.column] == OPEN)
         maze[p.row][p.column] = MARK;
   }
 
   public int countMarks() {
      int count = 0;
      for (int r = 0; r < nRows; r++)
         for (int c = 0; c < nCols; c++)
            if (maze[r][c] == MARK)
               ++count;
      return count;
   }
 
   public String toString() {
      String result = "";
      for (int r = 0; r < nRows; r++) {
         for (int c = 0; c < nCols; c++) {
            result += maze[r][c];
 
         }
         if (r < nRows-1)
            result += '\n';
      }
      return result;
   }
 
   public static void main(String [] args) {
      Maze maze = new Maze(args[0]);
      System.out.println(maze);
   }
}

Download these three files, compile Maze.java, and run it.

Solve the Maze

Finally, we are ready to solve a maze. To solve a maze, we will follow this algorithm:

  • Push position S on a stack, called “unexplored”.
  • Until the goal G is found:
    • Pop a position off the unexplored stack.
    • Check the four steps around this position for the goal. If found, stop.
    • Push the four new positions, if open, onto the unexplored stack.

Here is the first implementation, using a stack.

SolveMazeStack.java
public class SolveMazeStack {
 
   public static void main(String [] args) {
 
      if (args.length < 1) {
         System.out.println("Usage: java SolveMazeStack mazeFileName.txt <debug>");
         System.exit(1);
      }
 
      String filename = args[0];
      Maze maze = new Maze(filename);
 
      boolean debug = false;
      if (args.length > 1) 
         if (args[1].equals("debug"))
            debug = true;
 
      Position start = maze.findStart();
 
      Stack<Position> unexplored = new Stack<Position>();
      unexplored.push(start);
 
      boolean found = false;
 
      while( !unexplored.isEmpty() ) {
         Position tryFromHere = unexplored.pop();
 
         maze.mark(tryFromHere);
         if (debug) {
            System.out.println(maze);  // to show progress
            System.out.println("Size of unexplored is now " + unexplored.size());
         }
 
         Position up = tryFromHere.up();
         Position down = tryFromHere.down();
         Position right = tryFromHere.right();
         Position left = tryFromHere.left();
 
         if (maze.isGoal(up) || maze.isGoal(right) || maze.isGoal(down) || maze.isGoal(left)) {
            found = true;
            break;
         }
 
         if (maze.isOpen(up))    unexplored.push(up);
         if (maze.isOpen(right)) unexplored.push(right);
         if (maze.isOpen(down))  unexplored.push(down);
         if (maze.isOpen(left))  unexplored.push(left);
      }
 
      if (found) {
         System.out.printf("With Stack: Goal found! %d steps tried. unexplored contains %d positions.\n", maze.countMarks() , unexplored.size());
         System.out.println(maze);
      } else {
         System.out.printf("With Stack: Goal not found! %d steps tried. unexplored contains %d positions.\n", maze.countMarks() , unexplored.size());
         System.out.println(maze);
      }
   }
}

Download, compile, and run.

  • First run without command line arguments to see the “Usage” hint.
  • Then run it with the maze file name as a command line argument.
  • Now run again with the maze file name followed by the word “debug” to watch the progress of the search.

Now Solve the Maze Using a Queue

Now, see what happens when you use a Queue for the unexplored data structure instead of a Stack. To do this, do these steps.

  • Create a new class SolveMazeQueue in file SolveMazeQueue.java that initially has thesame contents as SolveMazeStack.java.
  • Make all changes necessary to replace the stack with a queue.
  • Run it on the same maze and observe the results.

Now, discuss the effect of using a queue versus a stack. Try to come up with your own maze file that shows a much bigger difference in the number of steps tried for the stack versus the queue.

Discuss why the number of positions stored in the unexplored data structure may become large, even larger than the total number of open positions in the maze.

Your Grade

Show the instructor your working SolveMazeQueue code. Sign the attendance sheet.

Recent changes RSS feed CC Attribution-Share Alike 3.0 Unported Driven by DokuWiki