In section 1.3 of SICP, a method for determining prime numbers is mentioned. The naive prime number determination, known to students around the globe, is if a number n's smallest divisor other than 1 is itself, then n is a prime number.
(define (remainder num divisor)
(- num (* divisor (round (/ num divisor)))))
;return the smallest divisor of n
(define (smallest-divisor n)
;find a divisor(for 2 to n) of n
(define (find-divisor n test-divisor)
(cond ((< n (sqrt test-divisor)) n)
((= (remainder n test-divisor) 0) test-divisor)
(else (find-divisor n (+ test-divisor 1)))))
(find-divisor n 2))
;check n is prime or not
(define (prime? n)
(= n (smallest-divisor n )))
However, this method is slow, with a complexity of O(n).
SICP then introduces Fermat's Little Theorem:
If a number n is a prime, and for any a<n, a^n≡a(mod n)
Unfortunately, the converse theorem does not hold, but it does not mean it is not useful. We can select a number a smaller than n and perform the aforementioned test. If a^n≡a(mod n) holds, then n is very likely a prime number. The more a's tested against n, the higher the likelihood n is a prime. Thus, it's a probabilistic algorithm; with enough tests, the probability of n not being a prime becomes very small.
This leads us to the fermat test program. Don't worry about the efficiency of calculating a^n, because it's easily solved:
Consider:
a^n = (a^(n/2))^2; when n is even
a^n = a* a^(n-1); when n is odd
Always holds, so with nearly every multiplication, n doubles. If you look at the number line, this is essentially binary search. Thus, the time complexity is reduced. This explains why the pow
function provided by programming languages is always faster than writing a*a*a…
yourself.
But we shouldn't directly use the built-in pow
function. The reason is our goal is not the nth power of a, but to know if the nth power of a modulo m equals a modulo m. Simplified, we only need to verify if this expression holds: a^n mod m = a
. If expressed directly as pow(a, n) % m == a
, it would be problematic because the value of pow(a, n)
can be very large. So, we should use a similar technique:
a^n mod m = ((a^(n / 2))^2) mod m; when n is even
a^n mod m = (a* a^(n-1)) mod m; when n is odd
This way, the calculated value never exceeds m, avoiding computations with excessively large numbers. Hence, we easily have the following program:
(define (remainder num divisor)
(- num (* divisor (round (/ num divisor)))))
(define (expmod base exps m)
(define (square n) (* n n))
(cond ((= exps 0) 1)
((even? exps)
(remainder (square (expmod base (/ exps 2) m))
m))
(else
(remainder (* base (expmod base (- exps 1) m))
m))))
;fermat test, check n. if (a^n) mod n == a then in all probability n is a prime
(define (fermat-test n)
(define (try-it a)
(= a (expmod a n n)))
;(= a (remainder (expt a n) n)))
(try-it (+ 1 (random (- n 1)))))
;get prime by fermat-test. test n is prime or not, try times time
(define (fermat-prime? n times)
(cond ((= 0 times) #t)
((fermat-test n) (fermat-prime? n (- times 1)))
(else #f)))
However, there's always imperfection.
There exists a type of numbers called Carmichael numbers that can trick the Fermat test. Between 2 and 10000, there are several such numbers:
561, 1105, 1729, 2465, 2821, 6601, 8911
These stubborn numbers are composite, but no matter what a you test them with, they can masquerade as primes in the Fermat test. They are not
few in number and are widely distributed, with a total of 255 Carmichael numbers within 100,000,000. So, if you do not wish to maintain a Carmichael number list, you can strengthen our prime number test method.
Fortunately, in exercise 1.28, Miller and Rabin brought us the Miller-Rabin test method. This method is based on a variation of Fermat's little theorem:
If n is a prime, for any a < n, a^(n-1) ≡ 1(mod n)
To use the MR test, one needs to check for a "non-trivial square root of 1 mod n" during the square step of the Fermat test. If there exists an s = a^2; and s <> 1 and s <> n-1, and s^2 mod m = 1, then n is not a prime.
Matrix67: The Miller-Rabin primality test is also a probabilistic algorithm, and we call composites that pass the Miller-Rabin test with base a as strong pseudoprimes to base a. The first strong pseudoprime to base 2 is 2047. The first strong pseudoprime to bases 2 and 3 is as large as 1,373,653.
So, when you have sufficient test quantity, this method can guarantee safety in industrial applications. Below is my answer to question 1.28:
(define (remainder num divisor)
(- num (* divisor (round (/ num divisor)))))
(define (mr-expmod base exps m)
(define (square x) (* x x))
(define (check-square-root x n)
(define (check r)
(if (and (not (= x 1)) (not (= x (- n 1))) (= r 1)) 0 r))
(check (remainder (square x) n)))
(cond ((= exps 0) 1)
((even? exps)
(check-square-root(mr-expmod base (/ exps 2) m) m))
(else
(remainder (* base (mr-expmod base (- exps 1) m))
m))))
;Miller-Rabin test, check n. if (a^(n-1)) mod n == 1 then in all probability n is a prime
(define (mr-test n)
(define (try-it a)
(= 1 (mr-expmod a (- n 1) n)))
(try-it (+ 1 (random (- n 1)))))
;get prime by Miller-Rabin-test. test n is prime or not, try times time
(define (mr-prime? n times)
(cond ((= 0 times) #t)
((mr-test n) (mr-prime? n (- times 1)))
(else #f)))
I compiled some data, seeking primes in the intervals [1000, 1100], [10000, 10100], [100000, 100100], and [1000000, 1000100], to see how these methods perform:
Conventional method Conventional method's divisor 2 optimization Miller-Rabin test method
[1000, 1100] 1 ms 1 ms 1 ms
[10000, 10100] 13 ms 8 ms 1 ms
[100000, 100100] 142 ms 78 ms 1 ms
[1000000, 1000100] 1337 ms 830 ms 1 ms
This was run on my Intel Core 1 1.83GHz machine (Ubuntu 8.04 + DrScheme).
I also wrote a C version, with the following results:
Range [2, 10000]:
$ ./prime
Normal: return 0 cost 10ms
Fermat: return 0 cost 24ms
Miller-Rabin: return 0 cost 28ms
Range [200000, 1000000]:
$ ./prime
Normal: return 0 cost 3088ms
Fermat: return 0 cost 824ms
Miller-Rabin: return 0 cost 959ms
The gprof analysis results are mostly as expected, with a portion of the report below:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls us/call us/call name
63.55 2.58 2.58 800000 3.23 3.23 is_prime(int)
15.76 3.22 0.64 800005 0.80 0.84 fermat_expmod(long, long, long)
10.10 3.63 0.41 800012 0.51 0.88 mr_expmod(long, long, long)
7.27 3.92 0.29 14813786 0.02 0.02 chk_unusual_square_root(long, long)
1.23 3.98 0.05 main
0.86 4.01 0.04 800000 0.04 0.93 mr_test(long, long)
0.74 4.04 0.03 14813658 0.00 0.00 square(long)
0.25 4.05 0.01 800000 0.01 0.94 mr_is_prime(int)
0.25 4.06 0.01 800000 0.01 0.85 fermat_is_prime(int)
0.00 4.06 0.00 800000 0.00 0.84 fermat_test(long, long)
The reason for poor performance with small n values, I guess, is due to the overhead of handling ordinary recursion in C. However, as the data size increases, this overhead becomes relatively insignificant. If someone could convert the above algorithms into iterative form (which I cannot do :P) or tail-recursion form (gcc can optimize tail recursion into iterative form), I expect the performance would be even more pronounced.