\( \newcommand{\NOT}{\neg} \newcommand{\AND}{\wedge} \newcommand{\OR}{\vee} \newcommand{\XOR}{\oplus} \newcommand{\IMP}{\Rightarrow} \newcommand{\IFF}{\Leftrightarrow} \newcommand{\TRUE}{\text{True}\xspace} \newcommand{\FALSE}{\text{False}\xspace} \newcommand{\IN}{\,{\in}\,} \newcommand{\NOTIN}{\,{\notin}\,} \newcommand{\TO}{\rightarrow} \newcommand{\DIV}{\mid} \newcommand{\NDIV}{\nmid} \newcommand{\MOD}[1]{\pmod{#1}} \newcommand{\MODS}[1]{\ (\text{mod}\ #1)} \newcommand{\N}{\mathbb N} \newcommand{\Z}{\mathbb Z} \newcommand{\Q}{\mathbb Q} \newcommand{\R}{\mathbb R} \newcommand{\C}{\mathbb C} \newcommand{\cA}{\mathcal A} \newcommand{\cB}{\mathcal B} \newcommand{\cC}{\mathcal C} \newcommand{\cD}{\mathcal D} \newcommand{\cE}{\mathcal E} \newcommand{\cF}{\mathcal F} \newcommand{\cG}{\mathcal G} \newcommand{\cH}{\mathcal H} \newcommand{\cI}{\mathcal I} \newcommand{\cJ}{\mathcal J} \newcommand{\cL}{\mathcal L} \newcommand{\cK}{\mathcal K} \newcommand{\cN}{\mathcal N} \newcommand{\cO}{\mathcal O} \newcommand{\cP}{\mathcal P} \newcommand{\cQ}{\mathcal Q} \newcommand{\cS}{\mathcal S} \newcommand{\cT}{\mathcal T} \newcommand{\cV}{\mathcal V} \newcommand{\cW}{\mathcal W} \newcommand{\cZ}{\mathcal Z} \newcommand{\emp}{\emptyset} \newcommand{\bs}{\backslash} \newcommand{\floor}[1]{\left \lfloor #1 \right \rfloor} \newcommand{\bigfloor}[1]{\Big \lfloor #1 \Big \rfloor} \newcommand{\ceil}[1]{\left \lceil #1 \right \rceil} \newcommand{\bigceil}[1]{\Big \lceil #1 \Big \rceil} \newcommand{\abs}[1]{\left | #1 \right |} \newcommand{\bigabs}[1]{\Big | #1 \Big |} \newcommand{\xspace}{} \newcommand{\proofheader}[1]{\underline{\textbf{#1}}} \)

CSC110 Fall 2025 Assignment 3

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.

Logistics

Starter files

To obtain the starter files for this assignment:

  1. Click this link to download the starter files for this assignment. This will download a zip file to your computer.
  2. Extract the contents of this zip file into your csc110/assignments/ folder.
  3. You should see a new a3 folder that contains the assignment’s starter files. This should look similar to what you had for Assignment 1.

General instructions

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.

Part 1: Proofs

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.

Reading: 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”.

  1. Prove by contradiction that for all integers \(n\), 4 does not divide \(n^2 - 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\).

  3. 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.

  4. 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:

Part 2: Generating Coprime Numbers

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:

Numbers coprime to 2 and 3

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_far

To prove that this algorithm works, we need to use some loop invariants to understand the while loop.

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.

  1. (not to be handed in, but VERY useful!) Consider the call coprime_to_2_and_3(20).

    1. Write the loop accumulation table for this call (showing the number of iterations and the value of next_number and nums_so_far after each iteration).
    2. Verify that all four of these loop invariants hold for the initial value of nums_so_far, [1, 5].
    3. Verify that all four of these loop invariants hold for every value of nums_so_far in your loop accumulation table.
  2. 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.

  3. 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).

    1. Prove that Loop Invariant 1 holds. You may use the statement you proved in Part 1, Question 2.
    2. Prove that Loop Invariant 2 holds.
    3. Prove that Loop Invariant 3 holds. (Hint: use Loop Invariant 2.)
    4. (hardest) Prove that Loop Invariant 4 holds. You may use the statement you proved in Part 1, Question 2.

Numbers coprime to a set of primes

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).

  1. 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.

  2. 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.

Part 3: Running-Time Analysis

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.

  1. 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.

  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.

  3. 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 * 2
  4. Analyze 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 + 1

    Hint: Since the number of steps varies depending on the value of the inputs, this requires a worst-case running time analysis.

  5. 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 //= 2

    Hint: 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.

Submission instructions

Please proofread, test, and fix PythonTA errors in your work before your final submission!

  1. Login to MarkUs.

  2. Go to Assignment 3, then the “Submissions” tab.

  3. 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.

  4. 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.

Strawberry under a blanket