🇯🇵 日本語 | 🇺🇸 English
広告スペース

C言語で学ぶ数値計算・数学アルゴリズム

数学の知識をプログラムに落とし込む。理工系の計算で頻出のアルゴリズムをC言語で実装します。

統計量の計算(平均・分散・標準偏差)

データの集まりから基本的な統計量を計算します。理工系のレポート・研究で最も頻繁に使う処理です。
平均: x̄ = (1/n) Σxi  分散: s² = (1/n) Σxi² - x̄²  標準偏差: s = √s²
#include <stdio.h>
#include <math.h>

int main(void) {
    double data[] = {72.5, 85.3, 91.0, 68.2, 77.8, 83.1, 90.5, 65.9};
    int n = 8;
    double sum = 0, sum2 = 0;

    for (int i = 0; i < n; i++) {
        sum  += data[i];       // 総和
        sum2 += data[i] * data[i]; // 二乗和
    }

    double avg = sum / n;
    double var = sum2 / n - avg * avg; // 分散 = E[X²] - (E[X])²
    double sd  = sqrt(var);

    printf("データ数: %d\n", n);
    printf("平均:     %.2f\n", avg);
    printf("分散:     %.2f\n", var);
    printf("標準偏差: %.2f\n", sd);
    return 0;
}
ポイント: 分散は「二乗の平均 - 平均の二乗」で1パスで計算できます。ループを2回回す必要はありません。

ニュートン法(方程式の数値解)

f(x) = 0 の解を数値的に求めるアルゴリズム。接線の x 切片を次の近似値として繰り返す。
xn+1 = xn - f(xn) / f'(xn)
例: x² - 2 = 0 の正の解(= √2 ≒ 1.41421356...)を求める。
#include <stdio.h>
#include <math.h>

double f(double x)  { return x*x - 2; }       // f(x) = x² - 2
double df(double x) { return 2*x; }            // f'(x) = 2x

int main(void) {
    double x = 2.0;  // 初期値
    double EPS = 1e-10;

    for (int i = 0; i < 100; i++) {
        double x_new = x - f(x) / df(x);
        printf("反復%2d: x = %.15f  誤差 = %e\n", i+1, x_new, fabs(x_new - x));
        if (fabs(x_new - x) < EPS) break;
        x = x_new;
    }
    printf("√2 ≒ %.15f\n", x);
    return 0;
}
反復 1: x = 1.500000000000000 誤差 = 5.00e-01 反復 2: x = 1.416666666666667 誤差 = 8.33e-02 反復 3: x = 1.414215686274510 誤差 = 2.45e-03 反復 4: x = 1.414213562374690 誤差 = 2.12e-06 反復 5: x = 1.414213562373095 誤差 = 1.59e-12 √2 ≒ 1.414213562373095
たった5回の反復で15桁の精度。 ニュートン法は2次収束なので、反復ごとに有効桁数がほぼ倍増します。
注意: 初期値が悪いと収束しない場合があります。f'(x)=0 の点にも注意。

モンテカルロ法(円周率の推定)

乱数を使ったシミュレーションで確率的に答えを求める手法。1×1の正方形にランダムに点を打ち、単位円の内部に入る割合から円周率を推定します。
π ≒ 4 × (円の内部に入った点の数 / 全体の点の数)
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {
    int N;
    printf("試行回数: "); scanf("%d", &N);
    srand((unsigned)time(NULL));

    int inside = 0;
    for (int i = 0; i < N; i++) {
        double x = (double)rand() / RAND_MAX; // 0〜1の乱数
        double y = (double)rand() / RAND_MAX;
        if (x*x + y*y <= 1.0) inside++;     // 円の内部?
    }

    double pi = 4.0 * inside / N;
    printf("π ≒ %.6f  (N=%d)\n", pi, N);
    printf("真の値: 3.141593\n");
    return 0;
}
試行回数による精度の変化:
試行回数 N推定値(例)誤差
1,0003.156~0.01
10,0003.1432~0.001
100,0003.14052~0.001
1,000,0003.141284~0.0003
10,000,0003.141552~0.00004
収束は遅い(誤差 ∝ 1/√N)が、多次元の問題にも適用でき、実装が簡単なのが利点。物理シミュレーション、金融工学などで広く使われます。

数値積分(台形公式・シンプソン法)

解析的に積分できない関数も、数値的に近似計算できます。

台形公式

ab f(x)dx ≒ h × [ f(a)/2 + f(x₁) + f(x₂) + ... + f(b)/2 ] (h = (b-a)/n)
#include <stdio.h>
#include <math.h>

double f(double x) { return sin(x); } // 被積分関数

// 台形公式
double trapezoid(double a, double b, int n) {
    double h = (b - a) / n;
    double sum = (f(a) + f(b)) / 2.0;
    for (int i = 1; i < n; i++)
        sum += f(a + i * h);
    return h * sum;
}

// シンプソン法(より高精度)
double simpson(double a, double b, int n) {
    double h = (b - a) / n;
    double sum = f(a) + f(b);
    for (int i = 1; i < n; i++)
        sum += (i % 2 == 0) ? 2*f(a+i*h) : 4*f(a+i*h);
    return h / 3.0 * sum;
}

int main(void) {
    printf("∫₀^π sin(x)dx の真の値 = 2.0\n\n");
    int ns[] = {10, 100, 1000};
    for (int i = 0; i < 3; i++) {
        printf("n=%4d  台形=%.10f  Simpson=%.10f\n",
            ns[i], trapezoid(0, M_PI, ns[i]), simpson(0, M_PI, ns[i]));
    }
    return 0;
}
精度の比較: シンプソン法は台形公式より桁違いに高精度(4次の精度)。

連立一次方程式(ガウスの消去法)

Ax = b の形の連立方程式を、前進消去→後退代入で解く。
2x + y - z = 8  -3x - y + 2z = -11  -2x + y + 2z = -3
#include <stdio.h>
#include <math.h>
#define N 3

void gauss(double a[N][N+1], double x[N]) {
    // 前進消去
    for (int i = 0; i < N; i++) {
        for (int j = i+1; j < N; j++) {
            double ratio = a[j][i] / a[i][i];
            for (int k = i; k <= N; k++)
                a[j][k] -= ratio * a[i][k];
        }
    }
    // 後退代入
    for (int i = N-1; i >= 0; i--) {
        x[i] = a[i][N];
        for (int j = i+1; j < N; j++)
            x[i] -= a[i][j] * x[j];
        x[i] /= a[i][i];
    }
}

int main(void) {
    double a[N][N+1] = {
        { 2,  1, -1,   8},
        {-3, -1,  2, -11},
        {-2,  1,  2,  -3}
    };
    double x[N];
    gauss(a, x);
    for (int i = 0; i < N; i++)
        printf("x[%d] = %.4f\n", i, x[i]);
    return 0;  // 答え: x=2, y=3, z=-1
}

二分法(方程式の根を挟み撃ち)

f(a) と f(b) の符号が異なれば、間に根がある。区間を半分ずつ狭めて解を求める。ニュートン法より遅いが、必ず収束するのが利点。
#include <stdio.h>
#include <math.h>

double f(double x) { return x*x*x - x - 1; } // x³ - x - 1 = 0

double bisection(double a, double b, double eps) {
    while (b - a > eps) {
        double mid = (a + b) / 2.0;
        if (f(a) * f(mid) <= 0)
            b = mid;  // 根は左半分にある
        else
            a = mid;  // 根は右半分にある
    }
    return (a + b) / 2.0;
}

int main(void) {
    double root = bisection(1.0, 2.0, 1e-10);
    printf("x³ - x - 1 = 0 の根: %.10f\n", root);
    printf("検算: f(root) = %e\n", f(root));
    return 0;
}
二分法: 遅い(1次収束)が安全。初期値の心配不要。
ニュートン法: 速い(2次収束)が導関数と良い初期値が必要。

チャレンジ問題

ここまでの知識を応用して、以下の問題に挑戦してみましょう。
C1. ニュートン法で立方根を求める
x³ = a となる x を求めよ。f(x) = x³ - a, f'(x) = 3x² でニュートン法を適用する。a=27 のとき x=3 になることを確認せよ。
C2. モンテカルロ法で定積分を求める
∫₀¹ e-x² dx をモンテカルロ法で計算せよ。(ガウス積分の一部。解析解は求められない。)
ヒント: 0〜1の乱数 x に対し f(x) = e-x² を計算し、その平均値が積分の近似値。
C3. 度数分布表とヒストグラム
100個の乱数(0〜1)を生成し、0.1刻みの度数分布を計算してテキストベースのヒストグラムを表示するプログラムを作成せよ。
C4. 信頼区間の計算
モンテカルロ法で円周率を推定する実験をN回行い、95%信頼区間(x̄ ± 2σ/√N)を求めるプログラムを作成せよ。
C5. 素数列挙(エラトステネスの篩)
N以下の素数を全て列挙するプログラムをエラトステネスの篩で実装せよ。配列を使い、O(N log log N) の計算量で動くことを確認せよ。
C6. 行列の積を関数で実装
n×n の行列 A, B の積 C = AB を計算する関数を作成し、3×3の行列で動作確認せよ。
この記事をシェア
X(Twitter)でシェア はてブ

数値計算をさらに深めるなら

数学×プログラミングの理解を深める書籍

📘
苦しんで覚えるC言語
MMGames 著
初心者向けの定番入門書。
Amazonで見る
📗
新・明解C言語 入門編
柴田望洋 著
図解が豊富で演習問題も充実。
Amazonで見る
📙
プログラミング言語C 第2版
B.W.カーニハン, D.M.リッチー 著
通称K&R。C言語の原典。
Amazonで見る

※ 上記リンクはアフィリエイトリンクです。購入によりサイト運営を支援いただけます。