CPSC 327 Data Structures and Algorithms Spring 2024

Programming Assignment 2 - Dijkstra's Algorithm
due Fri 3/8 in class

  1. Using the code provided in /classes/cs327/dijkstra/, implement Dijkstra's algorithm as discussed below.


Graph ADT and AdjacencyListGraph

Dijkstra's algorithm is a graph algorithm, and is most efficient with an adjacency list implementation of the Graph ADT. You can either use your own implementation (if you implemented AdjacencyListGraph in programming assignment 1) or the provided implementation in graph.jar. To use the jar file, import it into your Eclipse project, right-click on it in the Package Explorer, and choose Build Path -> Add to Build Path.

Implementing Dijkstra's Algorithm

Implement Dijkstra's algorithm as discussed in class (with the modification noted below) in Dijkstra.java — replace the method and constructor bodies that throw an UnsupportedOperationException with code that works as described in the method and constructor comments. (Note that init(s) and run(f) together cover Dijkstra's algorithm — init initializes the dist and prev labels and the priority queue, and run carries out the main loop.) The modification is to change prev so that prev[u] = e — store the edge along which a shorter path to u was discovered rather than the vertex v from which the shorter path to u was discovered. This makes it easier to implement getPathEdges.

Assume that the edge weights are double values, but do not make any assumptions about how the edge weights are stored in the graph. That's the reason for the EdgeWeight parameter to the constructor — EdgeWeight has a method weight(Edge), so w.weight(e) returns the weight for edge e.

Use Java's PriorityQueue for the priority queue. Two notes —

run(f) only needs to find the shortest path to vertex f, so it should break out of the loop after vertex f is removed from the priority queue. (f == null meeans that the shortest paths to all vertices should be found so the loop runs until the priority queue is empty — do this case need special handling?)

getPathVertices(f), getPathEdges(f), and getDist(f) return information about the shortest path to vertex f. getDist should return the dist label assigned to f. getPathVertices and getPathEdges should return the vertices and the edges, respectively, on the shortest path from s to f (including s and f). They return an Iterable; the simplest solution here is to create and return an instance of some already-existing collection that implements Iterable, such as a List of some kind (e.g. ArrayList or LinkedList).

You do not need to check the "requires init(s) [or run(f)] to have been called first" preconditions.

Other than the exception for decreaseKey, your implementation should match the running time discussed in class. In particular, make sure that getPathVertices and getPathEdges are O(1) per element, that is, O(n) and O(m), respectively.

Testing

The main program Main.java provides a test case --- it creates the graph shown, runs Dijkstra's algorithm to find the shortest paths from A to all other vertices, and prints out those paths and their lengths. Use it to test your implementation, and feel free to modify it however you want for further testing purposes.

The output of the provided main program should be:

A -> J: 10.0
  path vertices: A I J
  path edges: AI/9 IJ/1
A -> I: 9.0
  path vertices: A I
  path edges: AI/9
A -> H: 13.0
  path vertices: A D G H
  path edges: AD/4 DG/6 GH/3
A -> G: 10.0
  path vertices: A D G
  path edges: AD/4 DG/6
A -> F: 11.0
  path vertices: A B C F
  path edges: AB/6 BC/3 CF/2
A -> E: 7.0
  path vertices: A B E
  path edges: AB/6 BE/1
A -> D: 4.0
  path vertices: A D
  path edges: AD/4
A -> C: 9.0
  path vertices: A B C
  path edges: AB/6 BC/3
A -> B: 6.0
  path vertices: A B
  path edges: AB/6
A -> A: 0.0
  path vertices: A
  path edges:

About the Provided Code and Its Design

The Graph ADT only addresses the graph structure — it does not require elements like edge weights, nor does it prescribe how such additional information should be stored. In order to make the implementation of a graph algorithm like Dijkstra's algorithm as flexible as possible, it should similarly not make assumptions about how edge weights are stored. Instead, a "get weight" function that can be used when edge weights are desired is passed to the constructor — or, since Java doesn't truly support first-class functions, an object with a "get weight" function is passed to the constructor. (This is the same design principle behind the comparator object passed to PriorityQueue in order to allow different means of ordering the elements.)

"Object with a 'get weight' function" is implemented with the EdgeWeight interface in Dijkstra.java. The constructor's caller must then supply an instance of EdgeWeight. This can be done by defining a class that implements EdgeWeight and creating an instance of that class, but lambda expressions provide an alternative when the type is a functional interface (an interface containing a single subroutine). You can see the following in the provided Main:

  Dijkstra alg = new Dijkstra(g, e -> ((Integer) e.getObject()).doubleValue());

You don't need to know about lambda expressions in order to implement the methods in Dijkstra, but you can read more about them here if you are curious what the code above means.

Extra Credit

Address the following:

Handin

Note: hand in the redo as dijkstra-redo rather than copying over the original handin.

Hand in your code by copying your Java files into a folder called dijkstra within your handin directory (/classes/cs327/handin/username).

Hand in a hard copy of your writeup for the extra credit.