用Python模拟10000次,我彻底搞懂了那个反直觉的“三门问题”
用Python模拟10000次,我彻底搞懂了那个反直觉的“三门问题”
第一次听说三门问题时,我和大多数人一样坚信“换不换都一样”——直到我用代码模拟了上万次游戏过程。作为程序员,我们习惯用数据说话,这次我将带你用Python彻底拆解这个经典概率谜题,看看为什么直觉会欺骗我们。
1. 三门问题背后的数学原理
三门问题(Monty Hall问题)源自美国电视节目《Let's Make a Deal》,其反直觉特性曾让无数数学家和统计学家栽跟头。让我们先理清游戏规则:
- 三扇门后分别藏着两羊一车
- 玩家初始随机选择一扇门
- 主持人(知道门后情况)会打开一扇有山羊的未选门
- 玩家可选择坚持原选或更换到剩余未开门
- 最终揭晓是否赢得汽车
关键争议点在于:主持人行为是否改变了初始概率分布?让我们用集合论视角分析:
- 初始选择正确的概率:1/3
- 初始选择错误的概率:2/3(此时主持人被迫打开唯一剩下的山羊门)
# 概率分布可视化 import matplotlib.pyplot as plt labels = ['初始选择正确', '初始选择错误'] sizes = [1/3, 2/3] plt.pie(sizes, labels=labels, autopct='%1.1f%%') plt.title('初始选择概率分布') plt.show()当主持人打开一扇门后,情况发生了微妙变化:
| 初始选择 | 主持人行为 | 换门结果 | 不换结果 |
|---|---|---|---|
| 正确(1/3) | 随机开一羊 | 得羊 | 得车 |
| 错误(2/3) | 必须开特定羊门 | 得车 | 得羊 |
这个条件概率变化正是反直觉的核心——主持人提供的信息实际上浓缩了概率空间。
2. 构建Python模拟实验
让我们用面向对象的方式构建模拟器,确保代码可扩展性和可读性:
import random from collections import defaultdict class MontyHallSimulator: def __init__(self): self.doors = ['goat', 'goat', 'car'] random.shuffle(self.doors) def simulate(self, switch=False): choice = random.randint(0, 2) remaining_doors = [i for i in range(3) if i != choice] # 主持人必须打开有山羊的门 for door in remaining_doors: if self.doors[door] == 'goat': host_open = door break if switch: final_choice = [i for i in range(3) if i != choice and i != host_open][0] else: final_choice = choice return self.doors[final_choice] == 'car'测试单次模拟:
sim = MontyHallSimulator() print("换门获胜:" if sim.simulate(switch=True) else "换门失败") print("不换获胜:" if sim.simulate(switch=False) else "不换失败")3. 大规模模拟与结果分析
进行万次级模拟才能得到稳定统计结果:
def run_simulation(trials=10000): switch_wins = 0 stay_wins = 0 for _ in range(trials): sim = MontyHallSimulator() if sim.simulate(switch=True): switch_wins += 1 if sim.simulate(switch=False): stay_wins += 1 return switch_wins/trials, stay_wins/trials switch_prob, stay_prob = run_simulation() print(f"换门胜率: {switch_prob:.2%}") print(f"不换胜率: {stay_prob:.2%}")典型输出结果:
换门胜率: 66.72% 不换胜率: 33.28%用可视化对比更直观:
import numpy as np import matplotlib.pyplot as plt strategies = ['换门', '不换'] probabilities = [switch_prob, stay_prob] x = np.arange(len(strategies)) plt.bar(x, probabilities) plt.xticks(x, strategies) plt.ylabel('获胜概率') plt.title('不同策略获胜概率对比') plt.ylim(0, 1) for i, v in enumerate(probabilities): plt.text(i, v+0.02, f"{v:.2%}", ha='center") plt.show()4. 深入探讨与边界情况
4.1 主持人行为的影响
原始问题的关键前提常被忽视:
- 主持人必须打开有山羊的门
- 主持人知道门后情况
- 主持人不会打开玩家选择的门
如果改变这些规则,概率会如何变化?
| 主持人行为 | 换门胜率 | 数学解释 |
|---|---|---|
| 随机开门(可能开出门) | 50% | 变成传统条件概率问题 |
| 总是优先开特定编号门 | 66.6% | 保留原始问题特性 |
| 有时会故意开门 | 变化 | 引入博弈论因素 |
4.2 门数量扩展实验
增加门数量会放大概率差异:
def extended_simulation(n_doors=5, trials=10000): switch_wins = 0 stay_wins = 0 for _ in range(trials): doors = ['goat']*(n_doors-1) + ['car'] random.shuffle(doors) choice = random.randint(0, n_doors-1) # 主持人打开n_doors-2个山羊门 host_opens = [] for i in range(n_doors): if i != choice and doors[i] == 'goat' and len(host_opens) < n_doors-2: host_opens.append(i) if len(host_opens) < n_doors-2: continue # 无法满足主持人开门规则 remaining = [i for i in range(n_doors) if i != choice and i not in host_opens][0] # 测试换门策略 if doors[remaining] == 'car': switch_wins += 1 # 测试不换策略 if doors[choice] == 'car': stay_wins += 1 return switch_wins/trials, stay_wins/trials doors_range = range(3, 10) switch_probs = [] stay_probs = [] for n in doors_range: s, t = extended_simulation(n_doors=n) switch_probs.append(s) stay_probs.append(t) plt.plot(doors_range, switch_probs, label='换门') plt.plot(doors_range, stay_probs, label='不换') plt.xlabel('门数量') plt.ylabel('获胜概率') plt.title('不同门数量下的策略胜率') plt.legend() plt.grid() plt.show()4.3 心理学因素分析
为什么这个结果如此反直觉?人类认知存在几个盲点:
- 概率守恒谬误:认为概率必须始终平均分配
- 信息忽略:低估主持人行为提供的信息量
- 锚定效应:过度依赖初始选择
- 结果等效错觉:认为"剩下两门"等同于"随机二选一"
在代码仓库中,我还添加了交互式Jupyter notebook版本,可以实时调整参数观察概率变化。有个有趣的发现:当门数量增加到100扇,主持人打开98扇山羊门后,几乎所有人都会选择换门——这时直觉终于与数学达成一致。
