# CS302 Final Exam -- December 6, 2010Answers

## 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:
Your pivot will be the median of 58, 10 and 77, which is 58. Frankly, I didn't care how you went about the partition, just that it was correct: { 1, 24, 10, 58, 60, 85, 77 }. I also didn't care if you made recursive calls ignoring the pivot or including the pivot. The first call would be:

v = { 1, 24, 10, 58, 60, 85, 77 }, start = 0, size = 3 or 4.

When the second call is made, the elements in the first part are sorted already, so the second call would be:

v = { 1, 10, 24, 58, 60, 85, 77 }, start = 4, size = 3.

Call #2: v = { 46, 71, 12, 41, 18, 23, 93, 65, 19, 62, 55 }, start = 2, size = 5:
I've colored the relevant elements red. Your pivot will be the median of 12, 18 and 93, which is 18. The first call would be:

v = { 46, 71, 12, 18, 23, 41, 93, 65, 19, 62, 55 }, start = 0, size = 1 or 2.

The second call would be:

v = { 46, 71, 12, 18, 23, 41, 93, 65, 19, 62, 55 }, start = 2, size = 3.

Call #3: v = { 41, 28, 0, 77, 72, 12, 91, 65, 39, 99, 30, 75, 51, 13 }, start = 1, size = 11:
Your pivot will be the median of 28, 91 and 75, which is 75. The first call would be:

v = { 41, 28, 0, 30, 72, 12, 39, 65, 75, 91, 99, 77, 51, 13 }, start = 1, size = 7 or 8.

The second call would be:

v = { 41, 0, 12, 28, 30, 39, 65, 72, 75, 91, 99, 77, 51, 13 }, start = 9, size = 3.

• v = { 58, 25, 85, 65, 60, 1, 77 }, start = 0, size = 7.
• v = { 46, 71, 12, 41, 18, 23, 93, 65, 19, 62, 55 }, start = 2, size = 5.
• v = { 41, 28, 0, 77, 72, 12, 91, 75, 39, 99, 30, 75, 77, 12 }, start = 1, size = 11.
Grading: one point for each of the following:
• Call #1 - starts & size's correct
• Call #1 - you show that v is partitioned around 58
• Call #2 - starts & size's correct
• Call #2 - you show that v is partitioned around 18
• Call #3 - starts & size's correct
• Call #3 - you show that v is partitioned around 75
• You only show t two recursive calls in each case

## Question 3

Straight from the lecture notes:
• P is the class of all algorithms that can be solved in polynomial time (in the size of their inputs). (1 point)
• NP is the class of all algorithms whose solutions can be checked in polynomial time. (1 point)
• NP-Complete is the class of all algorithms in NP to which any algorithm in NP can be reduced in polynomial time. Put another way, if A is NP-complete, then you may convert any problem in NP to A in polynomial time. (2 points)
• To prove that a problem A is NP-complete, you first prove that it is in NP. In other words, given a solution, you can check its validity in polynomial time (1 point). Then you select a well-known NP-complete problem L, and show that you can convert an instance of L into an instance of A in polynomial time. (2 points)

## 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:

 S Sorted 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) } { }
• What problem does it solve: 1 point
• Description of the algorithm: 4 points
• Running time: 1 point
• Going through the example: 3 points

## 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:

• Max flow: 1 point
• Min cut: 2 points
• Path SAT: 1 point
• Path SDET: 2 points
• Path SCFGEDABT: 2 points
• Flow graph: 2 points

## 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:

• If l+3 < 10, cache[d+1][l+3] += cache[d][l]
• If l-2 &ge 0, cache[d+1][l-2] += cache[d][l]

Or backward:

• If l ≥ 3, cache[d][l] += cache[d-1][l-3]
• If l ≤ 7, cache[d][l] += cache[d-1][l+2]
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 #include #include #include using namespace std; typedef vector IVec; main(int argc, char **argv) { vector 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 #include #include #include using namespace std; typedef vector IVec; vector 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; } ```