SICP 习题 1.24 要求使用费马检测检测素数,可以说我的噩梦是从这道题开始的,从这道题开始的好几个星期内完全处于怀疑自己智商的状态中,因为我发现我要面对的不是会不会解题这个问题,而是我能不能理解题目的问题。
后来的努力证明,普通人也是可以理解复杂的数学问题的,所以各位可以继续努力!!
说到费马检测,首先是要去看看最朴素的素数检测方法,就是使用我们之前的smallest-divisor找最小因数的过程,如果一个数的最小因数就是它自己,那么这个数就是素数。
如果你对我上面说到的还是不太明白的话,就需要回去看看数论中有关素数,合数的基本讨论了。没事,我也是特意在网上找了一些资料重新看了有关素数的讨论才开始继续下面的解题过程的。
因为上面提到的朴素的素数检测方法比较耗时,所以大家就开始找方法更快地检测一个数是不是素数。费马检测就是其中的著名方法,SICP书中也比较详细地讲解了费马检测。
首先要明确的就是费马检测这个方法是一个“概率方法”,就是通过这个方法可以发现一个数是素数的可能性大不大,并不能准确地判断一个数是不是素数。
有关“概率方法”这个想法一定要理解清楚,后面好几道题都和这个概念有关。
然后就是理解费马检测的具体操作,如果要判断一个数n是不是素数,最基本的就是找一个比n小,比1大的数a,如果((a的n次方)对n求模)= a 的话,这个数n是素数的可能性就很大。
现在的问题是如何求((a的n次方)对n求模)),其实我最早想到的就是用我们前面的题目中做的快速求n次方的过程fast-expt,再加上remainder过程就可以了。没想到后来这个方法还在习题1.25中作为反例出现!伤自尊呀!
后来就去看别人实现的((a的n次方)对n求模))的过程,出乎我意料地长成这个样子:
(define (expmod base exp m)
(cond ((= exp 0) 1)
((even? exp)
(remainder (square (expmod base (/ exp 2) m))
m))
(else
(remainder (* base (expmod base (- exp 1) m))
m))))
接着看费马测试的过程就很简单了,实现如下:
(define (fermat-test n)
(define (try-it a)
(= (expmod a n n ) a))
(try-it (+ 1 (random (- n 1)))))
其实就是通过random过程随机找一个比n小比1大的数,然后通过expmod过程进行检测。
不过,以上方法只是对数n做了一次费马检测,如果数n通过检测的话只能说n这个数是素数的可能性大。如何让这种方法更厉害一点呢?简单的方法就是多做几次费马检测,如果都通过的话那n这个数是素数的可能性就更大了。
过程如下:
(define (fast-prime? n times)
(cond ((= times 0) true)
((fermat-test n) (fast-prime? n (- times 1)))
(else false)))
上面的过程可以指定一个数n进行费马检测,同时指定检测次数,检测次数越大,出来的结果就越准确。
事实上,悄悄告诉你,不管你检测多少次,有些数就是可以骗过费马检测的,那些数不是素数,不过它们可以百分百通过费马检测,后面的习题还会讨论这一点。
最后,结合之前的习题,可以通过以下过程对一个数n进行素数检测,同时报告检测所需要的时间,可以发现,下面的过程中调用fast-prime?时指定检测次数为100次。
(define (start-prime-test n start-time)
(if (fast-prime? n 100)
(begin
(report-prime n (- (real-time-clock) start-time))
#t)
#f))
通过以上方法就可以回答题中的有关计算时间的问题了。
我测试了比100,10000,100000000,10000000000000000大的三个素数,测试100, 10000的时候不明显,测试到100000000,10000000000000000的时候就比较明显了,1000000000比10000多用了一倍时间,而10000000000000000比1000000000又多用了一倍时间。
一切符合理论上的对数步数的预期。
同时惊叹一下,能找到对数步数的算法真的很牛X,计算10000000000000000左右的数值只比计算1000000000左右的数值花多了一倍的时间!