CS302 -- Final Exam. May 1, 2008 -- Answers and Grading Guide


Question 1 - 15 Points

Part A: You can either figure this out by eyeballing the graphs, or by running through an augmenting path algorithm. Graph I is the right one -- it has the right backedges, and you can't find another augmenting path. Graph B is similar, but it doesn't have backedges.

You can create this residual graph with two augmenting paths: S -> B -> E -> T (2 units of flow), and S -> A -> C -> D -> E -> T (60 units of flow).

Part B: 62.

Part C: The edges D -> C and B -> E compose the minimum cut.

Part D: This is the minimum hop path, which is S -> B -> E -> T. The resulting residual graph is graph A.

Part E: This picks the path by performing a depth-first search, traversing each node's edge list in descending order of edge capacity. So, S -> A, then A -> C, then C -> D, then D -> B, then B -> E and E -> T. Just two units of flow. Note, if we hadn't chosen that D -> B edge, we would have done better. Regardless, the residual is C.

Part F: Here, we choose the maximum flow path: S -> A -> C -> D -> E -> T: 60 units of flow. The residual is graph H.

Part G: We start with the final residual flow graph (the answer to A), and we partition the graph into two sets: nodes reachable from S and nodes not reachable from S. The first set is { S, A, B, C } and the second is { D, E, T }. The minimum cut is all edges in the original graph from one set to the other -- those are edges C -> D and B -> E. of flow.

Grading


Question 2 - 9 Points

Part A:

Part B: Here's the rendering of the algorithm at each step. The table states the minimum known distance to each node at each step. We underline the distance at the point when the node enters the minimum spanning tree:

Step S Z Y X W V T Edge Added
Start 0 - - - - - - -
1 0 12 9 - - - - -
2 0 8 9 93 41 2 - [S-Y]
3 0 8 9 16 41 2 94 [Y-V]
4 0 8 9 16 22 2 94 [Y-Z]
5 0 8 9 16 22 2 38 [V-X]
6 0 8 9 16 22 2 38 [Z-W]
7 0 8 9 16 22 2 38 [X-T]

So, the answer is [S-Y],[Y-V],[Y-Z],[V-X],[Z-W],[X-T].

Part C: The edges are considered from smallest to largest, and whenever an edge connects two distinct connected components, it is inserted into the MST:

So, the answer is [Y-V],[Y-Z],[S-Y],[X-V],[Z-W],[X-T].

Grading

Three points per part. In parts B & C you lost a point if you included extra edges. In part B, even if you missed everything, you at least got a point for starting with edge [S-Y].


Question 3 - 10 Points

This was typo #1: my picture had the -1's and NULL's left out. It sounds like Robert was able to get that cleared up though. While the omission is bad, what else could it be? Most of you figured it out correctly. You got credit for leaving NULL's and -1's blank.

Each iteration is going to take the first entry off the Dijkstra map and see if the path going through that node leads to shorter distances for every node to which it is connected.

In the first iteration, S is considered, and it puts V, Z and Y on the map:

In the second iteration, Y is considered. It puts U and W onto the map, and updates Z's entry. Here is the answer to part B:

Grading


Question 4 - 10 Points

This was typo #2. Again, thanks to Robert for getting the typo fixed. The recursive f() goes straight from the definition:

typedef vector <double> VD;

class FindA {
  public:
     vector <VD> A;
     double f(int i, int j);
}

double FindA::f(int i, int j)
{
  double a1, a2;

  if (i == 0 && j == 0) return A[0][0];

  if (j == 0) return A[i][0] + 0.5 * f(i-1, 0);

  if (i == 0) return A[0][j] + 1.5 * f(0, j);

  a1 = A[i][j] + 0.5 * f(i-1, j);
  a2 = A[i][j] + 1.5 * f(i, j-1);
  if (a2 < a1) a1 = a2;

  return a1;
}

To add memoization, simply add a two-dimensional cache. If we could assume that all elements of A are non-negative, we can use sentinel values of -1 for the cache:

typedef vector <double> VD;

class FindA {
  public:
     vector <VD> A;
     double f(int i, int j);
     vector <VD> cache;
}

double FindA::f(int i, int j)
{
  double a1, a2;
  int k;

  if (cache.size() == 0) {
    cache.resize(A.size());
    for (k = 0; k < A.size(); k++) cache[k].resize(A[k].size(), -1);
  }

  if (cache[i][j] != -1) return cache[i][j];

  if (i == 0 && j == 0) {
    a1 = A[0][0];
  } else if (j == 0) {
    a1 = A[i][0] + 0.5 * f(i-1, 0);
  } else if (i == 0) {
    a1 = A[0][j] + 1.5 * f(0, j);
  } else {
    a1 = A[i][j] + 0.5 * f(i-1, j);
    a2 = A[i][j] + 1.5 * f(i, j-1);
    if (a2 < a1) a1 = a2;
  }
  cache[i][j] = a1;
  return a1;
}

If we didn't want to make that assumption, there are a few alternatives. First, we could calculate an appropriate sentinel, like 1 + ( 1.5 * sum of the absolute values of all A[i][j]). Or we could have separate array of integers that say whether a cache entry has been calculated yet. Here's the first code:

typedef vector <double> VD;

class FindA {
  public:
     vector <VD> A;
     double f(int i, int j);
     vector <VD> cache;
     double sentinel;
}

double FindA::f(int i, int j)
{
  double a1, a2;
  int k, l;

  if (cache.size() == 0) {
    sentinel = 1;
    for (k = 0; k < A.size(); k++) {
      for (l = 0; l < A[k].size(); l++) a1 = A[l][k];
      if (a1 < 0) {
        sentinel += ( -a1 * 1.5 );
      } else {
        sentinel += a1 * 1.5 ;
      }
    }
    cache.resize(A.size());
    for (k = 0; k < A.size(); k++) cache[k].resize(A[k].size(), sentinel);
  }

  if (cache[i][j] != sentinel) return cache[i][j];

  if (i == 0 && j == 0) {
    a1 = A[0][0];
  } else if (j == 0) {
    a1 = A[i][0] + 0.5 * f(i-1, 0);
  } else if (i == 0) {
    a1 = A[0][j] + 1.5 * f(0, j);
  } else {
    a1 = A[i][j] + 0.5 * f(i-1, j);
    a2 = A[i][j] + 1.5 * f(i, j-1);
    if (a2 < a1) a1 = a2;
  }
  cache[i][j] = a1;
  return a1;
}

Here's the second:

typedef vector <double> VD;
typedef vector <int> VI;

class FindA {
  public:
     vector <VD> A;
     double f(int i, int j);
     vector <VD> cache;
     vector <VI> cachevalid;
}

double FindA::f(int i, int j)
{
  double a1, a2;
  int k;

  if (cache.size() == 0) {
    cache.resize(A.size());
    for (k = 0; k < A.size(); k++) cache[k].resize(A[k].size());
    cachevalid.resize(A.size());
    for (k = 0; k < A.size(); k++) cachevalid[k].resize(A[k].size(), 0);
  }

  if (cachevalid[i][j]) return cache[i][j];

  if (i == 0 && j == 0) {
    a1 = A[0][0];
  } else if (j == 0) {
    a1 = A[i][0] + 0.5 * f(i-1, 0);
  } else if (i == 0) {
    a1 = A[0][j] + 1.5 * f(0, j);
  } else {
    a1 = A[i][j] + 0.5 * f(i-1, j);
    a2 = A[i][j] + 1.5 * f(i, j-1);
    if (a2 < a1) a1 = a2;
  }
  cachevalid[i][j] = 1;
  cache[i][j] = a1;
  return a1;
}

Grading

There were many paths that you could take. Here are the basic grades of each path. If your program differed from any of these, I have explained your grade in your grading file.