CS302 Final Exam - Answers and Grading

James S. Plank - May 5, 2017

Question 1


Question 2

Part A: You process node 0 in the first iteration. That will assign shortest paths to nodes 2 (length=6), 3 (length=34), 4 (length=15) and (length=4). It will also subtract one from Nincident for each of those. At that point, node 5 will have zero incident edges, so you'll add it to the queue. The answer is:
Q =              { 5 }   -- I was ok with { 0, 5} too.
Nincident =      { 0, 2, 1, 3, 1, 0, 2 }
Sh_Path_Length = { 0, sent, 6, 34, 15, 4, sent }
Grading: 2 points for Q. For Nincident, each of the entries from 1 through 6 was worth 0.5 points. Same for Sh_Path_Length. Part B: You process node 1 in the second iteration, and it has a path length of two to node zero. You'll remove it from the queue. Now, you'll process: At the end, then you have:
Q =              { [3,5][4,6][7,3][12,2] }
or Q =           { [2,1][3,5][4,5][4,6][6,6][7,3][12,2] } if you don't delete nodes from Q.
Sh_Path_Length = { 0, 2, 12, 7, sent, 3, 4 }
Grading: Q: 0.75 each for [3,5], [4,6], [7,3] and [12,2]. I didn't take off for other wrong entries, because you typically lost points for them in Sh_Path_Length.

For Sh_Path_Length, you received .5 for each of 1 through 6 that you got right.

Part C: You process node 1 in the second iteration, and it has a path length of one to node zero. Node 1 has edges to nodes 1, 2, 3, 5, 6 and 7. Of those, nodes 3 and 7 are not already on the queue, so you add them to the queue. Their shoretest path lengths are two. So,

Q =              { 2, 5, 6, 3, 7 } -- I'm ok if you left 1 on there too.
Sh_Path_Length = { 0, 1, 1, 2, -1, 1, 1, 2, -1, -1 }
Grading: Q: 3 points for { 2, 5, 6, 3, 7 } or { 1, 2, 5, 6, 3, 7 }. I gave one point of partial credit if you instead added 1, 2, 3, 5, 6 and 7. For Sh_Path_Length, each element was worth 0.3 points.

Question 3

Grading: 2 points per part.

I'm going to provide some commentary here: If a problem specifies v and e, then why would you use n? While O(n) is linear, and O(v+e) is also linear, the two are different, and if I specify that a graph has v nodes and e edges, you should give me an answer in terms of v and e. I took off for answers that only had n in them.

Question 4

Problem 1: This is a shortest path problem on a directed, weighted graph. The teleport stations are the nodes, and there is an edge from node x to node y if there is a teleport from station x to station y. The weight of the edge is the time that it takes to teleport from x to y. The shortest path from A to B will be the shortest time that it takes to get from A to B. You use Dijkstra's algorithm to solve this.

Problem 2: This is a BFS problem. The nodes and edges are the same as in the previous problem, but the graph is unweighted. If a path from A to B has length l, then you'll wait 20*(l+1) minutes. Therefore the shortest path in this graph will give you the shortest wait time. You solve this with breadth-first search.

Problem 3: This is a minimum spanning tree problem. The nodes are the stations, and the edges are teleports. Because teleports are bidirectional with the same cost/speed, the graph is undirected and weighted. The weights are the maintenance costs. You want to find the minimum spanning tree of this graph, and that will correspond to the teleports that you don't perform maintenance upon. You perform maintenance on the other teleports.


A comment before grading. A lot of you answered "Edmonds Karp" for problem B, presumably meaning that as a shorthand for BFS. Edmonds-Karp refers to solving network flow by using a BFS to find augmenting paths. It is not shorthand for BFS.

Question 5

There are two ways to do this, and I was hoping that by specifying the problem as I did, that you would get the correct way:
  1. Do the recursion on Y: If X equals Y, then you return 1. If X and Y are the same size, and X doesn't equal Y, then return 0. Otherwise, do two recursions:

    This is not the best way to do this, because if the difference in X and Y's size is n, this can result in roughly 2n calls. Some of those may be duplicates, so you can help it with memoization, but it will be roughly an exponential blow-up.

  2. Do the recursion on X: If X equals Y, then you return 1. If X and Y are the same size, and X doesn't equal Y, then return 0. Otherwise, do two recursions: You'll note that this results in far fewer recursive calls, because you only make recursive calls when X ends with 'A' or X begins with 'B'.
Here's code for #2 with a main -- in q5.cpp. This only memoizes the 0 answers, because the 1 answers return instantly.

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <map>
using namespace std;

class Conversion {
    string Y;
    int Reachable(string X);
    map <string, int> Cache;

int Conversion::Reachable(string X)
  string rev;
  int i;

  if (X == Y) return 1;
  if (X.size() <= Y.size()) return 0;

  if (Cache.find(X) != Cache.end()) return Cache[X];

  if (X[X.size()-1] == 'A') {
    if (Reachable(X.substr(0,X.size()-1))) return 1;

  if (X[0] == 'B') {
    for (i = X.size()-1; i > 0; i--) rev.push_back(X[i]);
    if (Reachable(rev)) return 1;
  Cache[X] = 0;
  return 0;

int main(int argc, char **argv)
  Conversion c;
  int ans;

  if (argc != 3) { fprintf(stderr, "usage: a.out X Y\n"); exit(1); }
  c.Y = argv[2];
  ans = c.Reachable(argv[1]);
  printf("%s\n", (ans) ? "Yes" : "No");