CSCI 256
Design and analysis of algorithms
Midterm solutions

3/16/01


    1. Use the master theorem to find the solution to the following recurrence (up to Theta):

          T(n) = 9T(n/4) + n2

      Solution: p = log49. Therefore p < 2. Thus n2/np = n2-p where 2-p > 0.
      We must also show that a*f(n/b) < c*f(n) for some c < 1. But

          9*(n/4)2 = (9/16)n2
      so we can use c = 9/16. (Note: I forgot to include this clause when I presented the theorem in class. Hence I did not enforce this. I will in the future.)

      Therefore by the master theorem, T(n) is in Theta(n2)

    2. Let

             T(n)  = b, if n = 0
                   = nT(n-1) + b, if n > 0 
          
      Use expansion to "discover" that T(n) < b n! e, where e = 1/0! + 1/1! + 1/2! + ... + 1/n! + ...

      Solution:

          T(n) = n T(n-1) + b
               = n ((n-1) T(n-2) + b) + b = n (n-1) T(n-2) + b(n + 1)
               = n (n-1) ((n-2) T(n-3) + b) + b(n + 1)
               = n (n-1) (n-2) T(n-3) + b(n (n-1) + n + 1)
               ...
               = n (n-1) ... (n-k+1) T(n-k) + 
                          b(n(n-1)...(n-k+2) + ... + n(n-1) + n + 1)
           
      Letting k = n, we get
           T(n) = n! T(0) + b (n(n-1)...2 + ... + n(n-1) + n + 1)
                = b(n! + n!/1! + n!/2! + ... n!/(n-2)! + n!/(n-1!) + n!/n!)
                = b n!(1/0! + 1/1! + 1/2! + ... 1/(n-2)! + 1/(n-1!) + 1/n!)
                < b n! e

    3. A proof by induction that T(n) < b n! e fails because the induction hypothesis is too weak. By looking at your expansion in part (b), find a stronger induction hypothesis involving equality and complete the proof by induction. Make sure you include the argument at the end that T(n) < b n! e. (Hint: Consider using H'(n) = 1/n! + 1/(n+1)! +.... in the expression in the new induction hypothesis.)

      Solution: Claim T(n) = b n! (e - H'(n+1)).
      Base case: T(0) = b by definition. b 0! (e - H'(1)) = b (1/0!) = b = T(0).
      Induction case: Suppose T(n) = b n! (e - H'(n+1)) and show T(n+1) = b (n+1)! (e - H'(n+2)).

          T(n+1) = (n+1) T(n) + b = (n+1)(b n! (e - H'(n+1)) + b
                 = b (n+1)! (e - H'(n+1)) + b (n+1)!/(n+1)!
                 = b (n+1)! (e - H'(n+1) + 1/(n+1)!)
                 = b (n+1)! (e - H'(n+2))
      where the last equation holds because 1/(n+1)! + H'(n+2) = H'(n+1).

      Finally, T(n) = b n! (e - H'(n+1)) < b n! e because H'(n+1) > 0.

  1. Design a schedule for a round-robin tennis tournament. There are n = 2k players. Each player must play every other player, and each player must play one match per day for n-1 days. Denote the players by P1, ..., Pn. Output the schedule for each player.
    Hint: Use divide and conquer in the following way. First divide the players into two equal groups and let them play within the groups for the first n/2 - 1 days. Then, schedule the games between the groups for the other n/2 days. Try it by hand to see how the algorithm would work in general.

    Solution: Let A be an n-1 by n array where A[d,i] records who player Pi plays on day d.
    Algorithm RR(A,i,j) should fill in a round-robin tournament schedule in A for players i to j for days 1 to j-i.

    Base case: RR(A,i,i+1) -- 2 players. Schedule them to play each other on the first day.

    Recursive case: RR(A,i,j) where j > i+1. Note that we are assuming that the number of players n = (j-i+1) is a power of 2. Let k = i + n/2. Then call RR(A,i,k-1) and RR(A,k,j). This schedules a round-robin tournament among the first half of the players and the second half of the players in days 1 to n/2-1.

    Now we only need to schedule each player in the first half against all of the players in the second half. We will schedule these as follows. For m = 0 to n/2-1, we schedule player i against player k+m on day n/2 + m. We schedule player i+1 against player k+(1+m)%(n/2) on day n/2 + m. (In particular, on day n-1, player i+1 plays player k.) Continue like this for all players in the first half (in particular schedule player i+l against player k+(l+m)%(n/2) on day n/2 + m).

    Finish the algorithm by calling RR(A,1,n) and then print out the associated array a column at a time.

    The space complexity of the algorithm is clearly Theta(n2) since that is the size of A. The time complexity of RR(A,1,n) is given by T(n) = 2 T(n/2) + n2. By the master theorem, T(n) is in Theta(n2), because p = log2 2 = 1, and n2/n = n.

  2. The median of a set of k numbers is defined to be the ceiling(k/2)th-smallest number in the set. Given two arrays L[1..n] and R[1..n], each of which is sorted in increasing order, design an algorithm which will determine the median of the combined 2n elements in sublinear time (so any method that looks at O(n) elements is too slow!). Include a justification of its correctness and an analysis of its complexity. Also describe how your algorithm would work on the following input:

        L[1..7]  = 3,7,9,11,26,41,42  and  R[1..7]  = 1,14,18,21,30,33,39

    Solution: Notice that we will end up finding the median element of a set of 2n elements. By our conventions on the median, this will be the nth smallest element.

    The key idea behind our solution is that at each step we throw away 1/4 of the array elements, all of which are smaller than the median of the combined sets, and also throw away 1/4 of the elements, all of which are larger than the median of the combined arrays. Because we are throwing away the same number of elements that are larger and smaller than the median. Thus the median of the remaining elements is the same as the median of the original combined set.

    With this idea in mind, the algorithm is simple. The base case is where each array has only one element. In that case the median is the smaller of the two.

    Suppose n > 1. Start with L[fstL..lastL] and R[fstR..lastR], where each has n elements. Let mL be the subscript of the middle element of L and mR be the subscript of the middle element of R (remember both arrays are already sorted!). If n is even round down rather than up to find the appropriate subscript.

        if L[mL] < R[mR] then
           if n is odd, throw out L[fstL..mL-1] and R[mR+1...lastR]
           if n is even, throw out L[fstL..mL] and R[mR+1...lastR]
        else  // L[mL] >= R[mR]
           if n is odd, throw out R[fstR..mR-1] and L[mL+1...lastL]
           if n is even, throw out R[fstR..mR] and L[mL+1...lastL]
        Call the algorithm recursively on the remaining pieces of L and R.
        

    Let's run the algorithm on the sample data sets:

        L[1..7]  = 3,7,9,11,26,41,42  and  R[1..7]  = 1,14,18,21,30,33,39
    The value of n is 7, and the middle elements are 11 < 21. Because n is odd, we throw out 3, 7, and 9 from L, and 30, 33, and 39 from R. This leaves us with:
        L[4..7]  = 11,26,41,42  and  R[1..4]  = 1,14,18,21
    Now n is 4, and the middle elements are 26 > 14. Throw out 41 and 42 from L and 1 and 14 from R, leaving us with:
        L[4..5]  = 11,26  and  R[3..4]  = 18,21
    Now n is 2, and the middle elements are 11 and 18 with 11 < 18. Throw out 11 from L and 21 from R, leaving:
        L[5]  = 26  and  R[3]  = 18
    Thus the median of this pair of numbers is 18, which is also the median of the original arrays.

    The base case is clearly correct. In the recursive cases with arrays of size n > 1, we determine which of the two middle elements is the smaller of the two. Suppose for simplicity in the following argument that L's middle element is the smaller.

    Then all the elements to the right of L's middle element are larger, as well as the middle element of R and all the elements to its right.

    If n is even, this is n/2 elements from L and n/2+1 elements from R (and thus a total of n+1 elements) that are greater than or equal to L's middle element. Thus L's middle element cannot be the median (or at least if it is, there is another value not eliminated that has the same value).

    If n is odd then the same argument shows (n+1)/2 elements of L greater than or equal to the middle element of L and (n+1)/2 elements of R greater than or equal to L's middle element (for a total of n+1). Thus nothing to the left of L's middle element can be the median.

    By the remark at the beginning of this solution, because we threw away the same number of elements less than the median as are greater than the median, the median of the remaining elements is the same as the original arrays. Thus if the algorithm on the smaller arrays finds the median, then it does on the arrays of size n.

    Thus we reduce the size of the two arrays by half each time before a recursive call. Thus the time complexity is T(n) = T(n/2) + c (the constant c comes from the single comparison of middle elements and whatever arithmetic on subscripts is necessary). By the master theorem, T(n) in Theta(lg n). Intuitively, it is the same basic idea as the binary search. The space requirements are just those to hold the original arrays, Theta(n).

  3. Making change: The denominations of American coins have been chosen so that one can make change efficiently by using a greedy algorithm: Let L[1..5] hold the denominations of the most common American coins in order from largest to smallest: L[1] = 100 (dollar coin), L[2] = 25 (quarter), L[3] = 10 (dime), L[4] = 5 (nickel), and L[1] = 1 (penny). The following is a greedy algorithm to make change:

        Greedy_Change(n)
        // prints the coins to be exchanged which add up to n¢.
        coin_index := 1
        while n > 0 do
    	if n >= L[coin_index] then
    		n := n - L[coin_index]
    		writeln('Hand over a ',L[coin_index],' cent coin.')
    	else
    		coin_index := coin_index + 1
    	end
        end;

    It is easy to see that Greedy_Change prints out the smallest number of coins which add up to n¢ (if L holds the denominations of American coins, above).

    1. However the Greedy_Change algorithm need not produce the smallest number of coins adding up to n, if a different collection of coin denominations is chosen. Show by example that this algorithm does not always print the smallest number of coins if the denominations of coins are 25¢, 12¢, 10¢, 5¢, 1¢.

      Solution: 20¢ will work. The greedy algorithm gives one 12¢ coin plus one 5¢ and two 1¢ coins. The best solution is two 10¢ coins.

    2. Design a dynamic programming solution to this problem. Suppose the coin values are c1, ..., cm, that K is the largest amount of money we need to make change for, and that we have an unlimited number of each coin. Describe an algorithm which returns the minimum number of coins necessary in order to make change for n¢, as well as printing out how many of each coin is needed. Please describe carefully the shape of the table, and the information stored in each slot in the table. Explain carefully what your algorithm does at each stage and why the answer returned is correct.

      Solution: The solution is similar to those of the variations of Knapsack in problem 2 of the last homework. Let NumCoins(i,k) be the minimum number of coins of denominations c1 up to ci adding up to k. NumCoins(i,k) = inf (for infinite) if no solution is possible.
      Let inf+1 = inf and min(inf,n) = n = min(n,inf)
      Base case: NumCoins(1,k) = m for k = m*c1 and inf otherwise.
      Induction case: NumCoins(i,k) = min (NumCoins(i-1,k), NumCoins(i,k-ci)+1).

      Fill in the n by k table across the rows using the formula for NumCoins above. Notice that to fill in the ith row, you only need values from the i-1st row and earlier values in the ith row. The lower right hand corner value of the table will provide the number of coins necessary to add up to k. To make it easier to recover the number of coins of each denomination, set up a parallel n by k table, called NumThisDenom(i,k). This can be filled in at the same time as the table NumCoins. If the value of NumCoins(i,k) comes from row i-1 in computing the minimum, then copy NumThisDenom(i-1,k) in NumThisDenom(i,k). Otherwise set NumThisDenom(i,k) = NumThisDenom(i,k-ci)+1.

      It is now simple to recover the number of coins of each denomination:

         if NumCoins(n,k) = inf then print "no solution" & return
         i <- n
         while (i > 0)
            print "Need " + NumThisDenom(i,k) + " copies of coin " + ci
            k = k - NumThisDenom(i,k)*ci
            i--

      The space needed is Theta(n*k) for the table (two of them, actually). Filling in each value requires comparing two entries and possibly adding 1 to one of them. Hence each is bounded by a constant. Thus the time complexity is Theta(n*k).

      Finally illustrate your algorithm by showing the table which would be built by running it on coins with denominations of 12¢, 10¢, 5¢, and 1¢ with n = 14¢. You may include pseudo-code for your algorithm if you like, but I am most interested in a clear description of the data structures, what it does at each stage (and how it computes whatever is being computed), and why it works correctly. I need a discussion of all of these even if you have pseudo-code.

      NumCoins(i,k)
       01234 5678910 11121314
      12¢0infinfinfinf infinfinfinfinfinf inf1infinf
      10¢0infinfinfinf infinfinfinfinf1 inf1infinf
      0infinfinfinf 1infinfinfinf1 inf1infinf
      01234 123451 2123

      NumThisDenom(i,k)
       01234 5678910 11121314
      12¢0infinfinfinf infinfinfinfinfinf inf1infinf
      10¢0infinfinfinf infinfinfinfinf1 inf0infinf
      0infinfinfinf 1infinfinfinf0 inf0infinf
      01234 012340 1012

      From the tables we can see that the best solution involves two coins (the value found in NumCoins(4,14). By looking at NumThisDenom(4,14) we see the solution involves 2 pennies, taking us back to NumThisDenom(3, 12). We then follow the zeroes up until we get to the first row where we find NumThisDenom(1,12) is one, indicating that there is one 12¢ piece.


    Back to:

  4. CS256 home page
  5. Kim Bruce's home page
  6. CS Department home page
  7. kim@cs.williams.edu