Project: Weighted Graph Search

Shortest paths in undirected graphs (Kruskal, Prim, A*) and directed graphs (DAGS, Dijkstra, Bellman-Ford)

Version 11/18/17

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

Operational Objectives: Design and implement path discovery algorithms in weighted graphs, along with the support infrastructure.

Deliverables: Files:

wgraph.h        # weighted graph classes
graphsearch.h   # base class for search algorithms
priorityqueue.h # priority queue tools supporting path algorithms (Kruskal, Dijkstra, ...)
kruskal.h       # Kruscal's MST algorithm
prim.h          # Prim's MST algorithm
dag_sp.h        # DAG shortest paths
dijkstra.h      # Dijkstra's SSSP algorithm
bellman_ford.h  # Bellman-Ford 
astar.h         # A* algorithm
driver_wug.cpp  # driver program for undirected cases
driver_wdg.cpp  # driver program for directed cases

makefile        # builds all project object code and executables
manual.txt      # operating instructions for software [team document]
report.txt      # overview of team and project [team document]
log.txt         # personal log for team member [individual document]

Submit command: submit.sh deliverables.wgs

Code Requirements and Specifications

  1. Weighted Graph Files. These should be structured like the unweighted case:

    #  documentation to be skipped
    #  documentation to be skipped
    #  ...
    #  documentation to be skipped
    n     <-- begin with the number of vertices
    x y w <-- triple "from_vertex to_vertex weight" defines one weighted edge
    ...
    x y w <-- triple "from_vertex to_vertex weight" defines one weighted edge
    ...
    x y w <-- put in as many edges as needed
    ...
    

    The edge data does not have to be on a single line, but that may make the file more easiliy human-readable.

  2. Edges with state. Much of the technology used for unweighted graphs will need to be upgraded, because in the weighted case edges are more than abstractions, they now have state (weight). One way to start that process is to define Edge:

    template < typename N >
    struct Edge
    {
        const N from_;
        const N to_;
        double weight_;
        Edge () : from_(0), to_(0), weight_(0.0) {}
        Edge (N x, N y, double w = 0.0) : from_(x), to_(y), weight_(w) {}
        static bool UEqual (const Edge& e1, const Edge& e2); // equal as undirected edges
        static bool DEqual (const Edge& e1, const Edge& e2); // equal as directed edges
    };
    

    A weighted graph will need to maintain an "inventory" of unique edges, and the adjacency lists will be lists of pointers or references or index values into the inventory. A way to do this would be to use a vector to store edge objects and refer into that vector from adjacency lists, like this:

      ...
    private:
      fsu::Vector < fsu::List < Edge<N>* > > adj_;   // adjacency lists
      fsu::Vector < Edge < N > >             edges_; // edge inventory
    };
    
    void UWGraph<N>::InsertEdge(Vertex x, Vertex y, double w) // undirected case
    {
      edges_.PushBack(Edge(x,y,w));
      adj_[x].Insert(&edges_.Back());
      adj_[y].Insert(&edges_.Back());
    }
    
    void DWGraph<N>::InsertEdge(Vertex x, Vertex y, double w) // directed case
    {
      edges_.PushBack(Edge(x,y,w));
      adj_[x].Insert(&edges_.Back());
    }
    

    Similar solutions might use lists of vector indices or references to edges. Mainly what is needed is a way to access the edges (and their weights) during traversals of graphs. The pointer option results in the code

    (*i)->weight_
    

    (where i is an adjacency iterator), whereas the index option results in

    edges_[*i].weight_
    

    All other things equal, the pointer option seems to result in more natural and readable code.

    Once the team has decided how to represent edges and an edge inventory, prototypeing and implementing the standard Graph functionality should be a straightforward code upgrade from the unweighted cases.

  3. Priority Queues. The binary heap algorithms developed in a previous project provide the heavy lifting to implement priority queues with O(log n) Push/Pop operations and O(n) BuildQueue operation. The simplest way is probably to copy the namespace pq6 version of PriorityQueue<T,P> [see file LIB/tcpp/pq.h] and add the method

    BuildPriorityQueue (const fsu::Vector<T>& v);
    

    with an implementation that calls fsu::g_build_heap something like this:

    template < typename T , class P > 
    void PriorityQueue<T,P>::BuildPriorityQueue (const fsu::Vector<T>& v , P& p)
    {
      c.SetSize(v.Size());
      fsu::g_copy(v.Begin(), v.End(),c.Begin());
      fsu::g_build_heap(c.Begin(), c.End(), p);
    }
    

    The intended application here has v the edge inventory of the graph, and the predicate object just reverses edge weight as priority. This version of PriorityQueue supports Kruskal and Prim, where the elements of the priority queue are edges with priority the reverse of edge weight (so that smaller weight gives higher priority).

    A little more work is needed to support Dijkstra and A*, where the queue contains vertices x prioritized by the current estimate d[x] of the distance from s = start to x. The basic priority queue described above works, but the elements of the queue are pairs (d[x],x), where x is a vertex, and the predicate reverses the d values, so that smaller d[x] has higher priority.

    We are using the "profligate" queue strategy throughout, meaning that instead of implementing and using the DecreaseKey operation, we just re-insert with higher priority, leaving the stale lower priority version to languish out in the weeds of the queue until the algorithm has run its course.

  4. Algorithms. The following should be implemented (as classes holding a const reference to a graph, in the manner of fsu::BreadthFirstSurvey<G>):

    Suitable showcase applications for each algorithm should be included.