【C言語】rand関数による疑似乱数の求め方と応用


管理人
C言語で疑似乱数を求めるよ

疑似乱数とは、コンピュータの計算で疑似的に求める、ランダムな数(乱数)です。

 

この疑似乱数は、

  • 処理ごとに毎回違う結果を出したい
  • ランダムな入力でテストをしたい
  • 暗号処理

などの場合に使われます。

 

今回は、C言語のrand関数を使って、疑似乱数を求めます。

 


疑似乱数

疑似乱数のアルゴリズムの種類には、

  • 平方採中法
  • 線形合同法
  • 線形帰還シフトレジスタ
  • メルセンヌ・ツイスタ

などの種類があります。(Wikipediaを参照)

 

いづれの方法でも、乱数生成アルゴリズムでは、初期値(SEED)を与え、その値を基に順次、疑似乱数を求めています。

C言語で使われるrand関数は、「線形合同法」が使用されています。

 

この記事では、簡単のため、以下、疑似乱数を乱数と呼ばせてください。

rand関数による乱数の求め方

rand関数とsrand関数

rand関数の定義

定義
int __cdecl rand(void)
ヘッダファイル stdlib.h
戻り値 0 ~ RAND_MAX までのランダムな値
※RAND_MAX は環境によって異なるため注意!
私の環境では、32767でした。
引数 なし
説明 乱数を返却する関数です。アルゴリズムは「線形合同法」を使用しています。

※ランダム性が低い(規則性がでてしまう)アルゴリズムのため、大量の乱数を使うような科学技術計算や暗号などには非推奨とされています。

 

srand関数の定義

定義
void srand(unsigned int _Seed)
ヘッダファイル stdlib.h
戻り値 なし
引数 _Seed:乱数作成アルゴリズムの初期値

※初期値として、time関数がよく使われます。

補足 rand関数の初期値を設定する関数です。

 

サンプルコード

試しに、5つの乱数を取得するプログラムを作成しました。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {

    srand((unsigned int)time(NULL)); // rand関数の初期化

    const int N = 5;
    for (int i = 0; i < N; i ++) {
        int rand_num = rand(); // 乱数の取得
        printf("%d\n", rand_num);
    }

    return 0;
}

 

実行結果

<1回目>
13999
5522
7190
24245
5446
<2回目>
14662
24760
29137
26665
2153

毎回違う値が出力できていることが分かりました。

 

乱数の範囲指定 0から1の間の乱数をとる

0から1の間の浮動小数点で乱数をとりたい場合、rand関数で取得した値をRAND_MAXで割って疑似乱数を求めます。

サンプルコード

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {

    srand((unsigned int)time(NULL)); // rand関数の初期化

    const int N = 5;
    for (int i = 0; i < N; i ++) {
        double rand_num = (double)rand() / (double)RAND_MAX; // 0から1の間の疑似乱数の取得
        printf("%f\n", rand_num);
    }

    return 0;
}

11行目で乱数を求めていますが、rand関数とRAND_MAXは整数型のため、double型に型変換していることに注意してください。

実行結果

<1回目>
0.506851
0.252754
0.811274
0.488784
0.672109

<2回目>
0.507370
0.892850
0.537126
0.160558
0.245979

0から1の間の乱数の取得を確認できました。

 

RAND_MAX以上の乱数を作りたい場合

RAND_MAX以上の乱数を作る場合、次のように求めます。

  1. rand関数で乱数を2つを取得する(乱数A、乱数B と定義)
  2. 乱数を「RAND_MAX * 乱数A + 乱数B」で求める

これで、RAND_MAXの2乗個の乱数を求めることができます。

 

サンプルコード

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {

    printf("RAND_MAX=%d\n", RAND_MAX);
    srand((unsigned int)time(NULL)); // rand関数の初期化

    const int N = 5;
    for (int i = 0; i < N; i ++) {
        int a = rand();
        int b = rand();
        int rand_num = RAND_MAX * a + b;
        printf("%d\n", rand_num);
    }

    return 0;
}

実行結果

<1回目>
RAND_MAX=32767
653041211
268863945
858004590
759548375
73902871

<2回目>
RAND_MAX=32767
653150259
854204929
122291066
861516352
614912660

RAND_MAX(32767)以上の数が取得できていることが確認できました。

piyo
桁数に偏りがある気がする。。。
管理人
rand関数は乱数のランダム性が低いようですね。
メルセンヌ・ツイスタなどの方法を試してみたいですね。

メルセンヌ・ツイスタの乱数生成方法については、また別の機会にご紹介したいと思います。

 

応用 モンテカルロ法で円周率を求めてみる

モンテカルロ法とは

乱数を繰り返し使うことにより、近似解を求める手法です。

今回は、モンテカルロ法を使って、円周率の近似解を求めます。

考え方として、

  1. 半径1の円を4分割します。
    このとき、4分割した円の面積はπ/4となります。
  2. 長さ1の辺を持つ正方形で囲みます。
  3. この正方形の中に乱数を使って点を複数配置します。
  4. 配置した点を、円の外側と内側に分けます。
  5. 点が十分多い場合、「点の総数と内側の点」と「正方形の面積と4分割した円の面積」の比は
    同じになります。つまり、点の総数をN、内側の点の数をMとすると、M/Nがπ/4に近づくため、
    この性質を使ってπを求めます。

サンプルコード

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {

    /* Nを入力 */
    int n;
    printf("N=");
    scanf("%d", &n);

    /* Mを計算 */
    srand((unsigned int)time(NULL));
    int m = 0;
    for (int i = 0; i < n; i ++) {
        double x = (double)rand() / (double)(RAND_MAX + 1); // X座標
        double y = (double)rand() / (double)(RAND_MAX + 1); // Y座標
        if (x * x + y * y <= 1) { // 円の内側にある点の数をカウント
            m++;
        }
    }
    
    /* 出力 */
    printf("%lf\n", 4.0 * (double)m / (double)n);

    return 0;
}

 

実行結果

N=100
3.200000

N=10000
3.122000

N=100000000
3.141653

点の数(N)が多いほど、円周率に近づいていることが確認できました。

 

まとめ

rand関数を使った疑似乱数の実装方法について、学ぶことができたかと思います。

 

rand関数は、ランダム性が低く本格的に乱数を使いたい場合は向かないかもしれませんが、
簡単にサクッとプログラムを書くことができました。

 

本格的な疑似乱数については、また別途調査したいと思います。

 

管理人
最後までご覧いただきありがとうございました。