【问题标题】:PHP Shuffle in PHP 7.1+ produces different resultsPHP 7.1+ 中的 PHP Shuffle 产生不同的结果
【发布时间】:2021-01-31 22:02:12
【问题描述】:

我使用 shuffle() 函数来模拟文字游戏中的信袋。

我最近从 PHP 5 升级到 PHP 7,并发现在较新的版本上,shuffle 功能对我的目的不太好。

我过去常常从成群的字母开始,它会很好地把它们打乱,所以有一个体面但不可预测的字母顺序。在 7.1+ 上,它经常产生相同字母的块,而在 5.x 上很少得到这些块

我看到他们在 7.1 中更改了内部算法。

有没有什么方法可以模仿旧的 shuffle 工作方式,以便产生更接近 PHP 5 中的结果?

【问题讨论】:

  • 在什么方面“不太好”?这对于推荐替代方案可能很重要。
  • 需要注意的一点是,人类的大脑非常不擅长创造或判断随机性。您是否将每个版本的结果与实际从袋子中挑选字母的结果进行了比较?
  • 以前没有的团块可能表明更好随机性。见stackoverflow.com/questions/910215/…;我在stackoverflow.com/a/910280/1902010 的回答有一个有用的图形。该线程中还有关于构建避免结块的较少随机方法的指导。
  • 实现自己的shuffle函数并不难,可以根据需要自定义随机性
  • @CharlieSmith 就是这样,不过,找到四个相同的字母 的可能性很大。阅读“生日悖论”,并尝试一些实际实验;我打赌你会发现 PHP 7 给了你一个更现实的信袋。现在,您可能会认为随机性较小的会更有趣,但随后您必须确定要引入哪些偏见。

标签: php random shuffle


【解决方案1】:

什么是随机性?

计算机与人类大脑的共同点是它们非常不擅长随机化。

如果没有一些外部输入(掷骰子、测量放射性衰变、收听未调谐的收音机,staring at lava lamps),您实际上无法写下一组产生随机性的步骤。您所能做的就是制作一个看起来随机的实际可预测的序列。不幸的是,很容易欺骗人脑,让其认为某些东西是随机的,或者发现没有的模式,因此在设计这种“伪随机性”时必须非常小心。

人类大脑如何感知随机性的一个相关示例是,如果一个房间里只有 23 个人,那么其中两个人生日相同的可能性为 50%。这太令人惊讶了,它通常被称为“birthday paradox”,尽管它在数学上非常简单。你的文字游戏中的字母簇非常相似。

发生了什么变化?

长期以来,PHP 有两种不同的“伪随机数生成器”:

  • 一种依赖于系统的算法,以rand() 的形式向用户公开,存在很多问题。
  • 一种叫做“Mersenne Twister”的东西,以mt_rand() 的形式向用户公开,总体来说在所有方面都更好。

在 PHP 7.1 中,a series of changes were made 基本消除了rand() 版本,并在各处使用 Mersenne Twister 算法。这包括shuffle

算法的其余部分都没有改变:总是打算将元素按随机顺序排列,而不管它们的起始位置如何,所以你通过聚集起始位置来影响它的能力是错误,不是功能。

不同版本中随机播放的随机性如何?

我写了一个quick script 来测试shuffle 是否有任何偏见:

  1. 生成一个包含 100 个项目的数组,从 0 到 99
  2. 随机播放
  3. 标记每个元素的新位置
  4. 重复很多次

结果是一个 100 x 100 的网格,显示例如元素 0 在位置 42 结束的频率。

如果洗牌是真正随机的,我们应该期望,当我们重复洗牌足够多次时,每个元素都会以同样的频率出现在每个位置。换句话说,我们的网格对于每个组合都应该具有相同的计数。

我在 PHP 7.0(当 shuffle() 使用旧的 rand() 实现时)和 7.1(当它使用 mt_rand() 实现时)运行相同的代码并可视化结果(脚本还包括 in the gist)。图像的列表示数组中的 100 个元素,行表示它们最终可能出现的不同位置。蓝色像素表示发生频率低于预期的组合,红色像素表示发生频率高于预期的组合。

对于 PHP 7.1,图像看起来大部分是黑色的,因为所有组合中的数字非常接近完全相等:

然而,对于 PHP 7.0,有一些非常明显的“热”和“冷”区域!

因此,在旧算法中,某些交换发生的可能性要小得多。这些偏差可能恰好与您放置物品的顺序相吻合,从而降低了某些字母在游戏中特定时间出现的可能性。

如果你想调整你的随机播放怎么办?

新算法应该更准确地反映真实游戏中会发生的情况 - 所有字母在游戏中的任何时候出现的机会均等。

但也许您发现“操纵”订单更有趣,这样您就不会连续多次收到相同的字母,或者您可能会混合使用辅音和元音。

您可以通过重新实现随机播放算法,然后调整某些事情发生的概率来实现。

算法的核心是这样的(基本上7.1中改变的是RAND_RANGE的定义):

while (--n_left) {
    RAND_RANGE(rnd_idx, 0, n_left, PHP_RAND_MAX);
    if (rnd_idx != n_left) {
        temp = hash->arData[n_left];
        hash->arData[n_left] = hash->arData[rnd_idx];
        hash->arData[rnd_idx] = temp;
    }
}

正如您所期望的那样,这会一个接一个地随机选择项目(但从最后一个到第一个,因为它恰好更容易),但是为了节省每次“删除”一个项目时重新分配数组,它使用了一个巧妙的技巧:它交换数组的当前元素与数组中较早的随机元素。

在 PHP 代码中,它看起来像这样(未经测试!):

$n_left = count($array);
while (--$n_left) {
    $rnd_idx = mt_rand(0, $n_left);
    // The if statement is just skipping the swap if our random choice is for it to stay where it is
    if ($rnd_idx != $n_left) {
        $temp = $array[$n_left];
        $array[$n_left] = $array[$rnd_idx];
        $array[$rnd_idx] = $temp;
    }
}

要为此添加偏见,您只需更改选择$rnd_idx 的方式。例如,您可以说如果选择的字母 ($array[$rnd_idx]) 与之前选择的字母相同,请再次选择。你可以有一个完整的函数来决定它有多“好”,并以这种方式引入偏见。

由于“有趣”是主观的,无法通过数学定义,因此您只需测试一堆东西,看看您喜欢哪个结果。

【讨论】:

  • 这些图形以任何示例代码都无法做到的方式说明了这一点。太棒了!
【解决方案2】:

您可以使用适合加密使用的random_int 函数。 然后你可以使用 Fisher-Yates (aka Knuth) Shuffle 算法的实现:

function shuffle($array) {
    $random_array = array();
    $countArray = count($array) - 1;

    while (count($array) != 0) {
        $randomValue = random_int(0, $countArray);
        $random_array[] = $array[$randomValue];

        if(($randomValue + 1) == count($array)) {
            array_splice($array, $randomValue, ($randomValue - $countArray + 1));
        } else {
            array_splice($array, $randomValue, ($randomValue - $countArray));
        }

        $countArray--;
    }

    return $random_array;
}

【讨论】:

  • 正如我在 cmets 中所说,新结果几乎肯定是 更多 随机的,而不是更少,因此这很可能给出问题所考虑的相同模式“坏”。
  • 查看我的答案以确认旧算法的随机性远低于新算法。另请注意,PHP 中的 shuffle 算法比这更有效,因为它使用元素交换(有点像冒泡排序)而不是重复拼接。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-07-02
  • 2021-08-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多