In this lab you will automate the creation of graphs from problem
objects. To test the 8-puzzle graph, you will implement
problem
benchmarks.
From a
FarmerProblem object you will create a graph like:
This graph is the same as another you created manually in the first
graphs lab exercise (see menu).
This graph has 10 vertices.
You will also create a graph of 181,440 vertices from
a
PuzzleProblem object.
These graphs will be used to solve the Farmer-Wolf-Goat-Cabbage and
various 8-Puzzle problems.
The graph creation algorithm has been described previously and is
discussed under
Graph Creation Algorithm in the menu.
This lab consists of the following steps:
- Implement automatic graph creation
- Test automatic graph creation
- Add benchmarks to the framework
- Test searching of automatically created graphs
- Add a new Java class called GraphCreator to
your ProblemSolver project's frame.graph package, so
that your project structure looks like this:
- Replace the contents of GraphCreator.java with that
in the menu.
- Complete the createGraphFor method in
the GraphCreator class by implementing the graph creation algorithm.
-
Make sure that the hashCode methods for
your FarmerState and PuzzleState classes are
compatible with equals. See Hash Tables in Java in
the menu.
- Create a test class for GraphCreator.java by
right-clicking it and selecting Tools > Create/Update
Tests and completing the dialog. This will create the
file GraphCreatorTest.java in your project's Test Packages
under framework.graph:
- Replace the contents of GraphCreatorTest.java with that
in the menu.
- Run the GraphCreatorTest.java file. The test should
complete with these results:
- Create a test class for GraphSearcher.java
(from the framework.graph source package)
called GraphSearcherTest.java:
- Replace the contents of GraphSearcherTest.java with that
in the menu.
- Run the GraphSearcherTest.java file. The test should
complete with these results:
These files will be added to your problem solving framework in this
lab.
After creating the 8-puzzle graph, you will test searching it using a
number of
benchmarks of increasing complexity.
A benchmark consists of:
- A name
- An initial state
- A final state
- The minimum number of moves required to get from the initial to
final state
The puzzle benchmarks are shown under
8-Puzzle Benchmarks
in the menu.
For the 8-puzzle, the final state is the same for each benchmark, but
for other problems the final state may be different for each initial
state.
The framework's
Problem class should be modified to store a list
of benchmarks:
- Add a private instance field to hold the list:
The Problem constructor should initialize this field to an
empty list.
- Add the method addBenchmark to the Problem
API. addBenchmark takes a benchmark as an argument and adds
it the list.
- Add the method getBenchmarks to the Problem
API. getBenchmarks takes no arguments and returns the list
of benchmarks.
A simple test of these additions using null state objects is shown via
the menu under
ProblemTest.java. Here is the test output:
Now modify the
PuzzleProblem so that the 8-puzzle benchmarks are
added.
For example, here the 5-move benchmark ("
8-Puz 1") is added:
Benchmarks are implemented in a straightforward way in
the
Benchmark.java class accessible in the
menu.
To run the tests in this lab, you must add the
Benchmark class
to your
framework.problem package:
Name |
Initial State |
Minimum Solution Length |
Benchmark 1 |
|
5 |
Benchmark 2 |
|
10 |
Benchmark 3 |
|
13 |
Benchmark 4 |
|
18 |
Benchmark 5 |
|
20 |
Benchmark 6 |
|
24 |
Sides reversed
|
Benchmark 7 |
|
30 |
Corners and sides reversed
|
Benchmark 8 |
|
30 |
Corners reversed
|
Export your project to
ProblemSolver.zip
and submit it by going to
and clicking
Submission under
Lab: Graphs 2.
Note the general
Submission Policy in the menu at left.
Grading criteria:
- (8 points) Successful running of
the GraphCreatorTest class.
- (2 points) Successful running of
the GraphSearcherTest class.
We have a
Mover object that knows how to create new
State
objects through its
doMove method.
Idea:
If we view a State object as a Vertex, we can create a
graph for a problem by starting with one of the vertices and
"growing" the graph by using the Mover object to determine
adjacent vertices.
The process can be controlled by storing yet-to-be fully processed
vertices on a stack (or a queue).
See the menu on the left for a visual trace of the process for the
farmer problem. The process would be the same for the 8-puzzle
problem.
The algorithm below assumes that:
- P is a Problem instance, for example,
a FarmerProblem or a PuzzleProblem
- vertex(s) returns a Vertex whose data is
the State instance s
- doMove(m,u) performs the Mover.doMove operation on
the move name m and the State object represented by
the vertex u
- reclaim(v) returns the Vertex from the graph that
is equal to v
CreateGraphFor(P)
initialize empty graph G
initialize empty stack S
start = vertex(initial state of P)
push(S, start)
moves = list of move names in P
while S not empty
u = pop(S)
for each m in moves do
next = doMove(m,u)
if next != null
v = vertex(next)
if v in V(G)
v = reclaim(v)
else
push(S,v)
add (u,v) to G
Reclaiming vertices is necessary because invoking the
doMove
operation creates new states.
If a newly generated state created by
doMove is the same as (by
the state class's
equals method) a state whose vertex
representation is already on the graph, that vertex must be reclaimed
(or re-used) from the graph. If the vertex is not reclaimed, then the
graph will contain duplicate vertices.
We can efficiently check if a new vertex resulting from
doMove
is already on the graph by using the
Graph
class's
getVertices method. This method returns a hash map keyed
by vertices whose values are the vertices themselves.
If
graph refers to the graph being created, and
v refers
to a vertex resulting from
doMove, then:
graph.getVertices().containsKey(v)
returns whether
graph already contains
v, and
graph.getVertices().get(v)
returns the vertex equal to
v.
For more about these methods, see
the
HashMap class in
java.util.
We start by creating a vertex out of one of the problem's states and
pushing it onto a stack. In this example we will use the farmer
problem's start state,
1, although it could be any state of the
problem.
We show the graph on the left and the stack on the right:
In the first iteration, vertex
1 is popped from the stack and all of
the moves are tried on it.
One move, "
Farmer Takes Goat," is possible in the state
represented by vertex
1. It results in state 7 in the state space
depicted at the bottom of this page.
A vertex corresponding to state 7, call it vertex
7, is created, and
the edge (
1,
7) is added to the graph. The edge is shown
in red below.
Since vertex
7 is a newly introduced vertex on the graph, it is
pushed onto the stack for the next iteration.
In the second iteration, vertex
7 is popped from the stack and
all moves are tried on it.
Two moves are possible:
-
One move, "Farmer Takes Goat," returns the problem to state
1. Since state 1 is already represented by vertex 1 on the
graph, we reclaim vertex 1 and add the edge
(7,1) to the graph, shown in red. Since
vertex 1 has already been processed, it is not pushed onto
the stack again.
-
Another move, "Farmer Takes Self," puts the problem into
state 4 in the state space depicted below. A vertex corresponding
to state 4, call it vertex 4, is created, and the edge
(7,4) is added to the graph, shown in red. Since
vertex 4 is new, it is pushed onto the stack.
In the third iteration, vertex
4 is popped from the stack and
all moves are tried on it.
Three moves are possible:
-
One move, "Farmer Takes Self," returns
the problem to state 7. Since state 7 is already represented by
vertex 7 on the graph, we reclaim vertex 7 and add the
edge (4,7) to the graph. Since vertex 7 has
already been processed, it is not pushed onto the stack again.
-
Another move, "Farmer Takes Wolf," puts the problem into
state 10 in the state space depicted below. A vertex corresponding
to state 10, call it vertex 10, is created, and
the edge (4,10) is added to the graph. Since
vertex 10 is new, it is pushed onto the stack.
-
Another move, "Farmer Takes Cabbage," puts the problem into
state 8 in the state space depicted below. A vertex corresponding
to state 8, call it vertex 8, is created, and
the edge (4,8) is added to the graph. Since
vertex 8 is new, it is pushed onto the stack.
The three new edges of the graph are shown in red.
Pop: | Vertex 8 |
Add edge: | (8,4) — 4 is reclaimed |
Add edge: | (8,5) — 5 is new |
Push: | Vertex 5 |
Pop: | Vertex 5 |
Add edge: | (5,8) — 8 is reclaimed |
Add edge: | (5,9) — 9 is new |
Push: | Vertex 9 |
Pop: | Vertex 9 |
Add edge: | (9,5) — 5 is reclaimed |
Add edge: | (9,2) — 2 is new |
Add edge: | (9,3) — 3 is new |
Push: | Vertex 2 |
Push: | Vertex 3 |
Pop: | Vertex 3 |
Add edge: | (3,9) — 9 is reclaimed |
Add edge: | (3,10) — 10 is reclaimed |
Push: | Nothing, because no new vertices are introduced |
Pop: | Vertex 2 |
Add edge: | (2,9) — 9 is reclaimed |
Add edge: | (2,6) — 6 is new |
Push: | Vertex 6 |
Pop: | Vertex 6 |
Add edge: | (6,2) — 2 is reclaimed |
Push: | Nothing |
Pop: | Vertex 10 |
Add edge: | (10,4) — 4 is reclaimed |
Add edge: | (10,3) — 3 is reclaimed |
Push: | Nothing |
Since the stack is empty, the algorithm halts. The graph is complete
and ready for searching using breadth-first or depth-first search.