如何生成不会产生超过 X 个连续元素的随机数序列



好吧,我真的不知道如何正确地提出这个问题,因为我几乎不知道如何用一句话来描述我想要什么,我很抱歉。

让我开门见山,你可以跳过剩下的,因为我只是想表明我已经尝试了一些东西,而不是一时兴起来这里问问题。

我需要一个产生6个随机数的算法,在这个序列中,它可能不会产生超过2个连续数。

示例:3 3 4 4 2 1

^很好。

示例:3 3 4 4 2

^错误!

很明显,我不知道如何做到这一点而不经常绊倒自己。

有STL或Boost功能可以做到这一点吗?或者这里有人知道如何为它炮制一个算法。那太棒了。

我想做的和我已经尝试过的(可以跳过的部分)

这是用C++编写的。我正在尝试制作一个Panel de Pon/俄罗斯方块攻击/益智联盟的任何克隆来练习。游戏有6个方块行,3个或更多匹配的方块将摧毁这些方块。这是一个视频,以防你不熟悉。

当一个新行从底部出现时,它不能带有3个水平匹配块,否则它将自动消失。一些我不想要的水平。不过垂直还是可以的。

我试着做到这一点,但似乎我做不好。当我开始游戏时,大块的方块丢失了,因为它在不该检测的时候检测到了匹配。正如你所看到的,我的方法很可能过于严厉和复杂。

enum BlockType {EMPTY, STAR, UP_TRIANGLE, DOWN_TRIANGLE, CIRCLE, HEART, DIAMOND};
vector<Block> BlockField::ConstructRow()
{
    vector<Block> row;
    int type = (rand() % 6)+1;
    for (int i=0;i<6;i++)
    {
        row.push_back(Block(type));
        type = (rand() % 6) +1;
    }
    // must be in order from last to first of the enumeration
    RowCheck(row, diamond_match);
    RowCheck(row, heart_match);
    RowCheck(row, circle_match);
    RowCheck(row, downtriangle_match);
    RowCheck(row, uptriangle_match);
    RowCheck(row, star_match);
    return row;
}
void BlockField::RowCheck(vector<Block> &row, Block blockCheckArray[3])
{
    vector<Block>::iterator block1 = row.begin();
    vector<Block>::iterator block2 = row.begin()+1;
    vector<Block>::iterator block3 = row.begin()+2;
    vector<Block>::iterator block4 = row.begin()+3;
    vector<Block>::iterator block5 = row.begin()+4;
    vector<Block>::iterator block6 = row.begin()+5;
    int bt1 = (*block1).BlockType();
    int bt2 = (*block2).BlockType();
    int bt3 = (*block3).BlockType();
    int bt4 = (*block4).BlockType();
    int type = 0;
    if (equal(block1, block4, blockCheckArray)) 
    {
        type = bt1 - 1;
        if (type <= 0) type = 6;
        (*block1).AssignBlockType(type);
    }
    else if (equal(block2, block5, blockCheckArray)) 
    {
        type = bt2 - 1;
        if (type <= 0) type = 6;
        (*block2).AssignBlockType(type);
    }
    else if (equal(block3, block6, blockCheckArray)) 
    {
        type = bt3 - 1;
        if (type == bt3) type--;
        if (type <= 0) type = 6;
        (*block3).AssignBlockType(type);
    }
    else if (equal(block4, row.end(), blockCheckArray)) 
    {
        type = bt4 - 1;
        if (type == bt3) type--;
        if (type <= 0) type = 6;
        (*block4).AssignBlockType(type);
    }
}

叹气,我不确定这是否有助于展示。。。至少这表明我已经尝试过了。

基本上,我通过将BlockType枚举描述的随机块类型分配给block对象的构造函数来构造行(block对象具有BlockType和位置)。

然后我使用RowCheck函数来查看一行中是否有3个连续的块类型,并且我对所有块类型都这样做。*_match变量是具有相同块类型的3个块对象的阵列。如果我发现有3个连续的块类型,那么我只需将第一个值减去一。然而,如果我这样做的话,我可能会无意中产生另一个3匹配,所以我只需要确保块类型按顺序从大到小。

好吧,它很糟糕,很复杂,而且不起作用!这就是为什么我需要你的帮助。

只需记录前两个值,并在新生成的值与前两个值相匹配时循环即可。

对于任意的运行长度,动态调整历史缓冲区的大小并在循环中进行比较是有意义的。但这应该接近您的要求。

int type, type_old, type_older;
type_older = (rand() % 6)+1;
row.push_back(Block(type_older));
type_old = (rand() % 6)+1;
row.push_back(Block(type_old));
for (int i=2; i<6; i++)
{
    type = (rand() % 6) +1;
    while ((type == type_old) && (type == type_older)) {
        type = (rand() % 6) +1;
    }
    row.push_back(Block(type));
    type_older = type_old;
    type_old = type;
}

第一个想法

while(sequence doesn't satisfy you)
      generate a new sequence 

想法2。

Precalculate all allowable sequences (there are about ~250K of them) 
randomly choose an index and take that element.

第二个想法需要大量内存,但速度很快。第一个也不慢,因为while循环迭代不止一到两次的可能性非常小。HTH

迄今为止看到的大多数解决方案都涉及潜在的无限循环。我可以建议一种不同的方法吗?

// generates a random number between 1 and 6
// but never the same number three times in a row
int dice()
{
    static int a = -2;
    static int b = -1;
    int c;
    if (a != b)
    {
        // last two were different, pick any of the 6 numbers
        c = rand() % 6 + 1;
    }
    else
    {
        // last two were equal, so we need to choose from 5 numbers only
        c = rand() % 5;
        // prevent the same number from being generated again
        if (c == b) c = 6;
    }
    a = b;
    b = c;
    return c;
}

有趣的部分是其他块。如果最后两个数字相等,那么只有5个不同的数字可供选择,所以我使用rand() % 5而不是rand() % 6。这个调用仍然可以产生相同的数字,但也不能产生6,所以我只是将该数字映射到6。

具有简单的do-while循环的解决方案(在大多数情况下足够好):

vector<Block> row;
int type = (rand() % 6) + 1, new_type;
int repetition = 0;
for (int i = 0; i < 6; i++)
{
    row.push_back(Block(type));
    do {
        new_type = (rand() % 6) + 1;
    } while (repetition == MAX_REPETITION && new_type == type);
    repetition = new_type == type ? repetition + 1 : 0;
    type = new_type;
}

无循环的解决方案(对于那些不喜欢先前解决方案的非确定性性质的人):

vector<Block> row;
int type = (rand() % 6) + 1, new_type;
int repetition = 0;
for (int i = 0; i < 6; i++)
{
    row.push_back(Block(type));
    if (repetition != MAX_REPETITION)
        new_type = (rand() % 6) + 1;
    else
    {
        new_type = (rand() % 5) + 1;
        if (new_type >= type)
            new_type++;
    }
    repetition = new_type == type ? repetition + 1 : 0;
    type = new_type;
}

在这两种解决方案中,对于您的情况,MAX_REPETITION等于1。

将六元素数组初始化为[1, 2, 3, 4, 5, 6]并随机交换一段时间怎么样?保证没有重复。

很多答案都说"一旦你在一行中检测到X,就重新计算最后一个,直到你没有得到X"。。。。在这样的游戏实践中,这种方法比"实时"人类互动所需的速度快数百万倍,所以就这么做吧!

但是,你显然对它感到不舒服,并在寻找更内在的"有界限"和优雅的东西。因此,假设你生成的数字是从1..6开始的,当你检测到2个X时,你已经知道下一个X可能是重复的,所以只有5个有效值:生成一个从1到5的随机数,如果它>=X,再增加一个。

它的工作原理有点像:

1..6 -> 3
1..6 -> 3
"oh no, we've got two 3s in a row"
1..5 -> ?
        < "X"/3   i.e. 1, 2       use as is
        >= "X"         3, 4, 5,   add 1 to produce 4, 5 or 6.

然后你知道最后两个元素不同。。。当您继续检查一行中的2个元素时,后者将占据第一个位置。。。。

vector<BlockType> constructRow()
{
    vector<BlockType> row;
    row.push_back(STAR); row.push_back(STAR);
    row.push_back(UP_TRIANGLE); row.push_back(UP_TRIANGLE);
    row.push_back(DOWN_TRIANGLE); row.push_back(DOWN_TRIANGLE);
    row.push_back(CIRCLE); row.push_back(CIRCLE);
    row.push_back(HEART); row.push_back(HEART);
    row.push_back(DIAMOND); row.push_back(DIAMOND);
    do
    {
        random_shuffle(row.begin(), row.end());
    }while(rowCheckFails(row));
    return row;
}

这个想法是在这里使用random_shuffle()。您需要实现满足要求的rowCheckFails()

编辑

我可能不太理解你的要求。这就是为什么我把每种块类型的2个放在一行中。你可能需要投入更多。

I认为最好将随机数生成隐藏在方法或函数后面。它可以是一个同时返回三个随机数的方法或函数,确保输出中至少有两个不同的数字。它也可以是一个流生成器,确保它永远不会连续输出三个相同的数字。

int[] get_random() {
    int[] ret;
    ret[0] = rand() % 6 + 1;
    ret[1] = rand() % 6 + 1;
    ret[2] = rand() % 6 + 1;
    if (ret[0] == ret[1] && ret[1] == ret[2]) {
        int replacement;
        do {
            replacement = rand() % 6 + 1;
        } while (replacement == ret[0]);
        ret[rand() % 3] = replacement;
    }
    return ret;
}

如果你想要六个随机数(这对我来说有点难以判断,视频只是令人困惑:),那么生成if条件需要付出更多的努力:

for (int i=0; i<4; i++) {
    if (ret[i] == ret[i+1] && ret[i+1] == ret[i+2])
        /* three in a row */

如果你总是更改ret[1](三个中的中间一个),你就永远不会因为更改而连续出现三个,但输出也不会是随机X Y XX X Y发生得更频繁,因为它可能是随机发生的,并且在X X X的情况下是强制的

首先对上述解决方案发表一些评论。

  1. 如果一个随机值不令人满意,那么拒绝它的技术并没有错。这是一种广泛使用的技术——拒绝采样的一个例子。例如,用于生成随机高斯的几种算法涉及拒绝采样。一种是极性拒绝方法,它包括从U(-1,1)中重复绘制一对数字,直到它们都为非零并且不在单位圆之外。这就投出了超过21%的双人组。在找到一个令人满意的对之后,一个简单的变换产生一对高斯偏差。(极性抑制方法现在已经失宠,取而代之的是ziggurat算法。它也使用了抑制采样。)

  2. rand() % 6有很大问题。不要这样做。曾经来自随机数生成器的低阶比特,即使是一个好的随机数生成器,也不像高阶比特那样"随机"。

  3. rand()出现了很大问题。大多数编译器编写者显然不知道如何生成随机数。不要使用rand()

现在有一个使用Boost随机数库的解决方案:

vector<Block> BlockField::ConstructRow(
  unsigned int max_run) // Maximum number of consecutive duplicates allowed
{
  // The Mersenne Twister produces high quality random numbers ...
  boost::mt19937 rng;
  // ... but we want numbers between 1 and 6 ...
  boost::uniform_int<> six(1,6);
  // ... so we need to glue the rng to our desired output.
  boost::variate_generator<boost::mt19937&, boost::uniform_int<> >
    roll_die(rng, six);
  vector<Block> row;
  int prev = 0;
  int run_length = 0;
  for (int ii=0; ii<6; ++ii) {
    int next;
    do {
      next = roll_die();
      run_length = (next == prev) ? run_length+1 : 0;
    } while (run_length > max_run);
    row.push_back(Block(next));
    prev = next;
  }
  return row;
}

我知道这已经有很多答案了,但我突然想到了一个问题。你可以有7个数组,一个数组有6个数字,每个数组缺少一个给定的数字。像这样:

int v[7][6] = {
    {1, 2, 3, 4, 5, 6 },
    {2, 3, 4, 5, 6, 0 }, // zeros in here to make the code simpler, 
    {1, 3, 4, 5, 6, 0 }, // they are never used
    {1, 2, 4, 5, 6, 0 },
    {1, 2, 3, 5, 6, 0 },
    {1, 2, 3, 4, 6, 0 },
    {1, 2, 3, 4, 5, 0 }
};

然后你可以有一个2级历史。最后要生成一个数字,如果您的比赛历史记录小于最大值,请洗牌v[0]并取v[0][0]。否则,打乱v[n]中的前5个值,并取v[n][0]。类似这样的东西:

#include <algorithm>
int generate() {
    static int prev         = -1;
    static int repeat_count = 1;
    static int v[7][6] = {
        {1, 2, 3, 4, 5, 6 },
        {2, 3, 4, 5, 6, 0 }, // zeros in here to make the code simpler, 
        {1, 3, 4, 5, 6, 0 }, // they are never used
        {1, 2, 4, 5, 6, 0 },
        {1, 2, 3, 5, 6, 0 },
        {1, 2, 3, 4, 6, 0 },
        {1, 2, 3, 4, 5, 0 }
    };
    int r;
    if(repeat_count < 2) {
        std::random_shuffle(v[0], v[0] + 6);
        r = v[0][0];
    } else {
        std::random_shuffle(v[prev], v[prev] + 5);
        r = v[prev][0];
    }
    if(r == prev) {
        ++repeat_count;
    } else {
        repeat_count = 1;
    }
    prev = r;   
    return r;
}

这应该会产生良好的随机性(不依赖于rand() % N),没有无限循环,并且考虑到我们每次洗牌的数字数量很少,这应该是相当有效的。

注意,由于使用了statics,这是不线程安全的,这对于您的使用来说可能很好,如果不是,那么您可能希望将其封装在一个对象中,每个对象都有自己的状态。

最新更新