Graph Algorithms

This assignment has three main objectives:

We will briefly discuss these algorithms in class. But, you will have to use the Web and other books for detailed descriptions. In fact, I encourage you to find pseudocode on the Web and translate it to Scala.

Preliminaries

You should create a series of directories that look like this:

./graphs
|-- build.sbt
`-- project
   `-- plugins.sbt
`-- src
   |-- main
   |   `-- scala
   |       `-- your solution goes here
   `-- test
      `|-- scala
           |-- TrivialTestSuite.scala
           `-- your tests goes here

The build.sbt file should have exactly this line:

name := "graphs"

scalaVersion := "2.11.2"

libraryDependencies += "edu.umass.cs" %% "cmpsci220" % "1.4"

libraryDependencies += "org.scalatest" %% "scalatest" % "2.2.1" % "test"

The plugins.sbt file should have exactly this line:

addSbtPlugin("edu.umass.cs" % "cmpsci220" % "2.2")

Finally, here is TrivialTestSuite.scala:

class TrivialTestSuite extends org.scalatest.FunSuite {

  test("The solution object must be defined") {
    val obj : cmpsci220.hw.graph.GraphAlgorithms = Solution
  }
}

The Graph Abstract Data Type

In this assignment, you’re working with an abstract data type for graphs, Graph[N,E], where N is the type of nodes and E is the type of edges. The constructor for Graph creates an empty graph:

import cmpsci220.hw.graph._

val g = new Graph[String, Double]()

Graphs have methods to create nodes and edges:

g.mkNode("Amherst")
g.mkNode("Northampton")
g.mkEdge("Amherst", 7.9, "Northampton")

These methods return true if they successfully create a node or edge and otherwise return false. You should read the API documentation to understand exactly why mkNode and mkEdge may return false. But, here is a simple example: nodes must be created with mkNode before they are used in mkEdge:

g.mkEdge("Northampton", 19.5, "Springfield") // returns false
g.mkNode("Springfield")
g.mkEdge("Northampton", 19.5, "Springfield") // returns true

These graphs have no self-loops:

g.mkEdge("Northampton", 0, "Northampton") // returns false

Given two nodes, you can query the label on the edge between them:

g.getEdge("Amherst", "Northampton") // returns 7.9

These graphs are undirected, so the following query also returns 7.9:

g.getEdge("Northampton", "Amherst") // returns 7.9

From a node, you can also query its neighbors:

g.neighbors("Northampton") // return Set("Amherst", "Springfield")

The easiest way to create a simple graph is from an edge-list:

val g2 = Graph(("Amherst", 7.9, "Northampton"),
               ("Amherst", 19.5, "Springfield"))

The Graph type has several other methods that are documented with Scaladoc.

Programming Task

Your task is to define a Solution object that implements the GraphAlgorithms trait. To do so, you’ll have to implement several canonical graph algorithms. In order of difficulty:

You may use the following template to get started:

import cmpsci220.hw.graph._


object Solution extends GraphAlgorithms {

  def reachable[Node, Edge](graph: Graph[Node, Edge], start: Node): Set[Node] = {
    throw new UnsupportedOperationException("not implemented")
  }

  def isValidPath[Node, Edge](graph: Graph[Node, Edge], path: List[Node]): Boolean = {
    throw new UnsupportedOperationException("not implemented")
  }

  def depthFirstSearch[Node, Edge](graph: Graph[Node, Edge], start: Node, stop: Node): Option[List[Node]] = {
    throw new UnsupportedOperationException("not implemented")
  }

  def breathFirstSearch[Node, Edge](graph: Graph[Node, Edge], start: Node, stop: Node): Option[List[Node]] = {
    throw new UnsupportedOperationException("not implemented")
  }

  def shortestPath[Node](graph: Graph[Node, Float], start: Node, stop: Node): Float = {
    throw new UnsupportedOperationException("not implemented")
  }

}

Testing BFS and DFS

The key distinction between BFS and DFS is the order in which you visit nodes. For this assignment, that is the order in which you apply neighbors. You cannot observe this order from the output of these functions. But, Graph.getVisitOrder can help you do so.

Consider the following graph:

val g2 = Graph(("A", 1, "B"),
               ("A", 1, "C"),
               ("C", 1, "D"))

If you search for a path from “A” to “D”, both DFS and BFS will find exactly the same path (there is only one path). But, BFS will always visit B and C before it visits D:

In contrast, DFS will always visit “D” immediately after “C”:

You can use the getVisitOrder method to return the sequence in which neighbors is invoked. If you make several queries over the same graph object, use the resetVisitOrder method in between each query.

For example, suppose depthFirstSearch("A", "B") invokes neighbors in the following sequence:

g2.neighbors("A")
g2.neighbors("B")
g2.neighbors("C")
g2.neighbors("D")
g2.getVisitOrder() // produces List("A", "B", "C", "D")

If you invoke depthFirstSearch("A", "B") again, you may get the other sequence:

// Important! This clears the earlier nodes from the list
g2.resetVisitOrder()

g2.neighbors("A")
g2.neighbors("C")
g2.neighbors("B")
g2.neighbors("D")
g2.getVisitOrder() // produces List("A", "C", "B", "D")

Submit Your Work

Use the submit command within sbt to create submission.tar.gz. Upload this file to Moodle.