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
Maze
as a parameter and stores it in a variable that the subclasses can access. TheMazeSolver
constructor should take as a parameter theMaze
to 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
true
if 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
true
if there exists a path from the start to the exit, andfalse
otherwise. 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
MazeSolver
class 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 inSquare
to do this.In addition, have each
Square
keep track of whichSquare
added it to the worklist (i.e., “WhichSquare
was being currently explored when thisSquare
was added to the worklist?”). Use thesetPrevious()
method inSquare
to record this. Note that this means that eachSquare
is a node in a singly-linked list ofSquare
s, withprevious
pointing to the next node in this list.The
MazeApp
will repeatedly callstep()
untilisFinished()
returnstrue
, so make sure you correctly set theMazeSolver
’sfinished
andpathFound
boolean
s in yourstep()
function; otherwise, theMazeApp
will not work correctly. LinkedList<Square> getPath()
- Return an
LinkedList
consisting of eachSquare
on 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
previous
variable) 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 variablepath
which 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);
}