CS302 Final Exam, December 5, 2011 - James S. Plank - Answers & Grading

Question 1: 10 points

This is a nuts and bolts Topcoder D2, 250-point problem (SRM 354). Obviously there are many solutions, but they all involve testing each plan to see if its valid, and if so, counting the number of C's in the string; then returning the minimum. The code below uses trails.size()+1 as a sentinel for the minimum value. It also uses trails.size()+1 to find valid plans -- when the program discovers that a plan is not valid, the number of campsites is set to trails.size()+1, so that the plan will not be counted. The solution is in Trekking.cpp. There is also a main in Trekking-Main.cpp.

int Trekking::findCamps(string trail, vector <string> plans)
{
  int min, i, nc, j;

  min = trail.size()+1;

  for (i = 0; i < plans.size(); i++) {
    nc = 0;
    for (j = 0; j < trail.size(); j++) {
      if (plans[i][j] == 'C') {
        if (trail[j] == '.') {
          nc++;
        } else {
          nc = trail.size()+1;  
        }
      }
    }
    if (nc < min) min = nc;
  }
  if (min == trail.size()+1) return -1;
  return min;
}

Grading

10 points. This is one of those where I start with 10 points if you have the right basic structure, and I deduct points for problems, like:


Question 2: Ten Points

The examples help -- if there is a cycle anywere between s and t, then there is an infinite number of paths. You need to think about this a little. Below are four graphs that contain cycles:


Example from the problem.

Cycle on the way from s to t that doesn't include s.

Cycle involving s that's not on the way from s to t.

Cycle not involving s that's also not on the way from s to t.

In the first two cases, there are clearly an infinite number of paths.

In the third, there are still an infinite number of paths, because you can travel that cycle any number of times before heading to t.

In the fourth, there are not infinite paths because you can't get to t if you go to nodes B or E.

So, your problem solving should take multiple steps. The first is to see if s is involved in any cycles. That's DFS, straight from the lecture notes: O(E). You could say O(V+E); however, since all nodes are reachable from s, you know that E ≥ V+1, so O(E) suffices.

The second step should be to mark all nodes that can be in paths from s to t. You can do that with another DFS, where the "visiting" action is "can I reach t":

can_i_reach_t(node n)
{
  if (n->i_can_reach_t != -1) return n->i_can_reach_t;
  n->i_can_reach_t = 0;
  for (i = n->edges.begin(); i != n->edges.end(); i++) {
    if (can_i_reach_t(n->edges[i])) n->i_can_reach_t = 1;
  }
  return n->i_can_reach_t;
}

Initialize by setting i_can_reach_t to -1 for all nodes but t, which should be set to 1. This is O(E).

When you're done, delete any nodes that are not on paths to t. This is O(V).

Next, do a cycle detection pass using DFS, and if you detect any cycle, return -1: O(E).

Finally, perform a topological sort. When you do this, have each node store the number of paths to it from s. All nodes have that value initialized to zero, except s, which is initialized to one. When you visit a node n, in the topological sort, traverse its edges and add n's number of paths to each "to" node on the edge. We did this exact algorithm when I taught Toplogical sort. This is also O(E).

You can use dynamic programming for this last step, but it's more of a pain.

Voila -- problem solved in linear time with the help of CS302!

Grading

I was expecting this to be a hard problem. I was projecting that: In reality, I was right about #1 and wrong about the rest. Two people saw the connection between topological sort. No one saw a good DP solution or the issue with cycles not on the path. Many panicked and tried to use network flow. So it goes.

My grading was:

Other solutions that relied on enumeration or actually traversing every path, instead of the topological sort/DP, got up to two points total if they made some sense.


Question 3: Ten points

All lecture note stuff:

Grading

Three points each for the first two, and two points each for the second two. If you saw "Not precise," then your answer was not specific enough. For example, "finds the distance of each node from the source" is not precise enough for Dijkstra. Why? Well, what kind of graph? Directed? Undirected? Weighted? Unweighted? What's the "source" Also, it finds the minimum distance.

"Combination of correct and incorrect answers" should be self-explanatory. For example, if you said that Dijkstra's algorithm "counts the number of connected components and finds the minimum distance of each node from a given source," then you get points off for the first part because it's not correct.


Question 4: 12 Points

Step 1: Spot the recursion. With the fibonacci numbers, that's easy:

fib(0) = fib(1) = 1
fib(n) = fib(n-1) - fib(n-2)
The C++ version looks very similar:

int Fib::fib(int n)
{
  if (n == 0 || n == 1) return 1;
  return fib(n-1) + fib(n-2);
}

Step 2: Memoize (use a cache) to avoid repeating identical recursive calls. In this, we add a cache to the class definition. It is a vector of ints. We size it to the maximum n+1 that we want and set all values equal to -1. Then the method simply looks in the cache before making recursive calls:

int Fib::fib(int n)
{
  if (n == 0 || n == 1) return 1;
  if (cache[n] != -1) return cache[n];
  cache[n] = fib(n-1) + fib(n-2);
  return cache[n];
}

Step 3: Remove the recursion: Here, when you realize that you only make smaller recursive calls, you can instead build the cache from small to big:

int Fib::fib(int n)
{
  int i;

  cache[0] = 1;
  cache[1] = 1;
  for (i = 2; i <= n; i++) cache[i] = cache[i-1] + cache[i-2];
  return cache[n];
}

Step 4: Reduce the memory footprint: Since you're only using the last two entries of the cache, you simply hold the last two entries instead of the whole cache. Here's a fun way -- remember what a deque is?

int Fib::fib(int n)
{
  deque <int> cache;

  if (n == 0 || n == 1) return 1;
  cache.push_back(1);
  cache.push_back(1);
  for (i = 2; i <= n; i++) {
    cache.push_back(cache[0] + cache[1]);
    cache.erase(cache.begin());
  }
  return cache[1];
}

Grading

For each part, stating the step was 1.5 points, and showing how it mapped to the Fibonacci implementation was another 1.5 points.


Question 5: 12 points

Part A: This is not a hard graph to eyeball. Looks pretty easy to max out edges Ct and Dt with:

That's 113 units of flow.

Part B: The cut is straightforward: Ct and Dt. Remember, the minimum cut is not a number. It is a collection of edges that separates the source and the sink and has a minimum sum of edge weights.

Part C: Yes, this is a pain. Sorry, but you cannot have been surprised. With the greedy DFS, you perform a DFS, visiting edges from largest capacity to smallest. The first augmenting path is therefore sACBDt with a flow of 33. I can guarantee you that drawing the residual by hand is easier than using Open Office, as I do here:

The next path is sBDt with a flow of 51. Here's the residual:

The last path is not the shortest, but it cuts off the source from the sink: sADBCt with a flow of 29. The final residual:

Part D: It's a lot easier to simply use the three paths of length three from our initial eyeballing:

Those aren't the only paths that you can use. If you start with sADt, your journey to the max flow will be more tedious.

Grading