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.
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:
You are responsible for the following three deliverables in Part A:
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.
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.
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).
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:
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
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
You might find the following hints more or less helpful:
Graph <string, string, int>* graph = new Graph <string, string, int> graph->insertVertex("Parrish"); //do some more inserts, code is looking good! //now let's do something interest bfs("Parrish","Ville", graph); //Compiler errorC++ interprets "Parrish" as a c-string and doesn't like that you are trying to send that in for a string. Simply modify your call by casting it as a string explicitly.
bfs(string("Parrish"), string("Ville"), graph);
It is up to you how to represent infinity. There are three general strategies:
if (!distances.containsKey(v) || distances[v] > distances[u]+cost(u,v)):
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:
Dijkstras(source, G): empty priority queue PQ empty dictionary DIST dictionary COMPLETED, each vertex is false PQ.insert(0, source) DIST[source] = 0 while( !PQ.empty() ): u = PQ.removeMin() if( COMPLETED[u] ) // been there done that continue COMPLETED[u] = true for each (u,v) in E //for each neighbor of u if DIST does not contain v: // new priority DIST[v] = DIST[u] + cost(u,v) PQ.insert(DIST[v], v) else if DIST[v] > DIST[u] + cost(u,v) //updating priority DIST[v] = DIST[u] + cost(u,v) //update distance PQ.insert(DIST[v],v) return DIST