Project 4: Search in Weighted Graphs

Shortest paths and route planning in the plane

Version 11/30/18

Educational Objectives: After completing this assignment, the student should be able to accomplish the following:

Operational Objectives: Develop a graph framework to accommodate weighted edges in both undirected and directed graphs. Use the framework to develop an implementation of Algorithm A* for planar graphs, with heuristic defined as Euclidean distance in the plane. Illustrate the framework and algorithm A* with a "smart" route planning system.

Deliverables:

wgraph.h   # weighted graph framework
kruskal.h  # contains algorithm Kruskal <G>
prim.h     # contains algorithm Prim <G>
dijkstra.h # contains algorithm Dijkstra <G>
bellford.h # contains algorithm BellmanFord <G>

...        # client and helper programs TBA
log.txt    # work log / test diary 

General Discussion

Note the restriction on use of std:: libraries (under Requirements).

This is a large development project that is somewhat open-ended to give you the freedom of design decisions (constrained as usual by client program expectations). Due to the size of the code base you need to create, this will be take time. There are 5 weeks between the release date and due date, and you will need to take advantage of all of that lead time. Think, develop, and test as you go.

Waiting until late in the window to start will almost certainly lead to a disastrous confusion of software mis-steps, bugs, and a general un-rescuable mess. Don't be that person drowning in a whirlpool of bad code and misunderstandings.

To get started, you need to develop a weighted graph framework for weighted graphs, along the lines of the unweighted version in LIB/graph/graph.h. You need to represent weights of edges. There are at least three ways to do this:

  1. Maintain a mapping weight_:{edges} -> {real numbers}.
  2. Define an edge class that has 2 vertices and a weight as members. (Use or derive from fsu::Edge<N>.)
  3. Store a weight for the edge [x,y] as a pair <y,w> in adj_[x].

These each have advantages and dangers.

In #1 you need to be careful not to use too much memory. A matrix is out of the question for sparse graphs, so you would need some kind of associative array that can restrict the domain to actual edges of the graph (and not bloated to all possible edges, as a matrix would do). One advantage of this design direction is that most of the (unweighted) graph framework itself can be re-used. One pitfall is that in the undirected case an edge {x,y} is the same edge as {y,x}, so there should be only one entry for this edge in the mapping. This nuance can be accomplished by using two different definitions of a private member function

static fsu::String Key ( Vertex x , Vertex y );

that produces an unambigous key for an edge [x,y] whose weight is stored in the weight map. (Hint: see the code illustrating the memoized knapsack problem in the Dynamic Programming Notes. ToHex is probably faster than ToDec.)

In #2 you will need an "edge inventory" (a collection of Edge objects) and a way to refer into that inventory when defining a graph, so that there is never more than one copy of a given edge object anywhere. (E.g., make an adjacency list of pointers into inventory.) This is easier to manage for directed graphs. For undirected graphs you have to be careful that adjacent vertices refer to the same edge (not 2 copies), and ensure that {x,y} == {y,x} with un-ambiguous weight.

In #3 you have to be careful that the entries <y,w> in adj_[x] and <x,w> in adj_[y] agree on what the weight is for an edge [x,y] and, in the undirected case, {x,y} == {y,x}.

Whichever way you choose, use the same choice for both undirected and directed weighted graphs.

The design decision will have dramatic effect on implementations of the various weighted graph algorithms. You can be assured that either path can be made to work efficiently. The main thing you want to avoid is having to debug your weighted graph framework and your search algorithms at the same time.

Phase 1: The weighted graph and digraph classes

The project cannot move to graph search until the supporting weighted graph framework has been built, tested, and certified correct and bug-free. Start on this phase ASAP with a class and test design.

Use the names ALUWGraph and ALDWGraph for the undirected and directed classes. If feasible, it is highly beneficial to use object-based design to derive these classes from existing base classes. For example:

namespace fsu
{

  template < typename N >                             ALUG
  class ALUWGraph : public ALUGraph <N>              /    \
  {                                              ALDG      ALUWG
    ...                                              \
  };                                                  ALDWG

  template < typename N >                        blue = defined in graph.h
  class ALDWGraph : public ALDGraph <N>          red = defined in wgraph.h
  {
    ...                                          (plan A)
  };

} // namespace fsu

Deriving from the unweighted graph classes defined in LIB/graph/graph.h, as illustrated above, will save a fair amount of re-work, because the weighted graph objects "are" ordinary unweighted graphs and therefore functions and algorithms defined for the parent type can be applied directly to the child type. If such design is not possible, you are encouraged to seek another object-based design that accomplishes a comparable amount of code re-use. (Recall from the course syllabus that code re-use is a highly desireable property in design.)

If the class hierarchy pictured above (plan A) is not feasible, there are other ways that yield some code re-use, such as:


          ALUG                    ALUG     ALUWG
         /    \                  /        /
     ALDG      ALUWG         ALDG     ALDWG
              /
          ALDWG                  (plan C)

        (plan B)

One weighted graph utility you will need is a function

template < class G >
bool WLoad (std::istream& inStream, G& g);

that instantiates a weighted graph from data in a file (after first skipping file documentation, lines that begin with '#'). This can be modelled on the Load function for graphs.

WLoad will of course need a file specification to work with:

#
# documentation begins with '#' at the first character
# and continues as long as lines begin with '#'
#
# first entry is unsigned int n = number of vertices
# followed by triples x y w
# x,y represent vertices of edge from x to y (unsigned < n)
# w is a real number representing weight of edge
#

n 
x y w 
x y w
x y w
...
x y w

As in the unweighted case, the same file can represent either a directed or an undirected graph, determined by the type being loaded into.

Phase 2: Kruskal, Prim, Dijkstra, and Bellman-Ford

These algorithms should follow the general model we use for BreadthFirstSurvey: a template class that takes a graph reference in its constructor.

template < class G>
class Kruskal 
{
  typedef G                      Graph
  typedef typename G::Vertex     Vertex;
  typedef fsu::Edge<Vertex>      Edge;
  ... // more definitions optional

public:
  // constructor initializes all class variables in init list
  Kruskal (const G& g);

  // implementing the algorithm
  void Init(); // preps class variables for execution of algorithm
  void Exec(); // runs algorithm

  // extracting information
  const fsu::List<Edge>& MST() const;
  double                 Weight() const;
  ... // expand API optional
private:
  const Graph& g_;
  ...
};

template < class G>
class Prim; // same API as Kruskal

template < class G>
class Dijkstra
{
  typedef G                      Graph
  typedef typename G::Vertex     Vertex;
  ... // more definitions optional

public:
  // constructor initializes all class variables in init list
  Dijkstra (const G& g);

  // implementing the algorithm
  void Init(Vertex source); // preps variables for startup of algorithm
  void Exec();              // executes algorithm

  // extracting information
  const fsu::Vector<double>& Distance() const;
  const fsu::Vector<size_t>& Parent() const;
  void                       Path(Vertex x, fsu::List<Vertex>& path) const;
  ... // expand API optional

private: // data
  const Graph& g_;
  ...

private: // methods
  void Relax(Vertex v)
  ...
};

template < class G>
class BellmanFord; // same as Dijkstra 

The goal is for the following kind of declaration to work:

typedef size_t                   Vertex;       // template argument for N
typedef fsu::ALUWGraph <Vertex>  WGraphType;   // ALUWGraph
WGraphType                       g;
fsu::Prim <WGraphType>           prim(g);

The above should create an undirected adjacency list weighted graph g and a Prim<G> algorithm object prim that operates on g. This is following the precedent set in fbfsurvey.cpp for unweighted graphs.

The method Exec() should play the equivalent role in all of the algorithms. For example, after the declarations above,

prim.Init();
prim.Exec();

should be the calls that invoke the Prim process.

We will be releasing simple tester client code for each of the algorithms.

Note: You may implement the "lazy" versions of Prim and Dijkstra (i.e., using a min-heap priority queue without the extra functionality of "decrease_key").

Phase 3: A* and the route planning app

This last phase is to apply knowldege and experience gained to the more specific problem of finding shotest paths in planar graphs, using the EA* algorithm - which is the A* algorithm that uses euclidean "as the crow flies" distance as its heuristic. We will iron out details for phase 3 working with early finishers of phase 2. We already have the Point class and euclidean distance defined in euclidean.h.

Procedural Requirements

  1. The official development/testing/assessment environment is specified in the Course Organizer. Code should compile without warnings or errors.

  2. In order not to confuse the submit system, create and work within a separate subdirectory cop4531/proj4.

  3. Maintain your work log in the text file log.txt as documentation of effort, testing results, and development history. This file may also be used to report on any relevant issues encountered during project development.

  4. General requirement on genericity. Use generics whenever possible. For example:

    1. If you need to sort, use a generic sort algorithm or a container with built-in sorting.
    2. If a sort requires a specialized notion of order, create a function object that captures the desired order property.
    3. If you need a graph algorithm, use a component of the fsu graph library.
    4. In general, whenever a generic algorithm exists that can be deployed, do not circumvent that with specialized one-off code.
    5. Carefully choose all containers as the most appropriate for a particular purpose.

    In short: re-use components from LIB (or your own versions) whenever possible. Don't re-invent the wheels.

  5. Begin by copying all files in LIB/proj4 into your project directory. You should see at least these:

    deliverables.sh        # submission configuration file
    euclidean.h            # defines class Point and the euclidean distance metric
    ...
    

  6. Follow these steps when you are ready to submit:

    1. Be sure that you have established the submit script LIB/scripts/submit.sh as a command in your ~/.bin directory. [We are currently using version 2.0.]
    2. Copy the submit configuration script LIB/proj4/deliverables.sh into your project directory. [This is an important step, as deliverables may have changed.]
    3. Submit the project using the command submit.sh while in your project directory and logged in to shell or quake. Pay attention to the screen, which will warn you about missing deliverables and incorrect login.
    4. Check your CS email for feedback. If Robocheck is active for this project, read the results from automated testing.

    Warning: Submit scripts do not work on the program and linprog servers. Use shell.cs.fsu.edu or quake.cs.fsu.edu to submit projects. If you do not receive the second confirmation with the contents of your project, there has been a malfunction.

Code Requirements and Specifications

  1. These libraries may NOT be used:

    <string>
    <set>
    <unordered_set>
    <map>
    <unordered_map>
    <algorithm>
    

    There are equivalent components of the fsu::library in ~cop4531p/LIB that may be used.

Hints