1 数値計算の基礎ohno/4e-program/instruction.pdf · 2018. 7. 21. ·...

58
プログラミング技法 1 1 数値計算の基礎 1.1 計算機の誤差 コンピュータは,正確な計算をする機械と思われますが,実際はどうでしょうか? 以下のプ ログラムは x 0 から 2 まで 0.1 ずつ変化させ,x 1 のときに”OK” と印刷することを意図し ています.うまく行くでしょうか? プログラム例 #include<stdio.h> int main(void) { double x; for (x = 0; x < 2.0; x += 0.1) { printf("%f\n", x); if (x == 1.0) { printf("OK\n"); } } return 0; } 予想結果 0.000000 0.100000 ..... 0.900000 1.000000 OK ← こう出力させたい 1.100000 ..... 1.900000 実行結果 (抜粋 0.000000 0.100000 ..... 0.900000 1.000000 ← 成功していない 1.100000 ..... 1.900000 なぜ失敗したのか考えてみましょう

Upload: others

Post on 30-Jan-2021

1 views

Category:

Documents


0 download

TRANSCRIPT

  • プログラミング技法 1

    1 数値計算の基礎1.1 計算機の誤差

    コンピュータは,正確な計算をする機械と思われますが,実際はどうでしょうか? 以下のプログラムは xを 0から 2まで 0.1ずつ変化させ,xが 1のときに”OK”と印刷することを意図しています.うまく行くでしょうか?

    プログラム例

    � �#include

    int main(void){double x;for (x = 0; x < 2.0; x += 0.1) {printf("%f\n", x);if (x == 1.0) { printf("OK\n"); }

    }return 0;

    }� �予想結果

    0.0000000.100000.....0.9000001.000000OK ← こう出力させたい1.100000.....1.900000

    実行結果 (抜粋

    0.0000000.100000.....0.9000001.000000 ← 成功していない1.100000.....1.900000

    なぜ失敗したのか考えてみましょう

  • 2 1—計算機の基礎

    ■コンピュータの数字の表し方 コンピュータは 0か 1の数字のみを扱うので,2進数で数字を表します.人間の用いる 10進数は 10を基数に,2進数は 2を基数にした表現方法です.

    整数の場合

    10進数: 1543 = 1× 103 + 5× 102 + 4× 101 + 3× 100

    2進数: 10112 =

    小数の場合

    10進数: 0.54 =

    2進数: 0.1012 =

    ■二進数から十進数への変換 シンプルな繰り返しで変換することができます.

    整数の場合

    • 変換する十進数の値を 2で繰り返し割り,その余り記録する.

    • 商 (割った値)が0か1になるまで繰り返す.• 記録した余りを下から順にならべる.

    例:11を二進数に変換

  • プログラミング技法 3

    小数の場合

    • 変換する十進数の値を2で繰り返し掛る.• 小数部が 0になるまで繰り返す• 整数部分を上から順に並べる

    例:0.8125を二進数に変換

    問題 1–1 以下の 2進数を 10進数に,10進数を 2進数にせよ.

    001101112 = 221 =

    0.1011012 = 0.125 =

    110.1012 = 18.75 =

    ■(一般的な)コンピュータの数値の扱い方

    • 整数型 — 2進数で表される

    – 例:156 = 27 + 24 + 23 + 22 → (0000 0000 1001 1100)2

    • 実数型 — 浮動小数点で表される

    – 符号,仮数部,指数部の 3つの要素で表される.

  • 4 1—計算機の基礎

    ■その他の誤差の理由

    • 丸め誤差 – 四捨五入による誤差

    • 桁落ち – 近い値での減算で有効桁数が少なくなってしまう現象

    • 情報落ち – 桁の大きく異なる加減算において小さい値が無視される現象

    ■0.1を 10回足すと 1.0になる? 1ページ目の問題について考えてみましょう.人間の目には 0.1という値は,分かりやすい数字ですが,二進数ではどのように表されるでしょう.変換してみましょう.

    コンピュータの目から見た 0.1という値は,どのような値でしょうか?

  • プログラミング技法 5

    1.2 コンピュータによる数学へのアプローチ

    コンピュータと人間の計算の考え方は大きく異なります.例えば,1から 10までの和や,1から 100までの和を計算する場合は,人間ならば

    10∑x=1

    x = 11× 5 = 55,100∑x=1

    x = 101× 50 = 5050

    などと,自分で解き方を考えて,簡単な計算で解くようなことも出来ます.では,コンピュー

    タどうでしょうか?

    • 自分で判断して解法を考えるようなことは出来ません• 色々な場合について,同じ手順で計算が行なえるような方法が得意です

    問題 1–2 1から 100までの和を計算し表示するプログラムを,for文を使って作成せよ.

    � �#include int main() {int sum = 0, i;

    printf("%d\n", sum);return 0;

    }� �

  • 6 1—計算機の基礎

    ■例 プログラムを用いて以下の数列の a6を計算せよ

    a1 = 1, a2 = 2, an = an−1 + 2an−2

    考え方 1 力技?

    ....int a1,a2,a3,a4,a5,a6;a1=1; a2 = 2;a3 = a2 + 2 * a1;a4 = a3 + 2 * a2;a5 = a4 + 2 * a3;a6 = a5 + 2 * a4;

    考え方 2 繰返しによる方法

    a b 2a + b1(a1) 2(a2) 4(a3)

    2 4

    問題 1–3 考え方 2のような方法で a6を求めるプログラムを作成せよ.� �#include int main() {int a, b, c, i;a = 1; b = 2;

    printf("%d", c);return 0;

    }� �

  • プログラミング技法 7

    2 方程式f (x) = 0の解法方程式 f(x) = 0となるような xを解く方法について考えてみましょう.例えば f(x) = ax2 +

    bx+ cなどの代数方程式に限らず,f(x) = x+ lnxなどの一般的な方程式も対象に考えます.

    2.1 方程式の解き方

    ■代数方程式 代数方程式

    anxn + an−1x

    n−1 + · · ·+ a1x+ a0 = 0

    の解を求めることを考えましょう.

    -30

    -20

    -10

    0

    10

    20

    30

    -3 -2 -1 0 1 2 3 4 5

    例えば,f(x) = x3− 3x2− 13x+ 15としてf(x) = 0となるような xを求めるには,どう

    考えるでしょうか?通常は,

    f(x) = x3 − 3x2 − 13x+ 15= (x+ 3)(x− 1)(x− 5)

    のように因数分解を行うことで,

    x = 5, 1,−3

    となります.

    -4

    -2

    0

    2

    4

    0 0.5 1 1.5 2 2.5 3

    ■一般的な方程式では? f(x) = x + lnxな

    どの方程式ではどのように解くことができる

    でしょうか?因数分解などの手法は使えません.

    このような計算行うための,コンピュータ

    を用いた特殊な解法を以下に紹介します.

  • 8 2—方程式 f(x) = 0の解法

    2.2 二分法

    目標となる値x0 x

    y 上限の値

    下限の値

    y = f(x)

    二分法とは,y = f(x)で解が存在する区間を徐々

    に狭めて,最終的に区間と上限と下限を果てしなく

    近い値にすることで,解の近似値を求める方法です.

    ■計算手順 図のような区間 [a, b]に f(x) = 0となるような x0が存在するとします.� �1. aと bの中点 c = を求めます.

    2. 次に f(c)の符号を調べます.

    • f(c) ならば,cの値を に代入する

    • f(c) ならば,cの値を に代入する

    3. |f(c)| < ε (εは微小な値)になったら終了.そうでなければ 1に戻る.� �というで順を繰り返します.

    ab

    x0x

    y=f(x)

    x0x

    yy

    ab

    x0:目標となる値

    x0x

    y=f(x)y

    f(c)>0の場合

    f(c)

  • プログラミング技法 9

    ■例 f(x) = x+ lnx = 0の解を求める.

    a = 0.1, b = 0.8, ε = 1.0 × 10−5として,手順を始める.

    1. 中点は c = (0.8 + 0.1)/2 = 0.45

    2. f(c) = f(0.45) = −0.348508なので,a← c = 0.45とする.

    3. |(f(c)| = 0.348508 > εなので 1に戻る.

    左の手順を繰り返した結果が以下の表のようになる.

    a b c f(c)

    0.100000 0.800000 0.450000 -0.3485080.450000 0.800000 0.625000 0.1549960.450000 0.625000 0.537500 -0.0833270.537500 0.625000 0.581250 0.0386760.537500 0.581250 0.559375 -0.0215600.559375 0.581250 0.570312 0.008742

    ... ... ... ...最終的な答えは x = 0.567142となる.

    問題 2–1 方程式 f(x) = x2 − x− 2 = 0を解くことを考える,二分法の手順に従って,3回繰り返し計算し,以下の表を埋めよ.なお,a = 0, b = 5として,計算を開始せよ.また,解析的

    に解いた解に cの値が近付くか確認せよ.

    a b c f(c)

    0.00 5.00

  • 10 2—方程式 f(x) = 0の解法

    ■実際の二分法 これまでの手順は,曲線が右上がりの場合ついて解を求める方法になります.

    右下がりの場合については,手順 2において,f(c) > 0と f(c) < 0の実行内容が逆になります.

    a

    b

    x0x

    y=f(x)

    x0x

    y y=f(x)y

    x0:目標となる値

    傾きが右下がりの場合

    ab

    傾きが右上がりの場合

    両方の場合について対応するために,手順は以下のように修正されます.� �1. aと bの中点 c = (a+ b)/2を求めます.

    2. f(c)の符号を調べます.

    • ならば bに cの値を代入する

    • ならば aに cの値を代入する

    3. |f(c)| < ε (εは微小な値)になったら終了.そうでなければ 1に戻る.� �

  • プログラミング技法 11

    問題 2–2 二分法の手順によってある関数 f(x)=0を解くプログラムを作る.以下の空白に必要なプログラムを書き込め.なお,絶対値 |x|は fabs(x) 関数より得られる.

    � �#include #include #define EPS 1.0e-5 //終端条件の値 (ε)double f(double x); // 対象関数int main(void){double a = 0.1, b = 0.8;double c;while(1) { // 無条件に繰り返し続けます

    }printf("答えは %fです.\n", c);return 0;

    }double f(double x){return x + log(x);

    }

    � �

  • 12 2—方程式 f(x) = 0の解法

    2.3 ニュートン・ラプソン法

    ■考え方 以下のグラフについて,下に示した作業をしてみましょう.

    1. y = f(x)上の点 P1(x1, f(x1))から接線を引く2. 引いた接線と,x軸との交点をさがし x2とする.3. x2探した交点から垂直な線を引き,y = f(x)との交点を探し,点 P2とする.4. 点 P2から 1,2,3と同様の操作を行なう.

    x

    y y=f(x)

    x0x1

    Pf(x1)

    ■結果から考えてみましょう x1, x2, x3とx0の関係がどのようになっているか考えてみましょう.

  • プログラミング技法 13

    ■ニュートン・ラプソン法 まず,以下のグラフのように y = f(x)の曲線があるとします.グ

    ラフには x = x1(点 P)における,y = f(x)の接線が描かれています.関数の導関数を f ′(x)として,グラフの空欄を埋めてみましょう.

    x

    f(x)y=f(x)

    x0x1x2

    点Pにおける傾き

    f(x1) 接線の方程式

    接線とx軸の交点x2

    P

    更に,x = x2における y = f(x)の接線を引いて,接線と x軸との交点 x3の位置を確認して

    下さい.x1より x2の方が,x2より x3の方が,より解である x0に近付いているのが分かります.

    このような作業を繰り返していくと,接線と x軸の交点 xnは限りなく解 x0に近付いていき

    ます.このような考え方で,f(x) = 0の解の近似値を得る方法を, ニュートン・ラプソン法

    と呼びます.(以下ニュートン法と略します)

    ■計算手順 具体的な手順は,� �1. 適当な初期値 x1を選ぶ.

    2. xn(最初は n = 1)の値からニュートン法の更新式によって xn+1を求める.更新式は

    3. 終了条件 |f(xn+1)| < εならば終了.それ以外は 2へ戻る� �なお,終端条件を |(xn+1 − xn)/xn+1| < εとする場合もあります.

  • 14 2—方程式 f(x) = 0の解法

    ■例 f(x) = x+ lnx = 0の解を求める.

    導関数 f ′(x) = 1+ 1/xである.初期値 x1 = 0.1として,更新式を用いて計算をすると,以下

    のような表になります.

    xn f(xn) f′(xn) xn+1

    0.100000 -2.202585 11.000000 0.3002350.300235 -0.902955 4.330724 0.5087350.508735 -0.167094 2.965661 0.5650780.565078 -0.005715 2.769668 0.5671410.567141 -0.000007 2.763230 0.567143

    ■ニュートン法の利点・欠点 二分法に比べてみると,以下のような特徴があります.

    利点 収束が早いので,計算回数が少なくてすみます.

    欠点 初期値 x1が解 x0の近くに無いと解が求まらない場合があります

    注意点 導関数を求める必要があります.

    問題 2–3 問題 2–1と同様に,方程式 f(x) = x2 − x− 2 = 0を解くことを考える,

    1. 導関数 f ′(x)を求めよ2. ニュートン法の手順に従って,3回繰り返し計算し,以下の表を埋めよ.なお,初期値

    x1 = 10として,計算を開始せよ.また,解析的に解いた解に cの値が近付くか確認せよ.

    f ′(x) =

    xn f(xn) f′(xn) xn+1

    10

  • プログラミング技法 15

    問題 2–4 ニュートン法の手順によって関数 f(x) = x+ ln xに対する,f(x) = 0を解くプログラムを作る.以下の空白に必要なプログラムを書き込め.

    � �#include #include #define EPS 1.0e-8 // 終端条件の値 (ε)double f(double x); // 対象関数double df(double x); // f(x)の導関数int main(void){double x0, x1;x0=0.1; // 初期値の設定while (1) { // 無限に繰り返します

    }printf("解は%fです.\n", x1);

    }double f(double x) // 目的の関数{

    }double df(double x) // 目的関数の導関数{

    }� �

  • 16 2—方程式 f(x) = 0の解法

    — Memo –

  • プログラミング技法 17

    3 曲線の積分を求める関数 y = f(x)の定積分を求めるには,これまで習ってきた方法では関数を解析的に積分をす

    る方法が用いられてきました.

    ■解析的な積分 例えば,f(x) = 4x3− 12x2 +4x+5という関数があるとしましょう.この曲線を区間 [1, 2]で定積分をするには,

    S =

    ∫ 21

    4x3 − 12x2 + 4x+ 10 =[x4 − 4x3 + 2x2 + 10x

    ]21

    = (24 − 4× 23 + 2× 22 + 10× 2)− (14 − 4× 13 + 2× 12 + 10× 1) = 3

    のように,原始関数を導出するなどの作業を行うことになります.

    y = f(x)

    xs x

    y

    xe

    原始関数:F(x)

    Cのプログラムでは,原始関数を導出させるのは難しそうです.「単純な計算の繰り返し」の方法を使って積分を行う方法を紹介していきます.

  • 18 3—曲線の積分を求める

    3.1 計算機による積分法

    計算機で積分を求める方法として,積分区間を細かく分割し,その区間の積分を近似的に求

    め,それらの分割区間の積分の総和を積分値の近似値として計算する方法が良く用いられます.

    例えば,図のような曲線の積分をすることを考えます.このとき,曲線を微小区間∆xで切

    り,図のような短冊型の面積を考えます.例えば,区間 [x1, x2]の短冊型の面積は

    S1 =

    となります.このような,微小区間の面積の総和

    S =

    が,積分の値の近似値となります.このとき,下の表のように,区間の幅∆xが小さくなれば,

    総和 Sは積分の真値に近付くことになります.

    y = f(x)

    x1 x2 x6Δx

    S1 S2

    x

    y

    x3 x7

  • プログラミング技法 19

    長方形の和による積分

    区間 ∆x = 0.2

    1.000 - 1.200 1.20001.200 - 1.400 0.88641.400 - 1.600 0.61121.600 - 1.800 0.41281.800 - 2.000 0.3296合計 3.4400

    区間 ∆x = 0.1

    1.000 - 1.100 0.60001.100 - 1.200 0.52041.200 - 1.300 0.44321.300 - 1.400 0.37081.400 - 1.500 0.30561.500 - 1.600 0.25001.600 - 1.700 0.20641.700 - 1.800 0.17721.800 - 1.900 0.16481.900 - 2.000 0.1716合計 3.2100

  • 20 3—曲線の積分を求める

    問題 3–1 上記のような,微小区間の短冊の面積の総和から,積分値を求めるプログラムを作成する.プログラムの空白を埋め,プログラムを完成せよ.� �#include #include double func(double);int main(){

    int k;int n = 5; // 分割数double a = 1, b = 2; // 積分区間double h = (b-a)/n; // 刻み幅double s = 0; // 面積の和double x = a; // 計算用変数

    for (x = ) {

    }printf("区間 (%f,%f)の面積=%.8f\n",a,b,s);return 0;

    }double func(double x){

    return 4*x*x*x -12*x*x + 4*x + 10;}

    � �更に考えてみましょう: forループで,変数 xを用いた場合,発生する可能性がある問題を述べよ.また,forループで使用する変数を kにしてプログラムを考えてみよ.

    ■この方法の問題点 下図のように,長方形の短冊による面積では,誤差が大きくなってしま

    うことが分かります.

    y = f(x)

    x1 x2 x

    y

    Δxx3

  • プログラミング技法 21

    3.2 台形公式

    前節で説明した短冊型の面積の合計では,真の面積と大きな値の誤差が生じてしまいます.

    この誤差を減らすことができれば,より正確な面積を求めることができます.このための方法

    の 1つが台形公式を用いた求積方法です.下図のように,区間 [x1, x2]の面積の値を台形で近似します.この区間の台形の面積は

    S1 =

    という形で表すことができます.積分区間における面積は,長方形の場合と同様に

    S =

    となります.

    y = f(x)

    x1 x2 x

    y

    Δx

    x3

    f(x1)

    f(x2)f(x3)

    ■計算例 17ページの積分を長方形の和,台形公式で計算した結果を以下に示します.

    各種方法による積分値

    区間 長方形 (∆x = 0.2) 台形公式 (∆x = 0.2)

    1.000 - 1.200 1.2000 1.04321.200 - 1.400 0.8864 0.74881.400 - 1.600 0.6112 0.51201.600 - 1.800 0.4128 0.37121.800 - 2.000 0.3296 0.3648合計 3.4400 3.040

  • 22 3—曲線の積分を求める

    問題 3–2 長方形の和による積分の近似と,台形公式による積分の近似によって,区間 [1, 2]における exの定積分を求めよ.刻み幅は∆x = 0.2とせよ.

    区間 長方形 台形公式

    合計

    問題 3–3 以下のプログラムは,長方形の和による積分を求めるプログラムである.このプログラムを変更して,台形公式による積分のプログラムへ書き換えよ.

    � �#include #include double func(double);int main(void){int k;int n = 5; // 分割数double a = 1, b = 2; // 積分区間double h = (b-a)/n; // 刻み幅double s = 0; // s: 面積の和double x = a; // 計算用変数for (k = 0; k < n; k++) {s += h * func(x);x += h;

    }printf("区間 (%f,%f)の面積=%.8f\n",a,b,s);return 0;

    }double func(double x){return 4*x*x*x -12*x*x + 4*x + 10;

    }

    � �

    � �台形側変更点

    � �

  • プログラミング技法 23

    3.3 シンプソン法

    台形公式による積分方法より,更に精度を上げるために,考えられた方法です.シンプソン

    法では,2次曲線を用いて,微小区間の面積を求めています.図にあるように,点ABCを通るような 2次曲線を用います.この 2次曲線の式は

    P (x) =(x− x2)(x− x3)(x1 − x2)(x1 − x3)

    f(x1) +(x− x1)(x− x3)(x2 − x1)(x2 − x3)

    f(x2) +(x− x1)(x− x2)(x3 − x1)(x3 − x2)

    f(x3)

    で与えられます.これは,ラグランジェ補間と呼ばれる,n個の点から,その点を通る n− 1次曲線を求める方法によって,計算されています.この区間 [x1, x3]での面積は,途中式は割愛し

    ますが

    s =

    ∫ x3x1

    P (x)dx =

    となります.

    x1 x2 x3

    (x1 , f (x1))

    (x2 , f (x2))

    (x3 , f (x3))

    台形による近似 2次曲線による近似

    (x1 , f (x1))

    (x2 , f (x2))

    (x3 , f (x3))

    x1 x2 x3

    y = f(x) y = f (x)

    y = P(x)

    区間全体の積分の値に関しては,sk =∫ xk+2xk

    P (x)dx とすると,

    S =

    のようになります.

    ■計算例 17ページの積分をシンプソン法で計算した結果を以下に示します.

    各種方法による積分値

    区間 シンプソン (∆x = 0.1)

    1.000 - 1.200 1.04161.200 - 1.400 0.74401.400 - 1.600 0.50401.600 - 1.800 0.36001.800 - 2.000 0.3504合計 3.000

  • 24 3—曲線の積分を求める

    問題 3–4 シンプソン法による積分の近似によって,区間 [1, 2]における exの定積分を求めよ.刻み幅は∆x = 0.1とせよ.

    区間 シンプソン法

    合計

    問題 3–5 問題 3–3と同様にプログラムを変更し,シンプソン法による積分の近似を行うプログラムに書き換えよ.

    � �#include #include double func(double);int main(void){int k;int n = 5; // 分割数double a = 1, b = 2; // 積分区間double h = (b-a)/n; // 刻み幅double s = 0; // s: 面積の和double x = a; // 計算用変数for (k = 0; k < n; k++) {

    s += h * func(x);x += h;

    }printf("区間 (%f,%f)の面積=%.8f\n",a,b,s);return 0;

    }double func(double x){return 4*x*x*x -12*x*x + 4*x + 10;

    }

    � �

    � �シンプソン法変更点

    � �

  • プログラミング技法 25

    — Memo —

  • 26 4— 微分方程式の解法

    4 微分方程式の解法4.1 ベクトル場と微分方程式

    4.1.1 ベクトル場のイメージ

    以下の川の流れについて,考えてみましょう.川には岩などの障害物があり,星型のスター

    トの印にピンポン玉を落すと,水の流れに沿って流れていきます.

    上流

    下流

    スタート地点からどのような進路を取るか考えてみましょう

    水の流れのように,平面に力の方向が存在する場を, ベクトル場と呼びます.また,平面

    のある地点を初期地点として,その後どのような軌跡 (解軌跡)を描くのかを計算する問題を,初期値問題 と呼びます.

    ■ベクトル場の例 以下のようなベクトル場では,軌跡はどのような形になるか考えてみま

    しょう.

    x

    y

    x

    t

  • プログラミング技法 27

    4.1.2 微分方程式との関連

    以下のような微分方程式

    dx

    dt= f(x, t)

    とベクトル場の関連について考えてみましょう.

    ■例 例として,

    f(x, t) = x

    のような関数 f(x, t)を考えます.

    1. 図の x− tの平面に,f(x, t)に従って矢印を描き込む2. (x, t) = (1, 0)を開始点として,ベクトル場に沿って凡その軌跡を描く

    x

    tta

    xa

    点Aにおける接線

    A

    接線の傾きdx/dt = f(xa, ta)

    f(x,t)で規定されたベクトル場

    軌跡は矢印 (ベクトル)の方向に沿って描かれることから,軌跡の傾きは矢印の向きと同じになります.

    つまり

    (曲線の傾き) = f(x, t)

    ここで曲線の傾きは dx/dtであるので,

    dx

    dt= f(x, t)

    と表すことができます.これは微分方程式の形となっ

    ています.

    言い換えると,微分方程式はベクトル場を表わし,

    初期点を決めると,解軌跡を描くことができます.

  • 28 4— 微分方程式の解法

    4.2 コンピュータと微分方程式

    ■物理現象と微分方程式 電気回路の過渡現象の特性などは,微分方程式で記述することがで

    きます.例えば,RC直列回路について,電源電圧をE,抵抗をR,コンデンサの静電容量を

    Cとして,コンデンサに蓄えられる電荷を q(t)とすると,

    Rdq

    dt= E − q

    C

    記述されています.他にもある種の物理現象は微分方程式を用いて表わすことができます.

    ER

    Cq

    これらの微分方程式を用いてコンピュータによってシミュレーションが行なわれます.

    ■解軌跡の計算手法 コンピュータで微分方程式を解いて,解軌跡を求める手法は様々なもの

    がありますが,代表的なものとしては,

    • オイラー法• 改良オイラー法• ルンゲ・クッタ法

    などが上げられます.基本的な考え方は,与えられた微分方程式で定められたベクトル場で,

    ある点 (xn, tn)における状態から,微少時間∆t進んだ時点での,xn+1の値を計算するというも

    のです.数式で表現すれば,

    xn+1 = F (xn, tn)

    のようになります.

    x

    tt0 t1 t2

    x0

    初期点

    Δt

    真の解曲線

    x1

  • プログラミング技法 29

    4.2.1 オイラー法

    初期値問題をコンピュータに解かせる方法として,最も単純なものです.図に示される点

    (tn, xn)に対する微少時間∆t秒後の時間 tn+1における値 xn+1を直線を用いて計算します.

    x

    ttn tn+1

    xnΔt

    接線の傾き

    xn+1

    dx/dt = f(x, t)の曲線

    A

    B

    図から分かるように,xn+1の値は

    xn+1 =

    となります.

    問題 4–1 微分方程式

    dx

    dt= x

    について,初期条件を「t = 0で x = 1」として,解析的な解による解軌跡と,オイラー法によ

    る解軌跡の比較を行なう.以下の表を埋めよ.刻み幅は表に示された通り∆t = 0.1で計算せよ.

    また,その結果をグラフにプロットし,解析解の解軌跡とオイラー法による解軌跡を比較せよ.

    表: 微分方程式の解の値t x(真値) x(計算値) 誤差0 1.0000 1

    0.1 1.10510.2 1.22140.3 1.34980.4 1.49180.5 1.6487

  • 30 4— 微分方程式の解法

    問題 4–2 演習 1の表の計算をプログラムを用いて行う.プログラムを完成せよ.� �#include #include double f(double x, double t);int main(){double t, x, new_x, dt;

    return 0;}double f(double x, double t){return x;}� �

  • プログラミング技法 31

    4.2.2 連立微分方程式に対するオイラー法

    以下の微分方程式は自由落下の運動を表わす微分方程式です.

    d2y

    dt2(=加速度) = −g

    この式は,dy/dt = v(速度)であることを踏まえると,

    dy

    dt= v

    dv

    dt= −g

    のような 2変数の連立微分方程式となります.このような連立微分方程式の解軌跡を計算する方法について説明します.

    ■オイラー法 前節までは,1変数の微分方程式について扱ってきました.さらに,オイラー法ではN元の連立微分方程式でも計算可能です.例えば,以下のような 2元の連立微分方程式について,軌跡を計算する場合を考えます.

    dx1dt

    = f1(x1, x2, t)

    dx2dt

    = f2(x1, x2, t)

    この場合,(x1, x2)に対し,各方程式から更新値 (x′1, x

    ′2)を計算するとになります.その場合の

    オイラー法の更新式は以下のようになります.

    x = (x1, x2), f(x, t) = (f1(x1, x2, t), f2(x1, x2, t))のような表記を用いれば,微分方程式は

    dx

    dt= f(x, t)

    となります.この表記に従って,上記のオイラー法の更新式を表記すると

    のようになります.一般的なN元の微分方程式も,同様な形で表わされていることに注意してください.

  • 32 4— 微分方程式の解法

    問題 4–3 連立微分方程式

    dx1dt

    = f1(x1, x2, t)

    dx2dt

    = f2(x1, x2, t)

    を連立微分方程式のオイラー法の更新式を用いて,計算を行なう部分をプログラムで表せ.� �#include #include double f1(double x1, double x2, double t);double f2(double x1, double x2, double t);int main(){double t, dt = .., t_end = ...;double x1 = ..., x2 = ..., new_x1, new_x2; // 初期値は適当に…

    return 0;}double f1(double x1, double x2, double t){何らかの値を返す関数

    }double f2(double x1, double x2, double t){何らかの値を返す関数

    }� �

  • プログラミング技法 33

    � �■参考 高階微分方程式に対するオイラー法

    前述のオイラー法のような計算方法は,基本的に 1次の微分方程式を想定しています.そのため,

    d2x

    dt2+ A

    dx

    dt+Bx = f(x, t)

    のような,高次の微分方程式に対しては,そのまま使用することができません.この微分

    方程式の解軌跡を求めるためには,微分方程式に少々細工を施す必要があります.

    上記の 2次の微分方程式では,

    dx

    dt= y

    と変数を定義します.この yを上記の方程式に代入すると,

    dy

    dt+ Ay +Bx = f(x, t)

    となります.すなわち,方程式は次の 2つの連立微分方程式に変形することができます.

    dx

    dt= y

    dy

    dt= −Ay −Bx+ f(x, t)

    この 2つの式について以下のように

    F1(x, y, t) = y

    F2(x, y, t) = −Ay −Bx+ f(x, t)

    と定義すれば,前節の連立微分方程式の方法を適用するとができます.� �

  • 34 4— 微分方程式の解法

    4.2.3 その他の方法

    オイラー法は,示されたように,単純な直線による近似で,解軌跡を計算します.この方法

    は,考え方は分かり易いですが,誤差が大きくなってしまう欠点があります.

    より精度を上げるために,様々な方法が考案されてきました.以下に,2つの方法による更新式を紹介します.

    ○改良オイラー法

    x′n = xn +∆tf(xn, tn)

    xn+1 = xn +∆t

    2{f(xn, tn) + f(x′n, tn)}

    ○ルンゲ・クッタ法

    xn+1 = xn +∆t

    6(k1 + 2k2 + 2k3 + k4)

    k1 = f(xn, tn)

    k2 = f(xn +∆tk1/2, tn +∆t/2)

    k3 = f(xn +∆tk2/2, tn +∆t/2)

    k4 = f(xn +∆tk3, tn +∆t)

    問題 4–4 問題 4–1の微分方程式に関して,改良オイラー法とルンゲ・クッタ法により計算を行なえ.

    t x(真値) 改良オイラー ルンゲクッタ0 1.0000 1 1

    0.1 1.10510.2 1.22140.3 1.34980.4 1.49180.5 1.6487

  • プログラミング技法 35

    問題 4–5 問題 4–2のプログラムを変更し,改良オイラー法,ルンゲクッタ法によって,上記のような計算を行なうプログラムを作成せよ.� �改良オイラー法

    .......double f(double x, double t);int main(){double t, x, xd, new_x, dt;

    return 0;}.....以下演習 2と同じ� �� �ルンゲ・クッタ法

    .....double f(double x, double t);int main(){double t, x, xd, new_x, dt;double k1, k2, k3,k4;

    return 0;}......� �

  • 36 4— 微分方程式の解法

    � �■参考 — ルンゲ・クッタ法による連立微分方程式の計算手順オイラー法については,連立方程式の計算方法は説明しました.ルンゲ・クッタ法などでも同様の

    計算ができますが,手順がやや煩雑になります.

    以下のような微分方程式を考えます.

    dx

    dt= f(x, t)

    この式に関して,ルンゲ・クッタの更新式は 1変数の場合と同じ形で,以下のように表されます.

    x′ = x+∆t

    6(k1 + 2k2 + 2k3 + k4)

    k1 = f(x, t)

    k2 = f(x+∆tk1/2, t+∆t/2)

    k3 = f(x+∆tk2/2, t+∆t/2)

    k4 = f(x+∆tk3, t+∆t)

    ここで,k1,k2は

    k1 =

    k11k12... =

    f1(x1, x2, · · · , t)f2(x1, x2, · · · , t)... , k2 =

    k21k22... =

    f1(x1 +∆tk11/2, x2 +∆tk12/2, · · · , t+∆t/2)f2(x1 +∆tk11/2, x2 +∆tk12/2, · · · , t+∆t/2)...

    k3,k4も同様な形で表わされます.この計算をするのにあたって,例えば k21を計算するためには,k11, k12, · · · が必要である点に注意する必要があります.つまり,計算順としては,k11 ∼ k1nを全て計算してから,k21 ∼ k2nを計算するという順番になります.

    よくある間違えとして,先に k11, k21, · · · , kn1を計算し,そこから x1を計算するという誤りが見られます (k21などが計算されていないので本来計算はできません).数式で見ると明らかに間違えた手順なのは分るのですが,プログラムにする際に,手順を誤ってしまうようです.� �

  • プログラミング技法 37

    — Memo —

  • 38 5— 構造体

    5 構造体教科書の内容に沿います.

  • プログラミング技法 39

    — Memo —

  • 40 6— スタック/キュー

    6 スタックとキューデータの保持機構の一種として,スタック (stack)とキュー (queue)と呼ばれるものがあります.コンピュータのデータ構造で非常に多く用いられるものです.

    6.1 スタック

    イメージとしては,積み重ねた本がそれに当ります.使用頻度が非常に高く,プログラムの

    実行などにおいても,関数呼び出しに対して戻り場所を記録するなどの目的に用いられたりし

    ます.

    一番上にしか本は積めない

    一番上の本しか取り出せない

    最後に入れたデータが最初に出ていくデータ構造

    LIFO(Last In First Out)などとも呼ばれる

    本の山

    ■用語 スタックに関していくつか用語があります.

    要素を積む 要素を取り出す

    例題スタックが空の状態から以下のデータの出し入れをせよ.↑は popを意味する.

    5, 6, 2, ↑, 3, ↑, ↑, ↑

  • プログラミング技法 41

    6.2 キュー

    イメージとしてはATMなどの待ち行列などがあげられます.処理を依頼された仕事を順に処理していくための機構を表すのに便利なことから,プリンタの順番待ちにキューが用いられ

    ます.

    ATM列の最後尾にしか付けない 列の先頭から

    抜けていく

    FIFO(First In First Out)LILO(Last In Last Out)などとも呼ばれる

    最初に入ったデータが最初に出ていくデータ構造

    ■用語 スタックと同様に,いくつかの用語があります

    データを入れる データを取り出す

    例題キューが空の状態から以下のデータの出し入れをせよ.↑は popを意味する.

    5, 6, 2, ↑, 3, ↑, ↑, ↑

  • 42 6— スタック/キュー

    問題 6–1 以下の数列をスタック,キューに入れる操作を行ない,スタック,キューの中の状態の変化を記せ.ただし,↑は popもしくは,getと考えること

    4, 10, 3, ↑, ↑, 40, 9, ↑, ↑

    � �スタックの応用例 (逆ポーランド記法): コンピュータにおける計算の表わし方として,逆ポーランド記法と呼ばれる方法があります.例えば,10+20を逆ポーランド記法で表現すると,

    10 20 +となります.これは左から解釈して,「10と 20を加える」という意味になります.(5 + 10)× 4という式は

    5 10 + 4 *となります.これは,「5と 10を加えたものと 4を掛ける」という表現になります.この表記方法はコンピュータには非常に都合のよい表記法とされています.即ち,括弧や演算子の計算順などを考

    えずに表現することができるからです.この表記法による演算はスタックを用いて行なうことがで

    きます.以下のルールで行なわれます.

    1. 数値はそのまま push2. 演算子は先に pushされた 2つの値を popして演算を行ない,結果を pushする

    先の例をスタックで実行すると,以下のような様子になります.

    5 5 5 15 15 15 6010 10 4 4

    510 + 4 ×スタックされた

    2つの値を用い加算結果をPUSHする

    +

    このような,スタックによる表現を用いたコンピュータ言語として,Forthとよばれる言語が存在します.� �

  • プログラミング技法 43

    6.3 構造体の配列を用いた実装

    スタックやキューは,データ構造は比較的簡単なので,配列を用いることで,実装すること

    ができます.以下に,スタックとキューの実装例を説明します.

    6.3.1 スタックの実装

    スタックもキューも保持するデータが 1つの場合は,通常の配列でも実装可能なのですが,もう少し実用性を上げて,整数型のデータ (dat1)と実数型のデータ (dat2)を持つ構造体を例にして実装の説明をしていきます.� �struct Data {int dat1;double dat2;

    };� �

    6.3.2 スタックの実装

    上記のような構造体の配列を用いて実装します.スタックの実装をするとき,どこが底でど

    こが頂上かを明確にする必要があります.

    データ保持: struct Data stack[N];などで宣言頂上: int型の変数を用いて表します (topなど)底:常に 0番目の要素

    top=0

    空のスタック

    (5,1.0)をpushする

    top=1

    データが積まれた状態

    5 1.001

    番号データ

    dat1 dat2

    01

    番号データ

    dat1 dat2

  • 44 6— スタック/キュー

    j

    ■操作手順: 実装を行なうために,値の代入や頂上の位置の変更の順を明確にしましょう.

    pushpushしたいデータを datとして,top < Nならば以下を実行.それ以外は何もしない.

    1. stack[top]に dat代入2. topを+1

    5 1.001

    番号データ

    dat1 dat2

    23 2.0

    poptopが 0でなければ以下を実行.それ以外ならば何もしない.

    1. topを-12. stack[top]の値を取り出す.(表示をする,関数の返り値にするなど)

    5 1.001

    番号データ

    dat1 dat2

    23 2.0

  • プログラミング技法 45

    問題 6–2 実装例として,前述の配列を用いた方式をもとに,push,popの関数を実装せよ.関数の値の出入りは以下の図に示す.

    pushdat1dat2×

    スタックに値を積む関数

    popdat1dat2 ×スタックから値を取り出す関数

    � �#include #define N 10struct Data {int dat1;double dat2;

    };int top=0;struct Data stack[N];

    void push(struct Data d) // データの保存関数{

    }struct Data pop() // データの取り出し関数{

    }� �

    � �int main(){struct Data D;while (1) {printf("保存データ入力\n");printf("(dat1=-1でデータ取り出し):");scanf("%d %lf", &D.dat1, &D.dat2);if (D.dat1 == -1) {D = pop();printf("取り出したデータ:(%d %f)\n",

    D.dat1, D.dat2);} else {push(D);

    }}return 0;

    }

    � �

  • 46 6— スタック/キュー

    — Memo —

  • プログラミング技法 47

    6.3.3 キューの実装

    スタックと同じように,構造体 struct Dataの配列で実装を行ないます.

    データ保持: struct Data queue[N]などで宣言先頭: int型の変数で表わします (headなど)末尾: int型の変数で表わします (tailなど)

    ■キューを構造体の配列で実装しようとすると? 例えば,

    put データが入るので,tailが+1されるget データが取り出されるので,headが+1される

    として,考えるとどうなるでしょうか?

    空のキュー

    h,t

    ■リングバッファ スタックと違い,先頭と末尾は場所が固定できないので,スタックより工

    夫が必要です.下図のように,配列の先頭と末尾がつながったリング状の構造を用います.

    01

    234

    56

    78 9

    1011

  • 48 6— スタック/キュー

    ■操作手順: スタックに較べると,構造が複雑になるため (1回転する場合など),手順も複雑になります.

    putputしたい値を datとして,

    1. queue[tail] = dat2. tailを+13. tail >= Nなら tail = 0

    01

    2

    910

    11

    gettail != headなら

    1. queue[head]の値を読み取る2. headを+13. head >= Nならば,head = 0

    01

    2

    910

    11

  • プログラミング技法 49

    問題 6–3 実装例として,前述の配列も用いた方式をもとに,put,getの関数を実装せよ.関数の値の出入りは以下のようなものとする.

    put

    dat1dat2×

    キューに値を入れる関数

    get

    dat1dat2 ×キューから値を取り出す関数

    � �#include #define N 10struct Data {int dat1;double dat2;

    };int head=0, tail=0;struct Data queue[N];

    void put(struct Data d) // データの保存関数{

    }struct Data get() // データの取り出し関数{

    }� �

    � �int main(){struct Data D;while (1) {printf("保存データ入力\n");printf("(dat1=-1でデータ取り出し):");scanf("%d %lf", &D.dat1, &D.dat2);if (D.dat1 == -1) {D = get();printf("取り出したデータ:(%d %f)\n"

    ,D.dat1, D.dat2);} else {put(D);

    }}return 0;

    }

    � �

  • 50 7— リスト構造

    7 リスト構造7.1 リスト構造とは

    リスト構造とは,数などのデータを順に並べたもので,基本的なデータ構造の一種です.

    • スタックやキューなども一種のリストです.• 配列もリストの一種です.

    ■例 — 名簿 リスト構造として,身近なものとしては,名簿などがあげられます.下の表に示したように,名前などの個人データを連ねたものがリストの構造を持っています.

    名前 郵便番号 住所 · · ·伊藤 123-4567 ○市 ○○町 1-11太田 333-3333 △市 △△町 2-22大野 555-5555 ×市 ××町 3-33

    ... ... ...

    場合によっては,名簿を変更する必要が出てくることもありますし,個人データを調べるこ

    ともあります.下記のような作業が考えられますが,これらをリスト構造への操作と呼びます.

    • 転入 — 要素の挿入• 転出 — 要素の削除• 住所などのデータ変更 — 要素への書き込み• データの照会 — 読み出し,検索

    ■構造体によるリスト 例えば,以下のような構造体の配列によって,名簿の簡単な管理は行

    えるでしょう.

    � �struct profile {char name[50]; // 名前int zip; // 郵便番号char city[20]; // 市int phone; // 電話番号

    };� �実際に複雑な管理をする場合,以下に示すように配列で行うのはやや無理があります.

    名前 郵便番号 住所 電話番号No

    伊藤

    太田

    鳥居

    和田

    ………

    ………

    ………

    ………

    ……

    ……

    ……

    ……

    ……

    ……

    ……

    さらにデータを加えたい

    → どこに追加すればいいの?

    データを削除したい→ 削除した後はどうなるの?

    ……

    0

    1

    20

    60

    配列でリストの管理をするのはやや難しそうである

  • プログラミング技法 51

    7.2 連結リスト

    リスト構造を実現するために,様々な方法があります.その中の実装手法の一つとして,連

    結リストと呼ばれる構造があります.配列などに比べると複雑ですが,データの挿入,削除な

    どが行いやすい構造を持っています.

    ■記号的な表現 データの要素が次はどのデータかを指すことで,データの並びができあがり

    ます.数珠繋ぎのようなイメージです.

    A B C

    1つの要素(セル) ・ データ ・ 次のデータを指す矢印

    終端

    ■連結リストの例 セルのモデルを以下のようなものとして,もう少し具体的に連結リストを

    見てみましょう.

    セルの番号

    データ

    次のセル

    0L3

    1K2

    2E--

    3I1

    (例)

    問題 7–1 以下の連結リストの内容を読みとれ0S2

    1A4

    3T--

    2T1

    4R3

    0T2

    1A-

    3O4

    2O5

    4T1

    5Y3

  • 52 7— リスト構造

    7.3 連結リストの操作

    7.3.1 要素の挿入リストへ要素を追加する手順について考えましょう.以下のように,データの並びの途中に

    新しいデータを追加します.

    A B C A B CX

    このセルの次に追加

    1. 挿入するセルを準備する2. 挿入ポイントのセル (B)の「次のセル」の番号を,挿入したいセル (X)の「次のセル」に入れる.

    3. 挿入ポイントのセル (B)の「次のセル」の値を,挿入したいセル (X)の番号にする.

    0A1

    1B2

    2C--

    7.3.2 要素の削除リストから要素を削除する手順について考えましょう.

    A B C A C

    このセルを削除

    このセルの次のセルを削除

    言い換えると

    便宜上Aのセルを「削除ポイント」と呼ぶ

    1. 削除ポイントのセル (A)の「次のセル」のデータ,削除したいセル (B)の「次のセル」の値にする.

    2. 削除したいセル (B)を消去

    0A1

    1B2

    2C--

  • プログラミング技法 53

    問題 7–2

    1. 図 (a)の連結リストの 1番のセルの後に”X”のデータを持つセルを挿入せよ.2. 図 (a)の連結リストの 2番のセルの次のセルを消去せよ.3. 図 (b)の連結リストの保持している文字列を”COOK”になるようにリストの追加,削除を行え.

    0A2

    1B3

    3D--

    2C1

    (a)

    0C2

    1K3

    3E

    --

    2O1

    (b)

  • 54 7— リスト構造

    7.4 構造体の配列を用いた連結リスト

    これまで述べてきた内容をプログラム上で実装します.今回は簡単のため,構造体の配列を

    用いた実装を説明していきます.(より実用性の高い実装方法は,後期の授業などに行う予定です)

    7.4.1 連結リストの形式

    以下のように,データと次のセルの番号を保持する構造体を考えます.

    � �struct CELL {char data; // 保持したいデータ (複数でも可)int next; // 次のセルの番号を指すための変数};� �

    この構造体を下図のような配列にすることにより,連結リストを構成します.各データの意

    味も示します.

    番号 data next

    01234

    3

    4-1

    'x''b'

    0番めのdataには何も入れません

    この場合データの並びは "xb"となります

    終端記号は-1とします

    番号 data next

    01234

    -1

    リストが空の場合はこの様になります

    '\0''\0'

    セルが未使用であることを表します

    構造体 struct CELLを用いた連結リストのイメージ

    ■プログラムの構成例 リスト関連の関数を実装する前に,どのような感じで使用するのか概

    観をみてみましょう.� �#include #define MAX 100struct CELL {char data;int next;

    };int get_empty_cell();void initialize(); // リストの初期化void add_data(int no1, char data); // データの挿入void del_data(int no1); // データの削除int main(){initialize();char s;....

    add_data(0,s); // リストにデータを追加.....

    del_data(0); // リストからデータを削除

    return 0;}

    以下各関数の定義� �

  • プログラミング技法 55

    7.4.2 補助関数

    未使用の配列要素を探すために以下のような補助関数 get_empty_cellを使用します.

    • 空いている要素の番号を返す• 空いている要素がない場合は-1を返す

    � �int get_empty_cell(){int i,ret = -1;for (i = 1; i < MAX; i++) {

    if (list[i].data == '\0') {ret = i;break;

    }}return ret;

    }� �7.4.3 リストの初期化関数

    配列を初期化し,空のリストを用意します.

    1 全てのセルのデータを空 ('\n')にする.2 0番目のセルの nextを-1に

    � �void initialize(){

    }

    � �

  • 56 7— リスト構造

    7.4.4 リストの挿入関数

    引数の指すセル (no)の次に新しいセルを挿入します.

    1 挿入用セルの準備 (get_empty_cellを用いる)2 noの nextの値を新しいセルの nextへ代入3 新しいセルの番号を noのセルの nextへ代入� �

    void add_data(int no, char data){

    }

    � �7.4.5 リストの削除関数

    引数の指すセル (no)の次のセルを削除します.

    1 noの次のセルの nextの値を noの nextへ代入2 noの次のセルを消去� �

    void del_data(int no){

    }

    � �Note! 挿入や削除の関数は,実用的にはエラーチェックなどが必要となりますが,簡単のため,その類のコードは省いて考えます.

  • プログラミング技法 57

    問題 7–3 リストの内容を表示する関数 print_list を実装せよ.Hint: -1が終端記号になっているので,list[no].nextが-1になるまで list[no].dataの表示を繰り返せばよい.� �

    void print_list(){

    }� �問題 7–4 リストの長さを調べる関数 len_list,を実装せよ.Hint: 上の print_listを変更することで実装できる.� �

    int len_list(struct CELL *p){

    }

    � �

  • 58 7— リスト構造

    連結リストの使用例: リスト構造は,名簿のようにデータを表にまとめるだけではなく,

    さまざまな場面に使用することができます.

    下の図はロボットアームをコンピュータ上でシミュレートしたものです.このロボットの

    それぞれの関節にも連結リストを用いています.ロボットの関節と関節の間には,単純に

    どれくらいの角度で曲っているかというパラメータだけでなく,どの方向に曲るか,関節

    の位置関係など,様々なパラメータが必要となります.

    各関節を表わす構造体

    struct Unit {double a; // 前の関節との角度関係double alpha; // 同上double d; // 同上double theta; // 関節の移動量int type; // 関節の種類 (回転,直動)double m_min; // 関節移動量の最大値double m_max; // 関節移動量の最小値Draw* draw; // 関節の描画関数struct Unit *next; // つぎの関節のアド

    レス.....

    ロボットアーム