初歩的な質問ですみません。
参考書のじゃんけんゲームのプログラムでコンピューター側の手を乱数で
rand()%3;
と表現しているのですが、
ある特定の範囲の乱数を使うために、剰余を用いるのはなぜなのでしょうか?
いまいち理解できず、質問させていただきました。
どなたか分かりやすく教えてくださると助かります。
よろしくお願いいたします。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/05/17 01:09
2020/05/18 02:20
2020/05/18 04:02
2020/05/18 09:27
退会済みユーザー
2020/05/18 09:54
2020/05/18 09:59
2020/05/18 10:01
回答9件
0
そもそもMudulo biasという問題によって、生成確率が一様にならないため、乱数の範囲調整に剰余を用いることが誤りです。また偏りなく範囲を調整するのは全く容易ではないので、C++11のuniform_int_distributionを利用するべきです。
- いつからその方法で偏りのない乱数が得られると錯覚していた? - アスペ日記
- Cのrand()よりmt19937の方が速いことがあるという話 - Educational NLP blog
- random_shuffle - cpprefjp C++日本語リファレンス
- uniform_int_distribution - cpprefjp C++日本語リファレンス
まあせっかくなので、rand()の性能が悪いことで有名なMSVCでもcateyeさんのコードを実験してみました。
cpp
1#include <iostream> 2#include <cstdlib> 3#include <random> 4// 5using std::cout; 6using std::endl; 7// 8const int R_MAX = 1000000; 9 10int main(void) 11{ 12 int rr[4] = { 0 }; 13 int mr[4] = { 0 }; 14 15 for (int i = 0; i < R_MAX; ++i) { 16 rr[(rand() >> 7) % 3]++; 17 } 18 // 19 std::mt19937 mt(std::random_device{ }()); 20 std::uniform_int_distribution<int> dist(0, 2); 21 22 for (int i = 0; i < R_MAX; ++i) { 23 mr[dist(mt)]++; 24 } 25 // 26 for (int i = 0; i < 4; ++i) { 27 cout << i << ": " << rr[i] << " " << mr[i] << endl; 28 } 29 return 0; 30}
0: 335654 333673 1: 332437 333180 2: 331909 333147 3: 0 0
まあじゃんけんゲームには影響ないですよね。それはそう。
投稿2020/05/16 13:43
編集2020/05/17 01:39総合スコア5852
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/05/16 13:48
2020/05/16 13:52 編集
2020/05/16 13:52
2020/05/16 14:17
2020/05/16 21:51
2020/05/16 22:22
2020/05/16 23:02 編集
2020/05/17 01:24
2020/05/17 02:10
2020/05/17 02:29 編集
2020/05/17 02:39
2020/05/17 02:49 編集
2020/05/17 02:57
2020/05/17 04:48
2020/05/17 04:51 編集
0
rand()%3;
の場合は、rand()の発生する乱数を3で割ったあまり(つまり、0〜2)を使いたいからでしょう。
例えば、rand()%10とすれば0〜9が得られます。
試しに、通常のrand()関数とmt19937を使ったテストをしてみました。
cpp
1#include <iostream> 2#include <cstdlib> 3#include <random> 4// 5using std::cout; 6using std::endl; 7// 8const int R_MAX = 1000; 9 10int main(void) 11{ 12 int rr[4] = {0}; 13 int mr[4] = {0}; 14 15 for(int i = 0; i < R_MAX; ++i) { 16 rr[rand( ) % 3]++; 17 } 18 // 19 std::mt19937 mt(std::random_device{ }( )); 20 std::uniform_int_distribution<int> dist(0, 2); 21 22 for(int i = 0; i < R_MAX; ++i) { 23 mr[dist(mt)]++; 24 } 25 // 26 for(int i = 0; i < 4; ++i) { 27 cout << i << ": " << rr[i] << " " << mr[i] << endl; 28 } 29 return 0; 30}
結果は。以下のように成りました。(同一数値が連続する等の)発生状況は未検証ですが、1000回の試行での数値の発生件数に付いては、ほとんど差異はないように思います。
text
1usr ~/Project/test % ./a.out 20: 354 319 31: 305 345 42: 341 336 53: 0 0
環境は以下
usr /Project/test % uname -a18.04.2-Ubuntu SMP Thu Apr 23 14:27:18 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
Linux usr.Mint193 5.3.0-51-generic #44
usr ~/Project/test % c++ --version
clang version 10.0.0 (trunk 375507)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /usr/local/bin
参考:なぜ乱数ジェネレーターを使用しているときにモジュロバイアスがあると人々が言うのですか?
投稿2020/05/16 13:36
編集2020/05/16 17:43総合スコア6851
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/05/16 13:38
2020/05/16 22:30
2020/05/16 22:35
2020/05/17 07:28 編集
2020/05/17 07:14
2020/05/17 07:29
0
直接の回答ではないですが、rand() % N
方式の問題が見えるようにしてみました。
下記のコードをWindows上でcl(Visual Studioのコンパイラ)、clang、gcc(MinGW版を使うこと)のいずれかでコンパイルしてみてください。(Windows標準のC標準ライブラリが使われるようにすると言うこと)
C
1#include <stdio.h> 2#include <stdlib.h> 3 4#define TRIAL_NUMS 100000000 5#define N 100 6 7int main(void) 8{ 9 srand(0); 10 long long count[N] = { 0 }; 11 for (long long i = 0; i < TRIAL_NUMS; i++) { 12 count[rand() % N]++; 13 } 14 printf("RAND_MAX: %d\n", RAND_MAX); 15 for (int i = 0; i < N; i++) { 16 printf("%d: %lld\n", i, count[i]); 17 } 18 return 0; 19}
シードが固定なので、どのWindowsであっても次のようになるはずです。(もしかしたら、Windowsのバージョンによって異なる可能性はあります。下記はWindows 10 1909での結果です。)
RAND_MAX: 32767 0: 1000317 1: 1000941 2: 1001425 3: 1000824 4: 1000320 5: 1002156 6: 1000743 7: 1000490 8: 1002694 9: 1000754 10: 1000407 11: 999649 12: 1000486 13: 999705 14: 1001300 15: 1000246 16: 1001420 17: 1000176 18: 1000844 19: 999160 20: 1000688 21: 1001457 22: 1001543 23: 1000631 24: 999740 25: 1000769 26: 1001653 27: 1000206 28: 1000585 29: 1000811 30: 1000454 31: 1003263 32: 1001180 33: 1002142 34: 1002162 35: 1000753 36: 1000572 37: 1001244 38: 1000235 39: 1001291 40: 1001121 41: 1001745 42: 1001757 43: 1002708 44: 1000194 45: 1001196 46: 1001448 47: 1000214 48: 1002378 49: 1001071 50: 999699 51: 1000265 52: 1001959 53: 999487 54: 999405 55: 1003237 56: 1001845 57: 999192 58: 1000057 59: 999976 60: 1000253 61: 1001000 62: 999549 63: 999874 64: 1002895 65: 1000536 66: 999611 67: 1001572 68: 998691 69: 998146 70: 998725 71: 999580 72: 997056 73: 997362 74: 998375 75: 999660 76: 998156 77: 1000324 78: 997447 79: 997027 80: 998391 81: 996694 82: 999624 83: 997891 84: 996303 85: 998304 86: 997201 87: 997868 88: 996585 89: 998111 90: 998279 91: 997415 92: 997874 93: 999119 94: 997966 95: 997747 96: 998160 97: 997556 98: 1000191 99: 998492
67を境に1000000を越えている数と越えてない数の頻度にかなりの差が見られるかと思います。
Nが3程度ではそれほど違いは見えないでしょう(乱数の周期を越えるぐらいの試行回数を重ねないと、ランダム性から出る誤差の方が大きすぎて、有意義な結果が見えない)。ですが、「0から99までの数を乱数で取得(1D100をふるイメージ)し、50未満なら成功」みたいなゲームのプログラムを組んだとき、成功確率はわずかに50%を越えます(低い数値のほうが出やすいので、SANチェックもきっと乗り越えられるぜ!)。乱数の取り方とその利用方法によっては僅かな差がちょっとの差としてあられる可能性があると言うことです。といっても、その差は変わらずちょっとなのでゲーム程度では些細な問題です。しかし、数学的な厳密さが必要とされる統計やシミュレーション等では問題になる可能性があります。(そもそも、rand()
の実装は環境依存であり、良い乱数ではないことが多いので、厳密さが求められるものに使用するのは避けるべきではありますが。)
【おまけ】
rand() % N
はそれぞれが現れる確率が等しいとは言えないという話でした。では、どうすれば良いのかというと、はみ出てしまっている部分を切ればいいと言うことです。たとえば、RAND_MAXが32767の場合、0から順番に0,1,2と入れておくと、最後の方は、32766が0、32767が1となって、2にいれるものが一つだけ足りません。なので、乱数が32766や32767の場合は結果を切り捨てて、乱数取得からやり直しすれば、いいとなります。
では、早速、GCCのlibstc++を参考にしながら、100の場合のコードを書き換えて見ましょう。
C
1#include <stdio.h> 2#include <stdlib.h> 3 4#define TRIAL_NUMS 100000000 5#define N 100 6#define N_SCALING (RAND_MAX / N) 7#define N_PAST (N * N_SCALING) 8 9int main(void) 10{ 11 srand(0); 12 long long count[N] = {0}; 13 for (long long i = 0; i < TRIAL_NUMS; i++) { 14 int r; 15 do { 16 r = rand(); 17 } while (r >= N_PAST); 18 count[r / N_SCALING]++; 19 } 20 printf("RAND_MAX: %d\n", RAND_MAX); 21 for (int i = 0; i < N; i++) { 22 printf("%d: %lld\n", i, count[i]); 23 } 24 return 0; 25}
今度は少ない数の方に1000000越えが偏るということは起きなかったと思います。
さて、上のコードを見てみましょう。do ~ while (...);
の部分は最初に言った切り捨て部分です。最後の切れ端みたいな乱数になった場合(N_PAST
以上になった場合)はやり直ししています。そうして得られた乱数に対して、N_SCALING
を割っています。そう、剰余ではなく商を使っています。
なぜ、剰余ではなく商なのか?r % N
でもいいのでは無いのか?という疑問はありますが、GCCの実装がそうだったので、今回はそうしました。では、なぜ、GCCがそのようにしているのかについては、すいませんが、もっと詳しい人に聞いていただくようお願いします。
投稿2020/05/17 04:30
編集2020/05/17 12:01総合スコア21739
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/05/18 02:04
2020/05/18 09:21
0
整数を3で割った余りは3通りだからです。石、ハサミ、紙の3通りに当たるのでしょう。
他にも3通りの値を作り出す方法はありますが、これが一番ラクな方法ではあります。
投稿2020/05/16 13:33
総合スコア4830
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/05/18 01:52
2020/05/18 01:56
0
詳しい回答は既についているので、もう少し根本的な話を。
なぜ、剰余を使うのか?というと、
剰余には直線的な数値を循環する数値に変換する特性があるから、です。
x % 3
の場合で考えてみます。
まずは、xを0から順に増やしてゆきます。
0,1,2,3,4,5,6,7,8,9,10,11...
これを、% 3 で剰余を取ると
0,1,2,0,1,2,0,1,2,0,1,2...
と延々と0〜2をひたすら循環する形に変換できます。
xの値がどれだけ大きくとも必ず0〜2の範囲内に収まり、なおかつ循環なので基本的には出現数がほぼ等しくなる。
そういう特性が、出目を等しく分配するのに便利な為、剰余が使われているのです。
(それが完全ではない事はすでに他で書かれているとおりです)
数を3分割すれば良いのですから、例えば0〜8を分割するのに
0〜2、3〜5、6〜8という分け方も可能ですが、この方法は全体の数を把握している必要があり、少々面倒です。
イメージとしては、トランプを最初にプレイヤーに分配する時に、
52枚を参加人数で割って一人当たり何枚だから……なんて方法では配らずに
全員に順番に1枚ずつ配っていくことで、大体等しい枚数を配るのと同じ感じです。
(端数が誤差になるのも同じ)
剰余のこの連続した数値を循環する数値に変換するという特性は便利で、他の場所でも使われています。
x % 8 とすれば、0から順に増えた数値は7まで続き、8になった途端に0に戻ります。
この特性は8進数の一桁を表すのに実に好都合です。
同様に8をnに変えればn進数の計算に使えます。
ここらへんが、乱数の分配に剰余が使われる理由です。
投稿2020/05/17 21:45
編集2020/05/18 10:53総合スコア1218
0
整数演算で剰余を取れば、0~除数-1 のどれかしか出てこないから、お手軽に範囲内のそこそこバラバラの数が得られるから、です。
しかし、「乱数」として考えると、0~10の乱数があったとして、3の剰余を取れば
0,3,6,9 ->0
1,4,7,10 ->1
2,5,8 ->2
が得られることになり、明らかに偏りがあります。でも、実際にはrand()の範囲は32767とか20億とかだし、目的が「じゃんけんゲーム」なんでしょ...それくらい問題ナシ、ということで剰余でやっちゃえ、ということです。
真面目にやるなら、rand()が返す最大値をRAND_MAXとして0~N-1の整数が欲しいなら
(int)((double)N*rand()/(RAND_MAX+1))
とでもすれば均等になります。
投稿2020/05/16 13:59
総合スコア7703
0
皆さんたくさんコメントありがとうございました。
全て目を通させて頂きました。
ですが個人的に専門的と感じる意見が多く、私にはちょっと難しかったです...。
また、規約について細かく注意するのがお好きな方もいらっしゃったので、登録しはじめたばかりですが、teratailが嫌になってしまいました。
回答してくださった方々、お時間いただきありがとうございました。
投稿2020/05/18 10:07
退会済みユーザー
総合スコア0
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。