CS302 Final Exam -- December 6, 2010
Answers

Question 1

Part A: O(n)
Part B: Depth-first search: O(V+E)
Part C: O(E log(V))
Part D: O(1)
Part E: O(α(n)) (Inverse Ackerman)
Part F: This is just like Dijkstra's algorithm: O(E log(V))
Part G: O(n log(n))
Part H: O(n2) -- worst case.
Part I: Breadth-first search: O(E) -- you could say O(V+E) if you want.
Part J: Your cache holds at most n elements, and for each element, you check c coins: O(cn). Grading -- 1 point per part.


Question 2

Call #1: v = { 58, 25, 85, 10, 60, 1, 77 }, start = 0, size = 7: Call #2: v = { 46, 71, 12, 41, 18, 23, 93, 65, 19, 62, 55 }, start = 2, size = 5: Call #3: v = { 41, 28, 0, 77, 72, 12, 91, 65, 39, 99, 30, 75, 51, 13 }, start = 1, size = 11: Grading: one point for each of the following:


Question 3

Straight from the lecture notes:


Question 4

Dijkstra's algorithm finds the shortest path from a given starting node s to a given ending node in a weighted, directed graph (weights >= 0).

To perform Dijkstra's algorithm, you maintain a collection of nodes S such that you know the shortest path from s to every node in S. At each step, you add a another node to S, until the ending node is in S. To do this, you also maintain a sorted list of nodes not in S, ordered by their known minimum distance to s. At each step, you put the first node on the list into S, and then process each edge from that node to a node x not in S. If the shortest path to x using this edge is smaller than the currently known shortest path to x, then remove x from the sorted list and re-insert it with the new path.

When you start, you start with S empty. The list will contain all nodes with infinite-length paths, with the exception of node s, whose path length is zero.

Each time an edge is processed in Dijkstra's algorithm, it potentially removes and inserts a node into the sorted list. Thus, its running time is O(E log(V)).

The example is this graph:

The following table shows the steps until D is in S:

SSorted List
{}{ (A,0), (B,inf), (C, inf), (D, inf) }
{ (A,0) }{ (B,8), (C, 20), (D, inf) }
{ (A,0), (B,8) }{ (C, 20), (D, 38) }
{ (A,0), (B,8), (C,20) }{ (D, 26) }
{ (A,0), (B,8), (C,20), (D,26) }{ }
Grading:


Question 5

Edmonds-Karp uses an unweighted shortest algorithm to find the augmenting path. Thus, the first path is SAT with a weight of 8. When we process the residual graph, we get the following:

Residual Graph
Flow Graph

The next path is SDET with a flow of 6. Processing:

Residual Graph
Flow Graph

The next path starts with SCF. There are a bunch of these, but the one with the fewest hops goes through that newly created edge (ED) in the residual graph: SCFGEDABT -- flow of 5. As you can see, the residual has separated S from T, so we're done.

Residual Graph
Flow Graph

The maximum flow is 8+6+5 = 19. The minimum cut is composed of edges SA, SD and CF. The flow graph is above -- note, I asked for the flow graph and not the residual graph. Grading:


Question 6

This is a dynamic program very much like the "Dice or no Dice," or even the Topcoder lab problem that you did. You calculate cache[d][l], which is the number of righteous numbers that are d digits and end with l.

You start with cache[1][l] equal to 1 for l in { 1, 2, 3, 4}, and 0 otherwise. For all other values of d, set cache[d][l] to zero.

Then, you can either calculate cache[d][l] going forward or backward. Forward:

Or backward:

When you're done calculating the cache, you sum up cache[n][l] for all l.

Here are both programs -- first forward (without recursion) in righteous-forward.cpp:

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

typedef vector <int> IVec;

main(int argc, char **argv)
{
  vector <IVec> cache;
  int n, i, t, l;
  if (argc != 2) exit(0);
  
  n = atoi(argv[1]);
  if (n < 1) exit(0);
 
  cache.resize(n+1);
  for (i = 1; i <= n; i++) cache[i].resize(10, 0);
  
  for (l = 1; l < 5; l++) cache[1][l] = 1;

  for (i = 2; i < n; i++) {
    for (l = 0; l+3 < 10; l++) cache[i+1][l+3] += cache[i][l];
    for (l = 9; l-2 >= 0; l--) cache[i+1][l-2] += cache[i][l];
  }
  
  t = 0;
  for (i = 0; i < 10; i++) t += cache[n][i];

  cout << t << endl;
}

And then backward, using recursion and memoization in righteous-backward.cpp:

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

typedef vector <int> IVec;
vector <IVec> cache;

int righteous(int d, int l)
{
  int rv;

  if (cache[d][l] != -1) return cache[d][l];
  rv = 0;
  if (d == 1) {
    if (l > 0 && l < 5) rv = 1;
  } else {
    if (l-3 >= 0) rv += righteous(d-1, l-3);
    if (l+2 < 10) rv += righteous(d-1, l+2);
  }
  cache[d][l] = rv;
  return rv;
}

main(int argc, char **argv)
{
  int n, i, t;
  if (argc != 2) exit(0);
  
  n = atoi(argv[1]);
  if (n < 1) exit(0);
 
  cache.resize(n+1);
  for (i = 1; i <= n; i++) cache[i].resize(10, -1);
  
  t = 0;
  for (i = 0; i < 10; i++) t += righteous(n, i);

  cout << t << endl;
}

Grading: