1. map<int,bool> visited; 2. map<int,int> M; 3. 4. int mystery(Digraph G, int vid) { // G is a directed graph, vid is a vertex in G 5. if (visited.find(vid) == visited.end()) { 6. visited[vid] = 1; 7. M[vid] = vid; 8. vector<int> out_nbrs = G.out_nbrs(vid); // returns array of out-neighbors of vertex vid in G 9. for (int i = 0; i < out_nbrs.size(); ++i) { // iterates over each out-neighbor 10. int P = mystery(G, out_nbrs[i]); // recurse on neighbor 11. if (P > M[vid]) M[vid] = P; 12. } 13. } 14. return M[vid]; 15.}

Here is an example graph, too:

- vertex set = {1, 2, 3, 4, 5, 6, 7, 8, 9}
- directed edges = { (1,2), (1,3), (2,4), (2,5), (4,7), (4,8), (3,6), (3,7), (2,6), (1,4), (5,6), (1,7) }

1A. Run mystery(G, 3) on the digraph G above. What are the values of (M[1], M[2], ..., M[9]) when the algorithm finishes?

1B. Suppose G is ANY digraph without cycles. Describe, in words, what mystery(G, s) returns. (For example "mystery(G,s) counts the number of nodes reachable from vertex s" except that is the wrong answer.)

1C. Over all input graphs with N vertices and M edges, what is the worst-case running time of mystery(G,v)? (Use big-O notation to give your answer.) Explain, carefully, why.

1D. Carefully explain your reasoning that makes you believe your answer to 1B is correct. (If you are not sure that it is correct, be frank about what your doubts are.)

1. map<int,bool> visited; 2. map<int,int> S; 3. 4. int mystery2(Digraph G, int vid, int vid2) { 5. if (visited.find(vid) == visited.end()) { 6. visited[vid] = 1; 7. if (vid == vid2) S[vid] = 1; else S[vid] = 0; // this line changed 8. vector<int> out_nbrs = G.out_nbrs(vid); 9. for (int i = 0; i < out_nbrs.size(); ++i) { 10. int P = mystery2(G, out_nbrs[i], vid2); 11. S[vid] += P; // this line changed 10. } 11. } 10. return S[vid]; 11.}

2A. Run mystery2(G, 1, 6) on the example graph from problem 1. What are the values S[1], S[2], ..., S[9] when the algorithm finishes?

2B. In words, if G is ANY directed acyclic graph, what does mystery2(G, s, t) return?

Consider an Array class implemented by doubling the array size when necessary (as described in class).

Consider adding the following method (described in pseudo-code below) to that data structure:

set_size(int i) { max_referenced = i-1. // possibly resize the table: while (i >= table_size) grow() --- double the size of the table until i < table_size. while (i < table_size/2) shrink() --- cut the table in half until i >= table_size / 2. }

The function grow() doubles the size of the table as described in class (by replacing the current array with an appropriately initialized array twice as large). The operation grow() takes time proportional to the current table_size.

The function shrink() decreases the size of the table to half its current size, by replacing the current array with an appropriately initialized array of half the size. This saves memory. The operation shrink() takes time proportional to the current table_size.

Now suppose we implement a Stack class using the Array class as follows:

template <class VALUE> class Stack { Array<VALUE> array; public: void push(const VALUE &v) { array[array.size()] = v; } VALUE top() const { if (array.size() > 0) return array[array.size()-1]; else exit(-1); } VALUE pop() if (array.size() > 0) { VALUE v = top(); array.set_size(array.size()-1); return v; } else exit(-1); } }

3A. Show that with this implementation of the Stack class,
there is a sequence of N push() and pop() operations that takes total time proportional to N^{2}.

3B. Now suppose that the set_size() method of the Array class is modified as follows:

set_size(int i) { max_referenced = i-1. while (i >= table_size) grow() while (i < table_size/4) shrink() -- note that the "2" has been changed to "4" }

Claim: if the Stack class is implemented with the Array class modified in this way, then any sequence of N push() and pop() operations on such a Stack takes O(N) time.

Prove this claim, or find a counter-example (a sequence of N push() and pop() operations taking more than O(N) time). Hint: if a set_size() call does grow or shrink the table, how many push() or pop() operations must have preceded the call since the previous resizing of the table by set_size()?

*Note: this problem was discussed extensively in class Tuesday, Feb 22.
If you missed it, please find someone in the class to talk with about it.*

Recall the UNION-FIND data type discussed in class (Thursday, Feb 24th).

In that class we discussed a particular implementation of the data type and showed that, for that implementation, the total time taken to support N UNION or FIND operations on M elements was O(N + M log M).

Now, consider the following alternate implementation:

Each set is represented as a tree; the nodes of the tree represent the elements in that set. The tree edges are directed from each node to its parent node in the tree. The root of the tree has its parent pointer directed to itself.

FIND(e) is implemented by tracing the parent pointers from the node corresponding to e to the root of e's tree, and returning the name of the element stored at the root.

UNION(i,j) is implemented by taking the root node of i's tree and the root node of j's tree, and changing the parent pointer of the root of the smaller tree (the tree of the set with less elements) so that it points to the root of the other tree (thus making one tree out of two).

Suppose the data structure is implemented as described above.

4A. For any positive integers N and M with N > M, show that there is a sequence of N UNION and FIND operations on M elements that takes time at least proportional to N log M.

4B. For any positive integers N and M, prove that any sequence of N UNION and FIND operations on M elements takes time at most O(N log M).