テスト アプリケーションの最も基本的な問題は、 1 回呼び出してから 1 回srand
呼び出しrand
て終了することです。
関数の全体的なポイントは、疑似乱数のシーケンスをランダムシードでsrand
初期化することです。
これは、2 つの異なるアプリケーション (同じ/実装)で同じ値をに渡すと、その後両方のアプリケーションでまったく同じ値のシーケンスが読み取られることを意味します。srand
srand
rand
rand()
ただし、サンプル アプリケーションでは、擬似ランダム シーケンスは 1 つの要素のみで構成されます。シードから生成された擬似ランダム シーケンスの最初の要素は、現在の1 sec
精度の時間に等しくなります。その場合、出力に何が表示されると思いますか?
明らかに、同じ秒にアプリケーションを実行した場合-同じシード値を使用するため、結果はもちろん同じです(Martin Yorkが質問へのコメントですでに述べたように)。
実際には、srand(seed)
一度呼び出しrand()
てから何度も呼び出して、そのシーケンスを分析する必要があります。ランダムに見えるはずです。
修正 1 - コード例:
わかったよ。どうやら口頭での説明では不十分なようです (言語の壁か何か... :) )。
srand()/rand()/time()
質問で使用されたのと同じ関数に基づく昔ながらの C コードの例:
#include <stdlib.h>
#include <time.h>
#include <stdio.h>
int main(void)
{
unsigned long j;
srand( (unsigned)time(NULL) );
for( j = 0; j < 100500; ++j )
{
int n;
/* skip rand() readings that would make n%6 non-uniformly distributed
(assuming rand() itself is uniformly distributed from 0 to RAND_MAX) */
while( ( n = rand() ) > RAND_MAX - (RAND_MAX-5)%6 )
{ /* bad value retrieved so get next one */ }
printf( "%d,\t%d\n", n, n % 6 + 1 );
}
return 0;
}
^^^プログラムの 1 回の実行からのそのシーケンスは、ランダムに見えるはずです。
以下で説明する理由により、本番環境で / 関数を使用することはrand
お勧めしません。また、IMO がすでに非常に明白であるという理由により、関数をランダム シードとして使用することも絶対にお勧めしません。これらは、教育目的やポイントを説明するためには問題ありませんが、深刻な用途にはほとんど役に立ちません。srand
time
修正 2 - 詳細な説明:
現在のところ、実際にランダムなデータを確実に生成する (つまり、実際にランダムであることが標準によって保証されている) C または C++ の標準機能 (ライブラリ関数またはクラス)は存在しないことを理解することが重要です。この問題に対処する唯一の標準機能はstd::random_deviceであり、残念ながら実際のランダム性を保証するものではありません。
アプリケーションの性質に応じて、真にランダムな (予測不可能な) データが本当に必要かどうかを最初に判断する必要があります。真のランダム性が最も確実に必要な注目すべきケースは、情報セキュリティです。たとえば、対称キー、非対称秘密キー、ソルト値、セキュリティ トークンなどを生成します。
ただし、セキュリティ グレードの乱数は別の記事に値する別の業界です。この私の回答でそれらについて簡単に説明しています。
ほとんどの場合、疑似乱数ジェネレーターで十分です (科学シミュレーションやゲームなど)。場合によっては、一貫して定義された疑似乱数シーケンスが必要になることさえあります。たとえば、ゲームでは、ディストリビューションに大量のデータを保存することを避けるために、実行時にまったく同じマップを生成することを選択できます。
元の質問と繰り返される多数の同一/類似の質問 (およびそれらに対する多くの誤った「回答」でさえ) は、何よりもまず、乱数と疑似乱数を区別し、疑似乱数シーケンスとは何かを理解することが重要であることを示しています。そもそも疑似乱数ジェネレーターは、真の乱数ジェネレーターを使用できるのと同じ方法で使用されないことを認識してください。
直感的に乱数を要求するとき - 返される結果は、以前に返された値に依存してはならず、誰かが以前に何かを要求したかどうかに依存してはならず、どの瞬間に、どのプロセスによって、どのコンピューターに、どのジェネレーターから、どのようなものに依存してはなりません。要求された銀河。それが「ランダム」という言葉の意味です - 予測不可能で、何からも独立している - そうでなければ、それはもはやランダムではありませんよね? この直感を使えば、Web で魔法の呪文を検索して、あらゆる可能なコンテキストでそのような乱数を取得するのはごく自然なことです。
^^^この種の直感的な期待は、疑似乱数ジェネレーターが関係するすべての場合において非常に間違っており、有害です- 真の乱数に対しては妥当ですが。
「乱数」という意味のある概念は (一種の) 存在しますが、「疑似乱数」のようなものはありません。疑似乱数ジェネレーターは、実際に疑似乱数シーケンスを生成します。
実際、疑似乱数シーケンスは常に決定論的です (そのアルゴリズムと初期パラメータによって事前に決定されます)。つまり、実際にはランダムなものは何もありません。
専門家が PRNG の品質について話すとき、実際には、生成されたシーケンス (およびその注目すべきサブシーケンス) の統計的特性について話します。たとえば、2 つの高品質の PRNG を順番に使用して組み合わせると、それぞれが適切なシーケンスを個別に生成しているにもかかわらず、悪い結果のシーケンスが生成される可能性があります (これらの 2 つの適切なシーケンスは単に互いに相関するため、うまく結合されない可能性があります)。
具体的にはrand()
/srand(s)
ペアの関数は、実装定義のアルゴリズムで生成された単一のプロセスごとの非スレッドセーフ (!) 疑似乱数シーケンスを提供します。関数rand()
は range の値を生成します[0, RAND_MAX]
。
C11 規格 (ISO/IEC 9899:2011) からの引用:
このsrand
関数は、引数を、後続の への呼び出しによって返される疑似乱数の新しいシーケンスのシードとして使用しますrand
。が同じシード値で呼び出された場合
srand
、疑似乱数のシーケンスが繰り返されます。rand
の呼び出しが行われる前に が呼び出された場合、シード値 1 で が最初に呼び出さsrand
れたときと同じシーケンスが生成されます。srand
多くの人は、 から までの範囲の半独立な一様分布数のシーケンスが生成されると合理的に期待していrand()
ます。確かにそうすべきです(そうでなければ役に立たない)が、残念ながら標準がそれを必要としないだけでなく、「生成されたランダムシーケンスの品質に関する保証はありません」という明示的な免責事項さえあります。いくつかの歴史的なケースでは、実装は実際に非常に質の悪いものでした. 最新の実装ではおそらく十分ですが、信頼は壊れており、回復するのは簡単ではありません. その上、非スレッドセーフな性質により、マルチスレッド アプリケーションでの安全な使用はトリッキーで制限されます (まだ可能です - 1 つの専用スレッドから使用することもできます)。0
RAND_MAX
rand
srand
新しいクラス テンプレートstd::mersenne_twister_engine<> (およびその便利な typedef - std::mt19937
/std::mt19937_64
適切なテンプレート パラメーターの組み合わせ) は、C++11 標準で定義されたオブジェクトごとの疑似乱数ジェネレーターを提供します。同じテンプレート パラメーターと同じ初期化パラメーターを使用すると、C++11 準拠の標準ライブラリを使用して構築された任意のコンピューター上の任意のコンピューターで、異なるオブジェクトがまったく同じオブジェクトごとの出力シーケンスを生成します。このクラスの利点は、予測可能な高品質の出力シーケンスと、実装全体での完全な一貫性です。
また、C++11 標準で定義されている PRNG エンジンがさらにあります - std::linear_congruential_engine<>srand/rand
(一部の C 標準ライブラリ実装で公正な品質のアルゴリズムとして歴史的に使用されています) およびstd::subtract_with_carry_engine<>。また、完全に定義されたパラメーター依存のオブジェクトごとの出力シーケンスも生成します。
上記の廃止された C コードを現代の C++11 に置き換える例:
#include <iostream>
#include <chrono>
#include <random>
int main()
{
std::random_device rd;
// seed value is designed specifically to make initialization
// parameters of std::mt19937 (instance of std::mersenne_twister_engine<>)
// different across executions of application
std::mt19937::result_type seed = rd() ^ (
(std::mt19937::result_type)
std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()
).count() +
(std::mt19937::result_type)
std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::high_resolution_clock::now().time_since_epoch()
).count() );
std::mt19937 gen(seed);
for( unsigned long j = 0; j < 100500; ++j )
/* ^^^Yes. Generating single pseudo-random number makes no sense
even if you use std::mersenne_twister_engine instead of rand()
and even when your seed quality is much better than time(NULL) */
{
std::mt19937::result_type n;
// reject readings that would make n%6 non-uniformly distributed
while( ( n = gen() ) > std::mt19937::max() -
( std::mt19937::max() - 5 )%6 )
{ /* bad value retrieved so get next one */ }
std::cout << n << '\t' << n % 6 + 1 << '\n';
}
return 0;
}
std::uniform_int_distribution<>を使用する以前のコードのバージョン
#include <iostream>
#include <chrono>
#include <random>
int main()
{
std::random_device rd;
std::mt19937::result_type seed = rd() ^ (
(std::mt19937::result_type)
std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()
).count() +
(std::mt19937::result_type)
std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::high_resolution_clock::now().time_since_epoch()
).count() );
std::mt19937 gen(seed);
std::uniform_int_distribution<unsigned> distrib(1, 6);
for( unsigned long j = 0; j < 100500; ++j )
{
std::cout << distrib(gen) << ' ';
}
std::cout << '\n';
return 0;
}