CMSC 27100 — Lecture 1

Discrete mathematics and computer science

Discrete mathematics is the mathematics of discrete structures. What do we mean by discrete structures? By discrete, we mean things that can be thought of as distinct chunks or units, like whole numbers and counting. That sounds pretty elementary and it's an interesting quirk that the educational system seems to be eager to shuffle us off into things like fractions, "decimals", lines, and so on. These things are not discrete: lines are typically smooth and this smoothness is such a desirable feature that it plays a big role when we eventually get to calculus. Calculus is very concerned about continuous functions, which are very not discrete.

But why should we be concerned with discrete mathematics as computer scientists? There are a number of different levels from which we can examine this relationship. Here is a summary to start.

  1. We abstract and model problems that we want to solve using computer science as math problems, which are often discrete.
  2. We represent data in computer science as discrete structures.
  3. Proofs are solutions to math problems and programs are just very highly/formally specified proofs.
  4. We analyze our definitions and solutions by proving properties about them.

The most immediately obvious relationship is what happens when we ask students to complete a programming exercise in a class like CMSC 14100. For many people, programming has nothing to do with mathematics—after all, you're probably in this class after finishing CMSC 14100. Whether or not you took that class, we can think about what the problem solving process for a beginner programmer might look like. In the best case, we systematically think through modelling and abstracting our problem into a math problem and then try to solve that problem. What usually happens is that we throw ourselves against a wall trying things out until it works.

When people say that programming doesn't need math, this is what they're describing. You don't strictly need to know math, because you can just brute force your way into getting your program working. That's fine, but eventually, one gets tired of smashing their head against walls so much and you'd begin to wonder whether there was a better way.

This is the first level at which we can think about the connections between computer science and math. When we say that we want to solve problems using computer science, what we really want is a program that solves a mathematical problem that represents our "real" problem. Knowing more math gives us more options when mapping "real" problems to math problems.

Discrete structures and representation

What's the next step? At the top level, we model "real" problems onto mathematical problems. However, we then need to go a bit deeper and think about the structures that we use in order to this. That is, how is our data represented? In programming, we are introduced to the idea of types like integers, strings, and lists and even more complex types like trees. But these are discrete structures!

Here is the next idea: that many of the structures and representations in computer science are discrete. Hence, the need to understand the mathematics of discretre structures.

Let's start by talking about one of the discrete structures that we'll be examining in more detail later: the natural numbers. You may be familiar with the natural numbers already as "whole numbers" or maybe slightly more formally as "nonnegative integers" (we consider 0 to be a natural number, though this is disputed by various kinds of mathematicians).

For instance, here is a definition you will likely have run across recently as you began to learn how to program.

Let $n$ be a natural number. The factorial $n!$ is defined by \[n! = n \times (n-1) \times (n-2) \times \cdots \times 2 \times 1.\] This makes for a simple programming exercise.

def factorial(n: int) -> int:
    result: int = 1
    for i in range(1, n+1):
        result = result * i
    return result

However, you may also recall that the factorial function can be written recursively.

def factorial(n: int) -> int:
    if n == 0:
        return 1
    else:
        return n * factorial(n-1)

This function comes from the more formal definition for the factorial.

Let $n$ be a natural number. The factorial $n!$ is defined as follows:

Notice that this definition itself is already an algorithm. We can compute the factorial simply by using substitution and the definition.

What's $3!$? According to the definition, $3! = 3 \times 2!$. Okay, so what's $2!$? According to the definition, $2! = 2 \times 1!$. But what's $1!$? According to the definition, $1! = 1 \times 0!$. But what's $0!$? According to the definition, $0! = 1$. Putting it all together, we have the following. \begin{align*} 3! &= 3 \times 2! \\ &= 3 \times (2 \times 1!) \\ &= 3 \times (2 \times (1 \times 0!)) \\ &= 3 \times (2 \times (1 \times 1)) \end{align*}

Why does this definition work? We recall that recursive functions are computations on recursive data. So the definition of the factorial gets its structure from the formal mathematical definition for the natural numbers, due to the Italian mathematician Giuseppe Peano in the late 19th Century.

The natural numbers are defined as follows:

We denote the set of natural numbers by $\mathbb N$.

Here, we have an element that we've just called $z$—this happens to be the thing that we call zero or 0. Zero is a natural number. Here I've made a deliberate choice not to use 0 to make it clear there's nothing particularly special about this element. Then we have the function $\operatorname{succ}$, which is called the successor function.

This definition for the natural numbers (and the definition for factorial from above) is an example of a recursive or inductive definition. These are definitions that are defined in terms of itself. Let's dissect this.

The first part of the definition is called the base case. The base case defines the most basic element of the definition. Often, one can think of this as kind of the "smallest" or "least" case. The second part of the definition is called the recursive or inductive case. This is the part of the definition that refers to itself.

Surprisingly, even though this defintion is recursive, we can hammer out a definition of the type (yes, even in Python! crazy!) in code just like before.

@dataclass
class Succ:
    p: Natural

Natural = None | Succ

Here, we define the type Natural to be

In case you really want to try working with this yourself, here are some notes on implementation that are not that important to the mathematics:

The key idea here is that types in computer science and structures in math are often the same thing. The reason that we can have data structures in CS is because they have formal mathematical definitions.

It so happens that all of the numbers we already know and love can be described using this definition. "Zero" or "0" we already said was $z$. Then "one" or "1" is $\operatorname{succ}(z)$. "Two", or "2", is $\operatorname{succ}(\operatorname{succ}(z))$, and so on. But notice that we never actually defined these other natural numbers: the only natural number we explicitly defined was zero. Every other number is expressed as the successor of another number.

In a way, we might think of this as naming constants like

zero = None
one = Succ(zero)
two = Succ(one)
three = Succ(two)
    ...

Is $3$ a natural number? Here, we're using $3$ as a shorthand for \[\operatorname{succ}(\operatorname{succ}(\operatorname{succ}(z))).\] So how do we know if $\operatorname{succ}(\operatorname{succ}(\operatorname{succ}(z)))$ is a natural number? Well, we have to figure out if it's the successor of a natural number, or if the argument to $\operatorname{succ}$ is also a natural number. So we look at $\operatorname{succ}(\operatorname{succ}(z))$ and see if that's a natural number. Again, this depends on the argument to $\operatorname{succ}$, so we check to see if $\operatorname{succ}(z)$ is a natural number. We go through the same argument and check if $z$ is a natural number. It is! So we can conclude that $3$ was a natural number after all.

Now that we've defined our data type, we can use it to "do" things...