Final exam tomorrow 5-8pm in 10 Evans
Two 8.5x11 double-sided cheat sheets allowed
No regrades for HW8 (not enough time) or final exam (UCB policy)
HW8, review solutions to be posted shortly

Review
We defined two sets A and B to have the same cardinality if there
is a bijection between A and B.

We defined a set A to be countable if there is a bijection between
A and some subset of N. If A is countable and infinite, then it
is countably infinite.

All countably infinite sets have the same cardinality, since they
can be put into a bijection with N.

To show that a set A is countable, it suffices to demonstrate a
(possibly infinite) enumeration of A that lists all elements of A.

Any set N∪{a}, where a∉N, is therefore countable, since
we could list N∪{a} as
N∪{a} = {a, 0, 1, 2, ...}.
(So in some sense, ∞+1 = ∞, perhaps to the chagrin of
some children.)

We used diagonalization to show that the set of real numbers in the
interval [0, 1] is uncountable, i.e. uncountably infinite. The
technique was a follows:
(1) Assume that a set S can be enumerated.
(2) Consider an arbitrary list of all the elements of S.
(3) Use the diagonal from the list to construct a new element
t.
(4) Show that t is in S but is different from all elements in the
list and so is not in the list. Contradiction.
This shows that the original assumption that S is countable was
false, so S is uncountably infinite.

Now we can understand the difference between discrete and continuous
random variables. The range of a discrete random variable is a
countable subset of R, while that of a continuous random
variable is an uncountable subset of R. (As an exercise, show
that any interval [a, b] of real numbers, a < b, is uncountably
infinite. Hint: demonstrate a bijection between [a, b] and [0, 1].)

Let's use diagonalization to show that the set FB of all functions
from finite binary strings to {0,1} is uncountable. This is the set
of all functions of the form
f:{0,1}^*->{0,1}.
(Note that we define a function by its mapping from inputs to
outputs, not by its functional form. Thus, the following function on
real numbers
f:R->R  f(x) = x
is the same as
g:R->R  g(x) = x + 1 - 1.)

Let's start by defining a representation for functions in FB. We
know that BS = {0,1}^* is countable, so its elements can be listed
in some order
BS = {s0, s1, s2, ...}.
So let's represent a function f∈FB as a corresponding list of
outputs
f = (f(s0), f(s1), f(s2), ....).
This representation is infinite, like that of real numbers, so it
should be no surprise that FB is uncountable.

Let's assume that FB is countable. Then its elements can be listed:
i             f∈FB
0    (0, 1, 0, 1, 0, 1, ...)
1    (1, 1, 1, 0, 0, 1, ...)
2    (0, 0, 0, 0, 0, 0, ...)
3    (0, 1, 0, 0, 1, 1, ...)
4    (1, 1, 1, 1, 0, 1, ...)
5    (0, 0, 0, 1, 1, 1, ...)
...            ...
Then we can construct a new function g that is different from all
of the functions in the list:
g = (1-f0(s0), 1-f1(s1), 1-f2(s2), ...)
= (1, 0, 1, 1, 1, 0, ...).
Since g∈FB but not in the list, this is a contradiction, so FB
is uncountable.

As might be clear from the above, our representation for functions
demonstrates a bijection between FB and the set IBS of infinite
binary strings. This immediately implies that FB is uncountable.

Computability
We defined the set FB of functions that take in finite bitstrings
as input and output 0 or 1:
FB = {f:{0,1}^*->{0,1}}.
We saw that this set is uncountable.

A function f is "computable" if there is a computer program P that
computes it, meaning that for any input bitstring s, P terminates
when run on s and outputs f(s).

Is the set CP of computer programs that take in a finite bitstring
and produce 0 or 1 countable? A computer program must be finite, so
it can be represented as a finite bitstring, implying that there is
a bijection between CP and some subset of BS, the set of finite
bitstrings. Since BS is countable, this implies that CP is
countable.

Since FB is uncountable and CP is countable, the cardinality of FB
is strictly larger than that of CP, implying that there are
functions that are not computable.

The above, however, is a non-constructive proof. It merely tells us
that there are uncomputable functions, without demonstrating an
example of a function that is uncomputable.

In order to demonstrate a concrete example, we first note that the
set CP x {0,1}^*, the Cartesian product of the set of computer
programs and the set of finite bitstrings, is countable. Then there
is a bijection between CP x {0,1}^* and {0,1}^*, implying that we
can represent an element (P, I) of CP x {0,1}^* as a finite
bitstring.

Now define the function
h: {0,1}^* -> {0,1}
h(x) = { 1 if the program P halts when run on I, where x = (P, I)
0 otherwise
Then h∈FB, the set of functions that we demonstrated is
uncountable.

Is the function h computable? Let's assume that it is computable, so
there exists a program
HaltOrNot(P, I):
if P halts when run on I then 1
else 0.
Not that in order for h to be computable, then HaltOrNot must
terminate. So it is not sufficient for it to call P as a subroutine
and return 1 when P halts, since HaltOrNot would not terminate if P
does not. (As a side note, since {0,1}^* x {0,1}^* is countable, it
has a bijection with {0,1}^*, so we can convert multiple inputs to a
program into a single input. However, it is more convenient to
explicitly write two inputs, so that is what we will do.)

Now if HaltOrNot exists, then Alan Turing argued that he could
construct the following program that calls HaltOrNot as a
subroutine:
Turing(P):
if HaltOrNot(P, P) = 1 then go into an infinite loop
else halt immediately, returning 0.
The program Turing, given another program P, calls HaltOrNot to
determine if P halts when run on itself. (Recall that a program has
a bitstring representation, so that representation can be passed
into a program itself as input.) If so, Turing does the opposite,
going into an infinite loop. Similarly, if P does not halt, then
Turing does the opposite, halting.

What happens when we call Turing(Turing)? There are two
possibilities:
Case 1: Turing(Turing) halts. Then when Turning(Turing) runs, it
calls HaltOrNot(Turing, Turing), which will return 1 since
Turing(Turing) halts. Then Turing(Turing) will go into an
infinite loop, so it won't halt, which is a contradiction.
Case 2: Turing(Turing) doesn't halt. Then when Turing(Turing) runs,
it calls HaltOrNot(Turing, Turing), which will return 0
since Turing(Turing) doesn't halt. Then Turing(Turing) will
halt immediately, contradicting the fact that it does not
halt.
In either case, we end up with a contradiction. Thus, our original
assumption that HaltOrNot exists is false, and h is uncomputable.

Here is another way to express this proof using a modified form of
diagonalization. Since we know that the set of programs CP is
countable, we can list all its elements. Lets list them in both
dimensions of a 2D table:
P0  P1  P2  P3  P4 ...
---------------------
P0 |  H   H   H   H   H  ...
P1 |  H   H   H   H   H  ...
P2 |  N   N   N   N   N  ...
P3 |  H   N   N   H   N  ...
P4 |  H   H   H   N   N  ...
...          ...
The table entries represent what happens when the program in the
vertical axis is run on the input in the horizontal axis. For
example, the entry in the second row and third column is what
happens when running P1(P2). Either the program halts, which
we denote by 'H', or not, which we denote by 'N'.

Now if HaltOrNot exists, we can write the program Turing that does
the opposite of the diagonal in the table. Thus, when Turing is run
on P_i, it does the opposite of P_i(P_i), halting if P_i(P_i) does
not, going into an infinite loop if it does. This implies that
Turing is different from any program P_i on the list, since its
behavior differs from that of P_i when run on P_i. But since the
list enumerates all programs in CP, this implies that Turing is not
in CP. This further implies that HaltOrNot isn't either, since we
can easily write Turing if we have HaltOrNot.

So we have shown that there is no program HaltOrNot that will tell
us whether or not another programs halts. Bummer.

It gets worse. We can demonstrate that any function that answers an
interesting question about computer programs is uncomputable. For
example, suppose we want to know whether or not a program will print
"Hello world!" when run on a particular input. Assume that there is
a program PrintsHW that computes this:
PrintsHW(P, I):
if P prints "Hello world!" when run on I then 1
else 0.
Then we could write HaltOrNot:
HaltOrNot(P, I):
1. Remove all print statements from P.
2. Add a statement to print "Hello world!" before each halt
statement in P.
3. Return PrintsHW(P, I).
Then the modified P will halt if and only if it prints "Hello
world!", so if we can compute whether or not it prints "Hello
world!", we can compute whether or not it halts. Since we know we
can't do the latter, we can't do the former either.

We can repeat the above procedure for any interesting question about
programs, showing that it is uncomputable. As an example, we can
show that no program exists that can determine with certainty
whether or not another program is a virus. The best we can do is
approximate, using heuristics.