CSCI 150 - Discrete Mathematics
The math for computer science and every day...

Programming Explorations and Fun Activities (optional, only for those who are interested)
[Integer games]
- Write a function that takes a positive integer $n$ and outputs the Collatz sequence starting with $n$ and ending with $1$. Recall that, given a positive integer $n$, the next number is either $n/2$ if $n$ is even, or $3n+1$ if $n$ is odd. Example: 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1.
Additional idea: Replace the sequence by a binary sequence as follows: If the number goes up, place a 1, and if the number goes down, place a 0. The above sequence becomes: 1 0 0 1 0 0 0 1 0 0 0 0. Explore how different initial numbers lead to different binary patterns. How many 1s are there? What's the average distance between consecutive 1s? Etc...
- Write a function that takes a list of integers $(a_1, a_2, \ldots, a_n)$ of some length $n$ and outputs its Ducci sequence until $(0, 0, \ldots, 0)$ (such algorithm might not stop in general, but it will if $n=4$). Recall that $(a_1, a_2, \ldots, a_n)\rightarrow (|a_1-a_2|, |a_2-a_3|, \ldots, |a_n-a_1|)$. Example: $(1,2,3,4), (1, 1, 1, 3), (0,0,2,2), (0,2,0,2), (2,2,2,2), (0,0,0,0)$.
Additional idea: Find examples for $n\neq 4$ where the sequence never ends. For instance, experiment with $n=3$.

[Listing pairs]
Recall the socks example. Write a function that takes an integer $n$ and outputs all possible pairs; for instance, when $n=5$, the output should look like:

   x x - - -
   x - x - -
   x - - x -
   x - - - x
   
   - x x - -
   - x - x -
   - x - - x
   
   - - x x -
   - - x - x
   
   - - - x x
Additional idea: Can you modify the function to display only pairs that are at a minimum distance $d$? For instance, if the minimum distance is $d=2$, the above example for $n=5$ will become:
   x - x - -
   x - - x -
   x - - - x

   - x - x -
   - x - - x

   - - x - x
Explore how that changes the number of pairs from $n(n-1)/2$. Find a formula in terms of $n$ and $d$.

[Snakes and Ladders]
A snakes and ladders board can be encoded using an array $a[0\ldots n]$, where $a[0]=0$ and $a[n]=n$, and $a[i]=j$ for $0\lt i \lt n$ such that:
 $j\gt i$ means there is a ladder from square $i$ to square $j$, and
 $j\lt i$ means there is a snake with its head on $i$ and tail on $j$, and
 $j=i$ means that square $i$ is just a regular square.
Using such array, one can start at position $0$, and repeatedly obtain a random number in $[1,6]$ and advance the position by that number (if the new position is greater than $n$, the position is unchanged). When in position $i$, $a[i]$ is checked to see if one has to take a ladder or a snake (and update the position accordingly) or simply stay. Depending on the array $a$, one might have to repeatedly check $a[i]$ until $a[i]=i$ (for instance, if $a[i]=j$ and $a[j]=k$, etc...). When positision $n$ is reached, the game ends. Initialize the array $a[0\ldots n]$ to encode the board shown in Slides 3, and simulate the snakes and ladders game multiple times to find the average number of turns needed to finish.
Additional idea: you can explore the average number of turns for a fixed number of snakes and ladders that are placed randomly, and compare that to a board that has none.

[The next permutation]
Write a function that, given a permutation of $(1,2,\ldots,n)$, will change it to the next permutation. For example, if the permutation is $(1,4,6,2,9,5,8,7,3)$ (here $n=9$), it will become $(1,4,6,2,9,7,3,5,8)$, which is the permutation that should appear next in "alphabetical order"; we call it lexicographic order. Here's an algorithm of how this may be done assuming the permutation is stored in an array.
1. Find the largest $i$ such that $a[i]\lt a[i+1]$
2. Find the largest $j$ such that $a[j]\gt a[i]$
3. If $i$ and $j$ are within bounds, swap $a[i]$ and $a[j]$
4. Reverse $a[i+1\ldots n-1]$
Additional idea: You can use this function to generate all anagrams of a given word $w$ (even when letters repeat). Simply replace $``\lt"$ with its appropriate meaning for letters. Alternatively, you could start with an array representing the word; for instance, "mathematics" corresponds to $[0,1,2,3,4,0,1,2,5,6,7]$. You can then output $w[a[0]], w[a[1]], \ldots, w[a[n-1]]$, and repeatedly obtain the next $a$ and output letters of $w$. You stop when you reach the original $a$ again.

[Integer solutions]
By now you know how to count the integer solutions to $x_1+x_2+\ldots+x_n=k$, where $x_i\in\{0,1,2,3,\ldots\}$. In this programming exercise, you will write a function to count the number of integer solutions when $x_i\in \{a,b\}$, where $0\leq a\lt b$ are two integers. Therefore, your function takes as parameters four integers $n$, $k$, $a$, and $b$, and returns the number of integer solutions to $x_1+x_2+\ldots+x_n=k$ if each $x_i\in\{a,b\}$. Hint: Making the function recursive is very helpful here. We are not after a closed form expression for the number, just a program to compute it. If we define this number of be $T(n,k)$, the following recurrence will help figure out the recursion: $T(n,k) = T(n-1, k-a) + T(n-1,k-b)$ (why?). Appropriate base cases must be used of course to stop the recursion.
Additional idea: Can you figure out what $T(n,k)$ is for special cases; for instance, what is $T(n,k)$ when $a=0$ and $b=1$? You are welcome to think about other special cases.
Extra idea: The recursive implementation, while easy, is not efficient. For instance, it recomputes the same quantities multiple times. Here's an example: Assume $a=0$ and $b=1$. Then, $T(n,k)=T(n-1,k)+T(n-1,k-1)$ $=[T(n-2,k)+T(n-2,k-1)]+[T(n-2,k-1)+T(n-2,k-2)]=\ldots$. You can see that $T(n-2,k-1)$ has already appeared twice. A more efficient implementation uses a two-dimensional array $T$ of size $(n+1)\times(k+1)$ to store computed values, where $T(i,j)$ is computed by accessing the pre-computed entries $T(i-1,j-a)$ and $T(i-1,j-b)$. Therefore, each entry is computed in terms of previously computed entries for smaller $i$ and $j$, starting with $T(0,0)$. When we find $T(n,k)$, we are done. This is the power of memoization!

[The Pascal triangle walk]
Given an integer $n\geq 0$, and two integers $a\geq 0$ and $b\geq 0$ such that $a+b>0$, start on the first number in the $n^{th}$ row of the Pascal triangle. Then move $a$ steps to the right, and $b$ steps diagonally up, to land on another number. Repeat this process until you exit the Pascal triangle. Write a function to compute the sum of all the numbers you land on before you exit. Find interesting patterns for special cases of $a$ and $b$. For instance, what do you get when $a=1$ and $b=0$, or when $a=0$ and $b=1$, or when $a=1$ and $b=1$? Avoid computing $\binom{n}{k}$ explicitly, so to compute the number that is $a$ steps away to the right and $b$ steps away up, update the current number by making appropriate multiplications and divisions. To do this, think about how to get the number that is one step away to the right, and how to get the number that is one step away up.
Extra idea: Instead of starting on the first number in row $n$, let $0\leq k\leq n$ be another integer input, and start on the $k^{th}$ number in row $n$. Experiment with different values of $n$ and $k$ for $a=0$ and $b=1$. What do you observe?

[Playing with arrays]
- Let $a$ be an array of integers of size $n$, where $a$ is supposed to encode a function $f:\{0,\ldots, n-1\}\rightarrow\{0,\ldots,n-1\}$. For instance, if $a[i]=j$, then this means $f(i)=j$. Write a function to test whether $f$ is one-to-one, and a function to test whether $f$ is onto. Extra idea: Given two such arrays $a$ and $b$ encoding functions $f$ and $g$, construct an array $c$ that represents $f \circ g$. Note $f\circ g(x)=f(g(x))$. Check the three properties: one-to-one, onto, and bijection. Which of them carry over; for instance, if $f$ and $g$ both satisfy a property, does that mean $f\circ g$ will? Similarly, if $f\circ g$ satisfies a property, does that mean either $f$ or $g$ must?
- Given an array $a$ as described above where all elements are in $\{0,\ldots,n-1\}$, imagine the following jumping game: start at 0 and move to $i=a[0]$. Then move to $j=a[i]$, and so on until you come back to 0 (you may never come back to 0). Write a function that determines whether the jumping game ends. Your function must always terminate with a decision. Extra challenge: what if your function cannot modify the array, and cannot use more than a constant amount of memory in addition to the array itself (so it cannot copy the array, for instance)?
- Write a function that takes a non-negative integer as input and returns an 0-1 array containing its binary representation (the same integer in base 2).

[Boolean functions]
Write a function that accepts an array of 8 bits $[b_0, b_1, ..., b_7]$, and outputs the Boolean function $f(x,y,z)$ corresponding to $f(0,0,0)=b_0, f(0,0,1)=b_1,\ldots, f(1,1,1)=b_7$. For example, if the array is $[0,0,0,1,0,1,1,1]$, the output will be: (~x /\ y /\ z) \/ (x /\ ~y /\ z) \/ (x /\ y /\ ~z) \/ (x /\ y /\ z).
Extra idea: choose whether to encode the function based on the number of 0s or the number of 1s, whichever leads to a smaller representation. For instance, if we have fewer zeros, then negate the array, obtain the Boolean function, and finally negate it (using DeMorgan's law). Here's an example: Given [0,1,1,1,0,1,1,1], we would negate this to get [1,0,0,0,1,0,0,0]. This gives the Boolean function (~x /\ ~y /\ ~z) \/ (x /\ ~y /\ ~z). Negating this, we get (x \/ y \/ z) /\ (~x \/ y \/ z).

[Prime numbers]
We have seen in class that primes are infinite. One interesting computer program is to generate all primes less than a given integer $n$. A simple way to do this is the sieve of Erathosthenes, which is essentially this: Start with an array $a[0...n-1]$ of all 1s and make $a[0]=a[1]=0$ indicating that 0 and 1 are not prime. The first prime is therefore 2, with $a[2]=1$ and let $p=2$. The program makes $a[i]=0$ for $i = 2p, 3p, 4p,\ldots$ i.e. every $i$ that is a multiple of $p$ (except $p$ itself), indicating that these numbers cannot be prime, and thus eliminating them. Then $p$ is updated by scanning the array to find the next prime, which is first number $i>p$ such that $a[i]$ is still 1. The process of elimination is repeated until the entire array has been scanned. Finally, $a[i]=1$ means that $i$ is prime for all $0\le i\le n-1$. Implement this idea. Note 1: For large $n$, this becomes not very practical in terms of memeory (but time is almost linear in $n$). Note 2: You can google the sieve of Erathosthenes to learn more about it and explore alternatives.

[Divisibility]
Write a function that accepts a positive integer $n$, and an array $p$ of length $k$, such that $p[0], p[1], \ldots, p[k-1]$ are all prime, and returns the number of integers in $\{1,2,\ldots, n\}$ that are divisible by any $p[i]$, $i=0,\ldots,k-1$. Explore three approaches:
- (Approach 1) The brute force approach: go through every integer $m$ in $\{1,\ldots,n\}$ and check if $m$ modulo $p[i]$ is 0 for any $i$. This essentially consists of two nested loops
    for m in range(1,n+1):
      for i in range(0,k):
        #check if m modulo some p[i] is zero and increment a count
This will be $O(nk)$ time, which is pretty slow especially when $n$ is large; for instance, try it on $n=1000000000$ (a billion) and $p=[2,3,5]$, and see how long it takes in seconds.
- (Approach 2) The inclusion-exclusion approach: For every non-empty subset S of $\{0,\ldots, k-1\}$, find $\Big\lfloor \frac{n}{\prod_{i\in S}p[i]}\Big\rfloor$ and sum these up with the appropriate sign. This is $O(k2^k)$ time, which is better if $k$ is less than logarithmic in $n$; for instance, if $k$ is just a constant like 3. The difficulty of this approach lies in being able to go through all subsets. One way to do this is to go through all numbers in $[1,2^k-1]$, convert each to its binary representation, and if the $i^{th}$ bit is 1, then $p[i]$ is included in the product.
- (Approach 3) If every $p[i]$, for $i=0,\ldots,k-1$, is a prime factor of $n$, find a very fast way of achieving the same task.

[Twos, Threes, and Fives]
Imagine a world where the prime numbers are only 2, 3, and 5. Then there is no 7, 11, 13, or even 14 (because $14=2\times7$ and there is no 7). Of course this makes no sense, but what if we want to generate all numbers that have no prime factors other than 2, 3, and 5? Here's how the list looks like: $1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, \ldots$
Using an array of size $n$, generate the first $n$ integers of this sequence. In other words, the first $n$ positive integers that are the product of just 2s, 3s, and 5s. Explore two approaches.
- (Approach 1) One way to do this is to go through the integers in $\mathbb{N}=\{1, 2, 3,\ldots\}$ one at a time and check for each whether it is a product of 2s, 3s, and 5s, until you fill the $n$ entries of the array.
- (Approach 2) A more efficient way is based on the following idea: Assume that you have generated the sequence up to some integer $m$ and you are about to generate the next integer, call it $w$. The integer $w$ must be either $2x$, $3y$, or $5z$, for some $x$, or $y$, or $z$ that has been generated already. Furthermore, $w$ must be the smallest such integer greater than $m$. Therefore, it is enough to know the smallest $x$ among those generated such that $2x\gt m$, and similarly, the smallest $y$ such that $3y\gt m$ and the smallest $z$ such that $5z\gt m$. So what if we just keep track of the indices of these three as we fill the array? Initially, $a[0]=1$, and the indices of $x$, $y$, and $z$ are all $0$, meaning $x=y=z=a[0]=1$. The next number of be generated is the smallest among $2x$, $3y$, and $5z$. It is obviously $2x=2$. So $a[1]=2$, and the index of $x$ is incremented by 1, making $x=a[1]=2$. There will be times where more than one among $x$, $y$, and $z$ can generate the next number $w$; for instance, when $2x=3y$ (what do we do?).

[Right triangles]
This problem was designed with the idea of pigeonhole in mind. In an $n\times n$ grid, place $2n-1$ points. Then three of the points must make a right triangle (can you prove this fact?). To encode the grid and the points, you may consider a two dimensional array of size $n\times n$ initialized to all 0s, and randomly make $2n-1$ of the entries 1s. Write a program to find a right triangle (three 1s that make a right triangle). You can output the right triangle in any way you want; for instance, you can display the grid with $2n-4$ points represented by "." and the three points of the right triangle by "o". Alternatively, you can simply output the coordinates of the three 1s making the right triangle. Hint: To find a right triangle, you should know that there must be one with two 1s in the same row and two 1s in the same column.
Additional ideas: Your algorithm to find a right triangle needs not be efficient. For instance, the two dimensional array occupies $O(n^2)$ memeory, even though only $2n-1$ points are needed. Furthermore, the search for the right triangle can be exhaustive. But how can we make the algorithm efficient? In addition, if one point moves, how can we find a new triangle quickly?
For an illustration of this, check here (make the width of your browser narrow to see the entire grid).

[Trominos]
A classical proof by induction establishes that a two dimensional array a of size $2^n\times2^n$ with one missing square can be tiled with L-shaped trominos. Look for this classical proof, which suggests how to tile the array with trominos. Then write a recursive function that takes $n$, $i$, $j$, $k$, and $l$ as parameters and returns the id of the tromino that covers $a[k,l]$ if $a[i,j]$ is the missing square. The requirement is that the function must return the same positive integer id for all squares that are covered by the same L-shaped tromino. If $(k,l)=(i,j)$, return 0. Remark: This is for those who want to experiment with a non-trivial handling of recursive information. I had to think about this one!

[Recurrence]
Write a function that takes the parameters $a_0$, $a_1$, $A$, $B$, and $n$ and returns $a_n$ assuming that $a_n$ satisfies the recurrence $a_n = Aa_{n-1} + Ba_{n-2}$. Make two version of this function, one recursive, and one that uses an array $a[0\ldots n]$. Compare the performance of the two functions in terms of running time on large values of $n$.
Extra idea: Use the characteristic equation method to find $a_n$.

[Euclidean Algorithm]
Implement the Euclidean algorithm: Given two integers $a$ and $b$, write a function that returns $\gcd(a,b)$.
(Approach 1) Use recursion: your implementation will rely on the idea that $\gcd(a,b)=gcd(b,a\bmod b)$, with the base case given when $b=0$.
(Approach 2) Use iteration: Keep two variables $a$ and $b$, and repeatedly make the change $a\leftarrow b$ and $b\leftarrow a\bmod b$, until $b$ becomes 0.
Extra Idea: Compute the greatest common divisor of multiple integers: The input to your function is an array of length $n$, and the function should return $\gcd(a[0], \ldots, a[n-1])$.
(Approach 1): Use the fact that $\gcd(a,b,c)=\gcd(\gcd(a,b),c)$.
(Approach 2): Generalize the Euclidean algorithm to handle $n$ integers (instead of just two). You start with the array $a[0\ldots n-1]$ such that $a[0]\gt a[1]\gt\ldots\gt a[n-1]$ (although this order is not really necessary), and two indices $i=0$ and $j=n-1$. Both $i$ and $j$ advance circularly throughout the execution of the algorithm. In other words, if we advance $i$, we make $i\leftarrow (i+1)\bmod n$. We then repeatedly perform the following. Find $a[i]\bmod a[j]$. If the result is non-zero, advance both $i$ and $j$ and write the result in $a[j]$. If the result is zero, only advance $i$ and do not change the array. When $i=j$, $a[i]$ holds the greatest common divisor. Try this algorithm when $n=2$ as well.

[Inverse]
Write a function that takes two positive integers $a$ and $n$ and returns the inverse of $a$ modulo $n$ if it exists. In other words, an integer $b$, such that $ab\equiv 1\ (\bmod n)$ (we typically denote $b$ by $a^{-1}$). If such an integer does not exist, your function can return 0.

[Program termination]
Implement the following program and run it several times and observe that it always terminates. In each iteration, print the tuple $(z,x)$ to observe that it's decreasing according to some partial order relation (which one?). Here's the pseudocode for the program (modify as needed):
x = rand(1, n)
y = rand(1, n)
z = rand(1, n)
while x>0 and y>0 and z>0
  control = rand(1,2)
  if control == 1
    x = rand(x, n)
    y = rand(y, n)
    z = z-1
  else
    y = rand(y, n)
    x = x-1

[Primality testing]
Implement the prime test algorithm based on Fermat's theorem to decide whether an integer $n$ is prime or not. Use repeated squaring and modulo $n$ calculations.