You are on page 1of 9

Consider the problem of sorting only the prime elements in a list of integers while keeping

the composite ones unchanged in their original places:


Sort the prime elements on a list (SORTPRIMES)
Input: 𝐴[1 … 𝑛], a list of positive integers.
Output: List 𝐴 ′ [1 … 𝑛] such that 𝐴 ′ [𝑖] = 𝐴[𝑖] if 𝐴 ′ [𝑖] is composite, and 𝐴 ′ [𝑖] ≤ 𝐴 ′ [𝑗] for
all 1 ≤ 𝑖 ≤ 𝑗 ≤ 𝑛 such that 𝐴 ′ [𝑖] and 𝐴 ′ [𝑗] are both prime.

1. (1 point) Below is a self-reduction for that problem, where we define 𝑘 = minpx(𝐴[𝑎 …


𝑏]), the position of the smallest prime on 𝐴[𝑎 … 𝑏]. State a recursive algorithm (using
pseudocode) for solving the SORTPRIMES problem based on this self-reduction. Hint: call
𝑖𝑠_𝑝𝑟𝑖𝑚𝑒(𝑋) where needed, and don’t forget to compute 𝑘 explicitly. Also, pay attention to
the opening and closing parentheses and brackets!

Grading: You will be docked a full point if the parenthesis and brackets in your algorithm
are in the wrong positions or don’t match (or if the algorithm is simply wrong).

SORTPRIMES(A[a...b])
if a > b: return A
k = minp(A[a...b])
if a <= b and (not is_prime(A[a]) or a = k): return A[a] || SORTPRIMES(A[a+1...b])
if a <= b and is_prime(A[a]) and a < k: return A[k] || SORTPRIMES(A[a+1...k-1] || [A[a]] ||
A[k+1...b])
END SORTPRIMES

2. (2+2 points) Using the same self-reduction as in the previous question, now state a
recurrence 𝑇(𝑛) that expresses the worst case running time of the recursive algorithm,
assuming that list concatenation takes constant time. Solve that recurrence and state the
tight (Big Theta) bound on 𝑇(𝑛). Hint: look for a similar recurrence in the course notes.

Grading: If the recurrence contains one error but reflects the overall structure of the
algorithm, and the provided solution is systematically derived and formally correct, the
first two points are docked but the last two are given. However, if the recurrence is
unrelated to the algorithm (extensive errors suggesting plain guessing and blind copying of
the corresponding solution from a textbook or other source), all four points are docked.

The recurrence for the worst-case running time of the algorithm can be expressed as follows:
T(n) = T(n-1) + O(is_prime(A[n]))
where is_prime(A[n]) is the time taken to check if the current element is prime, which takes
O(sqrt(A[n])) time.
Since the algorithm splits the array into two subarrays at each step, the time taken to sort the
prime numbers in the array is proportional to the time taken to sort the prime numbers in each
subarray.
Therefore, the recurrence can be written as:
T(n) = 2T(n/2) + O(sqrt(A[n]))
This is a similar recurrence to the Master Theorem case 2, where T(n) = aT(n/b) + f(n). So, we
have a = 2, b = 2 and f(n) = O(sqrt(A[n]))
Using the Master Theorem, we can solve the recurrence to obtain the tight bound on T(n):
T(n) = O(n^(log_2 2)) = O(n)
So, the tight (Big Theta) bound on T(n) is O(n).

3. (1 point) Below is another self-reduction for the SORTPRIMES problem. State a


recursive algorithm using pseudocode for solving the SORTPRIMES problem based on this
self-reduction.

APS(A[a...b])
if a >= b: return A
if b = a + 1: return pairsort(A[a], A[b])
if b > a + 1:
t2 = (a + b) // 2
A1 = APS(A[a...t2])
A2 = APS(A[t2 + 1...b])
t1 = a + length(A1) - 1
A3 = A1[a...t1] || APS(A2[t1 + 1...b])
return APS(A3[a...t2]) || A3[t2 + 1...b]
END APS

4. (2+2 points) Using the same reduction as in the previous question, state a recurrence 𝑇 ′
(𝑛) that expresses the worst case running time of the recursive algorithm corresponding to
that reduction. From this recurrence, find a tight (Big-Theta) expression for 𝑇 ′ (𝑛).
Hint #1: the Master Theorem may be the easiest solution here.
Hint #2: how many recursive calls are made, and how many elements are passed to them?

Grading: If the recurrence contains one error but reflects the overall structure of the
algorithm, and the provided solution is systematically derived and formally correct, the
first two points are docked but the last two are given. However, if the recurrence is
unrelated to the algorithm (extensive errors suggesting plain guessing and blind copying of
the corresponding solution from a textbook or other source), all four points are docked.

The recurrence for the running time of the algorithm corresponding to the reduction in the
previous question can be expressed as follows:
T'(n) = 2T'(n/2) + O(n)
Using the Master Theorem, we have: a = 2, b = 2, and f(n) = O(n)
Since a = b, we have
logb(a) = log2(2) = 1.
Since f(n) = O(n) and logb(a) = 1, we have
f(n) = O(n^(logb(a))) = O(n).
Since f(n) = O(n^(logb(a))) = O(n), we have
T'(n) = O(n log n)
Therefore, the tight (Big Theta) expression for T'(n) is Theta(n log n).

Now consider the problem of finding the concatenation of all strings whose length is a
perfect square from a given list:
Concatenate All Strings with Length a Perfect Square (CONCATSQR)
Input: 𝑆[𝑎 … 𝑏], a list of strings.
Output: 𝑆[𝑖1 ] ∥ … ∥ 𝑆[𝑖𝑚] where 𝑖1 < ⋯ < 𝑖𝑚, 𝑖𝑠_𝑠𝑞𝑟(len(𝑆[𝑖𝑘 ])) is true for all 1 ≤ 𝑘 ≤ 𝑚,
and 𝑖𝑠_𝑠𝑞𝑟 (len(𝑆[𝑖𝑗 ])) is false for all other indices 𝑗; or 𝜀 if 𝑆 contains no string whose length
is a perfect square.

5. (2+2 points) State two different divide-and-conquer self-reductions 𝐶1 (𝑆[𝑎 … 𝑏]) and 𝐶2
(𝑆[𝑎 … 𝑏]) for the above problem on input 𝑆[𝑎 … 𝑏]. Hint: call 𝑖𝑠_𝑠𝑞𝑟(𝑛) where needed.

𝐶1(𝑆[𝑎 … 𝑏]) = {
● 𝑆[𝑎 … 𝑏], if 𝑎 >= 𝑏;
● 𝑆[𝑎], if 𝑏 = 𝑎 + 1 and len(𝑆[𝑎]) is a perfect square;
● 𝑆[𝑎 + 1… 𝑏], if 𝑏 = 𝑎 + 1 and len(𝑆[𝑎]) is not a perfect square;
● 𝑆′ ∥ 𝑆′′, if 𝑏 > 𝑎 + 1,
where 𝑆′ ≔ 𝐶1(𝑆[𝑎 … 𝑡1]), 𝑆′′ ≔ 𝐶1(𝑆[𝑡1 + 1 … 𝑏]);

𝐶2(𝑆[𝑎 … 𝑏]) = {
● 𝑆[𝑎 … 𝑏], if 𝑎 >= 𝑏;
● 𝑆[𝑏], if 𝑏 = 𝑎 + 1 and len(𝑆[𝑏]) is a perfect square;
● 𝑆[𝑎… 𝑏 − 1], if 𝑏 = 𝑎 + 1 and len(𝑆[𝑏]) is not a perfect square;
● 𝑆′′ ∥ 𝑆′, if 𝑏 > 𝑎 + 1,
where 𝑆′ ≔ 𝐶2(𝑆[𝑎 … 𝑡2]), 𝑆′′ ≔ 𝐶2(𝑆[𝑡2 + 1 … 𝑏]);

6. (2+2 points) Give two recursive algorithms (in pseudo-code) based on those
divide-and-conquer selfreductions to solve the CONCATSQR problem.

Here is the first recursive algorithm based on 𝐶1(𝑆[𝑎 … 𝑏]):


CONCATSQR_C1(S, a, b):
if a >= b: return ""
else if is_sqr(len(S[a])): return S[a] || CONCATSQR_C1(S, a+1, b)
else: return CONCATSQR_C1(S, a+1, b)
END CONCATSQR_C1

And here is the second recursive algorithm based on 𝐶2(𝑆[𝑎 … 𝑏]):


function CONCATSQR_C2(S, a, b):
if a >= b: return ""
else if is_sqr(len(S[b-1])): return CONCATSQR_C2(S, a, b-1) || S[b-1]
else: return CONCATSQR_C2(S, a, b-1)
END CONCATSQR_C2

7. (1+1 points) Calculate the tight (Big-Theta) worst-case complexities of the solutions you
obtained, expressed as functions of the number of elements on S.

For the first algorithm using the self-reduction 𝐶1(𝑆[𝑎 … 𝑏]), the time complexity can be
calculated using the following recurrence:
T1(n) = 2 * T1(n/2) + O(n) for n > 1
T1(1) = O(1)
This recurrence can be solved using the Master Theorem to give a time complexity of T1(n) =
O(n log n).

For the second algorithm using the self-reduction 𝐶2(𝑆[𝑎 … 𝑏]), the time complexity can be
calculated using the following recurrence:
T2(n) = T2(√n) + O(n) for n > 1
T2(1) = O(1)
This recurrence can be solved to give a time complexity of T2(n) = O(n).

Therefore, the tight (Big-Theta) worst-case complexities of the two solutions are:
● T1(n) = Θ(n log n)
● T2(n) = Θ(n)

Now consider the following recurrence:

8. (3 points) Use the repeated substitution method to come up with a function 𝑔(𝑛) that
constitutes a good guess on the asymptotic complexity of recurrence 𝑇(𝑛).
Hint: use sentinels to simplify the analysis.

One possible function g(n) that constitutes a good guess on the asymptotic complexity of T(n) is
𝑔(𝑛) = cuberoot(𝑛). To see why, consider the recurrence:
𝑇(𝑛) = 2𝑇(⌊(𝑛/8)⌋) + cuberoot(𝑛) for n ≥ 8
𝑇(𝑛) = 1 for n < 8

We will prove by induction that T(n) = O(g(n)).


Base case:
For n = 8, T(8) = 2T(1) + cuberoot(8) = 2 * 1 + 2 = 4, and 4 = O(cuberoot(8)) = O(g(8)).
Inductive step:
Assume T(m) = O(g(m)) for m < n.
Then: 𝑇(𝑛) = 2𝑇(⌊(𝑛/8)⌋) + cuberoot(𝑛) = 2 * O(g(⌊(𝑛/8)⌋)) + cuberoot(𝑛) = O(2 * g(⌊(𝑛/8)⌋)) +
cuberoot(𝑛) = O(g(n)), where the second equality follows from the induction hypothesis, and the
third equality follows from the fact that cuberoot(n) is a monotonically increasing function.

Thus, by induction, we have shown that T(n) = O(g(n)) for all n ≥ 8, and so g(n) = cuberoot(n) is
a good guess for the asymptotic complexity of T(n).

9. (3 points) State and prove by induction a theorem showing 𝑇(𝑛) ∈ 𝑂(𝑔(𝑛)) for the same
function 𝑔(𝑛) guessed above.
Hint: use sentinels to simplify the analysis.
Theorem: 𝑇(𝑛) ∈ 𝑂(𝑔(𝑛)) for the function 𝑔(𝑛) = 2^(3 * log_2(⌊(𝑛/8)⌋)) * cuberoot(𝑛).

Proof by induction:

Base case: 𝑛 < 8


For n < 8, 𝑇(𝑛) = 1, which is constant and thus in O(1) time. The function 𝑔(𝑛) is upper bounded
by constant value 2^3 * cuberoot(8) = 24 * 2^(3/3) = 24, which is in O(1) as well.

Inductive step: 𝑛 ≥ 8
Let's assume that 𝑇(m) is in O(𝑔(m)) for all m < n. Then we have:
𝑇(𝑛) = 2𝑇 (⌊(𝑛/8)⌋) + cuberoot(𝑛)
= 2 * 2^(3 * log_2(⌊(𝑛/8)⌋)) * cuberoot(⌊(𝑛/8)⌋) + cuberoot(𝑛)
= 2^(3 * log_2(⌊(𝑛/8)⌋) + 1) * cuberoot(⌊(𝑛/8)⌋) + cuberoot(𝑛)

By induction hypothesis,
𝑇 (⌊(𝑛/8)⌋) is in O(2^(3 * log_2(⌊(𝑛/8)⌋)) * cuberoot(⌊(𝑛/8)⌋)).

Thus, 𝑇(𝑛) is in O(2^(3 * log_2(⌊(𝑛/8)⌋) + 1) * cuberoot(⌊(𝑛/8)⌋) + cuberoot(𝑛)) = O(2^(3 *


log_2(⌊(𝑛/8)⌋) + 1) * cuberoot(𝑛)), which is equivalent to 𝑔(𝑛).

So, by induction, 𝑇(𝑛) ∈ 𝑂(𝑔(𝑛)) for all n.

10. (3 points) State and prove by induction a theorem showing 𝑇(𝑛) ∈ Ω(𝑔(𝑛)) for the same
function 𝑔(𝑛) guessed above.
Hint: use sentinels to simplify the analysis.

Let's assume that 𝑇(𝑘) ≥ c * 𝑔(𝑘) for all k >= n_0 where n_0 is some constant, and c is some
positive constant.
Then,
𝑇(𝑛) = 2𝑇 (⌊(𝑛/8)⌋) + cuberoot(𝑛) >= 2 * c * 𝑔(⌊(𝑛/8)⌋) + cuberoot(𝑛)
= 2 * c * cuberoot(⌊(𝑛/8)⌋) + cuberoot(𝑛) >= 2 * c * cuberoot(𝑛 / 8) + cuberoot(𝑛)
= cuberoot(𝑛) * (2 * c + 1).

So, 𝑇(𝑛) >= c * 𝑔(𝑛) with c = cuberoot(𝑛) * (2 * c + 1).

Thus, we have proved by induction that 𝑇(𝑛) ∈ Ω(𝑔(𝑛))

11. (1 point) Now use the Master Theorem to solve the same recurrence.

The Master Theorem states that if 𝑇(𝑛) is a recurrence of the form:


𝑇(𝑛) = 𝑎𝑇(⌊𝑛/𝑏⌋) + 𝑛^𝑘, where 𝑎 ≥ 1, 𝑏 > 1 and 𝑘 ≥ 0,
then
● 𝑇(𝑛) = 𝑂(𝑛^𝑘) if 𝑎𝑏^(𝑘−1) < 1,
● 𝑇(𝑛) = 𝑂(𝑛^𝑘 log 𝑛) if 𝑎𝑏^(𝑘−1) = 1,
● 𝑇(𝑛) = 𝑂(𝑛^(log_𝑏𝑎)) if 𝑎𝑏^(𝑘−1) > 1.
Applying this to our recurrence 𝑇(𝑛) = 2𝑇 (⌊(𝑛/8)⌋) + 𝑐uberoot(𝑛), we have 𝑎 = 2, 𝑏 = 8, and 𝑘 =
1/3.
Since 𝑎𝑏^(𝑘−1) = 2 * 8^(1/3 - 1)
= 2 * 2^3
= 16 > 1,
we have
𝑇(𝑛) = 𝑂(𝑛^(log_𝑏𝑎))
= 𝑂(𝑛^(log_8 2))
= 𝑂(𝑛^3).
Therefore, the tight (Big-Theta) worst-case complexity of 𝑇(𝑛) is 𝑂(𝑛^3).

Finally, consider the following observation on the randomized Quicksort algorithm, where
the pivot is chosen uniformly at random from the 𝑛 input elements. Because of the way the
pivot is chosen, its position on the sorted list lies between 𝑛/4 and 3𝑛/4 (i.e. closer to the
middle than to the ends of the list, in which case it is called a good pivot) with probability
1/2, and its position lies between 1 and 𝑛/4 or between 3𝑛/4 and 𝑛 (i.e. closer to one of the
ends than to the middle of the list, in which case it is called a bad pivot) with probability
1/2.

12. (bonus: 2 points) State a recurrence that expresses the worst case for bad pivots and
another recurrence that expresses the worst case for good pivots.

The recurrence for bad pivots:


T_bad(n) = 2T_bad(n/4) + O(n)

The recurrence for good pivots:


T_good(n) = 2T_good(n/2) + O(n)

13. (bonus: 3 points) Prove by induction that the arithmetic mean of the two recurrences
above, which expresses the expected worst case for any pivot, is Θ(𝑛 log 𝑛)

Let's assume that T(n) is the expected time complexity of the randomized Quicksort algorithm
when the input size is n. Then, we have:
T(n) = 1/2 T(n/4) + 1/2 T(3n/4) + c, where c is a constant representing the time spent on
partitioning.
The base case is T(1) = O(1).
Now, we will prove by induction that T(n) = Θ(n log n).

Base case:
For n = 1, T(1) = O(1) which is equal to O(1 log 1) = Θ(1 log 1).

Inductive step:
Assume that T(m) = Θ(m log m) for some m < n.
Then,
T(n) = 1/2 T(n/4) + 1/2 T(3n/4) + c
= 1/2 (n/4 log (n/4)) + 1/2 (3n/4 log (3n/4)) + c (by the induction hypothesis)
= n/2 (log (n/4) + log (3n/4)) + c
= n/2 (log n - log 4) + c
= n log n - n log 4 + c
= Θ(n log n).
Therefore, T(n) = Θ(n log n) for all n >= 1. This completes the induction proof.
References:

1. Recursive Algorithms, Slide 1 of TCSS 343 – Design and Analysis of Algorithms [Used
in Question 1]
2. Recurrences and Induction - TCSS 343 – Design and Analysis of Algorithms [Used in
Question 2]
3. Self-Reduction and Recursion, Slide 9 of TCSS 343 - Design and Analysis of Algorithms
[Used in Question 3]
4. Divide and Conquer Design, TCSS 343 - Design and Analysis of Algorithms [Used in
Question 5]
5. Sentinels, TCSS 343 - Design and Analysis of Algorithms [Used in Question 8]

You might also like