SICP 問題 1.22 (時間の増加の程度を求める)

なんか面倒くさいなぁ。。

【問題】
殆どのLispの実装には基本的手続きruntimeがあり、システムが走った時間を(例えばマイクロ秒で)示す整数を返す。
次のtimed-prime-test手続きは整数nで呼び出されるとnを印字し、nが素数かどうかを調べ、nが素数なら手続きは三つの星印とテストに要した時間を印字する。

※2010/2/22追記
 誰か存じませぬが、ソースコードを色づけで表示できるはてな記法をお教え頂いたので早速使ってみる。
 超便利♪ありがとうございました!
 

(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 を書け。
その手続きでそれぞれ 1,000、10,000、100,000、1,000,000 より大きい、小さい方から三つの素数を見つけよ。
素数をテストする時間に注意せよ。テストのアルゴリズムは Θ(√n)の増加の速度だから、10,000前後の素数のテストは1,000前後のテストの√10倍かかると考えよ。時間のデータはこれを支持するか。100,000、1,000,000のデータは√nの予想のとおりだろうか。
結果はプログラムが計算に必要なステップ数に比例した時間で走るという考えにあっているか。
 
 
【解答】
とりあえず、過去にでてきた次の関数群が必要なので、Gaucheに読み込ませておく。

;最少の序数を求める
(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)))

(define (square x)
  (* x x))

で、素数1999でtimed-prime-testを試してみる。


gosh> (timed-prime-test 1999)

ERROR: unbound variable: runtime

Stack Trace:
_______________________________________
0 (runtime)
At line 32 of "(stdin)"
1999gosh>

runtimeがないっす。
 
何をuseすればruntimeが使えるのか調べてみたけどわかんないので、代替できそうな関数を探してみた。
 
リファレンスのサイトで、current-timeなる関数を発見。引数は得に指定する必要がなさそうなので、こいつを調べてみる。

gosh> current-time
#
gosh>
お?SRFI-19ってあるのにそのまま使えるっぽい?大丈夫そうなのでruntimeをcurrent-timeにする。

(define (timed-prime-test n)
  (newline)
  (display n)
  (start-prime-test n (current-time)))

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

もう一度実行。


gosh> (timed-prime-test 1999)

ERROR: operation - is not defined between # and #

Stack Trace:
_______________________________________
1999gosh> 減算処理はそのままできないようなので時間差分を取り出せる関数を探すと、time-differenceが使えそう。
これでやってみよう。

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

読み込ませて、実行


gosh> (timed-prime-test 1999)

ERROR: unbound variable: time-difference

Stack Trace:
_______________________________________
0 (time-difference (current-time) start-time)
At line 56 of "(stdin)"
1999gosh> あれ〜?current-timeは使えるのにtime-differenceは使えない?んじゃ、SRFI-19を改めて読み込んでみるか。


gosh> (use srfi-19)
#
gosh> time-difference
#
gosh>
よっしゃ。じゃ、改めて。

gosh> (timed-prime-test 1999)

1999 *** ##
gosh>

ようやく使える状態になりました。
 
 
さて、問題を解いていこう。
 
指定範囲の連続する奇整数について素数性を調べる手続き search-for-primes を書くと。

(define (search-for-primes idx max)
  (cond ((even? idx) (search-for-primes (+ idx 1) max))
        ((> idx max) #f)
        (else
         (begin
           (timed-prime-test idx)
           (search-for-primes (+ idx 2) max)))))

こんな感じ?実行してみると、こうなった。
 
■1,000


gosh> (search-for-primes 1000 1020)

1001
1003
1005
1007
1009 *** #
1011
1013 *** #
1015
1017
1019 *** ##f
gosh>

 
■10,000

gosh> (search-for-primes 10000 10040)

10001
10003
10005
10007 *** #
10009 *** #
10011
10013
10015
10017
10019
10021
10023
10025
10027
10029
10031
10033
10035
10037 *** #
10039 *** ##f
gosh>

 
■100,000

gosh> (search-for-primes 100000 100050)

100001
100003 *** #
100005
100007
100009
100011
100013
100015
100017
100019 *** #
100021
100023
100025
100027
100029
100031
100033
100035
100037
100039
100041
100043 *** #
100045
100047
100049 *** ##f
gosh>

 
■1,000,000

gosh> (search-for-primes 1000000 1000050)

1000001
1000003 *** #
1000005
1000007
1000009
1000011
1000013
1000015
1000017
1000019
1000021
1000023
1000025
1000027
1000029
1000031
1000033 *** #
1000035
1000037 *** #
1000039 *** #
1000041
1000043
1000045
1000047
1000049#f
gosh>

 
 
 
次に、各オーダーでの計測時間の比較を。
 
■1,000の平均値と、その√10倍の値を求める。

gosh> (/ (+ 0.000029000 0.000028000 0.000026000) 3)
2.7666666666666667e-5
gosh> (* 2.7666666666666667e-5 (sqrt 10))
8.748968193132517e-5
gosh>
 
■10,000の平均値と、その√10倍の値を求める。

gosh> (/ (+ 0.000089000 0.000090000 0.000089000 0.000088000) 4)
8.9e-5
gosh> (* 8.9e-5 (sqrt 10))
2.8144271175498575e-4
gosh>
 
■100,000の平均値と、その√10倍の値を求める。

gosh> (/ (+ 0.000282000 0.000263000 0.000258000 0.000310000) 4)
2.7825e-4
gosh> (* 2.7825e-4 (sqrt 10))
8.799037589418517e-4
gosh>
 
■1,000,000の平均値を求める。

gosh> (/ (+ 0.000840000 0.000905000 0.000822000 0.000839000) 4)
8.514999999999999e-4
gosh>
 
 
桁が一つ増える度に大体√10倍になってるのが確認できる感じ。