疑似乱数とは、コンピュータの計算で疑似的に求める、ランダムな数(乱数)です。
この疑似乱数は、
- 処理ごとに毎回違う結果を出したい
- ランダムな入力でテストをしたい
- 暗号処理
などの場合に使われます。
今回は、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以上の乱数を作る場合、次のように求めます。
- rand関数で乱数を2つを取得する(乱数A、乱数B と定義)
- 乱数を「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)以上の数が取得できていることが確認できました。
メルセンヌ・ツイスタなどの方法を試してみたいですね。
メルセンヌ・ツイスタの乱数生成方法については、また別の機会にご紹介したいと思います。
応用 モンテカルロ法で円周率を求めてみる
モンテカルロ法とは
乱数を繰り返し使うことにより、近似解を求める手法です。
今回は、モンテカルロ法を使って、円周率の近似解を求めます。
考え方として、
- 半径1の円を4分割します。
このとき、4分割した円の面積はπ/4となります。 - 長さ1の辺を持つ正方形で囲みます。
- この正方形の中に乱数を使って点を複数配置します。
- 配置した点を、円の外側と内側に分けます。
- 点が十分多い場合、「点の総数と内側の点」と「正方形の面積と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関数は、ランダム性が低く本格的に乱数を使いたい場合は向かないかもしれませんが、
簡単にサクッとプログラムを書くことができました。
本格的な疑似乱数については、また別途調査したいと思います。