rand()在小范围内再次给出相同的数字


9

我正在尝试制作一种游戏,其中有20x20的网格,并显示一个玩家(P),一个目标(T)和三个敌人(X)。所有这些都有一个X和Y坐标,使用分配rand()。问题是,如果我尝试在游戏中获得更多积分(补充能量等),则它们会与一个或多个其他积分重叠,因为范围很小(包括1到20)。

这些是我的变量以及如何为它们分配值:(COORD是一个struct只有X和Y的a)

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
棘轮怪胎2015年

3
rand()是一个可怜的RNG,无论如何,只要射程很小,您都不必期待碰撞,而且几乎可以保证。
Deduplicator 2015年

1
虽然确实是rand()糟糕的RNG,但它可能适用于单人游戏,而RNG的质量并不是这里的问题。
Gort机器人2015年

13
在这里谈论质量rand()似乎无关紧要。不涉及加密,任何RNG都可能在如此小的地图中产生冲突。
Tom Cornebize

2
您所看到的被称为“生日问题”。如果您将随机数转换为小于PRNG自然范围的范围,那么获得两个相同数字实例的可能性比您想象的要高得多。不久前,我在这里的
ConcernedOfTunbridgeWells

Answers:


40

尽管抱怨rand()并推荐更好的RNG 的用户对随机数的质量是正确的,但他们也没有把握更大的前景。随机数流中的重复是不可避免的,这是生活中的事实。这是生日问题的教训。

在20 * 20 = 400个可能的产卵位置的网格上,即使仅产卵24个实体,也将期望有一个重复的产卵点(50%概率)。对于50个实体(仍然仅占整个网格的12.5%),重复的可能性超过95%。您必须处理冲突。

有时您可以一次绘制所有样本,然后可以使用随机播放算法绘制n保证有区别的项目。您只需要生成所有可能性的列表。如果可能性的完整列表太大而无法存储,则您可以像现在一样一次生成一个生成位置(只是使用更好的RNG),并在发生碰撞时简单地重新生成。即使可能会发生一些冲突,即使大多数网格都已填充,也不会连续发生多次冲突。


我曾考虑过在发生碰撞时重生,但是如果我有更多物品(如我打算的那样),那么查找碰撞将变得很复杂。如果要在游戏中添加或删除点,我还必须编辑检查。我没有经验,所以如果有解决方法,我看不到。
拉贝兹·里亚兹

7
如果您有20x20的棋盘,而不是20x20的连续(真实)XY平面,那么您将拥有一个400单元的查找表来检查碰撞。这是TRIVIAL。
约翰·R·斯特罗姆

@RabeezRiaz如果您有较大的地图,则将具有一些基于网格的数据结构(由某些单元格区域组成的网格,该单元格内的每个项目都存储在列表中)。如果您的地图更大,则将实现rect-tree。
rwong

2
@RabeezRiaz:如果查找过于复杂,请使用他的第一个建议:生成一个列表,列出所有400个可能的起始位置,将它们随机排列,以便它们以随机顺序排列(查找算法),然后在需要时从头开始使用位置生成东西(跟踪已使用的数量)。没有碰撞。
RemcoGerlich 2015年

2
@RabeezRiaz无需重新整理整个列表,如果只需要少量随机值,则只需重新整理所需的部分即可(例如,从1..400列表中获取一个随机值,将其删除,然后重复直到您有足够的元素)。实际上,这就是改组算法的工作方式。
Dorus

3

如果您始终希望避免在已经分配给其他位置的位置中播放新实体,则可以对过程进行些微更改。这样可以保证唯一的位置,但是需要更多的开销。步骤如下:

  1. 设置对地图上所有可能位置的引用的集合(对于20x20地图,这将是400个位置)
  2. 从这个集合400中随机选择一个位置(rand()可以正常工作)
  3. 从可能的位置集合中删除此可能性(因此,现在有399种可能性)
  4. 重复直到所有实体都具有指定位置

只要您从要选择的集合中删除位置,第二个实体就不会有可能收到相同的位置(除非您一次从多个线程中选择位置)。

现实世界中的类似情况是从一副纸牌中抽出一张纸牌。目前,您正在对牌组进行混洗,抽出一张牌并将其标记下来,然后将抽出的纸牌放回牌组中,重新混洗并再次抽签。上面的方法跳过将卡放回卡座的过程。


1

rand() % n不理想有关

做的rand() % n分布不均匀。您将获得不成比例的某些值,因为值的数量不是20的倍数

接下来,rand()通常是线性同余生成器(还有很多其他生成器,只是这是最有可能实现的一种-参数不理想(有很多选择参数的方式))。最大的问题是,其中的低位(使用% 20类型表达式获得的位)通常不是那么随机。我记得一个rand()从年前从哪里交替的最低位10每个调用rand()-这不是很随机的。

rand(3)手册页中:

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测试输出来测试随机数生成器的性能。


关于随机数的性质

对于“随机”还有其他的解释,它们并不是完全随机的。在随机数据流中,很可能两次获得相同的数字。如果您掷硬币(随机),很有可能连续获得两个头。或掷两次骰子,并连续两次获得相同的数字。或旋转轮盘并在那里获得两次相同的数字。

数字分布

播放歌曲列表时,人们希望“随机”表示不会连续播放同一首歌曲或歌手。让播放列表连续播放两次《甲壳虫乐队》被视为“不是随机的”(尽管它随机的)。对于四首歌曲的播放列表总共播放了八次的感觉:

1 3 2 4 1 2 4 3

比以下内容更“随机”:

1 3 3 2 1 4 4 2

有关歌曲“随机播放 ”的更多信息:如何随机播放歌曲?

重复值

如果您不想重复值,则应考虑使用其他方法。生成所有可能的值,然后将它们洗牌。

如果您正在呼叫rand()(或任何其他随机数生成器),则将其替换。您总是可以两次获得相同的号码。一种选择是一次又一次地扔掉这些值,直到您选择了一个符合要求的值为止。我将指出,这具有不确定的运行时,并且有可能您会陷入无限循环的情况,除非您开始进行更复杂的回溯。

列出并选择

另一种选择是生成所有可能有效状态的列表,然后从该列表中选择一个随机元素。查找房间中所有符合条件的空位,然后从该列表中随机选择一个。然后一次又一次地做,直到完成。

随机播放

另一种方法是重新整理,就像是一副纸牌。首先处理房间中的所有空白点,然后开始处理空白点,一次一次地分配给每个要求空白点的规则/流程,开始分配它们。当卡片用完或事情不再需要它们时,您就完成了。


3
Next, rand() is typically a linear congruential generator现在在许多平台上并非如此。在Linux的rand(3)手册页中:“ Linux C库中的rand()和srand()版本使用与random(3)和srandom(3)相同的随机数生成器,因此低位应该和高阶位一样随机。” 而且,正如@delnan指出的那样,PRNG的质量在这里并不是真正的问题。
查尔斯·E·格兰特

4
我对此表示反对,因为它不能解决实际的问题。
user253751

@immibis然后,另一个答案也不能“解决”实际问题,应该予以否决。我认为问题不是“修复代码”,而是“为什么我会得到重复的随机数?” 对于第二个问题,我相信这个问题已经回答。
尼尔

4
即使最小值为RAND_MAX32767,相差还是1638种可能的方法来获取一些数字,而其他方法则是1639。似乎不太可能对OP产生实际影响。
马丁·史密斯

@Neil“修复我的代码”不是问题。
Lightness Races in 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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.