用 TypeScript 验证三门问题:为什么换门胜率是 2/3?

问题速览

三扇门,1 车 2 羊。你选 A,知情 主持人打开一扇羊门 B,问换不换?直觉 50%,实际 换门胜率 2/3


直接上代码

typescript 复制代码
// 三门问题模拟器
// 运行:npx ts-node montyHall.ts

interface GameResult {
  myChoice: number;      // 我的初始选择
  hostOpens: number;     // 主持人打开的门
  switchTo: number;      // 换门后的选择
  initialWin: boolean;    // 不换是否赢
  switchWin: boolean;     // 换门是否赢
}

function montyHallSimulation(): GameResult {
  const car = 0; // 车在 0 号门(固定布局,通过随机选择模拟不确定性)

  // 我随机选一扇门(0, 1, 2)
  const myChoice = Math.floor(Math.random() * 3);

  // 主持人知道答案,必须开一扇羊门,且不能是我选的
  // 找出所有可选的羊门:不是车,且不是我选的
  const availableGoats: number[] = [];
  for (let i = 0; i < 3; i++) {
    if (i !== car && i !== myChoice) {
      availableGoats.push(i);
    }
  }

  // 如果我选的是车,availableGoats 有 2 个(1 和 2),主持人随机开一个
  // 如果我选的是羊,availableGoats 只有 1 个(另一个羊),主持人被迫开它
  const hostOpens = availableGoats[Math.floor(Math.random() * availableGoats.length)];

  // 换门:选既不是我的也不是主持人开的
  const switchTo = [0, 1, 2].find(i => i !== myChoice && i !== hostOpens)!;

  return {
    myChoice,
    hostOpens,
    switchTo,
    initialWin: myChoice === car,
    switchWin: switchTo === car
  };
}

// 跑 10 万次
const ITERATIONS = 100000;
let switchWins = 0;
let stayWins = 0;

for (let i = 0; i < ITERATIONS; i++) {
  const result = montyHallSimulation();
  if (result.switchWin) switchWins++;
  if (result.initialWin) stayWins++;
}

console.log(`迭代次数: ${ITERATIONS}`);
console.log(`换门胜率: ${(switchWins / ITERATIONS).toFixed(4)} (${switchWins})`);
console.log(`不换胜率: ${(stayWins / ITERATIONS).toFixed(4)} (${stayWins})`);
console.log(`胜率比: 换门 / 不换 = ${(switchWins / stayWins).toFixed(2)}`);

// 额外验证:初始选择分布
const choiceCount = [0, 0, 0];
for (let i = 0; i < ITERATIONS; i++) {
  const result = montyHallSimulation();
  choiceCount[result.myChoice]++;
}
console.log(`\n初始选择分布: [${choiceCount.join(', ')}]`);

运行输出

makefile 复制代码
迭代次数: 100000
换门胜率: 0.6671 (66710)
不换胜率: 0.3329 (33290)
胜率比: 换门 / 不换 = 2.00

初始选择分布: [33442, 33289, 33269]

代码输出解读:为什么是 0.666?

ini 复制代码
初始选择概率分布:
├─ 选 0 号门(车):概率 1/3 → 换门 = 输
├─ 选 1 号门(羊):概率 1/3 → 主持人被迫开 2 号 → 换到 0 号 = 赢
└─ 选 2 号门(羊):概率 1/3 → 主持人被迫开 1 号 → 换到 0 号 = 赢

P(换门赢) = 1/3 × 0 + 1/3 × 1 + 1/3 × 1 = 2/3 ≈ 0.6667

关键 :你初始选错的概率是 2/3 ,而选错时主持人没有选择权------他被迫指向唯一的车。这个"被迫"就是信息。


关键逻辑拆解:主持人"被迫"开门的条件分支

typescript 复制代码
// 核心逻辑可视化

// 情况 1:我选对了(概率 1/3)
if (myChoice === car) {
  // 主持人有选择权:B 和 C 都是羊,随机开一个
  // availableGoats = [1, 2]
  // hostOpens = random.choice([1, 2])
  // 换门结果 = 另一个羊 → 输
}

// 情况 2:我选错了(概率 2/3)
else {
  // 主持人没有选择权:只有一个羊可以开
  // availableGoats = [唯一的另一个羊]
  // hostOpens = 被迫指向那个羊
  // 换门结果 = 唯一剩下的车 → 赢
}

表面行为一致:无论对错,主持人都开羊门、都问换不换。

背后约束不同

  • 选对时 → 随机排除(无信息)
  • 选错时 → 被迫指向答案(高信息)

你的 2/3 初始错误概率 × 选错时换门必赢 = 2/3 总胜率


技术映射:文档 vs 源码(信息隐藏)

这和读第三方 SDK 源码时遇到的陷阱一模一样:

typescript 复制代码
// 表面 API:看起来一致
interface HostAPI {
  openDoor(myChoice: number): number; // 总是返回一个羊门
}

// 实际实现:约束条件完全不同
class InformedHost implements HostAPI {
  openDoor(myChoice: number): number {
    if (this.isCorrect(myChoice)) {
      // 我选对了:主持人在两个羊中随机
      return this.randomGoat();
    } else {
      // 我选错了:主持人被迫指向唯一的车(通过排除另一个羊)
      return this.forcedGoat(myChoice);
    }
  }
}

文档只暴露接口签名 ,但实现约束决定了信息结构。

读源码就是在读"知情排除"的规则------和三门问题完全同构。


变体代码对比:知情 vs 不知情主持人

版本 A:知情主持人(原题)→ 换门 2/3

typescript 复制代码
function informedHost(car: number, myChoice: number): number {
  // 知道答案,精准避开车
  const goats = [0, 1, 2].filter(i => i !== car && i !== myChoice);
  return goats[Math.floor(Math.random() * goats.length)];
}

版本 B:不知情主持人 → 换门 1/2

typescript 复制代码
function randomHost(car: number, myChoice: number): number | null {
  // 不知道答案,随机开一扇不是我选的
  const others = [0, 1, 2].filter(i => i !== myChoice);
  const opened = others[Math.floor(Math.random() * others.length)];

  // 如果开到车,游戏结束(这种情况要排除)
  if (opened === car) return null;
  return opened;
}

关键差异

维度 知情主持人 不知情主持人
availableGoats 长度 1 或 2(取决于我是否选对) 固定 2
是否可能开到车 不可能 可能(1/3 概率)
换门胜率 2/3 1/2
直觉是否正确 错误 正确

区别就在"故意"二字------知情排除携带信息,随机排除不携带。


一句话总结

主持人"总是做同样的事"是策略设计,不是随机噪声。他的一致性提问掩盖了不对称约束:你选错时他被迫替你指向答案。换门就是在系统性收割你 2/3 的初始错误概率


扩展:100 扇门版本

typescript 复制代码
function montyHall100Doors(): boolean {
  const totalDoors = 100;
  const car = 0;
  const myChoice = Math.floor(Math.random() * totalDoors);

  // 主持人从剩下的 99 扇中,精准避开唯一的车,打开 98 只羊
  const availableGoats = [0, 1, 2].filter(i => i !== car && i !== myChoice);
  // ... 压缩后只剩我的选择和另一扇门

  // 换门胜率 = 99/100
  return true;
}

极端情况让直觉修正更容易:你初始选错的概率 99/100,主持人帮你把 99 扇门压缩成 1 扇,那扇门几乎必然是车。


最近在重建概率认知体系,用代码验证每一个反直觉结论。

相关推荐
Yunzenn5 小时前
深度解析字节最新研究-Cola DLM 第 06 章:分块因果 DiT 先验 —— 在隐空间里做 Flow Matching
人工智能·算法·架构
HjhIron5 小时前
数组去重:从零开始,写一个靠谱的工具函数
算法·面试
阿黎梨梨5 小时前
#动态规划入门:从“爬楼梯”问题理解核心思想
算法
黎阳之光5 小时前
技术赋能智慧新能源|黎阳之光风电叶片光栅载荷+声纹AI智能监测技术落地应用
大数据·人工智能·物联网·算法·安全
逻辑君5 小时前
物理生物学研究报告【20260016】
人工智能·算法·物理
Brilliantwxx5 小时前
【C++】 手撕 AVLTree :从零实现自平衡二叉搜索树
开发语言·c++·笔记·算法
机器学习之心5 小时前
顶刊《KBS》算法应用,PIMO-Transformer-LSTM-ABKDE:投影迭代优化算法概率区间预测,报告+代码
算法·lstm·transformer·投影迭代优化算法
XWalnut5 小时前
LeetCode刷题 day20
java·算法·leetcode
努力努力再努力wz5 小时前
【Redis入门系列】:从 hashtable到 listpack:深入理解 Hash 底层编码、字段级过期、核心命令与缓存应用
开发语言·数据结构·数据库·c++·redis·算法·缓存