rand()は、狭い範囲に対して同じ数値を再び与えます


9

20x20のグリッドがあり、プレーヤー(P)、ターゲット(T)、3人の敵(X)を表示するようなゲームを作成しようとしています。これらはすべて、を使用して割り当てられるX座標とY座標を持っていrand()ます。問題は、ゲームでより多くのポイント(エネルギーなどのリフィル)を取得しようとすると、範囲が小さいため(1から20まで)、他のポイントの1つ以上と重複することです。

これらは私の変数であり、私はそれらを割り当てていますどのようにして値:(COORDあるstructだけでXとYとの)

const int gridSize = 20;
COORD player;
COORD target;
COORD enemy1;
COORD enemy2;
COORD enemy3;

//generate player
srand ( time ( NULL ) );
spawn(&player);
//generate target
spawn(&target);
//generate enemies
spawn(&enemy1);
spawn(&enemy2);
spawn(&enemy3);

void spawn(COORD *point)
{
    //allot X and Y coordinate to a point
    point->X = randNum();
    point->Y = randNum();
}

int randNum()
{
    //generate a random number between 1 and gridSize
    return (rand() % gridSize) + 1;
}

ゲームにもっと追加したいのですが、そうするとオーバーラップの確率が高くなります。これを修正する方法はありますか?


8
rand()は悪いRNGです
ラチェットフリーク

3
rand()は残念なRNGであり、とにかくそのような狭い範囲では、衝突を予期する必要はなく、ほぼ保証されています。
デデュプリケータ、2015

1
これはrand()お粗末なRNGですが、シングルプレイヤーゲームにはおそらく適切であり、RNGの品質はここでは問題になりません。
Gort the Robot、

13
の質について言えばrand()ここでは関係ないようです。暗号化は含まれておらず、RNGはこのような小さなマップで衝突を引き起こす可能性があります。
Tom Cornebize、2015

2
あなたが見ているものは誕生日問題として知られています。乱数がPRNGの自然な範囲よりも小さい範囲に変換されている場合、同じ数の2つのインスタンスを取得する確率は、想像よりもはるかに高くなります。しばらく前に、私はここ
ConcernedOfTunbridgeWells

回答:


40

rand()より良いRNG について不満を示し、推奨するユーザーは、乱数の品質については正しいですが、全体像を失っています。乱数のストリームでの重複は避けられません、それらは現実の事実です。これは誕生日問題の教訓です。

20 * 20 = 400の可能なスポーン位置のグリッドでは、24エンティティのみをスポーンする場合でも、スポーンポイントの重複が予想されます(確率50%)。50エンティティ(グリッド全体の12.5%のみ)の場合、重複の確率は95%を超えます。衝突に対処する必要があります。

時にはすべてのサンプルを一度に描画でき、次にシャッフルアルゴリズムを使用してn保証された個別のアイテムを描画できます。すべての可能性のリストを生成する必要があるだけです。可能性の完全なリストが大きすぎて保存できない場合は、現在のように(より良いRNGを使用して)一度に1つずつ発生位置を生成し、衝突が発生したときに単純に再生成できます。持っていても、いくつかの衝突する可能性があり、行の多くの衝突は、グリッドのほとんどが移入されていても指数関数的にはほとんどありません。


衝突が発生した場合のリスポーンについて考えましたが、予定よりも多くのアイテムがある場合は、衝突のルックアップが複雑になります。また、ポイントがゲームに追加または削除された場合は、チェックを編集する必要があります。私はかなり経験が浅いので、これに対する回避策があると、それを見ることができませんでした。
Rabeez Riaz

7
20x20の連続した(実際の)XY平面ではなく、20x20のチェッカーボードがある場合、衝突をチェックするための400セルのルックアップテーブルがあります。これはトリビアルです。
John R. Strohm、

@RabeezRiazより大きなマップがある場合は、グリッドベースのデータ構造(セルの一部の領域で構成されるグリッド、およびそのセル内のすべてのアイテムがリストに格納されている)になります。マップがさらに大きい場合は、rect-treeを実装します。
rwong 2015

2
@RabeezRiaz:ルックアップが複雑すぎる場合は、彼の最初の提案を使用します:400の可能な開始位置すべてのリストを生成し、ランダムな順序になるようにそれらをシャッフルし(アルゴリズムを参照)、必要に応じて前面から位置の使用を開始しますものを生成します(すでに使用した数を追跡します)。衝突なし。
RemcoGerlich 2015

2
@RabeezRiazリスト全体をシャッフルする必要はありません。ランダムな値の数が少ない場合は、必要な部分をシャッフルするだけです(1..400のリストからランダムな値を取り、それを削除して、それまで繰り返します)十分な要素があります)。実際、それがシャッフルアルゴリズムの動作方法です。
Dorus

3

既に何か他のものに割り当てられている場所で新しいエンティティを再生することを常に避けたい場合は、プロセスを少し変更することができます。これは一意の場所を保証しますが、もう少しオーバーヘッドが必要です。手順は次のとおりです。

  1. マップ上のすべての可能な場所への参照のコレクションをセットアップします(20x20マップの場合、これは400の場所になります)
  2. この400のコレクションからランダムに場所を選択します(rand()はこれでうまくいきます)
  3. この可能性を可能なロケーションコレクションから削除します(したがって、399の可能性があります)。
  4. すべてのエンティティが指定された場所になるまで繰り返します

選択元のセットから場所を削除している限り、2つ目のエンティティが同じ場所を受け取る可能性はありません(一度に複数のスレッドから場所を選択している場合を除く)。

これと類似した現実の世界は、カードのデッキからカードを引くことです。現在、あなたはデッキをシャッフルし、カードを引き、それをマークダウンし、引き分けたカードをデッキに戻し、再度シャッフルして再び引きます。上記のアプローチは、カードをデッキに戻すことをスキップします。


1

rand() % n理想的ではないことに関する用語

実行rand() % nには不均一な分布があります。値の数は20の倍数ではないため、特定の値の不均衡な数が得られます

次に、rand()通常は線形合同ジェネレーターです(他にも多数あります、これは実装される可能性が最も高いものであり、理想的なパラメーターではありません(パラメーターを選択する多くの方法があります))。これに関する最大の問題は、その中の下位ビット(% 20型式で得られるもの)がランダムでないことが多いことです。呼び出しのたびにrand()最下位ビットが1to に切り替わった数年前のことを思い出します。これはそれほどランダムではありませんでした。0rand()

以下からのrand(3)のmanページ:

Linux Cライブラリのrand()とsrand()のバージョンは同じものを使用します
random()およびsrandom()としての乱数ジェネレーターなので、低次
ビットは上位ビットと同じくらいランダムでなければなりません。ただし、古い
rand()実装、および現在の異なる実装
システムでは、下位ビットは上位ビットよりもはるかにランダムではありません
オーダービット。目的のアプリケーションではこの機能を使用しないでください
良好なランダム性が必要な場合はポータブルです。

これは今や歴史に追いやられているかもしれませんが、スタックのどこかに隠れている貧弱なrand()実装をまだ持っている可能性はかなりあります。その場合、それはまだかなり適用可能です。

やるべきことは、実際に適切な乱数ライブラリ(適切な乱数を提供する)を使用して、必要な範囲内の乱数を要求することです。

コードの適切な乱数ビットの例(リンクされたビデオの13:00から)

#include <iostream>
#include <random>
int main() {
    std::mt19937 mt(1729); // yes, this is a fixed seed
    std::uniform_int_distribution<int> dist(0, 99);
    for (int i = 0; i < 10000; i++) {
        std::cout << dist(mt) << " ";
    }
    std::cout << std::endl;
}

これと比較してください:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
    srand(time(NULL));
    for (int i = 0; i < 10000; i++) {
        printf("%d ", rand() % 100);
    }
    printf("\n");
}

これらのプログラムの両方を実行し、その出力で特定の数値が出現する(または出現しない)頻度を比較します。

関連動画:rand()は有害と見なされます

Nethackでバグを引き起こしているrand()のいくつかの歴史的側面は、自分の実装で注意深く検討する必要があります。

  • Nethack RNGの問題

    Rand()は、Nethackの乱数生成のための非常に基本的な関数です。Nethackの使用方法はバグが多いか、lrand48()が誤った疑似乱数を生成すると主張されている可能性があります。(ただし、lrand48()は定義済みのPRNGメソッドを使用するライブラリー関数であり、それを使用するプログラムはすべて、そのメソッドの弱点を考慮に入れる必要があります。)

    バグは、Nethackがlrand48()の結果の下位ビットに依存している(場合によってはrn(2)の場合と同様に)ものです。このため、ゲーム全体のRNGはうまく機能しません。これは、ユーザーアクションがさらにランダム性を導入する前、つまり、キャラクターの生成と最初のレベルの作成で特に顕著です。

上記は2003年のものですが、意図したゲームを実行しているすべてのシステムが、適切なrand()関数を備えた最新のLinuxシステムであるとは限らない場合があるため、注意が必要です。

自分でこれを実行しているだけの場合は、コード記述してentで出力をテストすることにより、乱数ジェネレータがどの程度優れているかをテストできます。


乱数の性質について

正確にランダムではない「ランダム」の他の解釈があります。データのランダムストリームでは、同じ数値を2回取得する可能性があります。コインを投げると(ランダム)、2つの表が連続する可能性が非常に高いです。または、サイコロを2回投げて、同じ数字を2回続けて取得します。または、ルーレットホイールを回転させて、そこで同じ数字を2回取得します。

数の分布

曲のリストを再生するとき、「ランダム」とは、同じ曲またはアーティストが2回続けて再生されないことを意味することを期待します。プレイリストに2回続けてビートルズを再生させること、「ランダムでない」と見なされます(ランダムですが)。4曲のプレイリストで合計8回再生されたという認識:

1 3 2 4 1 2 4 3

より「ランダム」です:

1 3 3 2 1 4 4 2

曲の「シャッフル」の詳細:曲をシャッフルするには?

繰り返し値について

値を繰り返したくない場合は、考慮すべき別のアプローチがあります。可能なすべての値を生成し、それらをシャッフルします。

あなたがrand()(または他の任意の乱数ジェネレータ)を呼び出している場合は、それを置き換えて呼び出しています。同じ番号をいつでも2回取得できます。1つのオプションは、要件を満たす値を選択するまで、値を繰り返し破棄することです。これは非決定論的なランタイムであり、より複雑なバックトレースを開始しない限り、無限ループが発生する可能性があることを指摘しておきます。

リストとピック

別のオプションは、考えられるすべての有効な状態のリストを生成し、そのリストからランダムな要素を選択することです。部屋のすべての空のスポット(いくつかのルールを満たす)を見つけ、そのリストからランダムに1つ選択します。そして、それが終わるまで何度も繰り返します。

シャッフル

もう1つの方法は、カードのデッキのようにシャッフルすることです。始め、すべての部屋の中の空のスポットや、空のスポットを求めて各ルール/プロセスに、空のスポットを1つずつ出て扱うことによって、それらを割り当てる開始。あなたはカードを使い果たすか、物事がそれらを求めるのをやめると完了します。


3
Next, rand() is typically a linear congruential generatorこれは現在、多くのプラットフォームでは当てはまりません。Linuxのrand(3)manページから: "Linux Cライブラリのrand()およびsrand()のバージョンは、random(3)およびsrandom(3)と同じ乱数ジェネレーターを使用しているため、下位ビット上位ビットと同じくらいランダムでなければなりません。」また、@ delnanが指摘しているように、PRNGの品質はここでは実際の問題ではありません。
Charles E. Grant、

4
これは実際の問題を解決しないので、反対票を投じています。
user253751 2015

@immibis次に、他の回答も実際の問題を「解決」しないため、反対票を投じる必要があります。問題は「コードを修正する」ことではなく、「なぜ重複した乱数を取得するのか」ということです。2番目の質問には、答えられていると思います。
Neil、

4
RAND_MAX32767 の最小値でさえ、いくつかの数値を取得するための1638の方法と他の数値の1639の方法の違いです。OPに実用的な違いをもたらす可能性は低いようです。
Martin Smith、

@Neil「コードを修正する」は問題ではありません。
Orbitの軽量レース、

0

この問題の最も簡単な解決策は、以前の回答で引用されています。400セルのすべてのセルの横にランダムな値のリストを作成し、このランダムなリストを並べ替えることです。セルのリストはランダムリストとして並べ替えられ、こうしてシャッフルされます。

この方法には、ランダムに選択されたセルの重複を完全に回避できるという利点があります。

欠点は、セルごとに個別のリストでランダムな値を計算する必要があることです。そのため、ゲームの開始中はやめた方がいいでしょう。

これを行う方法の例を次に示します。

#include <algorithm>
#include <iostream>
#include <vector>

#define NUMBER_OF_SPAWNS 20
#define WIDTH 20
#define HEIGHT 20

typedef struct _COORD
{
  int x;
  int y;
  _COORD() : x(0), y(0) {}
  _COORD(int xp, int yp) : x(xp), y(yp) {}
} COORD;

typedef struct _spawnCOORD
{
  float rndValue;
  COORD*coord;
  _spawnCOORD() : rndValue(0.) {}
} spawnCOORD;

struct byRndValue {
  bool operator()(spawnCOORD const &a, spawnCOORD const &b) {
    return a.rndValue < b.rndValue;
  }
};

int main(int argc, char** argv)
{
  COORD map[WIDTH][HEIGHT];
  std::vector<spawnCOORD>       rndSpawns(WIDTH * HEIGHT);

  for (int x = 0; x < WIDTH; ++x)
    for (int y = 0; y < HEIGHT; ++y)
      {
        map[x][y].x = x;
        map[x][y].y = y;
        rndSpawns[x + y * WIDTH].coord = &(map[x][y]);
        rndSpawns[x + y * WIDTH].rndValue = rand();
      }

  std::sort(rndSpawns.begin(), rndSpawns.end(), byRndValue());

  for (int i = 0; i < NUMBER_OF_SPAWNS; ++i)
    std::cout << "Case selected for spawn : " << rndSpawns[i].coord->x << "x"
              << rndSpawns[i].coord->y << " (rnd=" << rndSpawns[i].rndValue << ")\n";
  return 0;
}

結果:

root@debian6:/home/eh/testa# ./exe 
Case selected for spawn : 11x15 (rnd=6.93951e+06)
Case selected for spawn : 14x1 (rnd=7.68493e+06)
Case selected for spawn : 8x12 (rnd=8.93699e+06)
Case selected for spawn : 18x13 (rnd=1.16148e+07)
Case selected for spawn : 1x0 (rnd=3.50052e+07)
Case selected for spawn : 2x17 (rnd=4.29992e+07)
Case selected for spawn : 9x14 (rnd=7.60658e+07)
Case selected for spawn : 3x11 (rnd=8.43539e+07)
Case selected for spawn : 12x7 (rnd=8.77554e+07)
Case selected for spawn : 19x0 (rnd=1.05576e+08)
Case selected for spawn : 19x14 (rnd=1.10613e+08)
Case selected for spawn : 8x2 (rnd=1.11538e+08)
Case selected for spawn : 7x2 (rnd=1.12806e+08)
Case selected for spawn : 19x15 (rnd=1.14724e+08)
Case selected for spawn : 8x9 (rnd=1.16088e+08)
Case selected for spawn : 2x19 (rnd=1.35497e+08)
Case selected for spawn : 2x16 (rnd=1.37807e+08)
Case selected for spawn : 2x8 (rnd=1.49798e+08)
Case selected for spawn : 7x16 (rnd=1.50123e+08)
Case selected for spawn : 8x11 (rnd=1.55325e+08)

NUMBER_OF_SPAWNSを変更してランダムなセルを取得するか、変更しても、タスクに必要な計算時間は変わりません。


「そして、それらすべてをソートする」-「シャッフル」を意味すると思います

私は少し説明を終えました。今はもっとはっきりしているはずです。
KwentRell 2015
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.