C言語/C++で範囲を指定した乱数を生成する

C言語やC++などでプログラミングをする際、指定した範囲内での乱数が欲しい場面があります。 巷には単に最大値で剰余を取ればいいとしか解説していないものもありますが、それでは偏りのある乱数となる場合があります(それで問題無い場面も多いですが、問題無いかどうかは利用者が判断すべきことでしょう)。

今回はC++のSTLを用いる場合とそうでない場合それぞれについて、指定した範囲内での整数乱数を取得する方法を(単に剰余を取る方法も含め)いくつか紹介します。

C++の場合(STLを使う場合)

C++11からSTLにrandomというライブラリが導入されました。これを用いることにより様々な種類の乱数生成が可能です。

ちなみに、本記事で紹介するコードは実装例の1つを挙げてライブラリの使い方を解説するためのものであり、これをすべての状況でコピペして使いまわすことを推奨するものではありません。使い方を理解した上で、ご自分の状況に合わせた実装をしてください。

乱数を生成する方法

std::mt19937 または std::mt19937_64 というクラスを用いることで、 質の良い擬似乱数で動作も高速と言われるメルセンヌ・ツイスタを用いた乱数を生成することができます。どちらを使うかは環境が64bitに対応しているかどうかで判断してください。

#include <random>

// 64bit環境の場合
uint64_t get_rand() {
    // 乱数生成器(引数にシードを指定可能)
    static std::mt19937_64 mt64(0);

    // [0, (2^64)-1] の一様分布整数を生成
    return mt64();
}

// 32bit環境の場合
uint32_t get_rand() {
    // 乱数生成器(引数にシードを指定可能)
    static std::mt19937 mt32(0);

    // [0, (2^32)-1] の一様分布整数を生成
    return mt32();
}

オブジェクトをstaticにしているのは、C言語標準のrand関数に似た使い勝手の関数を意識して実装したというだけで、必須ではありません。

範囲や分布を指定した乱数を生成する方法

上記で説明した乱数生成器を以下のように分布生成器に渡すことで、乱数の範囲を指定することができます。

#include <random>

uint64_t get_rand_range( uint64_t min_val, uint64_t max_val ) {
    // 乱数生成器
    static std::mt19937_64 mt64(0);

    // [min_val, max_val] の一様分布整数 (int) の分布生成器
    std::uniform_int_distribution<int> gen_rand_uni_int( min_val, max_val );

    // 乱数を生成
    return get_rand_uni_int(mt64);
}

また、分布生成器を以下で紹介するものに置き換えることで、値の型や分布の種類などを変えることができます。

ランダムな文字が欲しい場合は値の型と範囲を工夫することで実現可能です。

// [a-z] の一様分布生成器
std::uniform_int_distribution<char> gen_rand_uni_char( 'a', 'z' );

整数ではなく実数が必要な場合:

// [0.0, 1.0) の一様分布実数生成器
std::uniform_real_distribution<double> gen_rand_uni_real( 0.0, 1.0 );

一様分布だけでなく、例えば正規分布などもあります。

// 平均0.0、標準偏差1.0の正規分布生成器
std::normal_distribution gen_rand_norm_dist( 0.0, 1.0 )

その他詳しいことは random - cpprefjp C++日本語リファレンス を参照ください。

C言語(STLを使わない場合)

STLのような便利なライブラリを使用せず、地道に範囲を指定した乱数を得る方法を紹介します。 同様のアルゴリズムを実装できればどんな言語にも応用可能です。

乱数を生成する方法

上で『STLのような便利なライブラリを使用せず』と書きましたが、さすがに乱数生成自体に関しては、既に存在するものを利用することとします。

乱数生成関数と言えばC言語付属のrand関数が有名ですが、rand関数は乱数の質が悪いことでも有名です。 C++のSTLでも採用されているメルセンヌ・ツイスタを実装したライブラリを使用することをおすすめします。

メルセンヌ・ツイスタの開発者の一人である 松本眞氏によりC言語用のライブラリが以下にて公開されています。

SIMD-oriented Fast Mersenne Twister (SFMT)

下記の記事にてrand関数とメルセンヌ・ツイスタの違いや、上記ライブラリの使い方などを解説しています。

範囲を指定した乱数を生成する方法

以下では一様な乱数整数を返す関数としてgenrand()という関数が存在し,その生成しうる最大値がRANDMAXであることとします。そのままコピペしても動きませんのであしからず。

まず、よく用いられる方法として範囲の最大値で剰余をとる方法があります。

0からmax_valの範囲の乱数が欲しい場合は

int get_rand( int max_val ) {
    return (int)( genrand() % ( max_val+1 ) );
}

min_valからmax_valの範囲の乱数が欲しい場合は

int get_rand( int min_val, int max_val ) {
    return (int)(( genrand() % ( max_val+1 - min_val ) ) + min_val );
}

とします。

別の方法として、0からmax_valの範囲の乱数が欲しい場合は

int get_rand( int min_val, int max_val ) {
    return (int)( genrand() / ( (double)RANDMAX+1.0 ) * ( max_val+1 );
}

min_valからmax_valの範囲の乱数が欲しい場合は

int get_rand( int min_val, int max_val ) {
    return (int)( genrand() / ( (double)RANDMAX+1.0 ) * ( max_val+1 - min_val ) + min_val );
}

もあります。

どちらの方法も手軽でわかりやすいのですが、問題もあります。

問題点

まず、得られる乱数に偏りがあります。特に前者の最大値で剰余を取る方法では、範囲ごとに出現頻度がきっぱりと分かれます。

例として、仮にgenrand()の生成する範囲を0〜99、max_valを29としましょう。 すると、genrand()が0〜89を出力する場合は0〜29が均等に得られますが、genrand()が90〜99を出力する場合は0〜9の間しか得られません。つまり0〜9と10〜29で得られる確率が違います。この例だとそれぞれの範囲の値(例えば0と10)が出現する頻度の割合は10:9となります。つまり0は10より1割ほど出やすくなります。

後者の方法では、ある値を境として範囲ごとにきっぱりと確率が分かれるということはありませんが、それでも値ごとに出現頻度は異なります。上述の例の通りgenrand()の生成する範囲を0〜99、max_valを29と仮定すると、genrand()が0〜3を出力する場合は結果が0、4〜6を出力する場合は結果が1となり、0が出る頻度と1が出る頻度の割合は4:3となります。つまり0は1より3割ほど出やすくなります。

また先の問題と近い話ですが、前者の方法ではmax_valの値がgenrand()が出力する値の最大値より大きい場合、一部の範囲の値しか得られません。 当たり前ですが、genrand()の範囲が0〜99だと、0〜1000の範囲で乱数が欲しいと思っても、絶対に0〜99の範囲しか出ません。

ちなみに後者の方法だと、出現する値の候補が0〜1000の範囲で飛び飛びになります。

解決策

まず、これらの問題はRANDMAXmax_valから見て十分大きくない場合に発生する問題と言えます。しかし、逆にRANDMAXが十分大きい場合においては問題となりません。

例えば、32bit整数の乱数(0〜4294967295)を生成できる場合、それを前者の剰余を取る方法によって0〜999の範囲に絞ると、0〜295の範囲の任意の値aが出る確率と296〜999の範囲の任意の値bが出る確率の比は4294968:4294967となります。 つまりaが出る確率とbが出る確率の差は約0.00002%です。この差をどう捉えるかは状況によりますが、これが無視できる程度であるという場面も少なくはないでしょう。

中には0.00002%の差すら許されない場面も存在するかと思います。 そのような厳密に偏りの無い一様な乱数が必要とされる場合は以下のようにします。

int get_rand( int min_val, int max_val ) {
    int randmax_limit = (int)( RANDMAX / ( max_val+1 - min_val ) ) * ( max_val+1 - min_val );
    int r;
    while ( ( r = genrand() ) > randmax_imit );
    return ( r % ( max_val+1 - min_val ) ) + min_val;
}

偏りの原因はgenrand()で得られる値の幅がmin_valからmax_valの値の幅の整数倍になっていないことに起因します。つまり、genrand()で得られる範囲をmin_valからmax_valの値の幅の整数倍であるrandmax_limitに制限する(randmax_limit以下の値が出るまでやり直す)ことで、無理矢理一様になるようにしています。

もちろん、乱数ですのでこのwhileが終了する保証はありませんが、よほど質の悪い乱数生成器を使用しない限り問題になることは無いかと思われます。(whileの条件を満たす確率はmin_valmax_valによりますが必ず50%未満となります。50%としても10回連続でwhileの条件を満たす確率が0.1%未満であることを考えれば、このループが問題になる場面はかなり限られるでしょう。)

最後に

C言語とC++それぞれで範囲指定した乱数の生成方法を紹介しました。やはりC++のSTL(特にC++11/14)がいかに便利で強力かがよくわかりますね。


Effective Modern C++ ―C++11/14プログラムを進化させる42項目

Effective Modern C++ ―C++11/14プログラムを進化させる42項目