timothy235 icon

sicp-1-2-6-primality-testing

timothy235 | PRO | 12/19/24 01:21:56 AM UTC (Edited) | 0 ⭐ | 369 👁️ | Never ⏰ | []
Racket |

11.62 KB

|

None

|

0 👍

/

0 👎

#lang racket
 
;; 1.21
 
(define (divides? a b)
  (zero? (remainder b a)))
(define (find-divisor n test-divisor)
  (cond [(> (sqr test-divisor) n) n]
        [(divides? test-divisor n) test-divisor]
        [else (find-divisor n (+ test-divisor 1))]))
(define (smallest-divisor n)
  (find-divisor n 2))
 
(smallest-divisor 199)
;; 199
(smallest-divisor 1999)
;; 1999
(smallest-divisor 19999)
;; 7
 
;; 1.22
 
(define (my-prime? n)
  (= n (smallest-divisor n)))
 
;; Time my-prime? to verify it is O(sqrt(n)).
 
(define (timed-prime-test n)
  (define (start-prime-test num start-time)
    (cond [(my-prime? num)
           (printf "~a is prime *** ~a ms ~n"
                   num
                   (inexact->exact
                     (truncate
                       (- (current-inexact-milliseconds) start-time))))
           #t]
          [else #f]))
  (start-prime-test n (current-inexact-milliseconds)))
 
(define (find-primes start times)
  ; find and display times many primes, with timing info, beginning at start
  (cond [(zero? times) (newline)]
        [(timed-prime-test start) (find-primes (add1 start) (sub1 times))]
        [else (find-primes (add1 start) times)]))
 
;; I had to make the numbers big to get noticeable elapsed times.  The starting
;; numbers go up by a factor of 100.  So with a sqrt(n) time complexity, the
;; elapsed times should be going up by a factor of 10 each time, which we do see.
 
(find-primes 10000000000000 3)
;; 10000000000037 is prime *** 115 ms
;; 10000000000051 is prime *** 106 ms
;; 10000000000099 is prime *** 115 ms
 
(find-primes 1000000000000000 3)
;; 1000000000000037 is prime *** 1201 ms
;; 1000000000000091 is prime *** 1200 ms
;; 1000000000000159 is prime *** 1203 ms
 
(find-primes 100000000000000000 3)
;; 100000000000000003 is prime *** 12009 ms
;; 100000000000000013 is prime *** 12025 ms
;; 100000000000000019 is prime *** 12140 ms
 
;; 1.23
 
;; Redo 1.22 with an improved find-divisor that does not unnecessarily check even
;; numbers greater than 2.
 
(define (next n) ; return 3 if n is 2 else n + 2
  (if (= n 2) 3 (+ n 2)))
 
(define (new-find-divisor n test-divisor)
  ; same as find-divisor but uses next instead of incrementing by 1
  (cond [(> (sqr test-divisor) n) n]
        [(divides? test-divisor n) test-divisor]
        [else (new-find-divisor n (next test-divisor))]))
(define (new-smallest-divisor n)
  (new-find-divisor n 2))
 
(define (new-my-prime? n)
  (= n (new-smallest-divisor n)))
 
(define (new-timed-prime-test n)
  (define (start-prime-test num start-time)
    (cond [(new-my-prime? num)
           (printf "~a is prime *** ~a ms ~n"
                   num
                   (inexact->exact
                     (truncate
                       (- (current-inexact-milliseconds) start-time))))
           #t]
          [else #f]))
  (start-prime-test n (current-inexact-milliseconds)))
 
(define (new-find-primes start times)
  (cond [(zero? times) (newline)]
        [(new-timed-prime-test start) (new-find-primes (add1 start) (sub1 times))]
        [else (new-find-primes (add1 start) times)]))
 
(new-find-primes 10000000000000 3)
;; 10000000000037 is prime *** 82 ms
;; 10000000000051 is prime *** 62 ms
;; 10000000000099 is prime *** 79 ms
 
(new-find-primes 1000000000000000 3)
;; 1000000000000037 is prime *** 748 ms
;; 1000000000000091 is prime *** 748 ms
;; 1000000000000159 is prime *** 770 ms
 
(new-find-primes 100000000000000000 3)
;; 100000000000000003 is prime *** 7588 ms
;; 100000000000000013 is prime *** 7591 ms
;; 100000000000000019 is prime *** 7603 ms
 
;; The new elapsed times are a little more than half the old times.  I think this
;; reflects the fact that timed-prime-test has some unavoidable printing overhead
;; that the improved my-prime? cannot help.  Nonetheless I think this is a
;; significant improvement, not a big-O improvement, but a nice leading
;; coefficient improvement.
 
;; 1.24
 
;; Redo 1.22 using fast-prime? instead of my-prime? and check that we get
;; logarithmic time complexity.
 
(define (expmod base expo m)
  (cond [(zero? expo) 1]
        [(even? expo)
         (remainder (sqr (expmod base (/ expo 2) m)) m)]
        [else (remainder (* base (expmod base (sub1 expo) m)) m)]))
 
(define (fermat-test n)
  (define (try-it a)
    (= (expmod a n n) a))
  (try-it (add1 (random (min 4294967087 ; biggest number accepted by random
                             (sub1 n))))))
 
(define (fast-prime? n times)
  (cond [(zero? times) #t]
        [(fermat-test n) (fast-prime? n (sub1 times))]
        [else #f]))
 
(define (fast-timed-prime-test n)
  ; same as timed-prime-test but uses fast-prime? instead of my-prime?
  (define (start-prime-test num start-time)
    (cond [(fast-prime? num 10) ; run the fermat test ten times
           (printf "~a is prime *** ~a ms ~n"
                   num
                   (inexact->exact
                     (truncate
                       (- (current-inexact-milliseconds) start-time))))
           #t]
          [else #f]))
  (start-prime-test n (current-inexact-milliseconds)))
 
(define (fast-find-primes start times)
  (cond [(zero? times) (newline)]
        [(fast-timed-prime-test start) (fast-find-primes (add1 start) (sub1 times))]
        [else (fast-find-primes (add1 start) times)]))
 
;; First let's compare with the numbers we used earlier.
 
(fast-find-primes 10000000000000 3)
;; 10000000000037 is prime *** 0 ms
;; 10000000000051 is prime *** 0 ms
;; 10000000000099 is prime *** 0 ms
 
(fast-find-primes 1000000000000000 3)
;; 1000000000000037 is prime *** 0 ms
;; 1000000000000091 is prime *** 0 ms
;; 1000000000000159 is prime *** 0 ms
 
(fast-find-primes 100000000000000000 3)
;; 100000000000000003 is prime *** 0 ms
;; 100000000000000013 is prime *** 0 ms
;; 100000000000000019 is prime *** 0 ms
 
;; So we got the same primes but this time basically instantaneously.
 
;; Now we will have to use huge numbers to register any elapsed time at all.  With
;; log running time, doubling the number of digits in a number should double the
;; elapsed time.
 
(fast-find-primes (inexact->exact 1e25) 3)
;; 10000000000000000905969697 is prime *** 2 ms
;; 10000000000000000905969749 is prime *** 1 ms
;; 10000000000000000905969823 is prime *** 1 ms
 
(fast-find-primes (inexact->exact 1e50) 3)
;; 100000000000000007629769841091887003294964970946821 is prime *** 4 ms
;; 100000000000000007629769841091887003294964970947027 is prime *** 4 ms
;; 100000000000000007629769841091887003294964970947049 is prime *** 4 ms
 
(fast-find-primes (inexact->exact 1e100) 3)
;; 100000000000000001590289110975991804683608085639452813897813275577478387
;; 72170381060813469985856815251 is prime *** 15 ms
;; 100000000000000001590289110975991804683608085639452813897813275577478387
;; 72170381060813469985856815363 is prime *** 14 ms
;; 100000000000000001590289110975991804683608085639452813897813275577478387
;; 72170381060813469985856815719 is prime *** 17 ms
 
(fast-find-primes (inexact->exact 1e200) 3)
;; 999999999999999969733122212510361659474503275455023626482417509503468484
;; 355540755341963384047062518680275124159738824081821357343682784846393850
;; 41047239877871023591066789981811181813306167128854888513 is prime *** 69 ms
;; 999999999999999969733122212510361659474503275455023626482417509503468484
;; 355540755341963384047062518680275124159738824081821357343682784846393850
;; 41047239877871023591066789981811181813306167128854888901 is prime *** 73 ms
;; 999999999999999969733122212510361659474503275455023626482417509503468484
;; 355540755341963384047062518680275124159738824081821357343682784846393850
;; 41047239877871023591066789981811181813306167128854890043 is prime *** 71 ms
 
;; The run times did not double each time, they go up by a factor
;; of 4, but any constant factor increase is still evidence of logarithmic
;; time.  So our test is successful.
 
;; Note how (inexact->exact 1e200) is not 1 followed by 200 zeroes but is rather a
;; 200-digit number beginning with many 9's.  I guess that's due to some machine
;; limitation.
 
;; 1.25
 
;; Alyssa's expmod code does not go modulo after each squaring operation, but only
;; takes one remainder at the end.  Note that when we use expmod in the fermat
;; test, the number n we are testing for primality is the exponent, and this
;; number can be very large.  So the intermediate numbers in Alyssa's expmod
;; process will be super huge and probably cause some kind of register overflow
;; error.
 
;; 1.26
 
;; expmod is a recursive procedure and it's time complexity is the number of nodes
;; in the recursion tree.  By doubling the number of recursive calls, Louis's
;; expmod turns the recursion tree from a single branch with log(n) nodes, into a
;; full binary tree with n nodes.
 
;; 1.27
 
(define carmichael '(561 1105 1729 2465 2821 6601))
 
(define (passes-fermat? n)
  (for/and ([a (in-range 2 n)])
           (= (expmod a n n) a)))
 
(passes-fermat? 5)
;; #t
(passes-fermat? 6)
;; #f
(passes-fermat? 7)
;; #t
(passes-fermat? 8)
;; #f
(passes-fermat? 9)
;; #f
 
(for/and ([n carmichael]) (passes-fermat? n))
;; #t
 
(for/or ([n carmichael]) (my-prime? n))
;; #f
 
;; So the carmichael numbers are non-primes that fool the fermat test.
 
;; 1.28
 
;; the Miller-Rabin primality test
 
(define (special-expmod base expo m)
  ; same as expmod but immediately returns 0 on encountering
  ; a non-trivial square root of unity
  (cond [(zero? expo) 1]
        [(even? expo)
         (define u (special-expmod base (/ expo 2) m))
         (define u-squared (remainder (sqr u) m))
         (if (and (= u-squared 1)
                  (> u 1)
                  (< u (sub1 m)))
           0
           u-squared)]
        [else (remainder (* base (special-expmod base (sub1 expo) m)) m)]))
 
(define (miller-rabin-prime? n [times 10]) ; run 10 times by default
  (define (try-it a)
    (= (special-expmod a (sub1 n) n) 1))
  (if (< n 10)
    (member n '(2 3 5 7))
    (for/and ([i (in-range times)])
             ; do not test 0, 1, or n - 1
             (try-it (+ 2 (random (min 4294967087 (- n 3))))))))
 
;; easy tests
 
(miller-rabin-prime? 10)
;; #f
(miller-rabin-prime? 11)
;; #t
(miller-rabin-prime? 12)
;; #f
(miller-rabin-prime? 13)
;; #t
(miller-rabin-prime? 14)
;; #f
(miller-rabin-prime? 15)
;; #f
(miller-rabin-prime? 16)
;; #f
(miller-rabin-prime? 17)
;; #t
 
;; big primes from earlier problems
 
(miller-rabin-prime? 10000000000037)
;; #t
(miller-rabin-prime? 10000000000051)
;; #t
(miller-rabin-prime? 10000000000099)
;; #t
(miller-rabin-prime? 1000000000000037)
;; #t
(miller-rabin-prime? 1000000000000091)
;; #t
(miller-rabin-prime? 1000000000000159)
;; #t
(miller-rabin-prime? 100000000000000003)
;; #t
(miller-rabin-prime? 100000000000000013)
;; #t
(miller-rabin-prime? 100000000000000019)
;; #t
 
;; close but no cigar
 
(miller-rabin-prime? (* 10000000000037 10000000000051))
;; #f
(miller-rabin-prime? (* 1000000000000091 1000000000000159))
;; #f
(miller-rabin-prime? (* 100000000000000003 100000000000000019))
;; #f
 
;; the toughies
 
(for/or ([n carmichael])
        (miller-rabin-prime? n))
;; #f
 
;; lastly let's time it on some pretty big primes
 
(time (miller-rabin-prime?  100000000000000007629769841091887003294964970946821))
;; cpu time: 16 real time: 9 gc time: 0
;; #t
(time (miller-rabin-prime?  100000000000000007629769841091887003294964970947027))
;; cpu time: 15 real time: 8 gc time: 0
;; #t
(time (miller-rabin-prime?  100000000000000007629769841091887003294964970947049))
;; cpu time: 0 real time: 5 gc time: 0
;; #t
 
;; So Miller-Rabin looks like a very efficient and trustworthy primality checker.

Comments