Lab exercise #11: Implementing directed graphs

Course links

External links

The Graph interface

When last semester's Java class dealt with directed graphs, we didn't have an implementation like Weiss's to work from, so we designed our own. The file /home/stone/courses/java/examples/Graph.java shows what we came up with: a generic interface that allows the vertices of a graph to be objects of any kind. The roster of methods that we came up with differs radically from Weiss's selection, although there is some overlap. The techniques that we'll use to implement this interface also differ from the ones he proposes.

  1. Start Eclipse and create a new package called graphs, to hold the classes that we'll define in this lab.
  2. Import /home/stone/courses/java/examples/Graph.java into this package.

Implementing the Graph interface

To implement this interface, we need a data structure that holds three kinds of information: (a) the vertices; (b) the arcs -- that is, which vertices are connected to which others in the graph; and (c) the arc weight function -- that is, the weight associated with each arc (which Weiss calls the "edge cost").

One approach to choosing a data structure would be to follow the mathematicians' definition of a directed graph as closely as possible, using three fields: one being a Set<Vertex> of some kind, to hold the vertices; a second a Set<Arc<Vertex>>, for the arcs; and the third a Map<Arc<Vertex>, Double>, for the arc weights, since finite functions are generally implemented as maps. An Arc<Vertex> could then be a structure containing fields for the tail and tip vertices of the arc. Alternatively, we could eliminate the arc-weight map simply by storing each arc's weight in the arc itself.

  1. Look over the methods that you have to implement and consider whether they will be easier to write and more efficient in operation if you (a) maintain a separate arc-weight map or (b) store each arc's weight in the arc itself. Decide which scheme you want to implement.
  2. Define a generic Arc class suited to this implementation.
  3. Write a generic DirectedGraph class that implements the Graph interface.

Detecting paths

One limitation of the data structure described in the preceding section is that, in implementing the getSuccessors method, we have to look at every arc in the graph, adding the ones with a specified tail to our result set. In a large graph, the amount of computation could be substantial, and most of the work has no effect on the result. To make matters worse, it turns out that many common graph operations invoke the getSuccessors function frequently.

For instance, one common operation is to determine whether there is a path from a given vertex start to another given vertex finish. A path is a sequence of arcs in which the tail of the first arc is start, the tip of the last arc is finish, and the tail of each arc after the first is the tip of the arc that precedes it. (As a special case, when start is the same vertex as finish, the null sequence counts as a path.)

Another formulation of this definition of a path suggests a recursive approach: There is a path from start to finish if, and only if, either (a) start is the same vertex as finish or (b) there is a path from some successor of start to finish.

  1. Using the methods provided by the Graph interface, write a static method hasPath that takes a Graph<Vertex> and two vertices as its arguments and returns a boolean indicating whether there is a path in the graph from the first of the two vertices to the second.
  2. Unless you were exceptionally perspicacious in the preceding exercise, the method that you wrote may be susceptible to runaway (non-terminating) recursion. This could happen if the graph contains a cycle -- a path that returns to its starting point. In a cycle, the successive invocations of getSuccessors could take you around and around, without ever making progress towards finish. To prevent this, add an argument to the method that actually performs the recursion -- a Set<Vertex> containing all the vertices that are already on the path that you're constructing (i.e., all the vertices that have had the role of start in some recursive call that isn't yet finished). Then skip over these already-used vertices when considering possible successors of the current code; it's pointless to revisit them, because doing so would complete a cycle.

An alternative data structure

By selecting a different data structure for directed graphs, we can make the getSelections procedure much faster. The idea is that a directed graph could be a kind of map, in which the keys are the vertices of the graph and the values are sets of arcs. Each vertex would be mapped to the set of arcs that originate at that vertex. (In diagrams like Figure 14.1 of our textbook, these arcs would be the arrows leading away from the vertex.) To get the successors of a given vertex, we'd simply recover the corresponding set of arcs and collect their tips.

  1. I've provided an implementation using this data structure at /home/stone/courses/java/examples/HashGraph.java, but I left the bodies out of the last four methods. Fill in the missing code.
  2. Write a test program that creates at least one small graph (more if you have time) and tests the hasPath method that you wrote in the preceding section by determining which of the graph's vertices are connected by paths.