[ 目次, 前節, 次節, 索引 ]

1.3.3 一般的方法としての手続き



1.1.4節では数値演算のパターンを特定の数と無関係であるように抽象する機構としての合成手続きを紹介した. 1.3.1節のintegralのような高階手続きでは, 更に強力な抽象化: 特定の関数とは無関係であるような計算の一般的方法を表すための手続きを学び始めた. 本節ではより精巧な二つの例---関数の零点と不動点を探す一般的方法---と, これらの方法を手続きとして直接表現する方法を論じよう.
区間二分法による方程式の零点の探索
区間二分法(half-interval method)は連続関数fに対し, 方程式f(x)=0の根を探す単純かつ強力な技法である. その考え方は, 二点a, bf(a) < 0 < f(b)なら, fabの間に少くとも一つの零点を持つ筈というのである. 零点を見つけるのにxabの平均とし, f(x)を計算する. f(x) > 0ならfの零点はaxの間にある. f(x) < 0ならfの零点はxbの間にある. これを続け, fの零点のあるべき次第に狭い区間を見つけて行く. 区間が十分小さくなったらプロセスを止める. 不確かな区間はこのプロセスのステップごとに半分に減るから, Lを初めの区間の長さ, Tを許容誤差(「十分小さい」と考える区間の大きさ)として, 必要なステップ数はΘ(log (L/T))で増加する. この戦略を実装した手続きを示す.

(define (search f neg-point pos-point)
  (let ((midpoint (average neg-point pos-point)))
    (if (close-enough? neg-point pos-point)
        midpoint
        (let ((test-value (f midpoint)))
          (cond ((positive? test-value)
                 (search f neg-point midpoint))
                ((negative? test-value)
                 (search f midpoint pos-point))
                (else midpoint))))))

   初めに関数fと, 値が負と正の二点が与えられているとする. まず二点の中間点を計算する. 次に区間が十分小さいかを調べ, そうであれば単に中間点を答として返す. そうでなければ中間点におけるfの値をテスト値として計算し,テスト値が正なら初めの負の点から中間点までの新しい区間で作業を続ける. テスト値が負なら中間点から正の点までの区間で続ける. 最後にテスト値が丁度0の可能性もあり, その場合は中間点そのものがわれわれの探している解である.

   終端点が「十分近い」かどうかのテストには, 1.1.7節の平方根の計算に使ったものと似た手続きが使える.55

(define (close-enough? x y)
  (< (abs (- x y)) 0.001))

   searchは, うっかりするとfが要求された符号を持たない二点を与えられたりするから, 直接使うには厄介であり, その場合正しくない答が出る. その代りsearchを終端点のどちらが負の関数値で, どちらが正の関数値かを調べ, searchを正しく呼ぶ次の手続きを経て使うことにする. 関数が与えられた二点で同じ符号を持つなら, 区間二分法は使われず, 手続きはエラーとなる.56


(define (half-interval-method f a b)
  (let ((a-value (f a))
        (b-value (f b)))
    (cond ((and (negative? a-value) (positive? b-value))
           (search f a b))
          ((and (negative? b-value) (positive? a-value))
           (search f b a))
          (else
           (error "Values are not of opposite sign" a b)))))

   次の例は2と4の間のsin x = 0の根としてπの近似値をとるのに区間二分法を使う.

(half-interval-method sin 2.0 4.0)
3.14111328125

次の例は区間二分法を使い, 1と2の間で方程式x3 - 2x - 3 = 0の根を探す.

(half-interval-method (lambda (x) (- (* x x x) (* 2 x) 3))
                      1.0
                      2.0)
1.89306640625
関数の不動点の探索
xf(x) = xを満す時, xを関数f不動点(fixed point)という. ある関数fについて, 最初の予測値から始め, fを値があまり変らなくなるまで繰り返し作用させることで, 不動点を見つけることが出来る.

f(x), f(f(x)), f(f(f(x))), ...

この考えを使い, 入力として関数と最初の予測値をとり, その関数の不動点の近似値を返す手続きfixed-pointが作れる. 関数を二つの連続する値の差があらかじめ決めた許容量より小さくなるまで繰り返し作用させる:
(define tolerance 0.00001)


(define (fixed-point f first-guess)
  (define (close-enough? v1 v2)
    (< (abs (- v1 v2)) tolerance))
  (define (try guess)
    (let ((next (f guess)))
      (if (close-enough? guess next)
          next
          (try next))))
  (try first-guess))
例えば最初の予測値を1として, 余弦関数の不動点の近似値を得るのにこの方法が使える:57


(fixed-point cos 1.0)
.7390822985224023
同様に方程式y = sin y + cos yの解が得られる:


(fixed-point (lambda (y) (+ (sin y) (cos y)))
             1.0)
1.2587315962971173

   不動点のプロセスは, 1.1.7節で平方根を求めるのに使ったプロセスを思い出させる. どちらも結果がある条件になるまで予測値を繰り返し改良するという考えに基づく. 実際, 平方根の計算をすぐに不動点探索に書き直せる. あるxの平方根の計算はy2 = xなるyを探すことである. この方程式を等価な形y = x/yと書き, 関数y x/y~58 の不動点を探しているのだと思えば, 平方根の計算を

(define (sqrt x)
  (fixed-point (lambda (y) (/ x y))
               1.0))
とやってみることが出来る.

   残念ながら, この不動点探索は収束しない. 最初の予測値をy1としよう. 次の予測値はy2 = x/y1で, その次の予測値はy3 = x/y2 = x/(x/y1) = y1. つまり答のまわりで振動しながら, 二つの予測値, y1y2を何度も繰り返す無限ループになる.

   そういう振動を制御する一つの方法は予測値の大きな変化を防ぐことである. 答は予測値yx/yの間にあるから, yからx/y程は遠くない新しい予測値をyx/yの平均とすることが出来る. つまりyの次の予測値はx/yではなく, ½(y + x/y)である. そのような予測値の並びを作るプロセスは単にy ½(y + x/y)の不動点を探すことである:


(define (sqrt x)
  (fixed-point (lambda (y) (average y (/ x y)))
               1.0))
(y = ½(y + x/y)が方程式y = x/yの単純な変換であることに注意しよう; これを得るには方程式の両辺にyを足して2で割る.)

   この修正により, 平方根の手続きは働く. 実際この定義を解明してみると, ここで生成した平方根の近似値の並びは, 1.1.7節の平方根の初めの手続きが生成するものとまったく同じであることが分る. 解への逐次近似を平均化する方法を 平均緩和法(average damping)というが, 不動点探索で収束を助けることが多い.

問題 1.35


(1.2.2節の)黄金比φが変換x 1 + 1/xの不動点であることを示し, この事実を使いfixed-point手続きにより&\phi;を計算せよ.

問題 1.36


問題1.22で示した基本のnewlinedisplayを使い, 生成する近似値を順に印字するようfixed-pointを修正せよ. 次にx log(1000)/log(x)の不動点を探索することで, xx = 1000の解を見つけよ. (自然対数を計算するSchemeの基本log手続きを使う.) 平均緩和を使った時と使わない時のステップ数を比べよ. ({ fixed-pointの予測値を1にして始めてはいけない. log(1)=0による除算を惹き起すからだ.)

問題 1.37


a. 無限の連分数(continued fraction)は



の形の式である. 例えばNiDiがすべて1の無限連分数展開が1/φになることが示せる. φは(1.2.2節で示した)黄金比. 無限連分数の近似値のとり方の一つは, 与えられた項数で展開を中断することで, そういう中断--- k項有限連分数(k-term finite continued fraction)という---は



の形である. ndを一引数(項の添字i)で連分数の項のNiDiを返す手続きとする. (cont-frac n d k)k項有限連分数を計算するような手続きcont-fracを定義せよ.

(cont-frac (lambda (i) 1.0)
           (lambda (i) 1.0)
           k)
kの順次の値で1/φの近似をとり, 手続きを調べよ. 4桁の精度の近似を得るのに, kはどのくらい大きくしなければならないか.

b. cont-fracが再帰的プロセスを生成するなら, 反復的プロセスを生成するものを書け. 反復的プロセスを生成するなら, 再帰的プロセスを生成するものを書け.

問題 1.38


1737年, スイスの数学者 Leonhard Eulerは De Fractionibus Continuisというメモを発表した. その中にeを自然対数の底としてe - 2 の連分数展開がある. この分数ではNiはすべて1, Diは順に1, 2, 1, 1, 4, 1, 1, 6, 1, 1, 8, ... . 問題1.37のcont-fracを使い, Eulerの展開によりeを近似するプログラムを書け.

問題 1.39


正接関数の連分数展開は1770年にドイツの数学者 J. H. Lambertが発表した. xをラジアンで表し,



Lambertの式に基づいて正接関数の近似値を計算する手続き(tan-cf x k) を定義せよ. kは問題1.37と同様, 計算する項数を指定する.



55 0.001を計算の受け入れられる誤差の許容量を示す「小さい」数の代表としてきた. 実際の計算の適切な許容量は解くべき問題と計算機やアルゴリズムの限界に依存する. これは 数値解析の専門家やある種の魔術師の助けを必要とする微妙な問題である.

56 これは引数にエラーメッセージとして印字するいくつかの項目をとる errorを使って実現出来る.

57 講義がつまらない時にやってみるとよい: 電卓をラジアンモードにし, 不動点が得られるまでcosのボタンを押し続ける.

58 (「写像する」と読む)は lambdaの数学者流の記法である. y x/y$は(lambda (y) (/ x y))のことで, つまりyでの値がx/yの関数のことである.

[ 目次, 前節, 次節, 索引 ]