CS-151 Labs > Lab 5. Stack and Queue
Part 2. Solving the Maze
Now that you have a maze and working stack and queue data types, you can
use them to solve mazes! You’ll next be implementing the application portion
of this lab, writing up MazeSolver classes which will bundle up the
functionality of determining if a given maze has a valid solution. That is,
whether you can get from the start to the finish without jumping over any
walls.
Our maze solving algorithm goes something like this: begin at the start location, and trace along all possible paths to (eventually) visit every reachable square. If at some point you visit the finish Square, it was reachable. If you run out of squares to check, it isn’t reachable.
Boiling this down into pseudocode, we have the following:
At the start
- Create an (empty) worklist (stack/queue) of locations to explore.
- Add the start location to it.
Each step thereafter
- Is the worklist empty? If so, the exit is unreachable; terminate the algorithm.
- Otherwise, grab the “next” location to explore from the worklist.
- Does the location correspond to the exit square? If so, the finish was reachable; terminate the algorithm and output the path you found.
- Otherwise, it is a reachable non-finish location that we haven’t explored yet. So, explore it as follows:
- Add every neighboring square that is not a wall to the worklist for later exploration provided they have not previously been added to the worklist
Note that this pseudocode does not depend on what kind of worklist you use (namely, a stack or a queue). You’ll need to pick one when you create the worklist, but subsequently everything should work abstractly in terms of the worklist operations.
The MazeSolver abstract super-class
Thus, you will create an abstract class MazeSolver that will implement the above algorithm, with a general worklist. Its abstract methods add() and next() will be implemented differently depending on whether the worklist is a stack or a queue. The MazeSolver class should have a non-public instance variable of type Maze to store the maze, and the non-public instance variable path of type LinkedList to store the path from start to finish. It should have the following methods:
MazeSolver(Maze maze)- Create a (non-abstract) constructor that takes a
Mazeas a parameter and stores it in a variable that the subclasses can access. TheMazeSolverconstructor should take as a parameter theMazeto be solved, and should perform the two initialization steps of creating an empty worklist (using themakeEmpty()abstract method) and adding the maze’s start location to it (using theadd()abstract method). abstract void makeEmpty()- Create an empty worklist. This will be implemented in the subclasses of
MazeSolver, depending which data type is selected for the worklist. abstract boolean isEmpty()- Return true if the worklist is empty.
abstract void add(Square sq)- Add the given Square to the worklist.
abstract Square next()- Return the next item from the worklist.
boolean isFinished()- A non-abstract method that the application program can use to see if this
algorithm has solved this maze. That is, has it determined the path to the
exit or if there is no path.
This method will return
trueif either- a path from the start to the exit has been found; or
- you determine there is no such path (worklist is now empty, there is nothing more to explore).
boolean pathFound()- Return
trueif there exists a path from the start to the exit, andfalseotherwise. void step()- Perform one iteration of the algorithm above (i.e., steps 1 through 4). Note
that this is not an abstract method, that is, you should implement this method
in the
MazeSolverclass by calling the abstract methods listed above.In order to keep track of which squares have been already previously explored, after adding any new square to the worklist, you will “mark” it as explored. Then, before you add a new square to the worklist, you should first check that it is not marked (and if it is, refrain from adding it). You will use the
mark()andisMarked()functions inSquareto do this.In addition, have each
Squarekeep track of whichSquareadded it to the worklist (i.e., “WhichSquarewas being currently explored when thisSquarewas added to the worklist?”). Use thesetPrevious()method inSquareto record this. Note that this means that eachSquareis a node in a singly-linked list ofSquares, withpreviouspointing to the next node in this list.The
MazeAppwill repeatedly callstep()untilisFinished()returnstrue, so make sure you correctly set theMazeSolver’sfinishedandpathFoundbooleans in yourstep()function; otherwise, theMazeAppwill not work correctly. LinkedList<Square> getPath()- Return an
LinkedListconsisting of eachSquareon the path from start to exit.In order to create the path for
getPath(), you will need to keep track of the path that was followed in your algorithm. This seems to be a difficult proposition; however, you’ve already done most of the work when you marked your worklist nodes using the previous variable. Let me explain.In order to keep from wandering in a circle, you should avoid exploring the same location twice. You only ever explore locations that are placed on your worklist, so you can guarantee that each location is explored at most once by making sure that each location goes on the worklist at most once. You’ve already accomplished this by “marking” each square that you place in the worklist and only adding unmarked squares to the list.
You are keeping track of the solution path that includes a given square by putting an arrow (the
previousvariable) in it that points back to the square from which you added it to the worklist. Now, when you are at the exit, you can just follow the arrows back to the start. Of course, following the arrows gives you the path in reverse order. To get the sequence in the correct order, you probably want to add each previous square in front of theLinkedList.So once the exit square has been reached, you can compute the path in the private method
buildPath(Square exit), to have your path ready to be returned when asked by theMazeApp.If the exit is unreachable,
getPath()returns the instance variablepathwhich by default is initialized to null.
The MazeSolverStack and MazeSolverQueue subclasses
Now we just need to actually create two different implementations of the
MazeSolver class. Create two new classes MazeSolverStack and
MazeSolverQueue that extend the MazeSolver class.
Each class should contain as an instance variable a worklist of the
appropriate type (so, MazeSolverStack should have an instance member of type
MyStack<Square> and MazeSolverQueue should have one of type
MyQueue<Square>).
These are not the abstract classes, and so you must implement the
MazeSolver’s abstract methods. All you have to do to implement the abstract
methods is perform the appropriate operations on the stack or queue instance
member. For example, the MazeSolverStack’s add() method may look like this.
public void add(Square sq) {
stack.push(sq);
}
Recall that the constructors are not inherited by subclasses. You need to have a separate constructor for each subclass.
Don’t forget to include a call to super(maze) as the first line of your constructors.
For example the constructor for MazeSolverStack may look something like:
public MazeSolverStack(Maze maze) {
super(maze);
this.makeEmpty();
Square start = this.maze.getStart();
start.mark();
this.add(start);
}