Note: Any FAQs or clarifications relevant to the assignment will be posted here. This post will be continually updated (with newer updates at the bottom of the page), so make sure to check on it regularly – click the “Watch” button in the top-right corner to receive notifications about this thread. If you have a question which is not already addressed on this page, create a new thread to post your question on Ed.
In this assignment, you apply what you’ve learned in the past few weeks about number theory, asymptotic function growth, and algorithm running time. In Part 1, you prove some mathematical statements involving familiar definitions. In Part 2, you use loop invariants to develop an interesting algorithm involving numbers and divisibility. And in Part 3, you analyze the running time of some code that we provide, as well as some code you wrote yourself.
honour_code.txt file included in the starter files on
MarkUs (more below) which gives an overview of common mistakes students
make in regards to academic integrity.To obtain the starter files for this assignment:
csc110/assignments/ folder.a3 folder that contains the
assignment’s starter files. This should look similar to what you had for
Assignment 1.This assignment contains a mixture of both written and programming
questions. All of your written work should be completed in the
a3.tex starter file using the LaTeX typesetting language
(hand-written work will not be graded). You went through the process of
getting started with LaTeX in the software installation guide, but for a
quick start we recommend using the online platform Overleaf for LaTeX. Overleaf also
provides many tutorials
to help you get started with LaTeX.
Your programming work should be completed in the starter file provided. We have provided code at the bottom of each file for running doctest examples and PythonTA on each file. We are not grading doctests on this assignment, but encourage you to add some as a way to understand each function we’ve asked you to complete. We are using PythonTA to grade your work, so please run that on the Python file you submit using the code we’ve provided in the main block.
Warning: one of the purposes of this assignment is to evaluate your understanding and mastery of the concepts that we have covered so far. So on this assignment, you may only use parts of the Python programming language that we have covered in the first 9 chapters of the course notes. Other parts are not allowed, and parts of your submissions that use them may receive a grade as low as zero for doing so.
First, complete this reading on a new proof technique (for this course, but you may have seen it in other courses already) called proof by contradiction.
Given a statement \(P\) to prove, rather than attempt to prove it directly we assume that its negation \(\neg P\) is true, and then use this assumption to prove a statement \(Q\) and its negation \(\neg Q\). We call \(Q\) and \(\neg Q\) the contradiction that arises from the assumption that \(P\) is false.
Why does this work? Essentially, we argue that if \(P\) is false, then statement \(Q\) must be true, but its negation \(\neg Q\) must also be true. But these two things can’t be true at the same time, and so our original assumption must be wrong!
Proofs by contradiction often take a bit more thought than other proof techniques, because it isn’t necessarily clear what the contradiction (statement \(Q\)) should be. As an example of a proof by contradiction, we present one particularly famous proof dating back to the Greek mathematician Euclid. This proof makes use of one of the most important theorems in number theory: the Fundamental Theorem of Arithmetic, which says that any integer greater than 1 can be written as a unique product of primes. For example:
\[\begin{align*} 2 &= 2 \\ 4 &= 2^2 \\ 100 &= 2^2 \cdot 5^2 \\ 1450 &= 2 \cdot 5^2 \cdot 19 \\ 8840 &= 2^3 \cdot 5 \cdot 13 \cdot 17 \\ \end{align*}\]
Now we can proceed with the proof by contradiction of the following theorem:
Theorem. There are infinitely many primes.
Proof. Assume that this statement is false, i.e., that there is a finite number of primes. Let \(k \in \N\) be the number of primes, and let \(p_1, p_2, \dots , p_k\) be the prime numbers. Our statement \(Q\) will be “for all \(n \in \N\), \(n\) is prime if and only if \(n\) is one of \(\{p_1, \dots , p_k\}\).” \(Q\) is true because of our assumption that there are a finite number of primes, and the definitions of \(k\) and \(p_1, \dots, p_k\). Now we will show that \(Q\) is false. Define the number \[ P = 1 + \prod_{i=1}^{k} p_i = 1 + p_1 \times p_2 \times \dots \times p_k. \] As \(P\) is greater than any of the primes in the set, \(P\) cannot be prime and there must be some prime \(p \neq P\) that divides \(P\), by the fundamental theorem of arithmetic.
If \(p\) is in the set \(\{p_1, \dots , p_k\}\), then \(p\) would divide both \(P\) and \(p_1 \times \dots \times p_k\), so it must divide its difference \(P - p_1 \times \dots \times p_k = 1\). However, no prime can divide 1, so \(p \notin \{p_1, \dots , p_k\}\). So then \(p\) is a prime that is not one of \(\{p_1, \dots, p_k\}\), and so \(Q\) is false. Contradiction!
□
For this part, complete the following four proofs. You may use any results presented in the course notes or in class, as long as you explictly name the result, for example by saying “by the GCD characterization theorem”, or “by the theorem from lecture 8B relating modular equivalence and remainders”.
Prove by contradiction that for all integers \(n\), 4 does not divide \(n^2 - 2\).
Prove the following statement: \(\forall a, k, n \in \Z,~ \gcd(a, n) = 1 \Rightarrow \gcd(a + kn, n) = 1\).
Hint: as part of your proof, let \(e \in \Z\) and assume \(e \mid a + kn\) and \(e \mid n\), and prove that \(e \leq 1\).
Prove the following statement: \(\log_{4}n - \log_{12}n \in \Omega (\log_{14}n)\).
Hint: Review rules for changing the base of a logarithm.
The floor function \(\floor \cdot\) takes a real number \(x\) and returns the largest integer that is less than or equal to \(x\). You are given the following property of the floor function:
\[\forall x \in \R,~ \floor{x} \leq x < \floor{x} + 1\]
For each function \(f: \N \to \R^{\geq 0}\), we define its corresponding floor function, \(\floor{f}\), to be:
\[(\floor{f})(n) = \floor{f(n)} \quad \text{for all $n \in \N$}\]
Prove that for all functions \(f, g: \N \to \R^{\geq 0}\), if \(g \in \cO(f)\) and if for all \(m \in \N\), \(f(m) \geq 1\), then \(g \in \cO(\floor f)\).
Hints:
In lecture, we saw how more complex algorithms like the Extended Euclidean Algorithm can use while loops and variable updates in very non-obvious ways. To understand how these while loops actually accomplish a given computational task, we introduced loop invariants. In this part of the assignment, we explore the technique of using loop invariants in more detail to develop a new number-theoretic algorithm that is more efficient than a simple “brute force” approach.
Before starting Part 2, we recommend that you review:
Let’s start with a simple problem: generating a sorted list of all positive integers up to a given limit that are coprime to both 2 and 3. Here is one possible implementation of such a function; our first goal will be to understand how this function works.
def coprime_to_2_and_3(n: int) -> list[int]:
"""Return the natural numbers less than n that are coprime to both 2 and 3.
The returned list is sorted.
Preconditions:
- n >= 6
>>> coprime_to_2_and_3(20)
[1, 5, 7, 11, 13, 17, 19]
Implementation note: negative list indexing accesses items at the end of
the list. For all lists lst and integers i between 0 and len(lst) - 1
inclusive, lst[-i] == lst[len(lst) - i].
"""
nums_so_far = [1, 5]
while nums_so_far[-2] + 6 < n:
next_number = nums_so_far[-2] + 6
nums_so_far.append(next_number)
return nums_so_farTo prove that this algorithm works, we need to use some loop invariants to understand the while loop.
k in
nums_so_far is coprime to 2 and coprime to 3.i between 0 and
len(nums_so_far) - 3 inclusive,
nums_so_far[i] + 6 == nums_so_far[i + 2].i between 0 and
len(nums_so_far) - 2 inclusive,
nums_so_far[i] < nums_so_far[i + 1] (this means that
nums_so_far is always sorted).k between 0 and nums_so_far[-1]
inclusive, if k is coprime to 2 and coprime to 3, then
k in nums_so_far.Together, these loop invariants allow us to conclude that
nums_so_far is always a sorted list of the natural numbers
between 1 and nums_so_far[-1] that are coprime
to 2 and to 3. The loop ends when
nums_so_far[-2] + 6 >= n; using these loop invariants,
it is possible to prove (although we won’t do it here) that when the
loop ends, nums_so_far consists of exactly the positive
integers < n that are coprime to n.
(not to be handed in, but VERY useful!) Consider the
call coprime_to_2_and_3(20).
next_number and
nums_so_far after each iteration).nums_so_far,
[1, 5].nums_so_far in your loop
accumulation table.In a3_part2.py, translate each of the four loop
invariants into a Python assert statement at the top of the loop
body.
Each of these invariants should be written as just a single Python
expression, but can be split across multiple lines to keep within the
120-character line length limit. (A good convention here is to start the
for ... in ... part of a comprehension on a new line.) Use
math.gcd if you need to compute gcds.
Next, in a3.tex, you prove that each of these loop
invariants hold. To do this, pick an arbitrary loop iteration and
assume that the invariant is true at the start of the loop
iteration, and then prove that it is still true at the end of the
iteration. You may use any loop invariant you’ve already proved to prove
the following ones.
Note: Because these proofs involve code, your proofs will contain English sentences describing what the code is doing, in addition to some mathematical statements (e.g., about divisibility).
The coprime_to_2_and_3 function can be changed to be
more general. We can write a function called coprime_to_all
that is not limited to finding the coprimes of just 2 and 3, but of a
set of prime numbers. Below is a more fleshed out definition:
def coprime_to_all(primes: set[int], n: int) -> list[int]:
"""Return a sorted list of all natural numbers less than n that are coprime to every number in primes.
Preconditions:
- primes != set()
- every element of primes is a prime number
- n >= math.prod(primes)
>>> coprime_to_all({2, 3}, 20)
[1, 5, 7, 11, 13, 17, 19]
>>> coprime_to_all({2, 3, 7}, 50)
[1, 5, 11, 13, 17, 19, 23, 25, 29, 31, 37, 41, 43, 47]
"""Notice that the first doctest example produces the same result as
coprime_to_2_and_3 when n=20. Notice that the
second doctest example allows the caller to specify a larger set of
prime numbers (in this case: 2, 3, and 7).
Using what you’ve learned from the previous section, implement
coprime_to_all using a generalization of the
implementation we’ve provided for coprime_to_2_and_3. Here,
“generalization” means that your implementation follows the same
algorithm as coprime_to_2_and_3, but is modified to work
with a larger set of primes.
See a3_part2.py for details. Make sure you understand
the given helper function first before proceeding.
Write generalized versions of Loop Invariants 1 to
4 from coprime_to_2_and_3 as Python assertions at
the top of your while loop in coprime_to_all.
Note: You do not need to prove any loop invariants for this algorithm, we just want to see the Python expressions here.
Next, you’ll conduct a few running-time analyses on some functions as
well as the algorithms you studied and implemented in the previous part.
Complete all your work for this part in a3.tex.
In your running-time analyses, you should provide the exact step count (e.g., \(n^2 + 2n + 4\), with no remaining summations left in the step count) with justification for this step count, as well as a Theta expression (e.g., \(\Theta(n^2)\)). This matches the style of running-time analysis we’ve seen in lecture.
Note: ceilings/floors may be necessary to get the exact number of steps.
Note: for big O/Omega, you should also provide upper/lower bounds on step counts with justification, then translate this directly to the big O or Omega expression.
Note: you do not need to provide any formal proof or even justification for translating the exact count to its associated Theta expression.
Analyze the running time of coprime_to_2_and_3 in
terms of its input value \(n\). To make
the math a bit simpler, you are only required to find a tight asymptotic
upper bound (“at most”, Big-O) on the running time. By “tight”
we mean that it should be possible to prove that the algorithm is Theta
of whatever bound you choose, but you are not required to prove that
here (only Big-O).
In your running-time analysis, ignore the running time of the assert statements you added in Part 2, Question 2.
Analyze the running time of the provided helper function
starting_coprime_numbers in terms of two variables: \(P\), the size of the input set
primes, and \(m\), the
product of the numbers in primes. You need to find a Theta
bound here.
That is, your final expression should be a Theta bound involving \(P\) and/or \(m\) (e.g., \(\Theta(P + m)\)). Do not make any assumptions about the mathematical relationship between \(P\) and \(m\)—such relationships exist, but for this question we want you to treat them as independent variables.
Analyze the running time of f1 (below) in terms of
its input value \(n\).
def f1(n: int) -> None:
i = 1
while i < n: # Loop 1
j = i
while j > 1: # Loop 2
k = 0
while k < n: # Loop 3
k = k + 4
j = j // 2
i = i * 2Analyze the running time of f2 (below) in terms of
the size of the list \(lst\).
def f2(lst: list) -> None:
n = len(lst)
i = 0
while i ** 2 < n:
if i not in lst:
lst.insert(0, i)
i = i + 1Hint: Since the number of steps varies depending on the value of the inputs, this requires a worst-case running time analysis.
The running time of f3 (below) in terms of its input
value \(n\) is \(\Theta(n \log n)\). However, counting the
exact number of steps to show this Theta bound is difficult. Instead,
complete this analysis by showing that it has a matching upper bound
(“at most”, \(O(n \log n)\)) and lower
bound (“at least”, \(\Omega(n \log
n)\)).
def f3(n: int) -> None:
for i in range(n):
j = i
while j > 0:
j //= 2Hint: Remember that for upper bounds and lower bounds you do not need to count the exact number of steps, and can over- and underestimate, respectively. For the lower bound, try ignoring the steps from some iterations of the for loop.
Please proofread, test, and fix PythonTA errors in your work before your final submission!
Login to MarkUs.
Go to Assignment 3, then the “Submissions” tab.
Submit the following files: a3.tex,
a3.pdf (which must be generated from your
a3.tex file), a3_part2.py, and
honour_code.txt. Please note that MarkUs is picky with
filenames, and so your filenames must match these exactly, including
using lowercase letters.
Note: for your Python code files, please make sure they run (in the Python console) before submitting them! Code submissions that do not run will receive a grade of zero for that part, which we do not want to happen to any of you, so please check your work carefully.
Refresh the page, and then download each file to make sure you submitted the right version.
Remember, you can submit your files multiple times before the due date. So you can aim to submit your work early, and if you find an error or a place to improve before the due date, you can still make your changes and resubmit your work.
After you’ve submitted your work, please give yourself a well-deserved pat on the back and do something fun or eat some chocolate or warm up under a blanket! Holy cat, you’ve just completed your last assignment for CSC110.
