web-dev-qa-db-ja.com

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

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

これらは私の変数であり、それらに値を割り当てる方法です((COORDはXとYだけのstructです)。

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;
}

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

9
Rabeez Riaz

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

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

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

40
user7043

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

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

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

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

3
Lyise

Rand() % nが理想的とは言えない

Rand() % nを実行すると、分布が不均一になります。値の数は20の倍数ではないため、特定の値の不均衡な数を取得します

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

From Rand(3)man page: から

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()は有害と見なされます

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

  • 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つの方法は、カードのデッキのようにシャッフルすることです。部屋の空のスポットallから始めて、空のスポットを求める各ルール/プロセスに、空のスポットを一度に1つずつ割り当てることで、それらの割り当てを開始します。あなたはカードを使い果たすか、物事がそれらを求めなくなると完了します。

1
user40980

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

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

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

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

#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を変更するだけです。これにより、タスクに必要な計算時間が変更されることはありません。

0
KwentRell