CSC 505, Homework 2 Homework should be submitted using WolfWare Submit Admin in PDF, or plain text. To avoid reduced marks, please submit word/latex-formated PDF file, NOT scanned writing in pdf format. Scanned writing is hard to read, takes longer to grade, and produces gigantic files. All assignments are due on 9 PM of the due date. Late submission will result in 10%/40% point reduction on the first/second day after the due date. No credit will be given to submission that are two or more days late. Please try out Submit Admin well before the due date to make sure that it works for you. All assignments for this course are intended to be individual work. Turning in an assignment which is not your own work is cheating. The Internet is not an allowed resource! Copying of text, code or other content from the Internet (or other sources) is plagiarism. Any tool/resource must be approved in advance by the instructor and identified and acknowledged clearly in any work turned in, anything else is plagiarism. General instruction about how to “give/describe/...” an algorithm, taken from Erik Demaine. Try to be concise, correct, and complete. To avoid deductions, you should provide (1) a textual description of the algorithm, and, if helpful, pseudocode; (2) at least one worked example or diagram to illustrate how your algorithm works; (3) a proof (or other indication) of the correctness of the algorithm; and (4) an analysis of the time complexity (and, if relevant, the space complexity) of the algorithm. Remember that, above all else, your goal is to communicate. If a grader cannot understand your solution, they cannot give you appropriate credit for it. 1. Purpose: Apply recursion to solve a problem, practice formulating and analyzing algorithms. In the US, coins are minted with denominations of 50, 25, 10, 5, and 1 cent. An algorithm for making change using the smallest possible number of coins repeatedly returns the biggest coin smaller than the amount to be changed until it is zero. For example, 17 cents will result in the series 10 cents, 5 cents, 1 cent, and 1 cent. a) (4 points) Give a recursive algorithm for changing n cents. b) (4 points) Give an O(1) (non-recursive!) algorithm to compute the number of returned coins. c) (1 point) Show that the above algorithm does not always give the minimum number of coins in a country whose denominations are 1, 6, and 10 cents. a) (4 points) Here is the pseudo code for this recursive changing algorithm: int denomination[5] = {50, 25, 10, 5, 1}; MakeChange(int amount) { if(amount < 0) return INVALID; if(amount == 0) return; for(i = 0; i < 5; i++){ //assume the index starts from 0 if(amount >= denomination[i]){ print(denomination[i]); MakeChange(amount – denomination[i]); break; } } } Algorithm description: First, we examine if the input is negative or zero. Negative is an invalid input, while 0 means no further work to do. For positive input, the algorithm tries to find the largest denomination which is less than or equal to the amount. The algorithm outputs one coin with that denomination. Now the amount that needs to make change becomes original amount subtracting that denomination. Now the original problem converts to a new problem which is to make change for the updated amount. Therefore, the algorithm recursively calls itself with the updated amount. Since the smallest denomination is one cent, this function will always be able to find the coin series. Example: For the changing amount 57 cents, this algorithm first tests and finds that a 50 cent coin could be used. Then, a recursive call is executed on input of 7 cents. In this step of recursion, 50, 25 and 10 cents are tested but all larger than 7. The algorithm keeps going on until 5-cent which is the largest denomination less than or equal to 7. It takes the 5-cent coin. Now there remains 7-5 = 2 cents. A recursive call with updated amount which is 2. This time the algorithm will find 1 cent, because all other denominations are larger than 2 cents. Similarly, Taking 1 cent coin, it recursively calls with 2 – 1 = 1 cent. The same procedure as last call, the algorithm finds 1 cent, then it recursively calls with 1 – 1 = 0 cent. Since the updated value is 0, it means we have found the coin series corresponds to 57 coins, thus algorithm returns. Analysis of time complexity: Time complexity depends on the amount to make change. Let’s assume the amount is n. In every recursive call, n is reduced by at least 1 and at most 50. Therefore, the number 𝑛 of recursive calls is between 50 𝑡𝑜 𝑛. In every recursive call, the number of arithmetic operation is 1 (not considering i++). Therefore, the time complexity is 𝑂(𝑛). b) (4 points) Write an O(1) (non-recursive!) algorithm to compute the number of returned coins. The pseudo code is as following: int denomination[5] = {50, 25, 10, 5, 1}; Changing(int amount) { if(amount < 0) return INVALID; noOfCoins = 0; for (i=0; i<5; i++) { noOfCoins += floor ( amount / denomination[i] ); amount = amount % denomination[i]; } print(noOfCoins); return; } Algorithm description: First, we test for invalid input. Then, we compute in order of decreasing denominations how many coins of a certain denomination value have to be returned. We adjust the amount to change correspondingly, and iterate with the next denomination. Since the smallest denomination is one cent, this procedure will always be able to change a positive amount; it produces the same coin set as the previous recursive algorithm. Example: for the 57 cents example, in the for loop, when i equals 0, we get noOfCoins as 1 and amount as 7, meaning one 50-cent coin can be used, and another 7 cents need to be changed. When i equals 1 or 2, since the denominations of 25-cent and 10-cent coins are larger than 7 cents, noOfCoins keeps the same. When i equals 3, we get one 5-cent coin and the remainder is 2 cents. When i equals to 4, 2 1-cent coins are used. Therefore, the result is 4. For positive input, the number of performed operations is always the same: 5 loop iterations, all iterations have exactly the same number of basic operations. Therefore, the time complexity is 𝑂(1). c) (1 point) Show that the above greedy algorithm does not always give the minimum number of coins in a country whose denominations are 1, 6, and 10 cents. E.g. 12 cents need the changing of one 10-cent coin and two 1-cent coins using the above algorithm; however, the minimum number of coins changing is with two 6-cent coins. 2. Purpose: Practice solving recurrences. For each of the following recurrences, use the Master Theorem to derive asymptotic bounds for T(n), or indicate why the Master Theorem does not apply. If not explicitly stated, please assume that small instances need constant time c. Justify your answers, in particular, for case 3 of the Master Theorem show that the regularity condition is satisfied. (2 points each) (a) T(n)=8T(n/5) + n3. We have 𝑎 = 8, 𝑏 = 5, 𝑓(𝑛) = 𝑛3 𝑎𝑛𝑑 𝑛log𝑏 𝑎 = 𝑛log5 8 . Choosing = 0.25 we have 𝑓(𝑛) = 𝛺(𝑛log5 8+ ). Hence, MT case 3 applies if we can show that the regularity condition holds for 𝑓(𝑛). 𝑛 i.e. 𝑎𝑓 (𝑏 ) ≤ 𝑐𝑓(𝑛) for some constant 𝑐 < 1 and all sufficiently large n. 𝑛 3 => 8 ∗ ( 5) ≤ 𝑐 ∗ 𝑛3 . Which is true for 1 > 𝑐 ≥ 8/53 . So, 𝑇(𝑛) = Ѳ(𝑛3 ). (b) T(n)=3T(n/3) + n+1/sqrt(n). a=b=3, f(n)=n1 + n-0.5 ( n1 ) hence case 2, and T(n)= (nlgn) (c) T(n)=7T(n/3) + n11/5. We have 𝑎 = 7, 𝑏 = 3, 𝑓(𝑛) = 𝑛2.2 𝑎𝑛𝑑 𝑛log𝑏 𝑎 = 𝑛log3 7. Choosing = 0.25 we have 𝑓(𝑛) = 𝛺(𝑛log3 7+ ). Hence, MT case 3 applies if we can show that the regularity condition holds for 𝑓(𝑛). 𝑛 i.e. 𝑎𝑓 (𝑏 ) ≤ 𝑐𝑓(𝑛) for some constant 𝑐 < 1 and all sufficiently large n. 𝑛 2.2 => 7 ∗ ( 3) ≤ 𝑐 ∗ 𝑛2.2 . Which is true for 1 > 𝑐 ≥ 7/32.2 . So, 𝑇(𝑛) = Ѳ(𝑛2.2 ). (d) T(n)=4T(n/2) + nsqrt(n) - 1000. a=4, b=2, f(n)=n1.5 - 1000, for =0.25: f(n)=n1.5 - 1000 O( n2- ) hence case 1, and T(n)= (n2) (e) ½T(2n/3) + n3. MT does not apply since a = 0.5 < 1 3. a)(4 points) Purpose: More practice in algorithm design and algorithm analysis. Describe a non-recursive Θ( lg(n) ) algorithm which computes an, given a and n. Justify the asymptotic running time of your algorithm. You may assume that n is a positive integer, but do not assume that n is always a power of 2. (Here, lg := logarithm base 2) The algorithm uses the method of exponentiation by squaring to compute 𝑎𝑛 . We first initialize a return value by 1, and then iteratively replace a n/2 if n is even, or multiply the return value by a, and replace a by a2 and n by (n-1)/2 if n is odd. The pseudocode and example below illustrate the approach. Correctness follows from the fact that our approach implements the identity: 𝑎𝑛 = (a2)n/2 if n is even, a(a2)(n—1)/2 if n is odd. Psuedocode: 1 POWER(a, n) 2 val 1 3 while n > 0 4 if n % 2 != 0 #n is odd 5 val val * a 6 n floor(n / 2) 7 aa*a 8 return val Example: POWER(2, 5) a 2, n 5, val 1 while n > 0 if n is odd val val * a nn/2 aa*a 5>0 5 is odd val 1 * 2 = 2 n5/2=2 a2*2=4 2>0 2 is not odd val 2 n2/2=1 a 4 * 4 = 16 1>0 1 is odd val 2 * 16 = 32 n1/2=0 a 16 * 16 0=0 val 32 is returned. Run-time analysis: The while-loop is executed 𝑓𝑙𝑜𝑜𝑟(log 2 𝑛) + 1 times. The execution of the commands within the while loop are bounded by a constant number of basic operations. Hence, the asymptotic run-time of the algorithm is Θ(log n). (b) Algorithm: 1 Call_multiplier(matrix, power) 2 temp_matrix Diagonal matrix having 1 throughout the diagonal of same dimension of input matrix 3 4 5 6 7 8 while power > 0 if power%2 != 0 #power is odd temp_matrix Multiply_matrices(temp_matrix, matrix) matrix Multiply_matrices(matrix, matrix) power floor(power / 2) return temp_matrix 4. (5 points) Purpose: Practice algorithm design and the use of data structures. This problem was an interview question! Consider a situation where your data is almost sorted—for example you are receiving time-stamped stock quotes and earlier quotes may arrive after later quotes because of differences in server loads and network traffic routes. Focus only on the time-stamps. To simplify this problem assume that each time-stamp is an integer, all time-stamps are different, and for any two time-stamps, the earlier timestamp corresponds to a smaller integer than the later time-stamp. The time-stamps arrive in a stream that is too large to be kept in memory completely. The time-stamps in the stream are not in their correct order, but you know that every time-stamp (integer) in the stream is at most hundred positions away from its correctly sorted position. Design an algorithm that outputs the time-stamps in the correct order and uses only a constant amount of storage, i.e., the memory used should be independent of the number of timestamps processed. Tip: map the problem to a data structure covered in class. Algorithm description: To solve this problem, a linked list of 101 stream elements in ascending order of their time-stamps is continuously maintained. First, our algorithm builds an ascendingly sorted linked list of the first 101 stream elements. To accomplish this, the stream elements are inserted one by one from the stream. Initially, the list is empty. Each time a new stream element comes, it is inserted in the proper position so that the list remains sorted. Suppose, it is the turn of the i’th stream element (1<=i<=101). Before inserting the i’th element, the linked list comprises the first i-1 elements in sorted order. To determine the proper position of the i’th element, our algorithm iterates over the list to find the first element greater than the new element. If such an element is found, the i’th element is inserted before it by pointer manipulation; otherwise, the new element is inserted at the end of the list. This way, a sorted order is maintained and eventually a sorted list of the first 101 stream elements is built. Second, our algorithm reports the head element of the list of size 101 and removes it. Then, it inserts the next element of the stream into the list in its proper position; thus, maintaining a sorted list of 101 elements. Our second step is repeated for the remaining elements of the stream. This way, our algorithm uses only the (constant amount of) space used by the list to maintain 101 elements and the necessary pointers. Example: As 100 elements is a big number, let us illustrate with 2. Let the stream be, 3, 2, 1, 7, 4, … Current Stream Element 3 2 1 7 4 … Linked List 3 2 -> 3 1 -> 2 -> 3 2 -> 3 -> 7 3 -> 4 -> 7 Always a list of size 3 Output 1 1, 2 1, 2, 3, 4, 7, … Proof of correctness: The proof can be divided into two parts. First, a proof of correctness for the first part of our algorithm, namely, building a sorted linked list of 101 elements. Second, a proof of correctness for the second part, namely, reporting stream elements in an ascendingly sorted order. The proof for the first part is easy. This can be done via a loop invariant: after inserting the i-1’st element, the list comprises the first i-1 elements of the stream in an ascendingly sorted order. Initialization: When i=1, the invariant is trivially held, as a list of 0 elements is always sorted. Maintenance: At each iteration, the i’th element is inserted before the first element of the list greater than it, or at the end of the list if no such element exists. Since all elements are unique, the i’th element is thus inserted into its proper position. Since before the iteration, the list consisted of the first i-1 elements in sorted order, after inserting the i’th element in its proper position, the list remains sorted, however, grows by size 1. Thus, at the end of the i’th iteration, the list comprises the first i elements of the stream in an ascendingly sorted order. Termination: The loop terminates after the 101’th element is inserted. Since the loop invariant is maintained at each iteration, after the end of the loop, the linked list comprises the first 101 elements in an ascendingly sorted order. The second part of our algorithm is the main part. The proof of correctness of that part can be conducted via a loop invariant. The main argument is that during the execution of the loop, a linked list of 101 ascendingly sorted elements is maintained, and all written elements are smaller than the current list elements as well as all future stream elements. Initialization: Prior to removing the head element of the list for the first time, the list comprises the first 101 stream elements in an ascendingly sorted order, and there is no element written to the output file. Thus, initially, the loop invariant is maintained trivially. Maintenance: At each iteration, the algorithm outputs the head element x, which is the smallest element in the list. Thus, at the time an element x is written, it is the smallest among all 101 list elements e1, …, e101. Therefore, at that time, at least 100 elements larger than x have been observed. If at any future time, an element y with an even smaller time-stamp than x is to be observed, y would be distorted by at least 101 positions (the list elements e1, …, e101), in contradiction to the assumption that no stream element can be more than 100 positions out of order. Therefore, x is smaller than all other list elements and any future stream element, and can be written without violating the loop invariant. After removing the head element x, the list shrinks to a size of 100. Then, we insert the next element from the stream into the list in its proper position. By a similar argument, as presented in the proof of the first part of our algorithm, an ascendingly sorted order of elements is preserved in the list. Also, the list size becomes 101 again after the insertion. Thus, the loop invariant is maintained after every iteration; this implicates that each element is written in the correct order, maintaining a list of (constant) size 101. Termination: The loop invariant is maintained throughout the execution of the algorithm. Thus, the stream elements get written into the output file in the correctly sorted order. However, since the stream is endless the loop never terminates. Space Complexity: The proofs of the loop invariants above also prove that during the execution of our algorithm, a linked list of maximum 101 elements is maintained. We need storage for the elements, and the required pointers to maintain the linked list. However, all these are proportional to the list size, which is a constant, namely, 101. Thus, our algorithm uses O(1) (a constant amount of) storage. Time Complexity: First, building a sorted linked list of n elements, by the procedure described, requires O(n^2) time. However, in our case, the initial linked list comprises a constant number (101) of elements. Thus, the time required to build the initial list is O(1). Second, removing the head element requires O(1) time. Inserting a new element in a list of size n, in proper position, requires O(n) time. However, the size of our list is constant (100). Thus, the insertion requires O(1) time. Hence, each iteration of the main part of our algorithm requires O(1) time. Therefore, processing the first n stream elements and writing them to the output file in correctly sorted order requires O(n) time in total. The amortized execution time for processing and writing each input element is thus O(1).