プログラミング演習1 課題3 数値解析法

3. 非線形方程式

[参考図書:河村哲也著,数値計算入門,サイエンス社,pp.11-20.]

\( x \) に関する方程式を\( f(x) = 0 \) の形に書くことにする。\( f(x) \) が多項式でしかも次数が4以下の場合には、根を求める公式がある。しかし、それ以外の場合には根の存在がわかっていても、特殊な場合を除いて、公式の形では根は表せない。そこで数値的に根を求める必要がある。

その代表的な方法(3.1 二分法と3.2 ニュートン法)を紹介する。これらの方法は、\( f(x) = 0 \) の実根が \( y = f(x) \) と \( x \) 軸の交点であることを利用する。

3.1 二分法

\( y = f(x) = x^2-2\) のグラフにおいて、\( x \) 軸との交点を求めれば \(2\) の平方根 \( \sqrt{2} \) を求めることができる。 \( \sqrt{2} \) は区間 \( [1, 2] \) に含まれているが、この区間を中点 \( x_3 = 1.5\) で半分ずつに分け、交点を含む側を選択すると、新しい区間は\( [1, 1.5] \) となる。この操作を繰り返すことによって区間\( [x_1, x_2] \) を順次狭めていき、交点の近似値を求める方法を二分法という。
交点を含む側の選択法は、\( x\gt 0 \) で \( y = f(x)\) が右上がりであることを利用し、中点での関数値 \( f(x_3) \gt 0\) のときに中点 \( x_3\) を新しい区間の右端とし、そうでないときは \( x_3\) を区間の左端とする。

中央と右の図は、左のグラフの\( 0.5\lt x\lt 2\)の範囲を拡大表示したものである。緑の点線がそれぞれ \( x_1\) と \( x_2\) の初期値を示している。最初の反復では \( x_3=(x_1+x_2)/2=1.5\) であるから、\( x_3\) は青色の点線の位置になる。そして、\( f(x_3)\gt0\) であるから、\( x_2\) の値を更新して次の反復を行う。

この処理を記述したコードを以下に示す。ただし、アルゴリズムの核心部分(1)~(10)は穴埋めをして完成させる必要がある。

/*
    bisec.c: Bisection Method
*/
#include <stdio.h>

double f(double x)
//  f(x) = x^2 - 2
{
    return __(1)__;
}

int main(void)
{
    int n=0;
    double x1, x2, x3, err;
    double eps = 1.0e-6;

    x1 = __(2)__;
    x2 = __(3)__;
    printf("Iteration, approx, err\n");
    do {
        n++;
        x3 = __(4)__;
        if (__(5)__) __(6)__;  // f(x3)の値により、x1またはx2を更新
        else __(7)__;
        err = __(8)__;
        printf("%4d, % .15e, % .15e\n", n, x3, err);
    } while (__(9)__);   // errが十分小さければ計算終了
    fprintf(stderr, "\n sqrt(2) = % .15e\n", __(10)__);
    return 0;
}

練習1 二分法のプログラム

上記プログラムを完成させて実行してください。

err変数には \( x_2\) と \( x_1\) の差を代入しましょう。十分に狭い区間になったら計算を打ち切る、という考えです。

初期値の入力

いろいろな条件でプログラムを実行するために、初期区間と精度を入力できるようにしたい。しかし、scanf関数は、“最大文字数が指定できないこと”や“改行コードが空白として扱われてしまう” などの問題を抱えている(例題で学ぶC言語p.121 参照)。そこで、代わりにfgets関数sscanf関数の組み合わせを使う。通常は以下のように

int main(void)
{
    double x1, x2, eps;
    …
    printf("x1, x2, eps = ");
    scanf("%lf%lf%lf", &x1, &x2, &eps);
    …
}
と書くところを、fgets関数とsscanf関数で置き換えると以下のようになる。
int main(void)
{
    double x1, x2, eps;
    char s[128];

    …

    printf("x1, x2, eps = ");
    fgets(s, 128, stdin);
    sscanf(s, "%lf%lf%lf", &x1, &x2, &eps);

    …
}
標準入力(stdin)から決まった文字数だけ読み取ってchar型配列に格納し、その配列からsscanf関数で値を読み取っている。

練習2 初期値の入力

初期値の読み込みができるように二分法のプログラムを変更してください。

        [実行結果(例)]
        -----------------------
        x1, x2, eps = 1 2 1e-2
        Iteration, approx, err
           1,  1.500000000000000e+00,  5.000000000000000e-01
           2,  1.250000000000000e+00,  2.500000000000000e-01
           3,  1.375000000000000e+00,  1.250000000000000e-01
           4,  1.437500000000000e+00,  6.250000000000000e-02
           5,  1.406250000000000e+00,  3.125000000000000e-02
           6,  1.421875000000000e+00,  1.562500000000000e-02
           7,  1.414062500000000e+00,  7.812500000000000e-03
        
         sqrt(2) =  1.414062500000000e+00
    

負領域の解の計算

\( x\lt0\) では関数が右下がりになるので、区間を選択する条件が変わる。if 文を用いて条件を切り替える方法もあるが、たとえば \( f(x_1)*f(x_3)\) の符号で判断すれば、余計な場合分けをしなくて済む。

練習3 負領域の解

区間を更新する条件式を変更して、負領域の解も求められるようにしてください。初期値を x1 = -2, x2 = -1, eps = 1e-10 として動作を確認しましょう。

        [実行結果(例)]
        -----------------------
        x1, x2, eps = -2 -1 1e-10
        Iteration, approx, err
           1, -1.500000000000000e+00,  5.000000000000000e-01
           2, -1.250000000000000e+00,  2.500000000000000e-01
           …
         sqrt(2) = -1.414213562326040e+00
    

最大反復回数の制限

精度を入力できるようにしたプログラムでは、誤差を極端に小さくした場合に do~while の反復が止まらなくなる可能性がある。例えば、精度eps=1e-16として実行すると、どうなるだろうか?

練習4 最大反復回数

無限ループに陥ることを防ぐため、以下の実行例のようにエラーメッセージを表示して有限回(N = 100)で止まるようにプログラムを変更しよう。

        [実行結果(例)]
        -----------------------
        x1, x2, eps = 1 2 1e-16
        Iteration, approx, err
            1,  1.500000000000000e+00,  5.000000000000000e-01
            2,  1.250000000000000e+00,  2.500000000000000e-01
            …
          100,  1.414213562373095e+00,  2.220446049250313e-16
        
        *** 反復回数が100回を超えました。***
        
        sqrt(2) =  1.414213562373095e+00
    

初期区間の数値が異常な場合の対応

初期区間に解が含まれない場合は、誤った解に収束してしまう。例えば、\(x_1\) と \(x_2\) の初期値を逆に入力してみると、errが負の値になりepsよりも小さくなるため、すぐに計算が終了してしまう。あるいは、x1 = 1.5, x2 = 2 のように、解が含まれない初期区間を与えると間違った値に収束する。

        [実行結果(例)]
        -----------------------
        x1, x2, eps = 2 1 1e-6
        Iteration, approx, err
           1,  1.500000000000000e+00, -5.000000000000000e-01
        
         sqrt(2) =  1.500000000000000e+00
    

練習5

初期値が \(x_1\gt x_2\) の場合でも正しく解が求まるようにしてください。

errは \(x_2-x_1\) で計算されているので、\(x_1\) が大きいときに負の値になる。「区間の幅」(つまり、常に正の値であるべき)を正しく計算するには?

練習6

初期区間において \(f(x)\) が \(x\) 軸にまたがっていないことを調べれば、明らかに解が含まれない場合を除外することができます。具体的には、\(f(x_1), f(x_2), f(x_3) \) の符号がすべて同じ場合に、以下の実行例のようにメッセージを出して終了するようにプログラムを変更してください。

        [実行結果(例)]
        -----------------------
        x1, x2, eps = 2 3 1e-6
        *** 初期区間の数値が異常です。***
    
反復計算に入る前に条件をチェックして、符号がすべて同じ場合はreturn文で終了しよう。

レポート課題2 二分法

ここまでの作業で完成したプログラムを使って以下の5通りの条件で実行した結果を示し、正しく値が求まる場合は、それをExcelによりグラフ描画して示すこと。

ヒント: リダイレクト機能を使って出力する場合、初期値の入力を促すメッセージがリダイレクトされるのを防ぐためには、fprintf関数に変更すれば良い。

\(x_1\) \(x_2\) \(eps\)
1 2 1e-6
2 6 1e-6
2 1 1e-16
-1 1 1e-6
-1 -2 1e-6

二分法の特徴(まとめ)

3.2 ニュートン法

ニュートン法は以下の手順で方程式 \( f(x) = 0 \) の解を求める。
ポイントは、関数 \( y = f(x) \) と \( x \) 軸との交点を求める場合に「接線」を利用することである。

  1. 初期値 \( x_0 \) を与える。
  2. その点での関数値 \( f(x_0) \) と傾き \( f'(x_0) \) から \( x \)切片 \( x_1 \) を推定する。
  3. \( x_1 \) を新たな初期値 \( x_0 \)としてこの操作を反復する。
newton

\( (x_0, f(x_0)) \) を通る接線の式 \( y-f(x_0)=f'(x_0)(x-x_0) \) において、\( x \)切片では \( y=0, x=x_1 \) となるので
\( x_1=x_0-f(x_0)/f'(x_0) \) ……… (2.1)
\( f(x)=x^2-a \) のとき、\( f'(x)=2x \) だから
\( x_1=x_0-({x_0}^2-a)/2x_0=(x_0 + a/x_0)/2 \)……… (2.2)

真野芳久「Pascalプログラミングの基礎」サイエンス社 p.41 のプログラムと川上一郎「数値計算」岩波書店 p.180 付録3.1のプログラムを合体してCに移植し、手直ししたものを以下に示す。ただし、アルゴリズムの核心部分(1)~(4)は穴埋めする必要がある。

/*
    newton.c: ニュートン法
*/
#include <stdio.h> // printf, fprintf, fgets, sscanf
#include <math.h>  // fabs
double a = 0;

int main(void)
{
    int n = 0;
    double x, x0, err, eps = 1.0e-10;
    char s[128];

    fprintf(stderr, " a = "); fgets(s, 128, stdin); sscanf(s, "%lf", &a);
    while (a <= 0.0) {
        fprintf(stderr, "'a' には正の数を入れてください。\n");
        fprintf(stderr, " a = "); fgets(s, 128, stdin); sscanf(s, "%lf", &a);
    }

    x = (a + 1.0) / 2.0;
    printf("# n, approx, err\n");
    printf("%4d, % .15e\n", n, x);
    do {
        n++;
        x0  =  __(1)__;
        x   =  __(2)__;
        err =  __(3)__;
        printf("%4d, % .15e, % .15e\n", n, x, err);
    } while (__(4)__);
    fprintf(stderr, "\n# sqrt(%e) =  % .15e\n", a, x);
    return 0;
}
初期値 \(x_0=1\) とすると、式2.2より \(x\) の最初の値は \((1+a)/2\) となる。
その後、whileループ内で以下の処理を繰り返せばよい。ただし関数fおよび関数dfは自分で定義する必要がある。
  1. \(x_0 \leftarrow x\)
  2. \(x \leftarrow x_0 - f(x_0) / df(x_0)\) (式2.1より)
  3. \(err \leftarrow |x-x_0|\)

練習7 ニュートン法のプログラム

上記のプログラムを完成させよう。a = 2 を入力したときの実行結果は次のようになるはず。

newton-snewton-s

練習8 ニュートン法アルゴリズム部分の関数化

ニュートン法のアルゴリズムの部分(printf("# n, approx, err\n");以降)を newton() と名づけ、 main関数から独立させよう。
具体的には、double newton(double x, double eps)を定義し、main関数からは printf("\n# sqrt(%e) = % .15e\n", a, newton(x, eps));のように利用しよう。

ヒント:

レポート課題3 ニュートン法

ここまでの作業で完成したプログラムを使ってa = 自分の学籍番号の下3桁, eps = 1e-10 とした場合の実行結果を示し、正しく値が求まる場合は、それをExcelによりグラフ描画して示すこと。

ニュートン法の特徴(まとめ)

オプション課題

aの値をリダイレクトによって入力する方法を説明してください。実行した様子も示すこと。

オプション課題2(発展問題)

関数ポインタについての説明を読み、以下の課題に取り組んでください。

  1. 方程式の左辺の関数 \( f(x) \) をmain関数側で自由に指定できるようにbisection() の引数に関数引数用の定義(関数ポインタ)を追加する。具体的には以下の関数を作って実行結果を示すこと。
    
    double bisection(double (*f)(double), double x1, double x2, double eps)
    {
        …………………………
        二分法のアルゴリズム
        …………………………
        return (x1 + x2) / 2.0;
    }
    
  2. 方程式の左辺の関数 \( f(x) \) とその導関数 \(df(x) \) の名前をmain関数側で自由に指定できるように、newton() の引数に関数引数用の定義(関数ポインタ)を追加する。具体的には以下の関数を作って実行結果を示すこと。
    
     double newton(double x, double (*f)(double), double (*df)(double), double eps)
     {
         …………………………………
         ニュートン法のアルゴリズム
         …………………………………
         return x;
     }
    


【目次】 | 【1.】 | 【2.】