【问题标题】:Is there anything wrong with this shuffling algorithm?这种洗牌算法有什么问题吗?
【发布时间】:2010-12-30 15:03:29
【问题描述】:

我一直在做一些休闲度假计算。我的小项目是模拟意大利的“tomboli”游戏。一个关键的构建块是对以下过程的模拟;

游戏由一个男人控制,他拿着一袋 90 颗弹珠,编号从 1 到 90。他从袋子里随机抽出一颗弹珠,每次向玩家喊出弹珠编号。

经过一番思考,我为这个构建块编写了以下代码;

// NBR marbles, numbered 1...NBR are in a bag. Simulate randomly
//  pulling them from the bag, one by one, until the bag is empty
void bag( int random_sequence[NBR] )
{
    int i;

    // Store each marble as it is pulled out
    int *store = random_sequence;

    // Array of marbles still in the bag
    int not_yet_pulled[NBR];
    for( i=0; i<NBR; i++ )
        not_yet_pulled[i] = i+1;    // eg NBR=90; 1,2,3 ... 90

    // Loop pulling marbles from the bag, one each time through
    for( i=NBR; i>=1; i-- )
    {
        int x = rand();
        int idx = x%i;  // eg i=90 idx is random in range 0..89
                        // eg i=89 idx is random in range 0..88
                        //            ...
                        // eg i=1  idx is random in range 0..0
                        //    (so we could optimize when i=1 but not worth the bother)
        *store++  = not_yet_pulled[idx];

        // Replace the marble just drawn (so it cannot be pulled again)
        //     with the last marble in the bag. So;
        //     1) there is now one less marble in the bag
        //     2) only marbles not yet pulled are still in the bag
        // If we happened to pull the last marble in the *current subarray*, this is
        //    not required but does no harm.
        not_yet_pulled[idx] = not_yet_pulled[i-1];
    }
}

我知道在随机数的游戏模拟中到处都是微妙之处和陷阱,所以虽然我对我的代码很满意,但我的信心还不到 100%。所以我的问题是;

1) 我的代码有什么问题吗?

2) [如果 1) 的答案是否定的] 我是否在不知不觉中使用了标准洗牌算法?

3) [如果 2) 的答案是否定的] 我的算法与标准替代方案相比如何?

编辑 感谢所有回答的人。我将接受 Aidan Cully 的回答,因为事实证明我正在重新发现 Fisher-Yates 算法,并揭示了问题的核心。当然,我可以通过预先进行一些研究来节省自己的时间和精力,这并不奇怪。但另一方面,这是一个有趣的爱好项目。其余的模拟是例行公事,这是最有趣的部分,如果我自己不去,我会剥夺自己的乐趣。此外,我试图模拟一个人从袋子里取出弹珠,但在我意识到这种情况与洗牌完全相似的时候已经很晚了。

另一个有趣的地方是有一个小缺陷,由 Ken 指出,经常重复的模式 rand()%N 并不是从 0..N 范围内选择随机数的好方法- 1.

最后,我的Fisher-Yates 版本缺乏优雅的技巧,无法实现原地改组的良好特性。结果,我的算法会以同样随机但反向的随机播放结束。

【问题讨论】:

  • 评论好像是手写汇编;)
  • [@HamishGrubijan] 这就是为什么Bill 值得另一个 +1 来向路人解释一种新算法...... :-D。对了,我只是想提醒你Bill在调用这个方法之前不要忘记srand(time(0));...
  • @OmarOthman 谢谢。我记得当时想知道(这是一个老问题)哈米什是在拍我的后背还是相反。最好拍拍背:-)

标签: c++ c algorithm random simulation


【解决方案1】:

使用Fisher-Yates-Knuth shuffle

public static void shuffle(int[] array) 
{
    Random rng = new Random();       // java.util.Random.
    // n is the number of items left to shuffle
    for (int n = array.length; n > 1; n--) 
    {
        // Pick a random element to move to the end
        int k = rng.nextInt(n);  // 0 <= k <= n - 1.
        // Simple swap of variables
        int tmp = array[k];
        array[k] = array[n - 1];
        array[n - 1] = tmp;
    }
}

看起来您的代码可能有效,但我不确定。它比标准算法更容易混淆。

【讨论】:

  • 只是想通过管道输入 python 的 random.shuffle() 确实使用了 Fisher-Yates 算法。
  • Fisher-Yates 被证明是公正的,并且尽可能简单和高效。使用其他任何东西的理由很少。
【解决方案2】:

【讨论】:

  • 这基本上是正确的,唯一的区别是你存储到一个新的数组中,而 FY 算法(如上面的 rossfabricant 所述)存储到末尾的空白部分输入数组。
  • 嗯,这不是 完全 fFisher-Yates 洗牌算法,但它是如此接近你可以明智地说它是,所以我不理解 -1 并回馈一些要点。
  • @Edmund:实际上 FY 是一种数学算法,因此没有存储机制的概念。虽然 Durstenfeld 描述的标准实现确实做到了。
【解决方案3】:
int idx = x%i;  // eg i=90 idx is random in range 0..89

它在那个范围内,但不是均匀分布的,除非 90(或 NBR)除以 max(rand())。如果您使用的是 2 位计算机,那可能不是真的。例如,在这里,idx 为 0 的可能性比为 89 的可能性略高。

【讨论】:

  • 好点!如果 n 很小,这只是一个非常小的偏差,但如果你运行足够的测试,它会很明显。
  • @Ken: 想打赌 rand(n) 在许多实现上都有同样的偏见?
  • kriss:摆脱这种偏见很容易。例如,请参阅java.util.Random.nextInt(int)。我怀疑实现会在这种情况下产生有偏差的数字。
【解决方案4】:

分析算法以检查它们是否真的是随机的非常困难。
除了拥有大学数学水平的人(或者用美国人的话来说,数学专业的人),这远远超出了大多数人的技能,甚至无法验证。

因此,您应该尝试使用已经构建的算法。
你看过std::random_shuffle()吗?

 void bag( int random_sequence[NBR] )
 {
     for(int i=0; i<NBR; ++i) 
     {    random_sequence[i] = i+1;
     }
     std::random_shuffle(random_sequence,random_sequence + NBR);
 }

引用来自 std::random_shuffle() 页面:

该算法在 Knuth 的第 3.4.2 节中进行了描述(D. E. Knuth,计算机编程的艺术。第 2 卷:半数值算法,第二版。Addison-Wesley,1981)。 Knuth 归功于 Moses and Oakford (1963) 和 Durstenfeld (1964)。注意有N!排列 N 个元素的序列的方法。 Random_shuffle 产生均匀分布的结果;也就是说,任何特定排序的概率都是 1/N!。这条评论很重要的原因是,有许多算法乍一看似乎可以实现序列的随机混洗,但实际上并不能在 N 上产生均匀分布!可能的订购。也就是说,随机洗牌很容易出错

【讨论】:

  • +1,但有三件小事:1) 大学(没有 d)。 2)我认为这是一个学习练习而不是生产代码。 3) 我认为 OP 的算法并不是真正随机的,因为 rand() % i 会在 (RAND_MAX + 1) % i != 0 时支持较低的结果(这对于 i 的大多数值可能是正确的)。
  • @Chris:您能否详细说明为什么 rand() % i 应该支持较低的结果,即使 RAND_MAX 在 i 旁边足够大?我相信许多生成器都使用 LCG,如果您不使用完整序列,将无法检测到这种偏差。
  • @Kriss:是的,差别很小。但关键是它是可测量的,因此引入了基础。这就是为什么好的随机教科书详细解释了为什么你应该使用“ floor(rand()/(RAND_MAX + 1,o) * RANGE) ”(虽然仍然不完美,但比使用模数更好)。但是比这更好需要数学技能,为什么要结合我的能力。因此,我更喜欢使用由具有适当知识和教育的人编写的既定算法。
  • @Kriss:我认为更简单的答案如下:如果 RAND_MAX 为 4 而 i 为 3,则 0 和 1 将在 40% 的时间输出,而 2 将在 20% 的时间输出时间。随着数字变大,差距缩小,但永远不会为零。
【解决方案5】:

rand() % i 的替代品(int) ((rand() / (double) (RAND_MAX+1)) * i) 将具有更好的接近均匀分布(以牺牲性能为代价)。

或者,使用已知效果良好的伪随机数生成算法,例如Mersenne twister

【讨论】:

  • 不需要加倍。通过添加 1.0 而不是 1 你得到一个双倍。
  • 虽然这只是将强制转换的语义转移到编译器。在汇编级别,无论您是显式编码还是通过添加双精度来隐式要求它,仍然会发生强制转换。
【解决方案6】:

只是几个风格点:

  1. 您对具有给定长度的数组的签名可能会给人一种错觉,即编译器保证该参数至少包含 IDX 元素。不是。
  2. 我可能会给第二个 for 循环中的循环索引一个更具描述性的名称,例如 marblesRemaining,这样它是什么就更清楚了,不需要 cmets 解释。它还将它与它在第一个循环中的完全不同的用途分开。

【讨论】:

  • 1.是的,我意识到,我只在末尾添加了给定的长度作为一种可执行的注释。 2. 好点。我避免并讨厌带有长描述性无意义名称的循环计数器,但你是对的,这不仅仅是一个循环计数器,因此描述性名称会很有帮助。所以+1
【解决方案7】:

抛开随机数生成问题不谈,您的随机播放算法看起来是正确的。

不过,您可以改进它:稍加思考,您就会发现您可以将数字随机排列在适当的位置。因此,您可以使用输出缓冲区,而不是分配临时数组。

【讨论】:

  • 确实 +1。我最终会得到其他人提供的 Fisher Yates 算法
【解决方案8】:

正如其他人已经评论的那样,使用经过验证的洗牌算法。

值得注意的是,您的 C/C++ 库仅提供伪随机数。

需要高可靠性随机化算法的系统使用专用硬件来生成随机数。高端扑克网站就是一个很好的例子。例如,请参阅Pokerstars writeup 了解他们的随机数生成技术。

早期版本的 Netscape 加密已被破解,因为黑客能够预测所使用的“随机”数字,因为伪随机数字生成器是以当前时间为种子的。看到这个writeup on Wikipedia

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-04-08
    • 2013-08-27
    • 1970-01-01
    • 2014-12-22
    相关资源
    最近更新 更多