神奇的“三扇门” —— 用Python模拟搞定经典问题

教育   教育   2024-01-24 11:45   加拿大  

前几天发布了 “用VBA模拟14连号” 这篇文章后,很多同学都对蒙特卡洛分析产生了兴趣。比如在留言区中,就有朋友提到,可以用它来模拟著名的概率问题“三门问题”。

正好上次讲完VBA后,杨老师也想演示一下怎样使用Python实现蒙特卡洛分析。所以今天咱们就借这个话题,再来一次Python版的随机模拟,顺便也感受一下概率论中各种反直觉的神奇现象。

  1. 1.     什么是“三门问题”


“三门问题”出自1970年代美国电视主持人蒙提霍尔(Monty Hall)主持的一个游戏节目,因此它的正式名称就是“蒙提霍尔问题”。这个游戏整体上就是一次简单的抽奖:舞台上摆好3扇门,其中一扇背后藏着汽车,另外两扇后面各藏着一只山羊;然后让参赛者选中一个门,如果门后是汽车,就算是赢得抽奖,反之如果是山羊,则输掉游戏。

显然,如果游戏规则仅止于此,那么大家都会轻松得出结论:既然3个门中有1个是车,那么参赛者的中奖概率就是 1/3,或说33.33%

然而这个游戏的有趣之处在于,主持人不甘寂寞地给自己加了点戏,于是具体过程变成了下面这样:

(1)舞台上出现3个门,参赛者看不到门后有什么,但是主持人能够清楚看见每扇门后的物品


(2)参赛者选中一扇门,但此时门并不会打开,而是等待主持人的动作

(3)因为有2只羊,所以不管参赛者选了哪个门,剩下的两扇门一定是两只羊或一羊一车。于是主持人在剩下的两扇门中,选一个背后是羊的门并打开。假如两个门后面都是羊,就随机打开一个。

(4)这时参赛者看到:三扇门中有一扇已经打开,里面站着一只羊;还有两扇门仍然关闭,其中有一扇是自己之前选中的。此时主持人提问:“你是否想更改答案,重新选择另一扇门?”

大多数人都会直觉地认为“没必要修改答案” —— 虽然主持人排除了一个“羊门”,但对于剩余的两个门来说,藏有汽车的概率仍然是五五开,所以换不换答案都一样。从这个角度讲,无论参赛者是否修改答案,最终中奖的概率似乎都是33%。

然而如果抛开直觉,老老实实地按照高中数学的解题过程去推导,我们却会得到一个令人震惊的结论:修改答案、概率翻番!鉴于“每增加一个公式,读者就会减少一半”,杨老师就用文字方式简单说明这个推理过程:

(1)第一次选择时,参赛者有1/3的概率选中汽车。此时其余两个门一定全都是羊;接下来主持人从其余两个门中排除一只羊后,最终剩下的那个门一定也是羊。所以这种情况下,参赛者修改答案后必然失败

(2)第一次选择时,参赛者有2/3的概率选中羊。此时其余两个门一定是一羊一车;接下来主持人从中排除掉羊,于是最终剩下的那个门一定是车。所以在这种情况下,参赛者修改答案后必然胜利

总结一下:参赛者有1/3的概率在第一次选中汽车,此时修改答案必然失败;同样他有2/3的概率在第一次选中羊,此时修改答案必然胜利。所以综合来看,修改答案的胜利概率是 2/3,也就是66.67%

推理过程似乎很简单,但是学过概率论的同学都体会过:概率问题最可怕的地方就是“经不起琢磨”,或者说:越想越乱!就比如这个三门问题,很多朋友看到答案后,反而思考出各种不同的理论。随便在知乎上搜索一下,就可以看到各种热烈的讨论:

那么到底谁说的有道理?修改答案后的成功概率,到底是50%还是66%呢?事实胜于雄辩,咱们亲自试验!

  1. 2.     Python随机模拟的关键技巧


在上一篇文章 不懂数学,咱还不懂VBA么?—— 用“蒙特卡洛分析”搞定概率问题 中提到使用Python做随机模拟,要比VBA容易很多。因为Python自带的random模块非常强大,不仅提供了生成随机数的方法,而且还提供了“随机抽样”方法sample()、“随机洗牌”方法 shuffle(),以及从列表中随机抽取一个元素的choice() 等等。详见《全民一起玩Python 提高篇》第12回的讲解。

(1)使用shuffle随机布置三个门

仍然使用上篇文章中介绍的“随机洗牌”方法,也就是先生成一个数组(列表),初始内容为 [“车”,“羊”,“羊”],代表全部三个门;然后随机交换任意两个元素的位置,最终得到一个随机排列的列表比如[“羊”,“车”,“羊”] ,也就是最终摆到舞台上的方案。

不过对于随机交换功能,这一次我们不再需要编写复杂的代码,因为 random.shuffle( a ) 就可以把列表 a 随机乱排:

所以这里我们只需写下两行代码:

(2)模拟参赛者选择

调用 random库的randint(0,2),就可以生成0、1、2三者之一的随机整数。用这个速记整数代表门牌号,我们就模拟出了“参赛者选取某个门”的环节。

(3)模拟主持人的选择

按照规则,参赛者选中一个门之后,主持人要在剩下的两个门中选出一个“羊门”并打开。本质上,这个操作与参赛者相同,也是生成一个0、1、2之间的随机整数。但是麻烦的地方在于,主持人选中的门既不能是参赛者选中的那个、也不能是有汽车的那个。

所以在选中一个门之后,我们还要根据这些规则做一个判断:假如主持人选中的门牌号与参赛者相同或者背后藏着汽车,就重新抽取;然后再次判断,直到符合条件为止。对应的代码,就是下面这个while循环

理论上讲,这个while结构存在陷入“死循环”的可能,也就是每次生成的随机数都不符合条件,直到天荒地老。如果想避免这种风险,我们可以使用其他方式(比如用remove或pop方法从列表中删掉参赛者已选门牌)。

然而本案例一共只有3个门,所以对于每次循环生成的随机数,不符合条件的概率只有2/3。对应的,连续10次都不符合条件的概率就是 (2/3)的10次幂,也就是1.8%;而连续20次都不符合条件的概率则是万分之三 …… 。由此可见,我们根本不用担心出现死循环,可以放心使用这段代码,毕竟这种while循环的写法最是直观易懂。

(4)模拟“更改答案”

在参赛者和主持人都做出选择后,最终剩下的那个门牌号,就是参赛者想要更改的最终答案。然而在程序中,我们怎样得到这个门牌号呢?

这里杨老师用了一个简洁但略微有点“绕弯”的技巧:用门牌号的总和减去已选中的门牌号

举个例子:我们知道三个门牌号分别是0、1、2。那么假设用户选中了1、主持人选中了0,公式(0+1+2)- 1- 0 就会得到2 ,也就是两人都没有选中的那个门。同样,假设两个人选中的分别是2和1,那么(0+1+2)- 2 – 1 就会得到0,仍然是没有选中的门。是不是很有趣呢?

这个技巧其实也蕴含了深刻的道理,甚至可以与信息论挂上钩,改天杨老师再跟大家细聊。总之最后这几行代码,就可以得到最终参赛者更改的门牌号:

3.完整代码与模拟结果

把上面的过程写成函数,然后在主程序中循环运行10万次,同时用变量 win_times 统计抽中汽车的次数,就可以得到完整的代码:

接下来就是惬意的试验时间!随手运行十几次(每次试验10万轮),结果如下:

根据概率论中著名的大数定理,这些试验结果(0.6642、0.66888 …… )的平均值,可以被视作客观真实结论。所以我们算一下,答案真的是 0.666 !

那么假如参赛者执守初心、拒绝更改答案,最终概率又会如何呢?我们只要修改模拟函数的最后一句,把最终答案 final_door 改为用户一开始选择的随机数 user_choose ,就可以同样模拟出结果,中奖概率只有 0.333 :

总之,模拟结果显示:如果参赛者中途更改答案,中奖概率确实会从0.33提高到0.66!程序模拟与概率计算结论一致(证毕)。

当然,也不是不可以再杠一下 ……


杨氏在线教学
由杨洋博士主持,专注制作高品质教学视频,以清晰简洁、生动有趣的教学风格,普及推广Python、VBA、SQL等各类实用计算机技术。 官方网址请见:https://www.ukoedu.com
 最新文章