1.2.6 例子:素数的测试

1.2.6 例子:素数的测试
这一部分描述检测一个整数的素数性的两种方法。一个的成长序是平方根N,一个快速的方法是
log(n).这部分结尾的练习建议编程项目基于这些算法。

* 检索除数因子

从古代,数学家们已经痴迷于素数的问题。许多人研究测试一个数是不是素数的方法的问题。
测试一个数是不是素数的方式之一是找它的除数因子。如下的程序找一个给定的整数
的大于1的最小的整数的除数因子。它以一种很自然的方式,来测试这个数的可除性,以从2开始的
连续的整数。

(define (smallest-divisor n)
  (find-divisor n 2))

(define (find-divisor n test-divisor)
   (cond ((> (square test-divisor) n) n)
         ((divides? test-divisor n) test-divisor)
         (else (find-divisor n (+ test-divisor 1)))
   )
)

(define (divides? a b)
     (= (remainder b a) 0)
)

我们能测试一个数是不是素数,如下:一个数是素数,仅当这个数的最小的除因子是它本身。

(define (prime? n)
   (= n (smallest-divisor n))
)

find-divisor的终极测试是基于这个事实。即如果一个数是合数,那么它必须有一个除因子小于
等于这个数的平方根。 这意味着算法只需要测试除因子从1到这个数的平方根。因此,
测试一个数是不是一个素数的时间复杂度是n的平方根。


* 菲梅特的测试
对数级的素数性的测试是基于数论中的一个结果,这个结果即是有名的菲梅特的小定理。

菲梅特的小定理
如果n是一个素数,a是一个小于n的正整数,那么a^n与a在除以n时,同余。

(如果两个数在都除以n时有相同的余数,那么可以说这两个数是对于n同余的。)

如果n不是素数,总之,对于小于n的a的绝大多数的值,都不满足上述的关系式。
这导致了测试素数的如下的算法。 给出一个数n,随机地选择一个a,a小于n,并且计算
a^n对N取模。如果结果不等于a,这个n一定不是素数。如果它是a,n是很有可能是素数的。
现在再选择另一个随机数,再做相同的测试。如果还满足方程式,那么我们能对n
是素数更有信心了。通过尝试越来越多的a值,我们能对结果增加了信心。这个算法被称为
菲梅特测试。

为了实现菲梅特测试,我们需要一个程序计算一个数的幂对另一个数的模。

(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) )
     )
)

这与1.2.4部分中的程序 fast-expt 是很相似的。它使用连续的平方,
为了得到求幂运算的对数级的时间复杂度。

菲梅特测试被执行是通过选择一个在1到n-1之间的随机数a,来检查
是否满足同余的公式。随机数a的选择通过使用random程序来完成。
我们能假定这个random程序是scheme语言中的原生的程序。random程序
返回小于输入值的非负的随机的整数。因此,为了得到一个在到n-1之间的随机数,
我们能调用random,参数是n-1再加上1。

(define (fermat-test n)
    (define (try-it a)
           (= (expmod a n n) a))
  (try-it (+ 1 (random (- n 1))))
)

如下的程序能运行给定的次数,这个次数由一个参数指定。如果测试成功
它的值是真,否则是假。

(define (fast-prime? n times)
    (cond ((= times 0) true)
          ((fermat-test n) (fast-prime? n (- times 1)))
          (else false)
    )
)


* 概率方法

菲梅特测试 不同于之前的大部分的算法,这些算法在计算答案时,能保证它是对的。
这里的答案仅可能是对的。更确切的说,如果n没有通过测试,我们能确定n不是素数。
但是n通过了菲梅特测试的事实,仅是很强烈地表明,却没有保证它是一个素数。
我们要说的是,对于任何一个N,如果我们执行足够多的测试次数,发现n总是能通过测试,
那么在我们的素数测试中,出错的概率能达到我们想要的那么小。

不幸的是,这种断言并不是对的。确实存在这样的数,愚弄了菲梅特测试。这个数n不是素数,
却有这样的属性,对于小于N的所有的正整数,都满足同余公式。这样的数是极少的,
所以在实践来看,菲梅特测试还是相当可靠的。这有一些菲梅特测试的变种不能被愚弄。
在这些测试中,正如菲梅特方法。(这样的测试的一个例子见练习1.28).另一方面,
与菲梅特测试相反,对于任何一个N,能证明,如果n不是素数,大部分的小于N的数不能满足条件。
因此,n能通过一些随机数的测试,说明n很可能是素数。如果n通过了两个随机数的测试,n是素数的机会是
3/4以上。通过足够次数的测试,出错的概率能达到我们想要的那么小。

测试存在性,能证明出错的概念足够小。这种类型的算法,被称为概率算法。
在这个研究领域,有很多的研究活动,概率算法已经应用到了很多的领域。

练习1.21
使用程序smallest-divisor找到如下的数据的最小因子。
199,1999,19999

练习1.22
绝大部分的Lisp的实现,都包括了一个原生程序,叫做runtime(运行时),
它返回一整数,来指定系统已经运行的时间(这种度量,例如,微秒)。
如下的程序timed-prime-test,当被调用时,参数是整数n,打印n,检查n是否是素数。
如果n是素数,程序打印三个星号,再打印执行测试花费的时间。

(define (timed-prime-test n)
    (newline)
    (display n)
    (start-prime-test n (runtime))
)

(define (start-prime-test n start-time)
    (if (prime? n)
        (report-prime (- (runtime) start-time)))
)

(define (report-prime elapsed-time)
    (display " *** ")
    (display elapsed-time)
)

使用这个程序,写一个程序search-for-primes检查一个范围内的连续的奇整数的素数性。
使用你的这个程序找到大于1000的最小的三个素数,找到大于一千的最小的三个素数,
找到大于一万的最小的三个素数,找到大于十万的最小的三个素数,还有大于一百万
的最小的三个素数。注意测试需要的时间。
因为测试算法的时间复杂度是sqrt(n),因此你应该预期测试大于1万的素数需要的时间是
测试大于1千的素数的时间的sqrt(10)倍。你的测试实际值呢?对于10万和100万时,你的
测试能很好地支持sqrt(n)的预测吗?在你的测试结果中,符合这个理念吗?这个理念是
在你的机器上运行的程序的执行时间正比于这个计算需要的执行步骤数。

练习1.23
这部分开始的程序smallest-divisor做了很多的不必要的测试:在检查了一个数能否被2整
除后,没有必要再检查它能否被更大的偶数整除了。这个建议是test-divisor使用的除数不应该
是2,3,4,5,6...而是2,3,5,7,9。为了实现这个改进,定义一个程序next,
如果它的输入是2,返回是3,否则加2。修改smallest-divisor程序,用(next test-divisor)
代替(+ test-divisor 1)。 然后在timed-prime-test程序中,集成这个改进版本的
程序smallest-divisor,再运行练习1.22中的例子。因为这个改进减少了一半的测试步骤,你
可能预期它的运行是原来的两倍那么快。这个预期能得到确认吗?如果不能,这两个算法之间的
性能比例是多少?你又怎么解释这个比例值不是2的事实呢?


练习1.24
修改练习1.22中的程序time-prime-test,使用fast-prime?(菲梅特方法)测试那个练习中
的12个素数的例子。因为菲梅特方法有对数级的时间复杂度,你能怎么预期测试1百万附近的素数的时间
是测试1千附近的素数的时间?你的测试数据支持它吗?你能解释你发现的任何的异常吗?


练习1.25
Alyssa P.Hacker抱怨在写expmod程序中,我们做了太多的额外的工作了。她说,毕竟
我们已经知道了如何计算幂运算了,我们能简单地写成如下的程序:

(define (expmod base exp m)
    (remainder (fast-expt base exp) m))

她是对的吗?解释一下,这个程序能像我们的快速素数测试那么有效吗?

练习1.26
louis Reasoner 在做练习1.24时,遇到了很大的困难。他的fast-prime?测试
似乎比他的prime?运行得更慢了。Louis叫了他的朋友 Eva Lu Ator过来帮他。
当他们检查louis的代码时,他们发现他已经重写了expmod程序,使用显式的乘法,
而不是调用平方:

(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) )
     )
)
louis 说“我没有看出它们的区别来”,EVA说“我看出来了”,“通过像你这样的写,
你把时间复杂度从对数级,变成了线性级。” 解释一下怎么回事吧。


练习1.27
演示一下,在注脚47中列出的 卡迈克尔(carmichael)数,这个数确实是愚弄了菲梅特测试。
也就是说写一个程序进行菲梅特测试,在给定的 卡迈克尔(carmichael)数上进行尝试一下。

练习1.28

菲梅特测试的一个改进版是不能被愚弄的,叫做米尔拉宾(Miller-Rabin)测试。它开始于
菲梅特的小定理的一个变换的形式。它声明的是如果n是一个素数,并且a是一个小于n的正整数,
那么 a^(n-1)与1对于N取模,它们是同余的。为了使用米尔拉宾(Miller-Rabin)测试一个数
的素数性,我们选择一个随机数,使用程序expmod,计算a的(n-1)次幂再对n取模。然而,
当我们执行expmod程序中的平方步骤时,我们检查是否我们已经发现了一个 1对N取模的
非平凡的平方根。 也就是这个数不等于或者是n-1. 而1或者是n-1的平方等于1对N取模。
它可能证明 如果存在1的非平凡的平方根,那么N不是素数。它也可能证明n不是素数时,
至少有一半的a,以这种方式计算a^(n-1)能显示出有1的非平凡的平方根。(这是米尔拉宾
测试没有被愚弄的原因) 修改expmod程序来标识出它是否发现了1的非平凡的平方根,并且
使用这个程序来实现米尔拉宾(Miller-Rabin)测试。 这个程序与fermat-test程序是相似的。
通过测试各种已知的素数和非素数,来检查你的程序。提示:一个方便的做法是让expmod的标识是
有的时候,就返回0。

猜你喜欢

转载自blog.csdn.net/gggwfn1982/article/details/81412196