高校数学の窓過去問検索

質問<3643>2007/12/3 from=みのる 「積分のプログラム」

区間[a、b]をn等分し




によって、積分を近似する台形公式のプログラムを作れ。
但し、である。

ご指導ねがいます。

★希望★完全解答★




こう言う質問の仕方は大間違いの典型なんですが、どの道ここではSchemeしか使わないんで、まあ、いいでしょう。
(何が質問の仕方として間違えてるのか、もうお分かりでしょうね?)

取りあえず、ここではサクっと台形公式による定積分のアルゴリズムを擬似コードとして紹介しておきます。


  1. 関数integrateの引数を被積分関数func、積分下限a、積分上限b、分割(繰り返し)数nとして定義する。

  2. 局所変数を使って微小区間を定義する。

  3. 局所変数を使って面積を求める繰り返し計算の初期値を定義する。

  4. i=1〜n-1回の間で次の繰り返し計算を行う。

    1. を計算する。

    2. を計算する。


  5. 計算結果としてを返す。

このアルゴリズムを使えば基本的にどんな言語でも台形公式での数値積分を実装することが出来ます。
ところで、どうしてこう言うアルゴリズムになるのか、と言うと、問題の与式は結局、



と整理されるから、ですよね。台形公式による数値積分のアルゴリズムではが初期値、そして繰り返し計算の中身はを順次加算していってる、となってるワケです。
以前、繰り返し計算自体は再帰構造によって記述出来、本質的には再帰計算は数列そのものだ、と言う話をしました。ここでもその親和性を見て取る事が出来ます(この問題はそう言う意味ではプログラム構造の数学的記述による証明、と言う見方が出来ます)。
では、実際に上のアルゴリズムをSchemeによって実装してみます。
が、そろそろ(僕自身が)単純な再帰や末尾再帰を書くのも飽きてきましたし(笑)、ここでちょっと新しいScheme上での末尾再帰のテクニックを紹介しておこうと思います。名付けて名前付きlet構文と言われるテクニックです。
名前付きlet構文の書式は、

(let <適当な名前> <局所変数の初期値設定> <計算内容>)

と言うリストで表されます。
名前付きlet構文は局所変数を定義するletと違い、局所関数と言えるモノを定義する構文です。また、これは実際には今まで書いてきたような末尾再帰による繰り返し計算へとScheme内部で翻訳されます。言わば、本質的な意味での構文じゃないんですが、記述が簡単になるように敢えて「表面的に」付け加えられたモノです。こう言う「実は構文じゃないんだけど、簡略表現の為に敢えて付け加えられた擬似構文」をプログラミング用語で糖衣構文と呼んだりします。

註:例えばC言語なんかを学んだ事がある人は、変数aに1加えて新しくaとする場合、

a = a + 1

と書く代わりに

a++

と記述したりするのをご存知でしょう。これも本質的ではないんですが、記述の簡略化と言う意味では糖衣構文の一種です。

また、今までは関数(プログラム)を定義する時、馬鹿正直に

(define <プログラム名> (lambda (引数) <計算内容>))

と記述してきましたが、そろそろコレもメンド臭くなってきたので(笑)、ここにも糖衣構文を用います。
糖衣構文による関数(プログラム)の定義方法は以下の通りです。

(define <関数(プログラム)の実行形式> <計算内容>)

ここで<関数(プログラム)の実行形式>とは、実際に書いたプログラムを走らせる形式です。つまり、この問題では最終的には

(integrate func a b n)

の形で走らせるので、このプログラムを書く時点で

(define (integrate func a b n) <計算内容>)

と「実行形式」の後にいきなり<計算内容>を書き下すスタイルを用います。その方がタイピング量が少なくなりますからね。
では、先ほど書いた擬似コードをSchemeで実装したらどうなるのか、ソースをお見せします。

    (define (integrate func a b n);プログラムintegrateを定義
      (let ((h (/ (- b a) n)));微小区間hを定義
        (let loop ((i 1) (s (/ (+ (func a) (func b)) 2)));名前付きlet
          (if (= i n);iがnの時
            (* h s);解を返す
            (loop (+ i 1) (+ s (func (+ a (* i h)))))))));再帰呼び出し

Schemeではたった6行程度で台形公式による数値積分のアルゴリズムを実装出来るんですね。
ではちょっと見ていきます。
1行目では先ほど書いたように、糖衣構文を使って関数(プログラム)の定義を宣言しています。よって<計算内容>は2行目以降から、と言う事になります。
2行目では局所変数letを使って微小区間を定義していますね。これはプログラムintegrate内の全体で特に繰り返し計算によって変更される事のない数値なんで、ここで定義しているワケです。
3行目で名前付きlet構文が登場します。ここで局所関数loopを定義しているんですが、注意点を一つ。
まず、Scheme上では他のプログラミング言語(例えばBASIC等)と違い、特にloopと言う名前そのものには意味がありません。つまり、ここでは「名前付き」と言う通り、局所関数にloopと言う名前を勝手に付けてるだけで、別に何かの動作をloopと言う単語で呼び出してるワケではない、と言う事です。お好みでしたら、loopの代わりにhogeでもfugaでもminoruでも何でも付けて構いません。ここがちょっと誤解を受ける場合があるんですがSchemeにはloopと言う名前の関数は無い、と言う事だけは覚えておいてください。
そして、そのまま繰り返し計算の為のカウンターiの初期値を1にセットし、また、計算結果sの初期値をセットしています(これらは局所関数loopの引数となります)。
4行〜6行目はお馴染みif〜then構文です。そしてこれが名前付きlet構文での<計算内容>にあたります。
まずは、4行目と5行目で「脱出条件」を定義しています。まあ、ここは簡単でしょう(数列の和はn−1までなので、nになれば計算結果を返す、と言う構造に注意してください)。
6行目がポイントです。ここで局所関数loopを引数を変えて再度呼び出していますね。これが再帰的定義です。loopと言うのが局所関数、つまりプログラムintegrate内だけで通用するローカルな関数としての役割を果たしている事が分かると思います(従って、関数loopは外部から呼び出す事は出来ません)。ここでカウンターiを1増やし、また、sにf(a+ih)を順次加えて行ってるワケです。

まあ、この程度の単純なアルゴリズムではSchemeでも他のプログラミング言語でも実装のメンド臭さは殆ど変わらないんですが、一方、Schemeの方が全体的な見通しは良いとは思います。特に再帰的定義により、数学的な数列の表現を直接書き下す事が出来た、ってのは大きいでしょう。他のプログラミング言語のように、一々数列をどうやって繰り返し計算に翻訳しようか?と悩む煩わしさがありません。

では実際、台形公式による数値積分プログラム、integrateがキチンと動くかどうか試してみましょう。
ここでは例題として、次の積分が上手く行えるのか見てみます。



当然この解は1となるんですが、台形公式による数値積分は原則的に「近似計算」です。つまり「1」に近い数が計算により出てくればいいんですね。
また、台形公式による数値積分は分割数nにより、その精度が変わります。ここではnを10〜10万と範囲を変えてみて、試してみましょう。
さて、プログラムintegrateの<実行形式>がどうなのか、と言うのは既に分かってますので、上の積分を実行する為には、

(integrate sin 0 (/ pi 2) n)

とすれば良い、って事も自明です。Schemeではsin等の三角関数も用意されているので全然問題ありませんし(※)、また特殊定数なんかも定義されているんで、上のように入力するだけで数値積分を実行してくれます。
では結果を見てみましょう。



はい、ご覧のように成功してますね。
また、分割数nが増えるにしたがって精度が上がっていき誤差が少なくなっていってるのも分かると思います。

以上です。




※:余談ですが、通常のプログラミング言語ではこのように関数の名前(この例ではsin)を引数として渡して、作成したプログラムを実行してくれるとは限りません。
こう言う機能を高階関数機能と言うんですが、この機能をサポートしている言語はまだまだ限られています。Schemeの強力な能力としてこの高階関数機能を装備している、と言う事が挙げられます。
勘違いして欲しくないんですが、この高階関数と言う表現方法は数学の世界では当たり前です。あまりにも当たり前なんで数学では高階関数、なんて名前は特に存在してません(汎関数と言う対応概念は一応あるらしいですけど)。逆に言うとこう言う機能が「特殊な」機能になっているのは、通常「プログラミング言語」には制限が多すぎるから、なのです。あくまで設計/実装上の理由なんですね。
例えば数学では、定積分の場合、



等と書いて抽象的に「定積分と言う関数I」を表現します。この場合、「一般性を表現する為に」被積分関数をとして抽象化しています。つまり、「適切なはその場その場で変わる」事を前提にしていて、の具体的な関数としての内容は「外部から与えられなければならない」のです。
言わば関数そのものも関数Iの「引数」とならなければならないんですが、普通のプログラミング言語でこう言う「数式そのものの記述」で「関数を関数へと受け渡す」と言うのは凄く難しかったりするワケです。単なる「数値」(この場合は積分下限aと積分上限b)しか引数に取れない、と言うような制限が課されている場合が多いのです。
一方、Schemeでは高階関数機能のお陰で、具体的なの内容が無くても「抽象化された名前のままで」プログラム本体に「引数として」渡す事が出来ます。今回はこの機能を目立たない形ですが実はフルに活用しています。
また、この高階関数機能のお陰で、例えば、sin等の「Scheme組み込み関数」以外で数値積分する場合、わざわざ新しく関数をファイル内で定義しなくても、

(integrate (lambda (x) <計算内容>) a b n)
と「lambda式による関数定義」で「引数内で複雑な関数の計算を設定してしまう」ような離れ業を行う事さえ可能なんです(これは実際に試してみてください)。まあ、本文中のsinの数値積分の実験で(/ pi 2)と言う「計算」を既に引数内にブチ込んでいるんで、ある程度想像付くでしょうが、通常この形式は、普通のプログラミング言語に於いては当然許されていないのです。
この様に、Schemeは前置記法と言うかなり見慣れない(と言うより見にくい・笑)数式表現を用いていますが、反面、それさえ慣れればかなりの「数学的表現」をあまり意識すること無くそのままプログラムとして書き下すことが可能です。どうして「数学自習用のプログラミング言語」としてSchemeを取り上げたのか、こう言う部分でも実感して頂ければ幸いです。


高階関数 - Wikipedia

0 コメント: