# Assignment 6: Algorithm Design and Analysis

Assigned: Friday, February 26, 1999
Due: Monday, March 8, 1999

In this assignment, you will apply, explore, and expand your skills at algorithm design and analysis.

## Problem 1: Big-O Identities

One of the reasons that computer scientists developed a formal description of Big-O is that it permits us to develop and apply a number of important identities. For example, we might wish to consider whether BigO is transitive. That is, if f(N) is in O(g(n)) and g(n) is in O(h(n)), can we say that f(N) is in O(h(n))?1

### 1.a. Simplification

We've said that Big-O notation allows us to discard lower-order terms. Let's try to formalize that idea. Prove that if f(N) is in O(g(N)+h(N)), and g(N) is in O(h(N)), then f(N) is in O(h(N)).

### 1.b. Eliminating Constants

We've said that Big-O notation allows us to discard constant multipliers. Let's try to formalize that idea. Prove that if f(N) is in O(C*g(N)) then f(N) is in O(g(N)).

## 2. The Fibonacci Numbers

We've done some exploring of the Fibonacci sequence, 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ..., which might be defined by the following Java function.

```/**
* Compute the nth Fibonacci number.
* Pre: n >= 0
* Pre: the nth Fibonacci number <= Long.MAX_VALUE
* Post: returns the nth Fibonacci number
*/
public long fib(long n) {
if (n <= 1) return n;
else return fib(n-1) + fib(n-2);
} // fib(long)
```

### 2.a. Bounding the Running Time

Compute upper and lower bounds on the running time of the definition of `fib` given above. Express the running time in Big-O notation, in terms of the number of calls to `fib` in order to compute `fib(n)`. The bounds should be as tight as you can get them (preferably, both will be the same).

Hint: you can assume that the number of steps to compute fib(n) is at least as many as the number to compute fib(n-1).

### 2.b. Using Dynamic Programming

Improve the computation of the nth Fibonacci number using the technique of dynamic programming (caching previously computed results in a table). Test your revised function appropriately.

### 2.c. Re-Bounding the Running Time

Compute an upper bound on the running time for your new method of computing the Fibonacci numbers. Express the running time in Big-O notation, in terms of the number of calls to `fib` in order to compute `fib(n)`. The bound should be as tight as you can get it.

### 2.d. Iterative Computation

Rewrite your efficient Fibonacci computer iteratively (using loops rather than recursion).

## 3. Trinary Search

Theodore and Themla Trinary enjoyed the use of binary search so much that they've decided to develop their own variant, based on dividing the subarray into three parts, rather than two. Here's their algorithm.

```/**
* Determine the index of x in array A.
* Pre: A is sorted in increasing order.
* Post: If x is in A, then returns i s.t. A[i] == x
* Post: If x is not in A, then throws an exception
*/
public int trinarySearch(int x, int[] A)
throws Exception
{
// Use the marvelous helper function
return trinarySearch(x, A, 0, A.length-1);
} // trinarySearch(int, int[])

/**
* Determine the index of x in the subarray of A given by lb..ub.
* Pre: A is sorted in increasing order.
* Pre: If x is in A, then x is in the subarray.
* Pre: If x is not in A, then x is not in the subarray.
* Post: If x is in A, then returns i s.t. A[i] == x
* Post: If x is not in A, then throws an exception
*/
public int trinarySearch(int x, int[] A, int lb, int ub)
throws Exception
{
// Base case: empty subarray.  x is not in A.
// Base case: single-element subarray.  See if x is that element.
else if (lb == ub) {
if (x == A[lb]) return lb;
} // single-element subarray
// Recursive cases: split the array and search the appropriate subarray.
else {
// The first split point is one-third of the way from lb to ub.  We
// compute the distance from lb to ub, take 1/3 of that, and add it
// to lb.
int splitOne = lb + 1/3 * (ub-lb);
// The second split point is two-thirds of the way from lb to ub.  We
// compute the distance from lb to ub, take 2/3 of that, and add it
// to lb.
int splitTwo = lb + 2/3 * (ub-lb);

// Recursive case: in the first third of the array.
if (x <= splitOne) return trinarySearch(x, A, lb, splitOne);
// Recursive case: in the second third of the array.
else if (x < splitTwo) return trinarySearch(x, A, splitOne+1, splitTwo-1);
// Recursive case: in the third third of the array.
else return trinarySearch(x, A, splitTwo, ub);
}
} // trinarySearch(int, int[], int, int)
```

### 3.a. Errors in Trinary Search

Unfortunately, their code is riddled with errors. Identify and correct the errors, both syntactic and semantic. If you use a test suite to identify the errors, please include the test suite.

### 3.b. Running Time

Find a tight upper bound on the running time of the working trinary search, using Big-O notation.

Endnotes

1 The answer is yes. Here's a proof.

1. Because f(N) is in O(g(N)), there exist N1 and D1 such that for all N > N1, |f(N)| <= |D1*g(N)|
2. Because g(N) is in O(h(N)), there exist N2 and D2 such that for all N > N2, |g(N)| <= |D2*h(N)|
3. Let N3 = max(N1, N2)
4. Let D3 = max(1,D1) * max(1,D2)
5. Then for all N > N3, |f(N)| <= |D1*g(N)| (by the first rule and the definition of N3)
6. Then for all N > N3, |g(N)| <= |D2*h(N)| (by the second rule and the definition of N3)
7. Then for all N > N3, |D1*g(N)| <= |D1*D2*h(N)| (since D1 > 0, we can safely multiply both sides of an inequality for D1 without affecting the inequality)
8. Then for all N > N3, |f(N)| <= |D1*D2*h(N)| (plug together various inequalities using transitivity of inequality)
9. Then for all N > N3, |f(N)| <= |D3*h(N)| (D3 >= D1*D2)
10. Hence f(N) is in O(h(N)) by the definition of Big-O.

Disclaimer Often, these pages were created "on the fly" with little, if any, proofreading. Any or all of the information on the pages may be incorrect. Please contact me if you notice errors.