【问题标题】:How to produce a random number sequence that doesn't produce more than X consecutive elements如何生成不产生超过 X 个连续元素的随机数序列
【发布时间】:2011-09-26 14:03:25
【问题描述】:

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

让我直截了当,你可以跳过其余部分,因为我只是想表明我已经尝试过一些东西,而不是一时兴起来这里问问题。

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

示例:3 3 4 4 2 1

^很好。

示例:3 3 3 4 4 2

^不! 不!错了!

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

是否有 STL 或 Boost 功能可以做到这一点?或者也许这里有人知道如何为它设计一个算法。那太棒了。

我正在尝试做什么以及我已经尝试过什么。(您可以跳过的部分)

这是用 C++ 编写的。我正在尝试制作一个 Panel de Pon/Tetris Attack/Puzzle League 任何克隆进行练习。游戏有一个 6 块行和 3 个或更多匹配块将破坏块。 Here's a video in case you're not familiar.

当一个新行从底部出来时,它一定不能有 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 匹配,所以我只是确保块类型按从大到小的顺序排列。

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

【问题讨论】:

    标签: c++ boost stl random


    【解决方案1】:

    很多答案都说“一旦你连续检测到 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】:

      我知道这已经有很多答案了,但是我突然想到了一个想法。您可以有 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),没有无限循环,并且考虑到我们每次洗牌的数字数量很少,应该相当有效。

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

      【讨论】:

        【解决方案3】:

        目前看到的大多数解决方案都涉及潜在的无限循环。我可以建议不同的方法吗?

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

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

        【讨论】:

          【解决方案4】:

          首先介绍一下上述解决方案。

          1. 如果随机值不令人满意,则拒绝随机值的技术没有任何问题。这是拒绝抽样的一个例子,这是一种广泛使用的技术。例如,用于生成随机高斯的几种算法都涉及拒绝采样。一种是极坐标拒绝法,它涉及从 U(-1,1) 中重复绘制一对数字,直到两者都不为零且不在单位圆之外。这抛出了超过 21% 的对。找到满意的对后,简单的变换会产生一对高斯偏差。 (极坐标拒绝方法现在已经失宠,被 ziggurat 算法取代。这也使用了拒绝采样。)

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

          3. rand() 有很大问题,句号。大多数编译器编写者显然不知道有关生成随机数的 bean。不要使用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;
          }
          

          【讨论】:

          • 编译器编写器不实现rand(),库编写器执行。
          • @Evan:这有点迂腐,不是吗?我从未见过没有与标准 C 库捆绑在一起的 C 编译器,也从未使用过不与编译器捆绑在一起的标准 C 库。此外,其中一些库函数直接由编译器实现。
          • 内在函数是不同的,在 libc 实现中使用它们仍然取决于 库作者。此外,glibc 根本不与gcc“捆绑”。它们都是由 GNU 制造的,但这就是它的终点。您可以轻松地使用 uClibc。哎呀,我什至写了自己的 libc。
          • 不管是谁写的,我的印象是核心问题在于 C 标准......它显然对rand 的输出施加了足够的限制,以至于做一些不烂不值得努力。
          【解决方案5】:

          记录前两个值就足够了,当新生成的值与前两个值匹配时循环。

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

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

          【讨论】:

          • 我喜欢提出的许多其他想法,但我发现这是最容易做到的,而且似乎很有效!
          • 你确定你知道(type == type_old) == type_older是什么意思吗?
          • 我把 (type == type_old) && (type_old == type_older).
          • 不,等等,我想我过激了。我以某种方式结束了一场比赛。虽然我不明白为什么这会失败。也许我做了其他事情......
          • @Mathmagician,循环条件应该是type == type_old &amp;&amp; type_old == type_older。实际上,您仍然会有 3 个不等于 1 的连续值(假设 true == 1)。无论如何,该解决方案重复了部分代码,这并不优雅。一直查看我的解决方案...
          【解决方案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。

          【讨论】:

          • 跟踪运行长度的好方法。
          【解决方案7】:

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

          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 X 会比 X X Y 更频繁地发生,因为它可能是随机发生的

          【讨论】:

          • 将变量命名为new 是否明智?
          • @Dialectus,哈哈,说得好。我通常写C,没关系。 :) 感谢您指出。
          【解决方案8】:
          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 个放在行中。你可能需要放更多。

          【讨论】:

            【解决方案9】:

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

            【讨论】:

            • 但在这种情况下,你不会有这个序列,永远不会,这可能不利于 OP 的目的:3,3,4,3,4,4
            • 我确实想要重复,而不是 3+ 连续重复。
            • @Armen:是的,但是 OP 说只有这样的序列是“好的”,而不是它是想要的。
            • @Mathmagician:每个解决方案都应该有重复项吗?或者他们只是可以接受?
            • 并非总是如此。这个想法是避免连续 3 个块匹配,因此它不会过早消失。因为我正在尝试制作俄罗斯方块攻击类型的游戏。
            【解决方案10】:

            想法 1。

            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 循环迭代不止一次或两次的可能性很小。高温

            【讨论】:

            • 哦,是的,我喜欢第二个想法。不敢相信我没有想到。谢谢!
            • 第 2 个想法绝对是聪明的。
            • 你不应该在没有实际存储它们的情况下做idea no.2吗?从索引生成允许的序列应该相对容易 - 然后只需首先选择索引并从中计算序列。
            • @ltjax:确实,尽管这需要在纸上进行一些离散数学运算 :)
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-12-22
            • 2012-09-24
            • 1970-01-01
            相关资源
            最近更新 更多