CS35 Lab 11: Graphs, Ticket To Ride - Part A

Due by 11:59 p.m., Tuesday, December 9, 2014
Introduction

This lab will have you and your lab partner implement an advanced game advisor for the game Ticket To Ride. Ticket to Ride is a popular, award winning game with many spin-off variations. It is particularly well known for having a simple set of rules (unlike one of my favorite games, Settlers of Catan) but several strategies for success. This makes it quite popular for families with a board-game geek amongst them.

Details will be provided in Part B, but you've seen in lab how to play the game. The basic premise of the game is to construct railroads across the United States, connecting cities up along the way. You score points for claiming routes (i.e., connecting two adjacent cities). Often, you are competing to claim a route before your opponents. In addition, you collect destination tickets along the way which specify two distant cities. If you connect those two cities by claiming routes in between (i.e., form a path between the destinations), you obtain bonus points. Otherwise, you are penalized for any unfinished connections.

Naturally, this sets up well for a graph problem. In this part of the lab, you will concentrate on understanding your graph representation and implementing key algorithms in a generic manner that will assist you with Part B.


Ticket To Ride Links

Getting Started

As usual, you can get the initial files for this lab by running update35. The new files are listed here, and the files that you will need to edit are highlighted in blue:

Note: We've provided you with binaryHeap and HashTable implementations, but you're free to use your own if you like. If you do use your own implementations, you will need to add your HashTable implementation from last week into your library. That is, you should copy hashtable-inl.h to cs35/labs/11/library/. You'll also need your BinaryHeap implementation to solve Dijkstra's Algorithm.

You are responsible for the following three deliverables in Part A:

  1. Finish the implementation of UGraph
  2. Implement graph algorithms (e.g., BFS, DFS, Dijkstras) in graph-algorithms-inl.h
  3. Add several tests in testGraph to ensure that Graph, UGraph, and each graph algorithm works correctly


The Graph and UGraph class

While the Graph class been defined for you, it is very important that you dedicate time for reviewing the implementation. It is much more complex than previous data structures, and understanding the interface is essential to successfully completing the rest of the lab. Seriously. Understand the Graph class.

Our Graph represents graphs that are weighted and directed. It uses an adjacency-list representation: for each vertex we store a list (an STL vector to be precise) of the outgoing edges from that vertex. An edge is described by the Edge class, which stores a label to identify the edge (e.g., the flight number of the edge represents airline routes), a source vertex, a destination vertex, and a weight (uniform for unweighted graphs).

In an adjacency-list representation, we could store a vertex as an object with a label and a vector of Edge objects. Instead of explicitly representing a vertex object, our implementation of the Graph class stores the set of vertices in two HashTables.

  1. A hash table to store the vertices (i.e., their labels)
  2. A hash table to store vertex -- edge-list pairs (i.e., the adjacency list). That is, vertices are the keys and their list of outgoing edges are the values.

You will not need to implement any Graph methods, but you will need to understand how to use them. Before moving on, create a Graph object in your testGraph.cpp file and test each method in the interface.

UGraph

As mentioned, the Graph class represents graphs as being weighted and directed. This is the most general representation of a graph: unweighted graphs can be simulated by simply ignoring the edge weights (i.e., assigning the same cost to each). Similarly, we can simulate an undirected graph by simply ensuring each inserted edge has a complement. That is, for every edge A-->B, there must be a B-->A with matching weight and label.

We could simply rely on a user of the Graph class to maintain this property, but that would violate many of the OOP properties we have seen throughout the course! Instead, we will utilize inheritance/interfaces to ensure a guaranteed properties of undirected graphs.

In ugraph.h, you will find the declaration for UGraph, an undirected graph class. This class inherits most of its features from the Graph class. In fact, most of the methods will remain unchanged since their behavior does not depend on whether the graph is directed or undirected. Only four methods need to be over-ridden - insertEdge, removeEdge, getEdges, and graphViz. You must implement removeEdge and getEdges. You are also responsible for understanding this fact: our use of inheritance and polymorphism (i.e., virtual) allows us to write graph methods (e.g., Dijkstras, breadth-first search) that work the same regardless of whether we are working with a directed or undirected graph.

Note: recall that you can call the parent class version of a method by using the scope operator (e.g., Graph<V,E,W>::). For example, in the provide insertEdge, we simply add two directed edges using the Graph implementation of insertEdge. If we omitted the scope operator, C++ would call the UGraph implementation, leading to infinite recursion. removeEdge will be as simple as insertEdge (i.e., simply call the parent method twice). getEdges will be slightly more complicated: you must call the parent method, but ensure that each edge appears only once in the vector (i.e., only one of A-->B or B-->A should be in the list since they are the same exact edge).


Implementing the graph algorithms

Please complete each function declared in graph-algorithms.h These standard graph search algorithms will help you implement your Ticket To Ride program in Part B, but also ensure that you understand three traversal algorithms we discussed in class - depth-first search, bread-first search, and Dijkstra's. You must test each function you implement using the testGraph program. I suggest starting with the graphs we drew in class (i.e., the unweighted campus map and the weighted map of towns around Swarthmore.)

Feel free to add additional methods, although you will need to ensure you add their declaration in the header file. At a minimum, your functions are:

Recursive DFS

We've talked about using a stack to implement DFS or implementing DFS using recursion, letting the function stack act as our stack. Below is pseudocode for one possible recursive DFS implementation.
reachableDFS(G, source, dest) :
  dictionary VISITED, each vertex is false

  toReturn = recursiveDFS(G,source, dest, VISITED)
  return toReturn
}

recursiveDFS(G, current, dest, VISITED)
  if current == dest
   return true
  mark current as visited
  for each neighbor w of current
    if w not visited
      if recursiveDFS(g, w, dest, visited)
       return true
  return false

Testing

As an example, you could construct a graph to represent our search problem of trying to get from Parrish Hall to The Ville (way back in week 5). If you do this correctly, your test may look like this:

$ ./testGraph 
Let's build a map of Swarthmore!

The graph: 
Softball: {Ville, Soccer, Tunnel, Train}
Bridge: {Tennis, Wharton}
Sharples: {Clothier, Tunnel}
Soccer: {Softball, Tennis}
Wharton: {Bridge, Clothier}
Magill: {Parrish, Train}
Tennis: {Soccer, Bridge, Tunnel}
Parrish: {Magill, Clothier}
Train: {Magill, Ville, Softball}
Ville: {Train, Softball}
Tunnel: {Softball, Sharples, Tennis}
Clothier: {Wharton, Parrish, Sharples}

Can I get from Parrish to the Ville?

DFS: yes!

How do I get from Parrish to the Ville?

Using BFS:
                Parrish
                Magill
                Train
                Ville

The shortest path length is: 3


Some helpful hints

You might find the following hints more or less helpful:

Representing infinite distances

It is up to you how to represent infinity. There are three general strategies:

You must add a comment in the header file for what the user should expect. That is, if a node v is unreachable, should the user expect a value of "infinity" in the dictionary? Or will it simply be absent?

Updating priorities

There is no updatePriority function for your BinaryHeap. Implementing one is feasible, but a little tricky. You can do this if you wish. On the other hand, you can employ the following strategy: