第75章 乱数

 ゲームなどを作っていると、乱数が欲しいなと思うことがよくあります。しかし、乱数というのはコンピュータにとっては苦手な分野です。でも、なんとか乱数を使うことができます。今回はその方法を教えましょう。


 では、今回の要点です。


 では、いってみましょう。


 さて、サイコロ2つを振ってその合計を求めたいと思います。そのためには、サイコロの動作を真似する必要があります。

 これには、乱数を用いればいいですね。乱数とは、一言で言うと「無茶苦茶な値」です。例えば、まさにサイコロを適当に振ったときの目の値は乱数になりますね。

 この乱数は rand という関数で得ることができます。0から RAND_MAX までの範囲の乱数値を得ることができます(RAND_MAX はマクロです)。必要なヘッダファイルは stdio.h です。

 しかし、サイコロの目は1〜6です。そこで、rand の値にある計算をほどこして、1〜6の値に変換してしまいましょう。これには普通割り算の余りを使います。6で割った余りは0〜5になります。これに1を足せば1〜6になるという算段です。

 まぁ、RAND_MAX + 1 は実は6の倍数ではないので1〜6が完全に均等には現れませんが、RAND_MAX は結構大きいので実際上はあまり気にはなりません。

 さて、では次のプログラムを実行してみましょう。

プログラム
// Rand1.cpp
#include <iostream.h>
#include <stdio.h>

inline int Dice()
{
    return rand() % 6 + 1;
}

int main()
{
    int i;

    for(i = 0; i < 20; i++)
        cout << Dice() + Dice() << ' ';
    cout << flush;

    return 0;
}
実行結果例
12 10 11 2 8 12 6 8 5 5 5 7 10 7 9 7 7 9 8 3

 確かに乱数になっていますね。このように、rand を使えば乱数が使えるわけです。


 しかし、VC++を使っている人は上と全く同じ結果になっていませんか? VC++以外でも同じになることもあるかも知れません。そして、このプログラムを何回実行しても同じ結果になると思います。

 コンピュータは用意された値を正確に計算することは得意なのですが、乱数のように無茶苦茶な値を扱うのには向いていないのです。そこである計算を行い、乱数っぽい値を作ってごまかしているわけです。(しかもC/C++の rand はかなり乱数としての質も低く、乱数としては不自然な挙動を行うことがあります。)なので、コンピュータが計算してはじき出したこの偽物の乱数を疑似乱数と呼びます。

 で、やはり疑似乱数が毎回同じ結果をはじくのは困るので、それを防ぐ方法があります。疑似乱数はある初期値を元に計算を行って生成されます。初期値を設定する関数は srand です。この初期値に毎回違う値を設定してやれば、毎回同じ結果をはじくことはなくなります。

 しかし、この初期値に毎回違う値を入れるといわれても、「そういうことをするために乱数があるんじゃないのか?」と思うことでしょう。手動で値を入れるのも馬鹿馬鹿しいですよね。そこで、こういうときは普通現在の時間を元に初期化します。現在の時間自体は乱数として利用できませんが、初期値としては十分利用できます。

 現在の時間を取得する関数は time です。必要なヘッダファイルは time.h です。この値は「万国標準時(UCT)での1970年1月1日0時0分0秒からの秒単位での経過時間」になります。が、そんなことはどうでもいいです。とにかく、1秒ごとに値が違うということが重要なのです。

 この値の型は time_t です。この time_t という型は耳慣れませんが、これもどうでも構いません。これは srand の引数の型である unsigned int に安全にキャストを行うことができます。それさえ分かっていればいいのです。

 結局、srand((unsigned int)time(NULL)); という文を書けば、毎回違う結果になるわけです。一応理屈を説明しましたが、実用上はここだけ分かっていればいいです。

 で、最終的に、次のようになります。

プログラム
// Rand1b.cpp
#include <iostream.h>
#include <stdio.h>
#include <time.h>

inline void InitRand()
{
    srand((unsigned int)time(NULL));
}

inline int Dice()
{
    return rand() % 6 + 1;
}

int main()
{
    int i;

    InitRand();
    for(i = 0; i < 20; i++)
        cout << Dice() + Dice() << ' ';
    cout << flush;

    return 0;
}
実行結果例1
9 8 10 5 6 5 3 4 6 7 6 6 9 4 4 5 8 8 6 7
実行結果例2
10 11 8 8 9 9 6 7 5 6 4 10 7 9 6 9 4 8 6 12
実行結果例3
5 2 6 4 2 7 9 9 10 9 2 6 7 8 10 10 9 7 7 11

 毎回値が変わってくれました。これでやっときちんとした乱数として利用できそうですね。


 疑似乱数の生成法にはいろいろあります。rand は線形合同法という方法を使っています。他にもM系列乱数Mersenne Twister法(MT法)というのが有名です。

 MT法は2000年7月29日現在、最も質のよい疑似乱数生成法です。質を求めるなら迷わずMT法を使いましょう。質はMT法よりは落ちますが、速度を求めるならM系列乱数がいいでしょう。線形合同法は質があまり良くないので、普通のゲームなどには利用できますが、数学、物理、化学計算といった、精度の要求される場合には使えません。

 この中でMT法は日本人が開発したので、開発者のホームページは日本語です(結構英語のページもありますが)。興味のある人は一度検索してみてはどうでしょうか?


 さて、今回の要点です。


 次回は面白い型の作り方を紹介します。特定の値しかとることのできない型です。それでは次回まで。


第74章 もっとロジカルに | 第76章 列挙子

Last update was done on 2000.7.30

この講座の著作権はロベールが保有しています