Read Structure and Interpretation of Computer Programs Online
Authors: Harold Abelson and Gerald Jay Sussman with Julie Sussman
Using this procedure, write a procedure
search-for-primes
that
checks the primality of consecutive odd integers in a specified range.
Use your procedure to find the three smallest primes larger than 1000;
larger than 10,000; larger than 100,000; larger than 1,000,000. Note
the time needed to test each prime. Since the testing algorithm has
order of growth of θ(√
n
), you should expect that testing
for primes around 10,000 should take about √10 times as long
as testing for primes around 1000. Do your timing data bear this out?
How well do the data for 100,000 and 1,000,000 support the √
n
prediction? Is your result compatible with the notion that programs
on your machine run in time proportional to the number of steps
required for the computation?
Exercise 1.23.
The
smallest-divisor
procedure shown at the start of this section
does lots of needless testing: After it checks to see if the
number is divisible by 2 there is no point in checking to see if
it is divisible by any larger even numbers. This suggests that the
values used for
test-divisor
should not be 2, 3, 4, 5, 6,
...
, but rather 2, 3, 5, 7, 9,
...
. To implement this
change, define a procedure
next
that returns 3 if its input is
equal to 2 and otherwise returns its input plus 2. Modify the
smallest-divisor
procedure to use
(next test-divisor)
instead
of
(+ test-divisor 1)
. With
timed-prime-test
incorporating this modified version of
smallest-divisor
, run the
test for each of the 12 primes found in
exercise
1.22
. Since this modification halves the
number of test steps, you should expect it to run about twice as fast.
Is this expectation confirmed? If not, what is the observed ratio of
the speeds of the two algorithms, and how do you explain the fact that
it is different from 2?
Exercise 1.24.
Modify the
timed-prime-test
procedure of
exercise
1.22
to use
fast-prime?
(the
Fermat method), and test each of the 12 primes you found in that
exercise. Since the Fermat test has θ(
log
n
) growth, how
would you expect the time to test primes near 1,000,000 to compare
with the time needed to test primes near 1000? Do your data bear this
out? Can you explain any discrepancy you find?
Exercise 1.25.
Alyssa P. Hacker complains that we went to a lot of extra work in
writing
expmod
. After all, she says, since we already know how
to compute exponentials, we could have simply written
(define (expmod base exp m)
(remainder (fast-expt base exp) m))
Is she correct? Would this procedure serve as well for our fast prime
tester? Explain.
Exercise 1.26.
Louis Reasoner is having great difficulty doing
exercise
1.24
. His
fast-prime?
test
seems to run more slowly than his
prime?
test. Louis calls his
friend Eva Lu Ator over to help. When they examine Louis's code, they
find that he has rewritten the
expmod
procedure to use an
explicit multiplication, rather than calling
square
:
(define (expmod base exp m)
(cond ((= exp 0) 1)
((even? exp)
(remainder (* (expmod base (/ exp 2) m)
(expmod base (/ exp 2) m))
m))
(else
(remainder (* base (expmod base (- exp 1) m))
m))))
“I don't see what difference that could make,” says Louis. “I
do.” says Eva. “By writing the procedure like that, you have
transformed the θ(
log
n
) process into a θ(
n
) process.”
Explain.
Exercise 1.27.
Demonstrate that the Carmichael numbers listed in
footnote
47
really do fool
the Fermat test. That is, write a procedure that takes an integer
n
and tests whether
a
n
is congruent to
a
modulo
n
for every
a
<
n
, and try your procedure on the given Carmichael numbers.
Exercise 1.28.
One variant of the Fermat test that cannot be fooled is called the
Miller-Rabin test
(Miller 1976; Rabin 1980). This starts from
an alternate form of Fermat's Little Theorem, which states that if
n
is a prime number and
a
is any positive integer less than
n
, then
a
raised to the (
n
- 1)st power is congruent to 1 modulo
n
. To test
the primality of a number
n
by the Miller-Rabin test, we pick a
random number
a
<
n
and raise
a
to the (
n
- 1)st power modulo
n
using the
expmod
procedure. However, whenever we perform the
squaring step in
expmod
, we check to see if we have discovered a
“nontrivial square root of 1 modulo
n
,” that is, a number not
equal to 1 or
n
- 1 whose square is equal to 1 modulo
n
. It is
possible to prove that if such a nontrivial square root of 1 exists,
then
n
is not prime. It is also possible to prove that if
n
is an
odd number that is not prime, then, for at least half the numbers
a
<
n
, computing
a
n
-1
in this way will reveal a nontrivial
square root of 1 modulo
n
. (This is why the Miller-Rabin test
cannot be fooled.) Modify the
expmod
procedure to signal if it
discovers a nontrivial square root of 1, and use this to implement
the Miller-Rabin test with a procedure analogous to
fermat-test
.
Check your procedure by testing various known primes and non-primes.
Hint: One convenient way to make
expmod
signal is to have it
return 0.
29
In a real program we would probably use the
block structure introduced in the last section to hide the definition
of
fact-iter
:
(define (factorial n)
(define (iter product counter)
(if (> counter n)
product
(iter (* counter product)
(+ counter 1))))
(iter 1 1))
We avoided doing this here so as to minimize the number of things to
think about at once.
30
When we discuss the implementation of
procedures on register machines in chapter 5, we will see that any
iterative process can be realized “in hardware” as a machine that
has a fixed set of registers and no auxiliary memory. In contrast,
realizing a recursive process requires a machine that uses an
auxiliary data structure known as a
stack
.
31
Tail recursion has long been
known as a compiler optimization trick. A coherent semantic basis for
tail recursion was provided by Carl Hewitt (1977), who explained it in
terms of the “message-passing” model of computation that we shall
discuss in chapter 3. Inspired by this, Gerald Jay Sussman and Guy
Lewis Steele Jr. (see Steele 1975) constructed a tail-recursive
interpreter for Scheme. Steele later showed how tail recursion is a
consequence of the natural way to compile procedure calls (Steele
1977). The IEEE standard for Scheme requires that Scheme implementations
be tail-recursive.
32
An example of this was hinted
at in section
1.1.3
:
The interpreter itself evaluates expressions
using a tree-recursive process.
33
For example, work through in detail how the
reduction rule applies to the problem of making change for 10 cents
using pennies and nickels.
34
One
approach to coping with redundant computations is to arrange matters
so that we automatically construct a table of values as they
are computed. Each time we are asked to apply the procedure to some
argument, we first look to see if the value is already stored in the
table, in which case we avoid performing the redundant computation.
This strategy, known as
tabulation
or
memoization
, can be
implemented in a straightforward way. Tabulation can sometimes be
used to transform processes that require an exponential number of
steps (such as
count-change
) into processes whose space and time
requirements grow linearly with the input. See
exercise
3.27
.
35
The elements of Pascal's triangle are called the
binomial
coefficients
, because the
n
th row consists of
the coefficients of the terms in the
expansion of (
x
+
y
)
n
. This pattern for computing the coefficients
appeared in Blaise Pascal's 1653 seminal work on probability theory,
Traité du triangle arithmétique
. According to
Knuth (1973), the same pattern appears in the
Szu-yuen
Yü-chien
(“The Precious Mirror of the Four Elements”), published
by the Chinese mathematician Chu Shih-chieh in 1303, in the
works of the twelfth-century Persian poet and mathematician Omar
Khayyam, and in the works of the twelfth-century Hindu mathematician
Bháscara Áchárya.
36
These statements mask a
great deal of oversimplification. For instance, if we count process
steps as “machine operations” we are making the assumption that the
number of machine operations needed to perform, say, a multiplication
is independent of the size of the numbers to be multiplied, which is
false if the numbers are sufficiently large. Similar remarks hold for
the estimates of space. Like the design and description of a process,
the analysis of a process can be carried out at various levels of
abstraction.
37
More precisely, the number of multiplications
required is equal to 1 less than the log base 2 of
n
plus the number
of ones in the binary representation of
n
. This total is always
less than twice the log base 2 of
n
. The arbitrary constants
k
1
and
k
2
in
the definition of order notation imply that, for a logarithmic
process, the base to which logarithms are taken does not matter, so
all such processes are described as θ(
log
n
).
38
You may wonder
why anyone would care about raising numbers to the 1000th power. See
section
1.2.6
.
39
This iterative
algorithm is ancient. It appears in the
Chandah-sutra
by
Áchárya Pingala, written before 200
B
.
C
. See Knuth 1981, section
4.6.3, for a full discussion and analysis of this and other methods of
exponentiation.
40
This
algorithm, which is sometimes known as the “Russian peasant method”
of multiplication, is ancient. Examples of its use are found in the
Rhind Papyrus, one of the two oldest mathematical documents in
existence, written about 1700
B
.
C
. (and copied from an even
older document) by an Egyptian scribe named A'h-mose.
41
This exercise was
suggested to us by Joe Stoy, based on an example in Kaldewaij 1990.
42
Euclid's Algorithm is so
called because it appears in Euclid's
Elements
(Book 7, ca. 300
B
.
C
.). According to Knuth (1973), it can be considered the
oldest known nontrivial algorithm. The ancient Egyptian method of
multiplication (exercise
1.18
) is surely older,
but, as Knuth explains, Euclid's algorithm is the oldest known to have
been presented as a general algorithm, rather than as a set of
illustrative examples.
43
This theorem was proved in 1845 by Gabriel Lamé, a
French mathematician and engineer known chiefly for his contributions
to mathematical physics. To prove the theorem, we consider pairs
(
a
k
,
b
k
), where
a
k
>
b
k
, for which Euclid's Algorithm
terminates in
k
steps. The proof is based on the claim that, if
(
a
k
+1
,
b
k
+1
) ⟶ (
a
k
,
b
k
)
⟶ (
a
k
-1
,
b
k
-1
) are three successive pairs in the
reduction process, then we must have
b
k
+1
>
b
k
+
b
k
-1
.
To verify the claim, consider that a reduction step is defined by
applying the transformation
a
k
-1
=
b
k
,
b
k
-1
=
remainder of
a
k
divided by
b
k
.
The second equation means that
a
k
=
q
b
k
+
b
k
-1
for some
positive integer
q
. And since
q
must be at least 1 we have
a
k
=
q
b
k
+
b
k
-1
>
b
k
+
b
k
-1
. But in the previous
reduction step we have
b
k
+1
=
a
k
. Therefore,
b
k
+1
=
a
k
>
b
k
+
b
k
-1
. This verifies the claim. Now we can
prove the theorem by induction on
k
, the number of steps that the
algorithm requires to terminate. The result is true for
k
= 1, since
this merely requires that
b
be at least as large as
F
i
b
(1) = 1. Now, assume that the result is true for all integers less
than or equal to
k
and establish the result for
k
+ 1. Let
(
a
k
+1
,
b
k
+1
) ⟶ (
a
k
,
b
k
)
⟶ (
a
k
-1
,
b
k
-1
) be successive pairs in the
reduction process. By our induction hypotheses, we have
b
k
-1
>
F
i
b
(
k
- 1) and
b
k
>
F
i
b
(
k
). Thus, applying the claim we just
proved together with the definition of the Fibonacci numbers gives
b
k
+1
>
b
k
+
b
k
-1
>
F
i
b
(
k
) +
F
i
b
(
k
- 1) =
F
i
b
(
k
+ 1), which
completes the proof of Lamé's Theorem.
44
If
d
is a divisor of
n
, then so is
n
/
d
.
But
d
and
n
/
d
cannot both be greater than √
n
.
45
Pierre de Fermat (1601-1665) is considered to be the founder of
modern number theory. He obtained many important number-theoretic
results, but he usually announced just the results, without providing
his proofs.
Fermat's Little Theorem was stated in a letter he wrote in
1640. The first published proof was given by
Euler in 1736 (and an
earlier, identical proof was discovered in the unpublished manuscripts
of Leibniz). The most famous of Fermat's results – known as Fermat's
Last Theorem – was jotted down in 1637 in his copy of the book
Arithmetic
(by the third-century Greek mathematician
Diophantus) with the
remark “I have discovered a truly remarkable proof, but this margin is
too small to contain it.” Finding a proof of Fermat's Last Theorem
became one of the most famous challenges in number theory. A complete
solution was finally given in 1995 by Andrew Wiles of Princeton University.
46
The reduction steps in the cases where the exponent
e
is greater than 1 are based on the fact that, for any integers
x
,
y
, and
m
, we can find the remainder of
x
times
y
modulo
m
by computing separately the remainders of
x
modulo
m
and
y
modulo
m
, multiplying these, and then taking the remainder of the
result modulo
m
. For instance, in the case where
e
is even, we
compute the remainder of
b
e
/2
modulo
m
, square this, and take
the remainder modulo
m
. This technique is useful because it means
we can perform our computation without ever having to deal with
numbers much larger than
m
. (Compare
exercise
1.25
.)
47
Numbers that fool the
Fermat test are called
Carmichael numbers
, and little is known
about them other than that they are extremely rare. There are 255
Carmichael numbers below 100,000,000. The smallest few are 561, 1105,
1729, 2465, 2821, and 6601. In testing primality of very large
numbers chosen at random, the chance of stumbling upon a value that
fools the Fermat test is less than the chance that
cosmic radiation
will cause the computer to make an error in carrying out a “correct”
algorithm. Considering an algorithm to be inadequate for the first
reason but not for the second illustrates the difference between
mathematics and engineering.
48
One of the most striking applications of
probabilistic prime testing has been to the field of cryptography.
Although it is now computationally infeasible to factor an arbitrary
200-digit number, the primality of such a number can be checked in a
few seconds with the Fermat test. This fact forms the basis of a
technique for constructing “unbreakable codes” suggested by
Rivest,
Shamir, and
Adleman (1977). The resulting
RSA algorithm
has
become a widely used technique for enhancing the security of
electronic communications. Because of this and related developments,
the study of
prime numbers, once considered the epitome of a topic in
“pure” mathematics to be studied only for its own sake, now turns
out to have important practical applications to cryptography,
electronic funds transfer, and information retrieval.