程序员入伙书——换还是不换
- 2016年05月7日
话说,这道题,十几年前,玩论坛的时代,就出现在网上了:
三道门,一道门后面有车,另外两道门,后面都是羊。
有个节目主持人,他知道车在那道门后。
他请你选一道门,但不立即打开。
他在剩下的两道门里,打开一道,放出一只羊给你看。
现在台上还剩两道门关着,一道是你选的,一道是谁也没碰过的。
主持人说:现在你有个机会,可以改变主意,挑另一道门。
不管你换还是不换,你最终选中的那道门,后头的东西都归你。
你换不换?——假设价值观是车比羊好。
当时,论坛上吵成一片,有说该换的,有说不该换的。对于不换而中奖的概率,大家比较一致:1/3。而对于换而中奖的概率,基本上有三个说法:1/3、1/2、2/3。
论坛上贴题的那人说:“正确答案是应该换,中奖概率会从1/3提升到2/3。并且,此答案经过了智商排名极靠前的一位人士的认可。不过,还是有许多不愿盲信权威的年轻人,自己动手,用各种办法做实验,有大活人亲自做的,有用计算机模拟的,而结果一致确认:该换,中奖概率确实提升到了2/3。另外,虽然实验结果如此,有相当多的亲手做实验的人依然坚持(理论上)不该换。”
时至今日,这道题提到人口稠密的社交媒体上来,依然能够引起争论。
为了让读者老爷们怀着轻松的心情观看余下的内容,我提前把答案确认一下:
应该换。
我不是从一道门换到另一道门,
而是从一道门换到另外两道门。
“换并且选到车”的概率 = “第一次没选到车”的概率
三道门的迷惑性较大,读者可以极端化一下:如果是一亿道门,你挑一扇(选中车的概率很悲观),然后主持人把许许多多门打开,放出来满坑满谷的羊,最后只留下一道门,你换不换?
或者再换一个思路:主持人挑你和另外一个现场观众上来,让你选一道门,让那位观众选两道门,然后问你,愿不愿意拿你的一道门换那位观众的两道门?
“开门放羊”只是个迷惑动作,当门的总数大于2时,挑剩下的门里,一定能放出至少一只羊。所以它没有增加任何信息,也没有改变概率分布,这一步骤和事后开奖并无不同。
用概率论的运算过程则是:
- 换而中车的概率 =
首次选羊的概率 × 拿羊换到车的概率 +
首次选车的概率 × 拿车换到车的概率 - 首次选羊的概率 = 2/3
- 首次选车的概率 = 1/3
- 拿羊换到车的概率 = 100%
- 拿车换到车的概率 = 0%
- 因此,换而中车的概率 = 2/3
如果你看到这里还是觉得数学上不该换,那就可以关掉页面了。接下来并没有理论上的新内容,而且因为要用程序表达,技术上会比较烧脑。我不想让你同时背两个包袱走路,会芯片过热的。
虽然这道题没有数学理论难度,我还是想拿它做个编程的例子。原因是,我发现用计算机的思维,可以洞穿这道题的本质。做完实验依然坚持不该换的人里面,一定没有用计算机模拟的。
我的第一版程序很简单(手机读者可用手指左右拖动代码区):
import random def lottery(n, change): # n是门的个数,change设置“换还是不换” car = random.randint(0, n - 1) # 为车生成一个随机的门号 pick = random.randint(0, n - 1) # 我选择一个随机的门号 # 如果我未选中车但愿意换,或者我选中了车并且不换 if (pick != car and change) or (pick == car and not change): return True # 我都会中奖 else: # 否则 return False # 就不会 repeat = 100 # 重复做此实验100次 win = 0 # “中奖”计数器置零 for i in range(repeat): if lottery(3, True): # 每次都选择“换” win += 1 # 如果中奖,计数器加一 # 打印中奖百分比 print("%.2f%%" % (win / repeat * 100))
结果如何呢?运行三遍:
=============== RESTART: cargoat.py =============== 68.00% >>> =============== RESTART: cargoat.py =============== 71.00% >>> =============== RESTART: cargoat.py =============== 68.00% >>>
100次实验,如果选择“换”,中奖次数确实在2/3左右。当然我们也得反过来做一下,把lottery函数的第二个参数设为False:
repeat = 100 # 重复做此实验100次 win = 0 # “中奖”计数器置零 for i in range(repeat): if lottery(3, False): # 每次都选择“不换” win += 1 # 如果中奖,计数器加一 # 打印中奖百分比 print("%.2f%%" % (win / repeat * 100))
运行三遍,选择“不换”的中奖概率确实较低:
=============== RESTART: cargoat.py =============== 34.00% >>> =============== RESTART: cargoat.py =============== 34.00% >>> =============== RESTART: cargoat.py =============== 27.00% >>>
还可以发现,实验的重复次数(repeat的值)越高,中奖概率越是稳定在2/3上,下面是repeat = 100000的三遍运行结果:
=============== RESTART: cargoat.py =============== 66.55% >>> =============== RESTART: cargoat.py =============== 66.81% >>> =============== RESTART: cargoat.py =============== 66.70% >>>
在这个程序里,原本热热闹闹的挑门、开门放羊、问你换不换的那些动作,一下子被简化成了“你第一次猜中的概率有多大”的问题。我刚才说:“做完实验依然坚持不该换的人里面,一定没有用计算机模拟的。”是因为写程序前,我先假装认为不该换,而写下lottery函数的前两行时,就找到了事情本质的描述方式(上面的粗大字体)。
然而,读者可能认为,我并没有精确地按照剧本来写,所以运算结果不可信。为此,我必须按照剧本写一遍程序,像宋小宝吃面那样,辣根和蒜瓣这些步骤都不能省。说实在的,因为需要彻底改变思路,写新程序很累,但我还是勉力完成了:
猛击阅读全文