三门问题
[TOC]
问题介绍
起源
三门问题(Monty Hall problem)亦称为蒙提霍尔问题、蒙特霍问题或蒙提霍尔悖论,大致出自美国的电视游戏节目Let's Make a Deal。问题名字来自该节目的主持人蒙提·霍尔(Monty Hall)。
描述
参赛者会看见三扇关闭了的门,其中一扇的后面有一辆汽车,另外两扇门后面则各藏有一只山羊。选中后面有车的那扇门可赢得该汽车。当参赛者选定了一扇门,但未去开启它的时候,节目主持人开启剩下两扇门中的一扇,露出其中的山羊。随后,主持人会问参赛者要不要改选另一扇仍然关上的门。
问题
改变选择是否会增加参赛者赢得汽车的机率?
错误的理解
这个问题之所以迷人,是因为无论回答会或者不会的人,往往都给出了错误的解释,下面列出了两种常见的错误解释。
应该改选
理由是,
第一次三选一,主持人排除一扇门后变为二选一,所以应该改选。
这种观点也就认为,改选后选中的概率为.
不应该改选
理由是,
在主持人排除一扇门后,情况已经变成二选一,此时无论是否改选,选中的概率都自动提升为.
这种观点认为,无论是否改选,选中的概率都为.
实验
两种观点似乎都有道理,不如用计算机仿真程序实验一下。
问题建模
仿真程序的流程大致是先利用随机数生成器模拟参赛选手的选择过程,记录仿真结果,再统计中奖频率来估计概率。
仿真过程的结构是一个大循环,重复模拟结果、记录结果。考虑到我们需要比较不同参赛策略的胜率,应该将建模游戏规则(环境)和参赛选手(策略)的代码解耦合,即分别对环境和智能体建模。
环境
对环境(主持人和门)建模:
class Evn:
def __init__(self, doors=['', '', '']):
self.doors = list(doors)
def is_get_car(self, choice):
return self.doors[choice] == ''
def give_suggest(self, choice):
door_range = list(range(len(self.doors)))
door_range.remove(self.doors.index(''))
if choice in door_range:
door_range.remove(choice)
return random.choice(door_range)
环境向智能体提供两种服务:
- 为选择给出结果(是否中奖)
- 为选择给出建议(排除某扇门)
选手
对选手建模:
class Contestant:
def __init__(self, strategy, door_number=3):
self.door_number = door_number
self.strategy = strategy
self.last_choice = 0
def make_choice(self, suggest=None):
if suggest is not None:
return self.strategy(suggest, self.last_choice, self.door_number)
else:
self.last_choice = random.randrange(self.door_number)
return self.last_choice
选手根据环境中门的数量和所选策略,有如下行为:
- 做出初始选择
- 根据建议再次选择
注意,上文给出的代码把这两种行为集成在make_choice
方法中了。
策略
所谓策略,就是根据建议、上次的选择、门的数量(选择范围)做出这次的选择。
-
听从建议改变选择的策略
def strategy_change(suggest, last_choice, door_number): doors = list(range(door_number)) doors.remove(suggest) doors.remove(last_choice) return random.choice(doors)
-
不改变选择
def strategy_not_change(suggest, last_choice, door_number): return last_choice
-
参考建议,在剩下的选项中随机选择
def strategy_random_change(suggest, last_choice, door_number): doors = list(range(door_number)) doors.remove(suggest) return random.choice(doors)
随后的流程就是要对比这三种策略的胜率。
流程
对每种策略多次实验,记录结果:
iter_number = 100000
strategies = [strategy_change, strategy_not_change, strategy_random_change]
evn = Evn()
records = {}
for strategy in strategies:
for i in range(iter_number):
contestant = Contestant(strategy)
contestant.make_choice()
suggest = evn.give_suggest(contestant.last_choice)
new_choice = contestant.make_choice(suggest)
if evn.is_get_car(new_choice):
records[strategy] = records.setdefault(strategy, 0) + 1
# 显示每种方法的胜率
for s in records:
print(s.__name__, records[s] / iter_number)
每种策略实验十万次,统计中奖比例。
实验结果
策略 | 胜率 | 近似于 |
---|---|---|
改变选择 | 0.66334 | |
不改变选择 | 0.33294 | |
随机改变选择 | 0.50069 |
看来,最初提到的两种解释都是错误的:改选后胜率为而不是;不改选胜率保持,并不会自动提升。
思考
关于这个结果的标准概率分析容易找到,这里不再赘述。
最困扰我的一点其实是,为什么在主持人排除一个错误选项之后,第一次选项的概率没有提升(从实验结果上看,被排除选项的概率一点也没有加在选手初次的选择上,全部加在了另一个选项上)?毕竟,在获知一个选项被排除后,问题就变成从两扇门里选一个有车的了。
破局关键
关键就在于,主持人给出的建议和选手初次选择是有关系的。
这点可以从代码里清楚地看到,Evn
类的give_suggest
方法有choice
这个参数。换句话说,主持人排除的这个选项不是从所有不中奖的门里选的,而是更进一步,不包括选手第一次选择的门。
信息是不确定性的消除。主持人开门是信息,得到这个信息后,每扇门后有车的概率可能有所改变(具体计算参考这篇用贝叶斯公式分析三门问题)。而在这个问题中,由于主持人排除方法的特殊性(在剩下的门中排除),导致信息增量全部落在了另一扇门上。
附录
全部实验代码:
import random
class Evn:
def __init__(self, doors=['', '', '']):
self.doors = list(doors)
def is_get_car(self, choice):
return self.doors[choice] == ''
def give_suggest(self, choice):
door_range = list(range(len(self.doors)))
door_range.remove(self.doors.index(''))
if choice in door_range:
door_range.remove(choice)
return random.choice(door_range)
class Contestant:
def __init__(self, strategy, door_number=3):
self.door_number = door_number
self.strategy = strategy
self.last_choice = 0
def make_choice(self, suggest=None):
if suggest is not None:
return self.strategy(suggest, self.last_choice, self.door_number)
else:
self.last_choice = random.randrange(self.door_number)
return self.last_choice
def strategy_change(suggest, last_choice, door_number):
doors = list(range(door_number))
doors.remove(suggest)
doors.remove(last_choice)
return random.choice(doors)
def strategy_not_change(suggest, last_choice, door_number):
return last_choice
def strategy_random_change(suggest, last_choice, door_number):
doors = list(range(door_number))
doors.remove(suggest)
return random.choice(doors)
iter_number = 100000
strategies = [strategy_change, strategy_not_change, strategy_random_change]
evn = Evn()
records = {}
for strategy in strategies:
for i in range(iter_number):
contestant = Contestant(strategy)
contestant.make_choice()
suggest = evn.give_suggest(contestant.last_choice)
new_choice = contestant.make_choice(suggest)
if evn.is_get_car(new_choice):
records[strategy] = records.setdefault(strategy, 0) + 1
for s in records:
print(s.__name__, records[s] / iter_number)