Problem Set #1 Solutions: General Notes
Problem Set #1 Solutions: General Notes
• If you use pseudocode in your assignment, please either actually use psuedocode or
include a written explanation of your code. The TA does not want to parse through
C or JAVA code with no comments.
• If you fax in your problem set, please make sure you write clearly and darkly. Some
problem sets were very difficult to read. Also, make sure that you leave enough margins
so that none of your work is cut off.
• The version of the Master Theorem that was given in lecture is slightly different from
that in the book. We gave case 2 as
On exams, using the Master Theorem is normally quicker than other methods. But,
remember that cases 1 and 3 only apply when f (n) is polynomially smaller or larger,
which is different from asymptotically smaller or larger.
In addition, asymptotic bounds for Stirling’s formula are helpful in ranking the
expressions with factorials:
n! = Θ(nn+1/2 e−n )
lg(n!) = Θ(n lg n)
(lg n)! = Θ((lg n)lg n+1/2 e− lg n )
Each term gives a different equivalence class, where the > symbol means ω.
n+1 n
22 > 22 > (n + 1)! > n! > en >
nlg lg n
n · 2n > 2n > ( 32 )n > > (lg n)! >
(lg n)lg n
n2 n lg n n √
n3 > > > > ( 2)lg n >
4lg n lg(n!) 2 lg n
√ √
2 lg n
2 > lg2 n > ln n > lg n > ln ln n >
lg∗ (lg n) 1
n
lg(lg∗ n)
∗
2lg > > >
lg∗ n n 1/ lg n
In general, if you have multiple recursive calls, the sum of the arguments to those
calls is less than n (in this case n/2 + n/4 + n/8 < n), and f (n) is reasonably
large, a good guess is T (n) = Θ(f (n)).
(f) [2 points] T (n) = T (n − 1) + 1/n.
Answer: T (n) = Θ(lg n). We solve this problem by algebraic substitution.
T (n) = T (n − 1) + 1/n
= T (n − 2) + 1/(n − 1) + 1/n
...
n
X
= Θ(1) + 1/i
i=1
= ln n + Θ(1)
Problem Set #1 Solutions 5
√ √
(h) [2 points] T (n) = nT ( n) + n.
Answer: T (n) = Θ(n lg lg n). We solve this problem by algebraic substitution.
√ √
T (n) = nT ( n) + n
= n1/2 (n1/4 T (n1/4 ) + n1/2 ) + n
= n3/4 T (n1/4 ) + 2n
= n3/4 (n1/8 T (n1/8 ) + n1/4 ) + 2n
= n7/8 T (n1/8 ) + 3n
...
k k
= n1−1/2 T (n1/2 ) + kn
k
When, n1/2 falls under 2, we have k > lg lg n. We then have T (n) = n1−1/ lg n T (2)+
n lg lg n = Θ(n lg lg n).
Here, a and c are the higher order bits, and b and d are the lower order bits of u and v
respectively. Multiplications are done recursively, except multiplication by a power of
2, which is a simple bitshift and takes Θ(n) time for an n-bit number. Addition and
subtraction also take Θ(n) time.
(a) [4 points] Write a recurrence for the running time of this algorithm as stated.
Solve the recurrence and determine the running time.
Problem Set #1 Solutions 6
Answer: Insertion sort is stable. We will prove this using the following loop
invariant: At the start of each interation of the for loop of lines 1-8, if A[a] =
A[b], a < b ≤ j − 1 and distinct, then A[a] appeared before A[b] in the initial
array.
Initialization: Before the first loop iteration, j = 2 so there are no distinct
Problem Set #1 Solutions 7
Answer: Mergesort is stable. We prove this by induction on the fact that Merge-
sort is stable.
Problem Set #1 Solutions 8
Base Case: When we call merge-sort with indices p and r such that p 6< r (there-
fore p == r), we return the same array. So, calling mergesort on an array of size
one returns the same array, which is stable.
Induction: We assume that calling merge-sort on an array of size less than n
returns a stably sorted array. We then show that if we call merge-sort on an array
of size n, we also return a stably sorted array. Each call to mergesort contains two
calls to mergesort on smaller arrays. In addition, it merges these two subarrays
and returns. Since we assume that the calls to mergesort on smaller arrays return
stably sorted arrays, we need to show that the merge step on two stably sorted
arrays returns a stable array. If A[i] = A[j], i < j in the initial array, we need
f (i) < f (j) in the new array, where f is the function which gives the new positions
in the sorted array. If i and j are in the same half of the recursive merge-sort
call, then by assumption, they are in order when the call returns and they will
be in order in the merged array (since we take elements in order from each sorted
subarray). If i and j are in different subarrays, then we know that i is in the left
subarray and j is in the right subarray since i < j. In the merge step, we take
the elements from the left subarray while the left subarry element is less than or
equal to the right subarray element (line 13). Therefore, we will take element i
before taking element j and f (i) < f (j), the claim we are trying to prove.
Therefore, Mergesort is stable.
(c) [8 points] Quicksort: pseudocode given on page 146 of CLRS.
QUICKSORT(A, p, r)
1 if p < r
2 then q ← PARTITION(A, p, r)
3 QUICKSORT(A, p, q − 1)
4 QUICKSORT(A, q + 1, r)
PARTITION(A, p, r)
1 x ← A[r]
2 i← p−1
3 for j ← p to r − 1
4 do if A[j] ≤ x
5 then i ← i + 1
6 exchange A[i] ↔ A[j]
7 exchange A[i + 1] ↔ A[r]
8 return i + 1
Answer: Quicksort is not stable. To make it stable, we can add a new field to
each array element, which indicates the index of that element in the original array.
Then, when we sort, we can sort on the condition (line 4) A[j] < x OR (A[j] == x
AND index(A[j]) < index(x)). At the end of the sort, we are guaranteed to have
all the elements in sorted order, and for any i < j, such that A[i] equals A[j], we
will have index(A[i]) < index(A[j]) so the sort will be stable.
Problem Set #1 Solutions 9
// base cases
if (n == 1) return 1;
if (n == 0) return 0;
Sketch a recursion tree for this algorithm. During the computation of Fib(n), how
many times is Fib(n-1) called? Fib(n-2)? Fib(n-3)? Fib(2)? Use formula (3.23)
on page 56 of CLRS to conclude that this algorithm takes time exponential in n.
√ √
φi − φ̂i 1+ 5 1− 5
Fi = √ , where φ = and φ̂ = (1)
5 2 2
Answer:
F(n)
F(n−1) F(n−2)
We call F ib(n − 1) one time during the recursion, F ib(n − 2) 2 times, F ib(n − 3)
3 times, F ib(n − 4) 5 times, . . ., F ib(2) Fn−1 times. Each node in the recurrence
Problem Set #1 Solutions 10
tree takes constant time to evaluate, so we need to calculate the number of nodes
in the tree. T (n) = Θ( ni=1 Fi ). Using formula 3.23 in the book, we get
P
n
X φi − φ̂i
T (n) = Θ( √ )
i=1 5
As n grows large, the φ̂i term goes to zero, so we are left with
n
φi
√ ) = Ω(φn )
X
T (n) = Θ(
i=1 5
So, the algorithm takes time exponential in n.
(b) [5 points] Notice that our recursion tree grows exponentially because the same
computation is done many times over. We now modify the naive algorithm to only
compute each Fib(i) once, and store the result in an array. If we ever need Fib(i)
again, we simply reuse the value rather than recomputing it. This technique is
called memoization and we will revisit it when we cover dynamic programming
later in the course.
Prune your recursion tree from part (a) to reflect this modification. Considering
the amount of computation done at each node is Θ(1), what is the total amount
of computation involved in finding Fib(n)?
Answer:
Fib(n)
Fib(n−1) Fib(n−2)
Fib(n−2) Fib(n−3)
Fib(n−3) Fib(n−4)
... Fib(n−5)
As in part (a), the time to calculate the nth Fibonacci number Fn is equal to the
time to calculate Fn−1 , plus the time to calculate Fn−2 , plus the time to add Fn−1
to Fn−2 . However, because of memoization, the time to calculate Fn−2 is constant
once Fn−1 has already been calculated. Therefore, the recurrence for this modified
algorithm is T (n) = T (n − 1) + Θ(1). It is easy to use the algebraic substitution
method to verify that the solution to this recurrence is T (n) = Θ(n).
Problem Set #1 Solutions 11
for n = 1, 2, 3, . . . .
Hint: The last line of the question should immediately suggest what proof tech-
nique would be useful here.
Answer: We prove the statement by mathematical induction. For the base case
of n = 1, we get
!1 !
1 1 F2 F1
= (2)
1 0 F1 F0
which certainly holds. Now, we assume the statement holds for n − 1. Then,
!n !n−1 !
1 1 1 1 1 1
= ·
1 0 1 0 1 0
! !
Fn Fn−1 1 1
= ·
Fn−1 Fn−2 1 0
!
Fn + Fn−1 Fn
=
Fn−1 + Fn−2 Fn−1
!
Fn+1 Fn
=
Fn Fn−1
(a) [10 points] Design a Θ(n lg n)-time algorithm that, given an array A of n integers
and another integer x, determines whether or not there exist two (not necessarily
distinct) elements in A whose sum is exactly x. This is problem 2.3-7 on page 37
of CLRS, slightly reworded for the sake of clarity.
Answer: We first sort the elements in the array using a sorting algorithm such
as merge-sort which runs in time Θ(n lg n). Then, we can find if two elements
exist in A whose sum is x as follows. For each element A[i] in A, set y = A[i] − x.
Using binary search, find if the element y exists in A. If so, return A[i] and y.
If we can’t find y for any A[i], then return that no such pair of elements exists.
Each binary search takes time Θ(lg n), and there are n of them. So, the total
time for this procedure is T (n) = Θ(n lg n) + Θ(n lg n) where the first term comes
from sorting and the second term comes from performing binary search for each
of the n elements. Therefore, the total running time is T (n) = Θ(n lg n).
An alternate procedure to find the two elements in the sorted array is given below:
SUM-TO-X(A)
1 Merge-Sort(A)
2 i←1
3 j ← length(A)
4 while i ≤ j
5 if A[i] + A[j] equals x
6 return A[i], A[j]
7 if A[i] + A[j] < x
8 then i ← i + 1
9 if A[i] + A[j] > x
10 then j ← j − 1
We set counters at the two ends of the array. If their sum is x, we return those
values. If the sum is less than x, we need a bigger sum so we increment the bottom
counter. If the sum is greater than x, we decrement the top counter. The loop
does Θ(n) iterations, since at each iteration we either increment i or decrement j
so j − i is always decreasing and we terminate when j − i < 0. However, this still
runs in time Θ(n lg n) since the running time is dominated by sorting.
(b) [16 points] The majority element of an array A of length n is the element that
appears strictly more than bn/2c times in the array. Note that if such an
element exists, it must be unique.
Design a Θ(n)-time algorithm that, given an array A of n objects, finds the
majority element or reports that no such element exists. Assume that two objects
can be compared for equality, read, written and copied in Θ(1) time, but no other
operations are allowed (for instance, there is no ’<’ operator). Explain why
your algorithm is correct (no formal proof necessary).
Hint: It is possible to find a candidate solution using only one pass through the
array, with the help of an auxiliary data structure such as a stack or a queue.
Then with one more pass you can determine whether this candidate is indeed the
majority element.
Answer: We will give an algorithm to solve this problem which uses an auxiliary
Problem Set #1 Solutions 13
stack and a proof of why it is correct. The following is the pseudocode for the
algorithm.
FIND-MAJORITY-ELEMENT(A)
1 for i ← 1 to length[A]
2 if stack is empty
3 then push A[i] on the stack
4 else if A[i] equals top element on the stack
5 then push A[i] on the stack
6 else pop the stack
7 if stack is empty
8 then return NoMajority
9 candidate ← top element on the stack
10 counter ← 0
11 for i ← 1 to length[A]
12 if A[i] equals candidate
13 then counter ← counter + 1
14 if counter > blength[A]/2c
15 then return candidate
16 return NoMajority
The procedure first generates a candidate object from the array. It finds this
element using the stack by looping over the array. If the stack is empty or the
current element is the same as the top object on the stack, the element is pushed
onto the stack. If the top object of the stack is different from the current element,
we pop that object off the stack.
Claim: If a majority element exists, it will be on the stack at the end of this part
of the procedure.
Proof: (by contradiction) Call the majority element x and say it appears i >
bn/2c times. Each time x is encountered, it is either pushed on the stack or an
element (different from x) is popped off the stack. There are n − i < i elements
different from x. Assume x is not on the stack at the end of the procedure. Then,
each of the i elements of x must have either popped another element off the stack,
or been popped off by another element. However, there are only n − i < i other
elements, so this is a contradiction. Therefore, x must be on the stack at the end
of the procedure.
Notice that this proof does not show that if an element is on the stack at the end
of the procedure that it is the majority element. We can construct simple coun-
terexamples (A = [1, 2, 3]) where this is not the case. Therefore, after obtaining
our candidate majority element solution, we check whether it is indeed the major-
ity element (lines 9-16). We do this by scanning through the array and counting
the number of times the element candidate appears. We return candidate if it
appears more than bn/2c times, else we report that no majority element exists.
This procedure scans through the array twice and does a constant amount of
computation (pushing and popping elements on the stack take constant time) at
each step. Therefore the running time is T (n) = cn = Θ(n).