PHP 中的 Monty Hall 问题

蒙蒂霍尔问题是概率数学中的一个反直觉问题,它涉及从一组三扇门中挑选正确的奖品。这个问题以电视名人蒙蒂·霍尔命名,大致基于美国游戏节目让我们做个交易。

这已成为编程中的一个流行问题,因为它是思考问题以证明实际发生的结果的一个很好的练习。网上已经发布了很多例子,所以我想我会坐下来尝试自己解决它。该问题最常见的总结如下(此示例取自 Rosetta Code):

假设您正在参加一个游戏节目,并且您可以选择三个门。一扇门后面是一辆汽车;在其他人后面,山羊。演出前,汽车和山羊被随机放置在门后。
游戏展示规则如下:
选择门后,门暂时保持关闭状态。知道门后是什么的游戏节目主持人蒙蒂霍尔现在必须打开剩下的两扇门之一,而他打开的门后面肯定有一只山羊。如果剩下的两扇门后面都有山羊,他随机选择一扇。Monty Hall 用山羊打开一扇门后,他会要求您决定是留在您的第一选择还是切换到剩下的最后一扇门。
例如:
假设您选择了 1 号门,主人打开了 3 号门,里面有一只山羊。然后他问你“你想切换到 2 号门吗?” 改变你的选择对你有利吗?
请注意,玩家最初可以选择三扇门中的任何一扇(不仅仅是门 1),主持人打开另一扇门露出山羊(不一定是门 3),并且他让玩家在剩下的两扇未打开的门之间进行第二个选择门。
为每个策略使用三个门模拟至少一千个游戏,并以易于比较每个策略效果的方式显示结果。

与其仅仅发布一段代码,我还以为我会通过我采取的步骤来生成使用每种策略赢得的汽车数量。

我首先需要的是一个地方来存储每场比赛的结果。这意味着创建一个数组,以便我可以存储为 switch 和 stick 策略找到了多少山羊和汽车。

// 设置初始游戏结果表
$results = [
  'stick' => [
    'goat' => 0,
    'car' => 0,
  ],
  'switch' => [
    'goat' => 0,
    'car' => 0,
  ],
];

在创建循环以测试大量游戏之前,我决定为每种策略创建一个游戏。要做的第一件事是将游戏设置为三扇门,其中两扇门包含山羊,一扇门包含汽车。创建了一个简单的数组来存储这些信息,然后将门打乱以创造一定程度的机会。

// 设置游戏
$doors = array_fill(1, 3, 'goat');
$doors[array_rand($doors)] = 'car';
shuffle($doors);

参赛者在这个游戏中做的第一件事就是选一扇门。这是使用该array_rand()函数完成的,该函数从 Doors 数组返回一个元素键。

// 参赛者选择一扇门
$pick = array_rand($doors);

选择第一扇门后,Monty Hall 需要打开其他不是汽车的门。一个简单的循环用于遍历门阵列并挑选出第一扇门,该门是山羊并且还没有被参赛者挑选。我们通过简单地从门阵列中移除门来模拟这一点。

// Monty Hall 展示了一扇参赛者没有选择的门,里面有一只山羊。
foreach ($doors as $id => $door) {
  if ($door == 'goat' && $id != $pick) {
    unset($doors[$id]); break;
  }
}

现在我们来到参赛者选择坚持他们选择的门或选择另一扇门的部分,这就是结果数组进入的地方。 由于结果数组与门数组的值具有相同的键,我们只需使用参赛者的选择选择门元素并增加结果数组中的计数。我们可以在同一场比赛中同时执行这两种策略,但必须先执行棒策略。记录棒策略的结果很简单。

// 坚持同一个门
$results['stick'][$doors[$pick]]++;

切换策略略有不同。我们知道我们想要选择 door 数组中的另一个元素(记住我们之前删除了一个)但我们不能确定那个元素的键是什么。因此,我们需要遍历 door 数组并选择与当前参赛者选择具有不同键的元素。然后将选择存储在结果数组中。

// 切换到另一扇门
foreach ($doors as $id => $door) {
  if ($id != $pick) {
    $pick = $id; break;
  }
}
$results['switch'][$doors[$pick]]++;

到目前为止,我们所做的是模拟单个游戏并记录每个策略发生的结果。下一步是创建一个简单的循环,让游戏运行 100 万次。需要足够多的游戏来提高我们最终得到的结果的准确性。以下是包含在循环中的上述详细步骤。

// 双重迭代作为策略单独完成。
$iterations = 1000000;
for ($i = 0; $i < $iterations; ++$i) {
  // 设置游戏
  $doors = array_fill(1, 3, 'goat');
  $doors[array_rand($doors)] = 'car';
  shuffle($doors);
 
  // 参赛者选择一扇门
  $pick = array_rand($doors);
 
  // Monty Hall 展示了一扇参赛者没有选择的门,里面有一只山羊。
  foreach ($doors as $id => $door) {
    if ($door == 'goat' && $id != $pick) {
      unset($doors[$id]);
      break;
    }
  }
  // 坚持同一个门
  $results['stick'][$doors[$pick]]++;
  // 切换到另一扇门
  foreach ($doors as $id => $door) {
    if ($id != $pick) {
      $pick = $id;
      break;
    }
  }
  $results['switch'][$doors[$pick]]++;
}

在运行游戏一百万次之后,我们现在可以计算出每种策略的比较情况。这涉及对摇杆和转换策略的简单百分比计算。

// 打印结果
$string = '';
$string .= 'Stick: ' . ($results['stick']['car'] / $iterations) * 100 . '%' . PHP_EOL;
$string .= 'Switch: ' . ($results['switch']['car'] / $iterations) * 100 . '%' . PHP_EOL;
print $string;

这将打印以下内容:

Stick: 33.3427%
Switch: 66.6573%

所以我们在这里可以看到,最好的策略是切换门,这证实了概率计算的结果。如果您还没有发现,那么 Monty Hall 问题的答案是始终切换车门,因为有 33% 的机会获得汽车。

这里缺少的一件事是需要以一种非常清楚哪种策略是最好的方式来显示结果。为此,我使用了之前运行过的相同百分比计算,但将它们传递给一个str_repeat()函数,以便每个策略的百分比由一行星星表示。使用嵌套循环来打印为每个策略找到的山羊数量和汽车数量的结果。

$string = '';
foreach ($results as $strategy => $result) {
  $string .= PHP_EOL . $strategy . PHP_EOL;
  foreach ($result as $prize => $numbers) {
    $string .= $prize . "\t" . str_repeat('*', ($numbers / $iterations) * 100) . PHP_EOL;
  }
}
print $string;

这将打印出以下内容:

stick
goat ****************************************************************** 
car *********************************
 
switch
goat *********************************
car ******************************************************************

这个简单的图表非常清楚地表明转换策略比坚持策略更成功。我确信可以通过抽象重复功能的某些部分来稍微改进这段代码,但我只是想让事情变得简单并生成正确的结果。在研究 Monty Hall 问题的过程中,我还发现很多人都在试验问题中存在的门的数量,并且经常让用户最终选择两扇门。通过更改初始值可以轻松更改门的数量array_fill()函数调用并将第二个参数设置为更大的参数,但需要更多的工作来移除除参赛者当前门和后面有汽车的门之外的所有内容。我还没有研究这如何影响情况的结果,但我把它留给用户作为练习。

这里不是将这些代码拼凑在一起,而是上面的完整代码。随意玩弄它。

// 设置初始游戏结果表
$results = [
  'stick' => [
    'goat' => 0,
    'car' => 0,
  ],
  'switch' => [
    'goat' => 0,
    'car' => 0,
  ],
];
 
$iterations = 1000000;
for ($i = 0; $i < $iterations; ++$i) {
  // 设置游戏
  $doors = array_fill(1, 3, 'goat');
  $doors[array_rand($doors)] = 'car';
  shuffle($doors);
 
  // 参赛者选择一扇门
  $pick = array_rand($doors);
 
  // Monty Hall 展示了一扇参赛者没有选择的门,里面有一只山羊。
  foreach ($doors as $id => $door) {
    if ($door == 'goat' && $id != $pick) {
      unset($doors[$id]);
      break;
    }
  }
 
  // 坚持同一个门
  $results['stick'][$doors[$pick]]++;
 
  // 切换到另一扇门
  foreach ($doors as $id => $door) {
    if ($id != $pick) {
      $pick = $id; break;
    }
  }
  $results['switch'][$doors[$pick]]++;
}
 
// 打印结果
$string = '';
$string .= 'Stick: ' . ($results['stick']['car'] / $iterations) * 100 . '%' . PHP_EOL;
$string .= 'Switch: ' . ($results['switch']['car'] / $iterations) * 100 . '%' . PHP_EOL;
 
foreach ($results as $strategy => $result) {
  $string .= PHP_EOL . $strategy . PHP_EOL;
    foreach ($result as $prize => $numbers) {
      $string .= $prize . "\t" . str_repeat('*', ($numbers / $iterations) * 100) . PHP_EOL;
    }
}
print $string;