数值、随机与概率
IEEE 754、蒙特卡洛、信息熵与素数条纹
概率与数值常被当成两门课,在工程里却总撞在一起:浮点数决定算得准不准,随机决定算不算得出来。本文由八篇相关文章合并而成:IEEE 754 如何把实数装进 64 个比特,随机数在算法里的两种用法(蒙特卡洛与马尔可夫链),概率被精心设计的现场(抽卡保底、撒点实验、棋牌桌与桌游规则),信息熵如何给不确定性定价,最后以乌拉姆螺旋的素数条纹收尾。每个章节都能在站内的游戏与工具组件里动手验证。
IEEE 754:浮点数解剖
一切从 0.1 + 0.2 不等于 0.3 开始。这不是 JavaScript 的 bug,也不是哪门语言实现偷懒——Python、Java、C 里的 double 全都给出 0.30000000000000004。它是一套叫 IEEE 754 的标准把实数装进有限比特后的必然后果:只要看懂 0.1 在内存里到底长什么样,这个「反直觉」就变成了「顺理成章」。本章把双精度浮点数逐位拆开,看清舍入误差的产生位置,顺路认一认浮点家族的其他成员,最后把结论压成几条可以直接执行的工程守则。
动手版本:IEEE 754 可视化(单/双精度位布局)、位运算计算器、进制转换工具。
一个浮点数的解剖
64 个比特的分家方案
浮点数的名字来自科学计数法:把数写成 的形式,小数点位置随指数 浮动。
IEEE 754 的双精度格式(binary64,即 Java 的 double、JS 的 Number)把 64 个比特切成三段:
| 字段 | 位数 | 内容 |
|---|---|---|
| 符号 s | 1 | 0 正 1 负 |
| 阶码 E | 11 | 指数加偏置存储,偏置 1023 |
| 尾数 f | 52 | 规格化后的小数部分,52 位 |
解码公式只有一行:
这里有一个关键设计:隐藏位。
规格化的二进制数最高位必然是 1(就像十进制科学计数法最高位必然非零),所以这一位不必存储。52 位的尾数实际表达了 53 位有效数字。
单精度(binary64 的弟弟 binary32)则是 1 + 8 + 23 的切法,偏置 127,有效数字 24 位。IEEE 754 可视化里能逐位看到的就是这个单精度布局。
为什么用阶码而不是把 64 位全拿来当定点小数?因为动态范围。
定点数的网格是均匀的:能精确表示 ,就只能表示到 上下。而阶码把网格做成「离原点越远越稀疏」的对数分布。既能装下 ,也能装下 ,代价是任何一处精度都有限。
浮点数的全部怪癖都源于这个取舍:范围是买来的,精度是卖掉的。
三十秒亲手还原位模式
不满足于听讲的话,任何浏览器控制台都能把 0.1 的位模式现场拆出来。
技巧是借用类型化数组让同一块内存以两种视角被阅读:写进去的是 64 位浮点,读出来的是 64 位无符号整数。
const buf = new ArrayBuffer(8);new Float64Array(buf)[0] = 0.1;new BigUint64Array(buf)[0].toString(2).padStart(64, "0");// "0011111110111001100110011001100110011001100110011001100110011010"输出正好 64 位:第 1 位是符号 0;第 2 到 12 位是阶码字段 01111111011,读出 1019、减偏置得 −4;剩下 52 位是尾数,结尾的 1010 就是循环节 1001 被舍入进位的痕迹。
这个技巧在调试序列化精度问题时可以直接抄走,比读十六进制转储快得多。
把 0.1 拆开看
十进制小数转二进制的标准做法是乘 2 取整:对小数部分不断乘 2,整数位依次就是二进制各位。
对 0.1 操作一遍:
0.1 × 2 = 0.2 → 00.2 × 2 = 0.4 → 00.4 × 2 = 0.8 → 00.8 × 2 = 1.6 → 10.6 × 2 = 1.2 → 10.2 × 2 = 0.4 → 0 (回到起点,开始循环)余数从第二步起进入循环,所以:
这是无限循环小数,而尾数只有 52 位。
于是循环串在第五十三位处被截断,并按舍入规则进位一次,得到上面控制台里那串位模式。
把这块位模式按公式还原成十进制,0.1 的真实存储值是:
0.1000000000000000055511151231257827021181583404541015625所以严格地说,计算机里从来没有 0.1 这个数。
只有「离 0.1 最近、且能被 53 位二进制有效数字表达的那个数」,记作 。
0.2 和 0.3 同理,各自带着自己的偏差。
相比之下,0.5、0.25、0.75 都是精确的:它们是 形式的分数,二进制下天然有限(0.75 就是 0.11₂)。
一个可以背下来的判据:一个十进制分数能被浮点精确表示,当且仅当它约分后分母的质因数只含 2(比如 0.5、0.125、0.375)。
十进制里 0.2 能写成 1/5 的有限小数,但分母的 5 在二进制下无解。5 与 2 互质,乘 2 取整永远除不尽。
进制与表示能力的关系,用进制转换工具把同一个数在 2、8、10、16 进制间换来换去,体会会直接很多:换进制只改变书写,不改变那个数能不能被「写完」。
舍入:误差从哪里来
表示误差与运算误差
浮点误差有两个来源,分开看才看得清。
第一个是表示误差:实数是连续的,浮点数是离散的,两个相邻可表示数之间的间隙(单位 ulp)在 1 附近是 ,数越大间隙越宽。
凡是落不进网格的实数,存进去那一刻就带偏差。0.1 身上那份 5.5×10⁻¹⁸ 的偏差就来自这里。
第二个是运算误差:两个浮点数运算,数学上的精确结果大概率也落不进网格,硬件必须再舍入一次。
IEEE 754 的保证是单次运算精确舍入。结果等价于先算出无穷精度的真值、再舍入到网格。
这就是为什么每次加减乘除都可能引入半个 ulp 以内的误差,而误差会顺着计算链条累积。
0.1 + 0.2 的全过程
现在把 0.1 + 0.2 为什么不等于 0.3 的账算清楚。
加法硬件走三步:
**第一步:对阶。**两个操作数指数不同不能直接加尾数,要先把指数小的那个的尾数右移,对齐到大的指数。
的指数是 −4, 的指数是 −3,于是 0.1 的尾数右移一位。
右移把最低位挤了出去。这一位不会凭空消失,硬件把它记成「粘位」(sticky bit),供最后舍入使用。
精度从对阶这一刻就开始流失了。
**第二步:尾数相加。**对齐后的两个尾数相加,得到一个 54 位左右的中间和。
**第三步:规格化并舍入。**和的最高位进到了 ,于是整体右移归一,指数变 −2。紧接着按舍入模式把结果压回 53 位有效数字。
三步走完,得到的值是:
0.3000000000000000444089209850062616169452667236328125而 是:
0.299999999999999988897769753748434595763683319091796875两个数相差一个 ulp:一个从上往下逼近 0.3,一个从下往上。
用严格相等比较当然是 false。
也就是说 0.1 + 0.2 ≠ 0.3 这件事,在 0.1 和 0.2 被存储的那一刻就已经注定了。加法只是把各自的偏差暴露出来,再叠加自己的一份。
为什么有些加法「看起来是对的」
有意思的是 0.1 + 0.3 恰好等于 0.4(至少打印出来是)。
对阶后两者的舍入方向恰好互相抵消,和被舍回了 。
这说明浮点误差没有稳定的方向,同样形状的算式换个操作数就可能「对」或「不对」,靠肉眼枚举例子是撞大运。
另一个著名的例子:对 1.005 做保留两位小数的格式化得到 1.00,因为 1.005 的真实存储值是 1.00499999999999989…,四舍五入到两位自然落在 1.00。
想靠「先 round 再格式化」解决的人,先输给了表示误差。
同类的翻车现场还有一串,全部同根同源:十次累加 0.1 得到 0.9999999999999999;0.1 + 0.7 乘 10 不等于 8(先得 0.7999999999999999,乘完 7.999999999999999);1 − 0.9 = 0.09999999999999998。
遇到任何一个都不必惊讶。它们只是 0.1 + 0.2 的换装登场。
相消误差
乘除的相对误差顶多是「每步半个 ulp」,真正能造成惨案的相对误差来自相近数相减:两个几乎相等的数相减,结果的有效数字被大面积抵消,剩下的位数里错误占了主导。
经典案例是一元二次方程 :两根约 和 。
用求根公式算小根,分子是 ,其中 与 只差 。而在 量级上 double 的 ulp 约 ,被减数与减数各自舍入一次,差的计算结果直接不可信。
稳定解法是韦达定理绕道:先算大根(减法变加法,无相消),再用 导出小根。
数值分析里叫它相消误差(catastrophic cancellation),工程上的信号是:公式里出现「两个大数的差」且这个差比数本身小若干个量级。见到就该重构表达式。
默认舍入模式
IEEE 754 默认的舍入模式是 round to nearest, ties to even:先找最近的可表示数;恰好居中时,选尾数末位为偶数的那个。
为什么不像「四舍五入」那样逢五进一?因为逢五必进会让误差带有系统性偏置。一长串计算里每次都往大里去,误差单边累积。
向偶舍入在「进」与「舍」之间对称摇摆,长期统计近似无偏。
标准还定义了向零、向正无穷、向负无穷三种定向模式,用于区间运算和某些金融场景,但通用计算几乎都用默认模式。
阶码的边界
表示范围的悬崖
双精度能表示的最大有限值是 。超过它的有限运算结果会被舍入为 ±Infinity(上溢)。
往小走,最小规格化数是 。比它更小的数不再有隐藏位加成,进入非规格化区(subnormal):尾数可以以 0 开头,指数固定 −1022,一路细到 才真正归零。
非规格化数的设计初衷是「渐进下溢」。让下溢附近的相对误差缓慢变差而不是瞬间跳水,代价是这类数的运算在老硬件上可能慢一个量级。
阶码字段的两端被保留下来编码特殊值,这张表值得背下来:
| 阶码字段 | 尾数字段 | 含义 |
|---|---|---|
| 全 0 | 全 0 | ±0(有符号零) |
| 全 0 | 非 0 | 非规格化数 |
| 全 1 | 全 0 | ±Infinity |
| 全 1 | 非 0 | NaN |
NaN 有一整套自己的逻辑:NaN 与任何数比较都返回 false,包括 NaN 和它自己。
这也是为什么判断 NaN 要用专门的 isNaN 函数而不能用等号。从位模式看,NaN 是一族数(阶码全 1、尾数非 0,尾数位叫 payload),比较器无从断言两个 payload 不同的 NaN「相等」。
有符号零也有自己的暗坑:−0 与 +0 用等号比较是 true,但 1 除以 −0 得 −Infinity、除以 +0 得 +Infinity。除法看穿了它们的区别。
Infinity 的运算规则倒是符合直觉里的极限:加有限数仍是 Infinity,Infinity 减 Infinity 得 NaN(未定式)。
单精度的悬崖比双精度陡得多:上溢在 ,而 24 位有效数字意味着从 16777217(即 )开始,连续整数都存不下了。
float32 里 16777216 加 1 仍然等于 16777216。
图形学和机器学习常用半精度(fp16),边界更夸张:最大值只有 65504,超过即上溢。
把一个数扔进 IEEE 754 可视化里看单精度位布局,再手动算一遍阶码字段的值,边界感比读十遍文章都实在。
一整族的取舍
IEEE 754 是个家族,不同成员的分工值得认一遍,因为选错成员本身就是一类事故。
binary16(fp16):5 位阶码加 10 位尾数,范围小、精度粗,但存储和带宽减半。深度学习推理的常客,模型权重动辄上亿个,砍精度换吞吐。
后续的 bfloat16 更极端,直接保留 fp32 的 8 位阶码、把尾数砍到 7 位,牺牲精度保住动态范围,训练时不容易上溢下溢。
它们印证了阶码-尾数这对跷跷板的另一端用法:精度和范围可以按需重新分配。
另一端是金融。
二进制浮点天生无法精确表示 0.01,所以严肃的账务系统不用它。
Java 的 BigDecimal 是「任意精度十进制 + 标尺」的实现,IEEE 754-2008 也正式收录了 decimal128 这类十进制浮点格式。尾数按十进制位存储,0.01 就是精确的 0.01。
数据库的 DECIMAL 类型、Python 的 decimal 模块同属这一脉。
选型口诀因此可以一句话说完:科学计算用二进制浮点(快、硬件支持),钱用十进制(准、审计可解释),中间的统计与图形看相对误差能不能忍。
工程守则
比较:永远不要用严格相等
第一条守则人尽皆知:浮点数比较用误差容忍。
但怎么写容忍,错误率一样很高。
拿语言内置的最小精度常量(JS 里的 Number.EPSILON,即 )直接当绝对误差阈值,只在 1 附近有意义。数变大后它就形同虚设,在 量级上相邻两个 double 的间隙已经比它大近八个数量级。
通用的写法是相对误差:
function nearlyEqual(a, b, eps = 1e-9) { return Math.abs(a - b) <= eps * Math.max(Math.abs(a), Math.abs(b), 1);}max(..., 1) 保证接近零时退化为绝对误差比较,避免除以极小数造成的过宽容忍。
迭代算法的收敛判断(如牛顿法、梯度下降)都应该用这种形式,同时记得对迭代次数加上限。
浮点世界里「直到收敛」是一个必须设上限的循环条件,因为舍入误差可能让迭代在阈值附近永久徘徊。
累加:顺序有影响
加法结合律在浮点世界里不成立:0.1、0.2、0.3 三个数,两种加法顺序给出不同结果。
推广到求和:百万个数的和,结果依赖求和顺序。
这也是同一个程序在不同机器、不同优化等级下跑出细微不同数值的原因之一(编译器有权重排浮点运算链,除非显式禁止)。
两个经典症状值得记住。
其一,大数吃小数:往 上累加 1.0,结果纹丝不动。1.0 早被舍掉了。
其二,同号数从小到大累加通常误差更小(让小的数在「账面还干净」时先加上去)。
对精度要求高的求和,标准武器是 Kahan 补偿求和:用一个变量把每步被舍掉的部分记回来,下次再加进去:
function kahanSum(xs) { let sum = 0, c = 0; // c 是被丢掉的低位的"账本" for (const x of xs) { const y = x - c; const t = sum + y; // 这里发生舍入 c = (t - sum) - y; // 反推这一步丢了多少 sum = t; } return sum;}代价是四倍运算量,换来把误差从随项数线性增长的量级压回与项数无关的量级。
统计库、线性代数库的求和内核里几乎都有它的变体(成对求和是另一种树状思路)。
日常业务里更实用的规则反而是:能不累加浮点就不累加。
金钱用整数,计数用整数
涉及钱,直接用最小货币单位的整数。分。
1.10 元存成 110,加减乘除全是整数运算,只在最终展示时格式化。
同理,统计量、序号、库存数量这些天然离散的量都不该碰浮点。
语言层面的配套事实也要知道:JavaScript 的位运算会把操作数截断成 32 位有符号整数再算,超过 的数参与位运算会悄悄变号。
排查这类问题时,位运算计算器能以十进制、二进制、十六进制三路对照显示每个按位运算的结果,比在控制台里猜快得多。
还有一个数字要背:安全整数上限 (Number.MAX_SAFE_INTEGER)。
它就是隐藏位那笔账的另一面:53 位有效数字恰好能精确存下 以内的全部整数,再往上,相邻整数开始共用同一个浮点表示。
长整型 ID(雪花 ID、数据库自增主键过亿的场景)一律用字符串或 BigInt 传输,不要过 Number 的手。
JSON 解析器把超出安全区的整数读成失真浮点数,是前后端联调时的经典暗坑。
浮点什么时候是对的
守住边界的同时也要说句公道话:科学计算、图形渲染、信号处理、统计推断,这些领域的正确工具就是二进制浮点。
原因在于它们的误差观天然是相对的:物理仿真差 相对误差没有意义,渲染差半个像素没有意义,测量噪声本来就比舍入误差大几个量级。
浮点把 64 比特同时铺满 的动态范围,正是为这类问题设计的。
所以工程判断不是「浮点坏、整数好」,而是先问误差的容许形态:答案需要「分毫不差」(钱、计数、ID)就用整数或十进制类型;答案只需要「相对够准」(物理量、梯度、坐标)就放心用 double,把心智花在控制误差传播上。
三个派生谜题
理解是否到位,最好的检验是拿边缘案例试试。
下面三个都是真实代码里出现过的现象,答案全部能从本文的规则推出来,建议先自己想再看解答。
谜题一:0.5 × 3 精确等于 1.5,0.1 × 3 却不等于 0.3,差在哪?
0.5 是 ,天生在网格上。0.5 × 3 = 1.5 = ,53 位有效数字绰绰有余地装下,全程无舍入。
而 0.1 存储时已经带着 5.5×10⁻¹⁷ 量级的相对偏差,乘 3 把这份偏差也乘了 3,结果落在 0.3 附近的另一个格点上。
同样的乘法,一个从源头就精确,一个从源头就带账。
结果是否精确,取决于操作数是否精确,与运算本身无关。
谜题二:把 -0.5 取整(Math.round)得到什么?
答案是 −0,一个负零。
规则上取整是「四舍五入、逢五向上」,-0.5 的「向上」就是 0,而 IEEE 754 里 0 有一正一负两个位模式,这条路径产生的是 −0。
它与传统意义的 0 严格相等,但做除数时现出原形:1 除以 −0 是 −Infinity。
顺带的教训是:任何「逢五进一」的实现细节在浮点世界里都要额外指定方向,因为「恰好在中间」的数比想象中常见。
谜题三:parseInt(0.0000001) 的返回值是多少?
不是 0,是 1。
因为 parseInt 只接受字符串,0.0000001 先被隐式转成字符串。而浮点转字符串在数值极小时切换到科学计数法 「1e-7」,parseInt 从头解析数字,读到 e 为止,得 1。
这个谜题的教训不在 parseInt,而在浮点数的字符串表示本身带舍入。
转字符串是「选一个能唯一还原的最短书写」,是一个有损观察浮点的窗口,把它当数值本身用就会踩坑。
浮点数解决的是「数怎么存」;而很多问题里的数根本还没算出来,它们由随机过程产生。下一章先把随机数在算法世界里的两种用法拆开:用随机去算确定的东西,以及用矩阵装下一个随机过程。
随机的方法:蒙特卡洛与马尔可夫链
随机数在算法世界里有两种截然不同的用法。
第一种:用随机去算确定的东西。答案是个定值(比如 π),随机只是计算手段,这是蒙特卡洛方法。
第二种:用矩阵装下一个随机过程。未来本身就是随机的,我们要研究它的长期行为,这是马尔可夫链。
两条线看似无关,最后会在 MCMC 处会师:沿着马尔可夫链采样,成为蒙特卡洛攻克高维问题的最强形态。
上半场:蒙特卡洛
方法本体:面积就是概率
向一个单位正方形里均匀撒 N 个点,数一数落在四分之一圆里的比例 p,那么 π ≈ 4p。
这个游戏简单到不像正经营生,但它背后的方法支撑着金融定价、粒子物理、图形学渲染和贝叶斯统计的半壁江山。
四分之一圆嵌在边长 1 的正方形里,随机点 (x, y) 均匀撒入正方形,落在圆内当且仅当 x² + y² ≤ 1。
设这个事件的概率为 p,则 p = 圆面积/正方形面积 = π/4。
伯努利大数定律保证:N 次试验的命中频率 p̂ 依概率收敛到 p,于是
就这么朴素。
蒙特卡洛的通用配方也在这里了:把要算的量伪装成某个随机实验的期望值或概率,然后靠大量重复把频率压成概率。
之所以需要它,是因为很多高维积分(金融的路径依赖期权、物理的配分函数、渲染方程)解析积不出来、数值网格又维度爆炸。
而随机撒点的成本不随维度指数增长,这是蒙特卡洛唯一的、但决定性的优势。
代价:√N 律的残酷
命中/未命中是伯努利试验,估计量的标准误差是
换算成 π 的误差:要多一位准确数字(误差缩小 10 倍),N 要乘 100。
实际算一下就有体感:
| N | π 的典型误差量级 |
|---|---|
| 100 | ~0.1 |
| 10,000 | ~0.01 |
| 1,000,000 | ~0.001 |
| 100,000,000 | ~0.0001 |
撒一百万个点,也就稳定到小数点后三位。
这就是蒙特卡洛的极限:它能处理维度爆炸,但永远给不出高精度。
需要十位有效数字的场合,它连入场资格都没有。只需要三位、但维度是 500 的场合,它没有对手。
选不选它,先看精度需求。
(常见误解:误差是统计涨落不是系统偏差。估计值围绕 π 波动而不是偏向某侧,加大 N 只是压缩波动区间。)
从撒点到积分
把撒点算 π 改写成积分语言,蒙特卡洛的通用形态就露出来了。
圆内判定函数 I(x,y) 在圆内取 1、圆外取 0,则 π = 4·E[I(U)]。
U 是均匀随机变量,估计 π 就是估计一个随机变量的期望。
一般的定积分同理:∫f(x)dx ≈ (b−a)·mean(f(U)),把”扫网格算函数值”换成”随机抽样取平均”。
这个视角把方差缩减的手段接上了源头:它们不是撒点的特殊技巧,而是降低估计量方差的通用统计工具,对任何期望估计都适用。
也解释了维度问题的真正来源:网格法的节点数随维度指数增长(每维 100 格,十维就是 100²⁰ 个点),而随机抽样的误差只依赖样本总数 N、与维度无关。
维度越高,蒙特卡洛的优势越大。
π 是一维问题,用它入门是”牛刀杀鸡给大家看”。
方差缩减
既然 σ ∝ 1/√N 由方差 p(1−p) 决定,把方差做小就是直接赚钱。
经典手段有三种,都在同一个思想下:消去已知部分,只随机制未知部分:
- 对偶变量:每撒一个点 (x, y),同时用它的”镜像估计”一起平均,让两侧的涨落相互抵消;
- 条件期望:落点统计里能解析积掉的部分直接积掉,只模拟真正随机的部分。比如把”x 方向”解析化、只随机制 y。方差立减一个因子;
- 重要性采样:按”接近被积函数形状”的分布撒点,让每个样本的方差贡献变均匀。
在算 π 这个玩具上,这些手段能把误差系数压小几倍。
注意是系数,√N 律本身纹丝不动。
方差缩减改变的是常数,不改变指数。
这是蒙特卡洛方法里”能改的”和”不能改的”的分界线。
随机数的质量与并行
随机数质量。
程序里的”随机”是伪随机。确定性的种子跑出确定性的序列。
这对蒙特卡洛是利好(可复现、可调试),也带来一个底线要求:序列的统计性质要过关(周期够长、分布均匀、相关性低)。
语言标准库的默认生成器对玩具够用,严肃模拟要换更高质量的生成器。
再进一步是拟随机序列(低差异序列,如 Sobol):放弃”独立随机”、改用刻意均匀铺开的确定性点位,对光滑被积函数能把误差从 1/√N 提速到接近 1/N。
高维金融定价的标配武器,代价是对”锯齿状”被积函数可能失效。
并行化。
蒙特卡洛堪称”天生并行”:样本之间相互独立,百万个点撒到一百台机器上各撒一万,最后把估计值加权平均即可。
但要看清这笔账的本质:合并后的估计量方差是各方差之和,误差仍然只按总样本数 N 的 √N 律收缩。
并行改变的是墙钟时间,不改变”精度要一位数、样本乘一百”的经济规律。
并行买时间,方差缩减买精度,两条路不要混淆。
更古典的版本:蒲丰投针
1777 年蒲丰提出的问题早于”蒙特卡洛”两百年,骨架却完全一致:长为 l 的针扔到间距 t(l ≤ t)的平行线组上,相交概率 p = 2l/(πt),反过来用投针频率就能估 π。完整的几何推导、浏览器实验工具,以及 1901 年 Lazzarini 3408 次投针、精确到小数点后六位的著名轶事,都见后文蒲丰投针一节;这里只留它对蒙特卡洛的意义:命中事件更稀有,方差更大,同一个 √N 律换了个更费劲的实验——实验设计决定的是常数。
上半场收个尾:蒙特卡洛用”维度的自由”换”精度的天花板”,√N 律是这笔交易的价格标签。
而它所有的样本都有一共同前提:相互独立。
把”独立”放开,让样本沿着一条链演变,就进入了下半场的主角。
下半场:马尔可夫链
定义:一个矩阵就是一条链
马尔可夫链是那种”名字唬人、内核极简”的数学工具:下一个状态的概率分布只依赖当前状态,与更早的历史无关。
这一条”无记忆”假设砍掉了时间序列的全部复杂性,却仍然撑起了 PageRank、隐马尔可夫模型和无数文本生成玩具。
设系统有 n 个状态,转移矩阵 P 的第 i 行第 j 列 p_ij 表示”当前在状态 i,下一步到状态 j 的概率”。
每一行加起来必须等于 1(总得去某个地方)。
马尔可夫性(无记忆性)写成公式只有一行:
给定现在,未来与过去独立。
这个假设强得近乎粗暴。但很多系统的真实动态确实近似如此:下周的天气主要看这周,棋局主要看当前盘面,页面跳转主要看当前页。
玩具例子:晴雨模型
两个状态 {晴, 雨},转移矩阵:
晴 雨晴 0.9 0.1雨 0.5 0.5今天晴,明天下雨概率 0.1。今天雨,明天转晴概率 0.5。
问:长期来看,晴天占比多少?
直觉尝试”按比例折算”往往答错。正确工具是平稳分布。
平稳分布
平稳分布 π 是一个行向量,满足 π = πP。
再走一步,分布不变。
对遍历的链(任何状态都能到任何状态,且没有周期性),这个分布存在、唯一,而且无论从哪个初始状态出发,t 步后的分布都会收敛到它。
这就是”链忘了自己的起点”。
晴雨模型的平稳分布:解 π晴 = 0.9π晴 + 0.5π雨、π晴 + π雨 = 1,得 π = (5/6, 1/6)。
长期看 83% 的日子是晴天。
注意这个数字与今天是晴是雨无关。初始条件的影响随每一步转移指数衰减。
工程上不必真解线性方程组:把 P 自乘很多次( 每行都趋于 π),或者直接模拟几万步数频次,都收敛到同一个答案。
收敛的过程可以亲手算一步:P² 的第一行是 (0.9×0.9 + 0.1×0.5, 0.9×0.1 + 0.1×0.5) = (0.86, 0.14)。
今天晴、后天雨的概率从 0.1 挪到了 0.14,已经朝 (5/6, 1/6) ≈ (0.83, 0.17) 靠近。
每自乘一次,两行就更接近,最后重合成同一个向量。
两个常见误区
- “无记忆 = 完全随机”。错。链的状态分布可以高度结构化。平稳分布就是结构。无记忆只是说历史不额外提供信息,不是说转移没有偏好;
- “周期链也会收敛”。错。若链有周期性(比如完美二部图,永远在两群状态间摆动), 不收敛,只在取平均意义下收敛到 π。判定遍历性(不可约 + 非周期)是应用平稳分布前的必要检查。
吸收链
遍历链”哪里都能到”,还有一族重要链条走另一条路:吸收链里有几个”进去就出不来”的状态。
最经典的是赌徒破产模型:本金以 1 为步长随机涨跌,输光(0)或达标(N)就停局。0 和 N 是吸收态。
这类链的平稳分布退化得只剩一个答案:“最终全在吸收态”。
但真正有意义的问题换成两个:平均多少步被吸收(期望持续时间)和被哪个吸收态吸走(破产概率对达标概率)。
两个问题都有干净的线性代数解:把转移矩阵按”瞬态 / 吸收态”分块,瞬态块的幂和(所谓基本矩阵)直接给出期望步数,吸收概率由一个矩阵乘法给出。
赌徒破产模型还能进一步解析求解:公平赌局下从 k 起步的破产概率是 (N−k)/N。
期望意义上”久赌必输”不是道德判断,是矩阵求逆的结果。
吸收链是”遍历性不成立时怎么办”的完整答案,也是排队系统、保险精算、生物种群灭绝问题的共用骨架。
同一个骨架的三件大事
文本生成:把”状态”换成词或字,转移概率从语料统计(“红”后面跟”楼”的频率),就能生成一段”看起来像那么回事”的文本。
二阶模型(看前两个词)效果立刻提升。这暴露了马尔可夫假设的天花板:语言是有长程依赖的。
实践里还有个立竿见影的坑:语料里没出现过的二连词概率是 0,生成走一步就撞墙,所以要平滑。从总概率里匀一小块分给”没见过”的组合。
但正是这个玩具一路演进成了今天的语言模型:把”上一个状态”换成”上下文向量”、把计数换成神经网络。
下一词预测的框架从 1950 年代至今没变过。
PageRank:把网页当状态,链接当转移(每个页面把概率均分给出链),随机游走者的平稳分布就是每个页面的权重。
原始公式里的”阻尼因子 0.85”是一个工程补丁:以 0.15 的概率跳到任意页,它把链强行变成不可约非周期的,保证平稳分布存在唯一。
数学上恰好是消除悬挂节点和蜘蛛网孤岛的那一步。
MCMC 把平稳分布定理反过来用:想从一个写不出表达式的复杂分布里采样,就构造一条以它为平稳分布的马尔可夫链,跑到收敛后沿着链走。
链的轨迹就是样本。
这正是上半场那个悬念的答案:蒙特卡洛被 √N 律和维度掣肘的地方,恰恰是”怎么在复杂分布里取样”。
沿链采样让独立撒点做不到的高维问题变得可行,贝叶斯统计的现代计算全压在这上面。
两条线在这里会师:马尔可夫链负责”造出正确的随机性”,蒙特卡洛负责”用随机性算出想要的数量”。
从语料到生成器的五步
文本生成玩具的实现短得超出预期,五个步骤:
- 切分:按词或按字切语料。字级模型状态少、什么都连得上但语句更破碎;词级模型更通顺但容易”只走语料里出现过的老路”。粒度本身就是一对取舍;
- 计数:扫一遍语料,数出每个(当前词,下一词)对的出现次数,存成计数表;
- 抽样:不必真把计数归一化成概率。按计数加权随机抽下一词即可,等价且少一步浮点运算;
- 成句:从起始状态(句首或任意词)出发,循环”查表 → 抽样”,直到抽到句末符号或达到长度上限;
- 补漏:遇到”没有后继”的死胡同(语料里这个词从没当过句中词),回退到随机重启或抽最高频词。
生成质量的两个开关都在第 1 步和第 2 步:切分粒度、以及阶数(一阶看一个词,二阶看两个词)。
阶数加一,通顺度立涨,但语料需求指数增长、稀疏问题加重(又回到平滑)。
这个”阶数 vs 数据量”的跷跷板,正是马尔可夫生成器”投入十行代码、看懂一半语言模型史”的教学价值所在。
动手感受收敛
在马尔可夫链可视化里可以调转移概率、看分布随步数流动。
值得专门看两个现象:一是收敛速度。转移矩阵越”连通”(次大特征值越小),忘掉初始条件越快。二是把某个转移概率调成 0 制造”半不可约”,观察分布如何卡住。
这两个感受比任何公式都直观。
马尔可夫链不只活在教材里。把「状态」换成保底计数器、把「吸收」换成必出货,它就成了商业产品里最精巧的概率设计——下一章看抽卡保底如何雕刻概率分布。
抽卡保底:雕刻概率分布
裸的独立概率是一头没人骑得住的野兽:0.6% 的单抽概率意味着接近三成的玩家两百抽颗粒无收,而这样的体验没有任何商业产品敢原样端给用户。
抽卡系统的全部设计技巧,本质上是在对概率分布做雕刻——截断长尾、垫高腰部、保住叙事,最后把「运气」包装成「可规划的预算」。
裸概率
最朴素的抽卡模型只有一个参数:每抽独立、概率恒为 。
第 抽恰好首次出货的概率是前 抽全空、第 抽命中:
这就是几何分布,它的期望和方差都有闭式解:
把 代进去,期望 166.7 抽。
但这个数字的误导性极强,因为几何分布是高度右偏的:中位数只有 抽,一半的人比「期望」早得多出货,而少数极端非酋把期望拖到了中位数之外。
看一眼尾部就知道问题在哪:前 199 抽全空的概率是 ,前 299 抽全空也有约 16.5%。
也就是说,按裸概率运营,每三个玩家里就有一个要空两百抽。
这还是期望意义上的「正常波动」,但它落在具体玩家头上就是灾难,而玩家从不按期望生活。
无保底的参照系
要看清「没有雕刻的原始随机」长什么样,最好的参照物是一台无保底的老虎机。
站内的老虎机小游戏就是最简模型:三个转轮各六种符号独立等概率落定,三连中大奖(面值 10 倍)、两连中小奖(面值 2 倍)、其余颗粒无收。
把它的分布摊开算一遍:三连概率 ,恰好两连概率 ,什么都不得的概率是 。
有趣的是,按符号面值(5 到 100)粗算,这台机器平均每投入 10,期望能赢回约 36。
数学期望对玩家是正的,因为它是个教学玩具而非经济仿真。
但体验层面真正要看的不是期望而是形状:超过一半的回合零回报,收益高度集中在不到 3% 的瞬间里。
连败是常态、爆发靠极小概率事件,这就是未经驯服的分布给玩家的日常。
所有保底机制要解决的,都是这个形状。
硬保底
硬保底(pity)是一条最简单的规则:连续 抽没出货,第 抽强制出货。
分布从「无限长尾的几何」变成「截断的几何」:
期望闭式解异常干净,它就是对存活概率求和:
代 、:,于是 抽。
对比裸概率的 166.7,一刀砍掉了 58%。
而这 58% 全部来自长尾:保底没有改变前 90 抽的任何概率,它只是把「第 91 抽以后才出货」的所有人的结局统一压缩到了第 90 抽。
这个公式里还藏着一个反直觉的事实: 是「在保底触发前自然出货」的概率。
换句话说在 0.6% 这种量级下,多数出货(58.2%)其实是保底本身完成的,而不是「运气到了」。
保底不是外挂的福利,它就是出货的主通道。
设计者对此心知肚明。所以公示的「综合概率」才会远高于单抽概率,这个话题到软保底一节再展开。
硬保底的代价同样明显:它把第 90 抽变成了一个确定事件。
确定事件没有心跳。如果所有人都知道「最多 90 抽」,那么第 60 抽之后的每一抽在情绪上都只是倒计时。
这就是为什么实践中硬保底从来不是全部答案,还需要给概率本身装一个斜坡。
参数敏感度
硬保底还有一个容易被忽略的系统性效果:它压缩了基础概率 变化的杠杆作用。
同样 90 抽封顶,代入期望公式 算一组对照:
| 基础概率 | 无保底期望 | 90 抽硬保底期望 |
|---|---|---|
| 0.5% | 200 | 72.6 |
| 0.6% | 166.7 | 69.7 |
| 1.0% | 100 | 59.5 |
| 2.0% | 50 | 41.9 |
无保底时期望与 严格成反比,杠杆全开。
加上保底后, 从 1% 砍到 0.6%,期望只从 59.5 涨到 69.7(约 17%),远小于裸概率下的 67% 涨幅。
原因就是前面那个事实: 越小,出货越依赖保底本身,而保底是定值。
基础概率逐渐从「期望的决定者」退化为「保底内的小幅修正项」。
这解释了行业里基础概率一降再降的底气所在:只要保底封顶值不变,期望损失是有界的。
也解释了为什么讨论抽卡定价时,盯着保底参数比盯着单抽概率更接近真相。
软保底
软保底(soft pity)的想法是:在硬保底触发之前,先让概率随连续失败次数线性爬升。
一个广泛使用的参数化是:前 73 抽保持 ,从第 74 抽起每抽增加 6 个百分点,即:
这个模型的期望可以用存活函数手工算出来。
记 为「前 抽都没出货」的概率, 直到第 73 抽:。
从第 74 抽起逐项乘上 衰减:
| 抽数 | 存活概率 (累计未出货) |
|---|---|
| 73 | 64.4% |
| 74 | 60.2% |
| 76 | 42.8% |
| 78 | 22.4% |
| 80 | 8.2% |
| 82 | 1.9% |
| 84 | 0.25% |
| 86 | 0.015% |
| 90 | ≈0 |
期望就是全部存活概率之和:前 73 抽贡献 ,斜坡区 16 项再加约 2.4,合计:
从裸概率的 166.7 到 62.3,期望砍掉 63%。
更妙的是分布的形状:出货众数落在大约第 77 抽,斜坡区(74 抽之后)包揽了约 64% 的出货。
玩家的主观体验从「遥遥无期」变成了「越到后面越有盼头」,而期望几乎全靠这段 17 抽的斜坡压下来。前面 73 抽的「低概率叙事」原封未动。
这就是软保底的精髓:期望是后台的账本,前台只演概率爬升的戏剧。
两个数字的合谋
把 62.3 抽的期望折算成单抽综合概率,就是 。
于是每个抽卡系统都挂着两个看似矛盾的数字:宣传页上的 0.6%(单抽基础概率)和规则页的约 1.6%(含保底的综合概率)。
两者都真实、都合规,描述的是同一个分布的两个侧面。
概率监管要求公示出货概率与保底规则,设计者必须把这套分段模型原原本本写进公告。
而玩家需要理解的只是:单抽概率描述起点,综合概率描述终点,斜坡在中间把两者接起来。
分位数才是预算
期望 62.3 抽还有一个常被误读的地方:它不是「一半人在它之前出货」的位置。
上面算过,中位数只有 76 抽。分布右偏,期望被少数极端个体拉向后方。
做资源规划(「攒多少抽才够」)时,正确的问题从来不是「平均要几抽」,而是「九成玩家多少抽内能出」,也就是分位数。
把软保底模型的存活函数逐档查询,再与裸概率并排:
| 分位 | 软保底模型(抽) | 裸概率模型(抽) |
|---|---|---|
| 中位数 | 76 | 116 |
| 80% | 79 | 268 |
| 90% | 80 | 383 |
| 99% | 83 | 766 |
裸概率下,1% 的玩家要抽 766 次以上才能见到第一张金。
这条无界的尾巴就是保底存在的全部理由。
斜坡把它整个压进 90 抽的天花板之下,于是 99 分位从 766 掉到 83,缩水一个数量级。
注意两个分布的另一层反差:软保底的分位数曲线「挤」在一起。从 76 抽到 83 抽这 8 个格子,覆盖了一半以上玩家的结局(累计概率从 47% 直冲 99%),大多数人的体验高度相似。
裸概率则是「长尾摊开」,每个人的命运天差地别。
设计者选择前者,等于选择了可预期的挫败而不是不可预期的绝望。
挫败可以转化为消费,绝望只会转化为卸载。
大保底
限定卡池还有第二层雕刻:抽到最高稀有度(5★)后只有 50% 是当期 UP 角色,歪了则下一个 5★ 必中 UP(俗称大保底)。
这把「拿到 UP」的期望抽数变成:
注意这个系统的完整状态空间是二维的:(保底计数器、是否处于大保底状态)。
每一抽的概率只由当前状态决定,与更早的历史无关。也就是马尔可夫链的骨架,只不过这里的「状态」是设计者构造的计数器而非自然演化。
想要 UP 的过程是一条两阶段吸收链:「歪」是瞬态,「UP」是吸收态,吸收概率各半。
上一章随机的方法讨论过转移矩阵与平稳分布,抽卡系统是它最商业化的应用现场:整条链的转移概率表设计,就是整个付费体验的设计。
吸收链的矩阵解法
「想要 UP」这条链可以用标准的吸收马尔可夫链工具解出来。
状态取两个:歪(瞬态)与 UP(吸收态)。
从歪出发,下一步以 0.5 的概率进 UP、以 0.5 概率再歪一次。瞬态子矩阵只有一格 ,期望访问次数矩阵 。
从歪状态出发平均还要经历 2 次 5★ 窗口(含当前这次)才吸收,折回抽数就是 抽。
矩阵只有一格时解法退化成小学除法,但它的价值在于扩展性。
若规则改成「连歪两次必中」(三选一的 UP 池常见),状态扩成三个、 变成 ,同一套 原样给出答案。
规则复杂化时,马尔可夫工具箱不需要升级。
50/50 还制造了一种刻意的贫富差距:一半玩家 62 抽结束战斗,另一半要打满两个周期。
这种方差在纯粹的产品逻辑里是应当消除的缺陷,但在这里它是有意保留的社交话题。
「一发入魂」和「180 抽双歪」都是传播素材,而消除它(比如歪了退一半资源)反而会让每个玩家的结局趋于一致,少掉一半的故事。
谁来掷骰子
以上全部模型都默认骰子由服务端掷,但把视角拉到工程层,这本身是一个设计决策。
客户端先抽后报的架构里,概率表就在玩家手里。反编译客户端、抽出概率表甚至直接篡改提交结果,都是可控成本内的操作。
服务端裁决则把随机性收回黑盒,代价是接口需要签名、防重放与频控。
攻防的真实烈度可以参考米游社 salt(LK2 & K2)获取那篇逆向笔记:为了调用米游社的接口,需要从 apk 里把请求签名用的 salt 材料一层层挖出来。
也就是「接口结果由服务端裁决 + 请求全程签名」路线的攻防现场。
抽卡系统的概率设计得再精巧,若骰子放在客户端,一切模型都是纸糊的。
分布设计解决公平问题,接口安全解决作弊问题,两者缺一不可。
伪随机
保底是对分布的硬整形,还有一族更精细的工具叫 PRD(Pseudo Random Distribution,伪随机分布)。
Dota 2 的暴击机制是它的经典应用:第 次尝试的命中概率是 , 是小于标称概率的常数,一旦命中计数器归零。
标称 25% 的暴击对应 。第 1 刀 8.5%,连败后第 5 刀 42.5%,长期频率仍然收敛到 25%。
但单次体验的方差小得多:超长连败几乎消失,同时「连败越久越该出」的直觉被系统暗暗兑现。
为什么不能直接取标称值?算一下就知道。
若 ,则首击概率 25%、次击 50%、三击 75%、四击 100% 必中,期望尝试次数是 次。
折算长期频率约 45%,远高于标称的 25%。
所以 必须数值求解:约束是「期望尝试次数等于 」,25% 对应的解就是 。
这个看似别扭的机制其实是一台期望保持器。它重新分配了「何时出」的分布,却把「多久出一次」的长期账目钉死不动。
也就是「伪随机」三个字的准确含义:随机的形状是假的,随机的总量是真的。
这组工具可以排成一个谱系:一端是独立伯努利(真随机,方差最大),中间是 PRD(温和的相关性),另一端是计数器保底(强负相关,方差被压到最小)。
三者长期期望可以完全相同,玩家体验却天差地别。
行为经济学的背景音在这里响起:人类对随机的直觉本身就是错的。我们预期小样本里也该均匀,看到真随机序列里的连续聚集就喊「假」。
设计者面对的不是纠正这种直觉,而是顺着直觉重新制造随机性:让系统比真随机「更像玩家心中的随机」。
与 PRD 相对的是纯感知层面的现象:垫刀、玄学时间、保底前烧香。
玩家在无记忆的系统里强行寻找记忆。
有趣的是,带保底的系统确实有了记忆(计数器),玩家的玄学在统计上竟然变得半真半假:你确实知道自己的 pity 计数是多少,「我快保底了」是一句可验证的陈述。
保底机制把赌徒谬误从幻觉变成了账本,这是它比任何洗脑公告都高明的地方:它用确定性的承诺,买回了玩家对随机性的信任。
这套信任工程还有一件舶来的成品:「天井」(天井/ceiling,日本手游术语)。比硬保底更进一步的付费上限,抽满若干次后直接用资源兑换当期角色,随机环节彻底退场。
天井把「最坏情况」从概率事件变成预算事件,玩家第一次可以用记账的方式参与一个随机系统。
回看整条设计链会发现一个清晰的方向感:从裸概率到硬保底、到软保底、到 PRD、再到天井,随机性的地盘在一寸寸让渡给确定性。
每一层都在减少极端厄运、增加可规划性,同时又小心翼翼保留最顶上那层「可能一发入魂」的甜头。
从一抽到一队
期望抽数回答「出一金要多久」,但真实的目标往往是结构化的:集齐一套、抽满六命、从歪的角色里凑出可用的队伍。
这类问题一开口就要组合数学。
先看一个最常见的「结构化」操作:十连。
十连在概率上只是十次独立抽样,但因为共享同一个保底计数器,它的体验被重新包装了。
单抽出金概率 0.6% 时,十连至少出一金的概率是 ,而「不出金」的九连空白感被十倍的视觉冲击对冲掉。
更重要的是保底间隔若设为「十连必出 4★」这类规则(很多游戏的高稀有度保底以十连为周期),十连就成了截断长尾的第二层单位。
单抽与十连共享计数器时,先单抽垫刀再十连收尾的策略才会在社区流行。这本质上是玩家在系统规则下对计数器状态的最优使用,一个自发涌现的动态规划问题。
价格上,十连通常带约 5% 到 10% 的折扣,这才是它的经济学实质:用批量承诺换取确定性溢价,概率模型一分未变,付费心理模型大改。
最基础的计算工具是二项分布:忽略保底修正, 抽里恰好出 个 5★ 的概率是:
算「90 抽恰好出 2 个 5★」时,第一步就是那个组合数。,代入得 。
这种系数手算容易错,站内的排列组合计算器就是干这件事的:输入 和 ,同时给出排列数 与组合数 ,是这类估算的顺手工具。
再往上一层是规划问题。
以「满命」(同一个 UP 角色重复获得 7 次)为目标,每个 UP 平均 93.4 抽,期望总投入约 654 抽。线性,没有惊喜。
真正值得算的是分位数而不是期望:预算该按「八成玩家能达成」来定,而非按平均。
抽卡模拟器的价值所在:设定总抽数、单抽概率和保底间隔(0 表示无保底),跑一次模拟就能拿到「共多少抽、获得几次、首次出货在第几抽、平均几抽出一张」的完整记录。
单次模拟是一个具体玩家的命运,多跑几次就是分布本身。
期望是账本上的理论值,分位数才是做预算时该看的数字,这两者的差距在重尾分布里可以大得吓人。
保底是对分布的硬整形,而随机分布也有自己长出形状的方式:往一个几何结构里随机扔东西,然后数结果。下一章的三个撒点实验,恰好排成统计学的半部入门史。
撒点实验:高尔顿板与蒲丰投针
有三个历经百年的撒点实验,各自只做一件小事:往一个几何结构里随机扔东西,然后数结果。把它们排成一条线,恰好是统计学的半部入门史——高尔顿板把一堆硬币般的左右弹跳堆成钟形曲线,蒲丰投针用几何概率倒推出 π,向圆里撒点则是最直白的蒙特卡洛。这条线的终点是今天的做法:不再辛苦地撒点,而是直接从正态分布采样。
动手版本:弹珠盘游戏、蒲丰投针、蒙特卡洛算 π、高斯采样器。
高尔顿板
1876 年前后,弗朗西斯·高尔顿造了一块竖直木板:上方漏斗口,下方数十排交错排列的钉子,小球从顶部落下,每撞上一颗钉子就以某种偏好在左右之间做选择,最后落进底部的竖槽,槽里的球堆出一条钟形曲线。
高尔顿给这块板起名叫 quincunx(五点梅花阵),并在《自然遗传》里用它演示遗传与回归。
一群各自随机的微小扰动,叠加之后竟然总是同一个形状。
数学模型朴素到近乎敷衍。设板有 排钉子,小球每排随机偏左或偏右,落点位置就是 次独立伯努利试验的总和:
这就是二项分布。
系数 不是抽象符号,它数的是「从 次弹跳里恰好选出 次向右」的所有路径条数。
杨辉三角的第 行就是落点的全部概率质量。
时这一行是:
| (向右次数) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 12 | 66 | 220 | 495 | 792 | 924 | 792 | 495 | 220 | 66 | 12 | 1 |
下面把这张表直接跑给你看。小球逐排随机偏转,槽中的堆积随球数增多慢慢显出钟形轮廓,跑完 400 颗后叠加理论分布虚线对照:
站内的弹珠盘小游戏把这台机器搬进了浏览器:12 排钉子三角排布,摆动的发射口把球抛进去,小球带着重力、摩擦和弹性在钉阵间真实弹跳,底部 11 个槽按倍数计分(中间 10 倍,两侧一路衰减到 0.2 倍)。
按纯二项分布算, 时落点集中在中部: 取 4 到 8 的概率合计 ,占 的 85.4%。绝大多数球应该堆在正中。
真实弹珠不完全服从这个理想模型:球有动量,一次弹跳会影响下一次的弹跳方向,相邻落点之间不是独立的。
但「多排独立小扰动叠加、总和集中在中部」的骨架不变,这正是二项分布的全部要义。
底部那排倍数槽值得多看一眼:它把「落点分布」变成「赔付分布」,等于对二项随机变量做了一个非线性变换。
中间槽概率最大却给最高倍数,两侧概率极小给最低倍数。与真实弹珠机的「边槽高赔付」恰好相反,是刻意把期望拉高让玩家爽的教学设计。
你可以在纸上用 当权重、对每个槽的倍数加权求和,估一估这个设计的期望回报。这本身就是一道不错的组合数学练习。
从二项到正态
高尔顿板真正的戏法在极限处上演。
棣莫弗在 1733 年证明了后来被称为棣莫弗–拉普拉斯定理的东西:当 变大,标准化后的二项分布逼近正态分布。
用现代记号,若 ,则:
、 时均值是 6,标准差 。
12 排钉子的弹珠盘就已经能看到相当标准的钟形。
中心极限定理后来把这个现象推广到了任意独立同分布的小扰动之和。不管单次扰动服从什么分布,只要够多、够小、够独立,总和就趋向正态。
钟形曲线无处不在的原因不在曲线本身,而在于「大量微小独立因素叠加」这个生成机制实在太常见。测量误差是无数干扰的总和,身高是无数基因与环境因素的总和,弹珠落点是无数次钉上弹跳的总和。
所以高尔顿板的历史地位不在它的机械构造,而在它是第一条「从概率到正态」的可触摸通路:正态分布不是假设出来的,是从二项分布一层层堆出来的。
理解了这一点,统计课上那些「假设误差服从正态」的约定就不再是凭空规定,而是对叠加机制的合理近似。
历史线在这里再补一笔。
正态分布最早的登场角色正是「误差定律」:1809 年高斯在行星轨道计算中论证了测量误差应当服从这条曲线,并顺手建立了最小二乘法。「正态分布」这个名字至今仍被称作「高斯分布」,理由就是它作为误差模型的第一性推导。
后来拉普拉斯把棣莫弗的极限定理推广成中心极限定理的最初形态,「误差是大量微小扰动之总和」从假设升级成了定理。
统计物理里正态的另一个身份同样值得知道:在所有方差固定的分布中,正态是熵最大的那一个。「除了方差之外一无所知」时,正态是信息论意义上最诚实的假设。
均值回归的出生地
高尔顿造板的目的本来不是教学而是遗传学。
他的问题很具体:杰出的父亲会有杰出的儿子吗?
实测数据说「会,但没那么杰出」。极高者的子代平均而言只比平均水平高一点,极端值会向均值回落。
为了解释这件事,他把 quincunx 加装了一层:上半段的球先堆积在中层的槽里(代表父代的表型),再从槽里落下下半段钉阵(代表子代受到的随机扰动),最后形成的子代分布比父代分布更窄、更靠拢中心。
均值回归(regression to the mean)这个词就诞生于此。regression 本义是「退回」。
这块加装板留下的遗产远超遗传学。
极端值回落不是均值有什么引力,而是纯粹的抽样结构:极端值 = 高的基础水平 + 极端幸运的扰动,下一轮扰动回归常态,结果自然回落。
运动员的「冠军魔咒」、best-seller 的续作扑街、绩效排名的洗牌,多半是这个机制的表演。
识别它的判据很简单:只要「被选中」与「随机波动」相关,回落就会发生。而绝大多数排名场景恰好满足这个条件。
一个值得警惕的推论
反过来也要清醒:中心极限定理的三个前提,独立、微小、大量,缺一不可。
一旦扰动之间强相关(一次大风把所有球往同侧吹),或者单次扰动可能巨大(一排钉子直接把球弹飞到边缘),钟形就垮了。
金融里「资产收益率服从正态」的假设屡屡翻车,翻掉的多半就是「微小独立」这一条。市场恐慌时扰动高度相关,肥尾随之而来。
高尔顿板上堆不出肥尾,但现实世界会。
蒲丰投针
把撒点的对象从钉子换成针,故事回到 1777 年。
布丰问:一根长为 的针,随机扔到间距为 ()的平行线组上,与任一直线相交的概率是多少?
这个问题是几何概率的开山之作。样本空间不再是离散的计数,而是连续的位形:针的位置由中点坐标和角度决定,「均匀随机」意味着在这片连续空间上均匀撒点。
求解只需两步:设针中点到最近直线的距离为 、针与线的夹角为 (取 到 足够),则针跨越直线的条件是 。
在矩形区域 、 上均匀撒点,命中区域的面积除以总面积就是概率:
π 从天而降。
1780 年代没人觉得这有什么用,直到拉普拉斯指出它的对偶面:既然 ,反过来用频率估计概率,就有:
其中 是投针总数、 是相交次数。
历史上真有人靠手动投针估算 π,最出名的是 1901 年 Lazzarini 的报告:3408 次投针,算出 π ≈ 355/113,精确到小数点后六位。好得可疑——概率论圈子普遍的解读是,要么他在恰好的时刻停手(355/113 每多掷一次就会破坏),要么压根没做实验。这个轶事反而是个提醒:随机实验的「好结果」没有独立性检验,就只是个好故事。
用随机实验估计常数的整套思路在这里第一次成形,比「蒙特卡洛方法」得名(曼哈顿计划时期,冯·诺依曼与乌拉姆)早了一百六十多年。
要注意这层反推的严谨边界:公式里出现的 π 意味着「实验效率依赖于 π 的真值」,用它估计 π 在逻辑上是自洽的(频率收敛的是概率 ,π 由 与已知量解出),但这也正是它作为估算实验效率不高的根源。目标常数藏在命中概率的分母里,估计值天然带更重的非线性放大。
蒲丰投针工具实现了这个实验:每次点击投下 1500 根针(针长与线距均为 1),画面上与线相交的针以红色标出、其余灰色,同时给出 的估计值和累计投掷数。
相交比例应该在 附近徘徊,而 π 的估计值会在 3.0 到 3.3 之间抖动,多投几次才能看清它如何缓慢收拢。
两种实验的成本对比
把蒲丰投针和撒点求 π 放在同一张桌子上对比方差,能看清「实验设计」四个字的分量。
撒点求 π 的命中概率是 ,投针只有 。
伯努利试验估计量的方差是 ,代入算:撒点 ,投针 。
同样的样本量,投针的方差大 37%。
也就是说,要达到同样的精度,蒲丰投针要多扔约 37% 的针。
它是个更「贵」的实验,贵在命中事件更稀有、信息量更低。
两百年前的手工实验如此,今天的模拟系统同理:估计量的常数因子就是你的电费单。
撒点求 π
第三个实验最直白:在边长为 的正方形里均匀撒点,数落在内切圆里的比例 ,由面积比立即得到:
蒙特卡洛算 π就是它的浏览器版:正方形画布上画着内切圆,每次点击撒 2000 个点,圆内的点标红、圆外标灰,估计值 与累计样本数同时显示。
由于是伯努利估计,标准误差为:
这就是著名的 √N 律:误差与 成正比,精度每多一位数字,样本量要乘 100。
撒 2000 个点的典型误差在 0.04 量级,撒到 200 万也就稳定到三位小数。
上文随机的方法里已经把 √N 律的账算得很细,这里只补一句它的正面意义:误差按 收缩虽然慢,但它与维度无关。
二维正方形里撒点和五百维球体里撒点,精度规律一模一样。
撒点求 π 是在用一维难度的玩具,演示高维积分里唯一能打的方法的骨架。
三个实验到齐,可以并排看一眼了:
| 实验 | 随机化的对象 | 估计的目标 | 命中概率 |
|---|---|---|---|
| 高尔顿板 | 每排钉子的左右弹跳 | 叠加分布的形状 | — |
| 蒲丰投针 | 针的位置与角度 | π(几何常数) | |
| 撒点求 π | 点在正方形内的坐标 | π(面积比) |
前一个回答「分布从哪来」,后两个回答「常数怎么算」。
两条线索在「用随机样本推断确定答案」这一点上会师。
蒙特卡洛这个方法名的来历值得讲一段,它恰好是科学史上一段著名的「沙发灵感」。
1946 年,冯·诺依曼的同事乌拉姆生病卧床,一个人玩坎菲尔德纸牌接龙,想知道「这局能赢的概率有多大」。穷举牌序组合复杂度爆炸,他忽然意识到:玩几百局、数赢了多少局,答案自己就出来了。
战后他和冯·诺依曼把同样的思路搬到中子输运计算上,名字是同事梅特罗波利斯(Metropolis)起的(摩纳哥的赌城,说的是「用概率算数」而不是「靠运气吃饭」)。
撒点算 π 与纸牌接龙在方法上完全同构:把「能不能成功」这个问题转化为频率,然后让大数定律做剩下的工作。
今天金融定价、粒子输运、图形学渲染里的每一次「随机模拟」,血统都来自那张病床上的牌桌。
直接采样
高尔顿板用 12 排物理弹跳堆出钟形,蒙特卡洛用海量样本逼近 π。都是笨办法。
一旦知道目标就是正态分布,问题立刻换成:能不能跳过堆积过程,直接从 抽一个样本出来?
最通用的思路是逆变换法:算出目标分布的累积分布函数 ,抽一个均匀数 ,令样本 。
把概率尺度上的均匀拉伸成密度尺度上的目标形状。指数分布用 就这么造出来,干净利落。
但正态在这里卡壳: 没有初等闭式表达,数值求逆要么查表要么迭代。
于是轮到更巧的构造登场。
Box–Muller 变换(1958 年):取两个独立的均匀随机数 ,则:
推导的钥匙是二维正态的径向对称性。
二维标准正态的联合密度是 ,只依赖半径 、与角度无关。所以极坐标下半径与角度独立,角度均匀分布在 (这就是 的来历),而半径平方的分布要满足「密度随 指数衰减」,恰好是指数分布:令 ,解出 。
均匀随机数在二维平面上是方的,这条变换把它们挤成了圆对称的高斯云。
一次乘法、一次对数、一次余弦,两个均匀数换一个正态数(其实是一对:把 换成 还能再得一个,标准正态样本买一送一)。
工程实现里还有一个变体 Marsaglia 极方法:先在单位圆内拒绝采样得到极坐标,避免 与三角函数,拒绝率 ,在无硬件三角函数的年代是实打实的提速。
高斯采样器用的正是 Box–Muller 公式(,并对 做拒绝重抽):调 和 两个参数、拖动样本量滑块,直方图实时重画。
样本少时锯齿分明,样本多时钟形自动浮现,把「样本越多越像总体」这件事变成一次拖动就能体验的直观。
从教学顺序上讲,这条线是倒着走的:先有高尔顿板的物理堆积(分布是涌现的),再有蒲丰与撒点(分布是设计的),最后是 Box–Muller(分布是构造的)。
模拟系统今天的工作方式是第三种:需要什么分布,就从均匀数出发构造一条变换链。均匀到指数用 ,均匀到正态用 Box–Muller,任意分布用逆变换法或拒绝采样。
高尔顿板里的每一颗弹珠,如今都是一行 。
方差缩减
√N 律钉死了误差的指数,但常数因子还有得谈。这就是方差缩减技术的用武之地。
核心思想一句话:消去已知部分,只随机制未知部分。
- 对偶变量:撒点求 π 时,每撒一个点 就同时用它的「补点」 一起平均。两者一内一外高度负相关,涨落相互抵消,方差立减。蒲丰投针同理:针与其互补位形一起统计;
- 条件期望:把能解析积分的部分积掉。撒点求 π 的一个变体是只随机制 、对 方向解析求交。每扔一个点的信息量更大,常数因子再降;
- 重要性采样:按接近命中边界的分布撒点,让每个样本的信息贡献更均匀。对蒲丰投针这种「大部分针与线相距甚远、结果毫无悬念」的实验尤其对症。
这些手段能把常数压小几倍,但注意边界:方差缩减改常数,不改指数。
那个 2 纹丝不动,精度每多一位数字样本乘 100 的总账一分不少。
想要指数级的提速只能换估计量本身(比如从伯努利估计换成解析变换链),而不能在撒点方式上无限内卷。
对偶变量的收益还能用一行协方差算式说清。设 是互补位形的两次命中判定,均值估计量 的方差是:
独立重复采样时协方差为零,均值估计的方差是 。
而互补位形一内一外强烈负相关。点在圆内时它的镜像更可能在外,协方差为负,方差被压到 之下。
代价几乎为零:同一批随机数跑两份解析,样本量翻倍但有效性超过翻倍。
唯一的前提是函数对称性:互补位形的相关结构一旦被破坏(比如被积函数在 与 处行为迥异),负协方差消失甚至转正,对偶变量就只剩开销没有收益。
所有方差缩减技巧都对问题结构敏感,没有免费的午餐。
撒点实验的随机由实验者亲手制造,牌桌上的随机则由荷官发出——概率这门课最老的实验室从来不在黑板上,在牌桌里。
牌桌概率:五种棋牌结构
骰子和纸牌天然等概率、可重复、输赢立判,人类靠它们在微积分诞生之前就摸清了不少概率规律。五个棋牌游戏恰好对应五种不同的概率结构:21 点是「对手策略公开」的博弈,德州扑克是「信息不全」下的赔率计算,Yahtzee 是一张记分单上的期望规划,双陆棋是随机分支上的路线搜索,纸牌接龙则把「信息的价值」摆上了台面。所有规则口径都取自站内组件的源码,每一段数学都能在对应的游戏里亲手验证。
动手版本:21 点、德州扑克、Yahtzee、双陆棋、纸牌接龙。
21 点
站内 21 点的对局规则是一副标准教材。
每手用全新的 52 张牌洗牌发牌(洗牌用的是教科书级的 Fisher–Yates,从后往前、随机换位,这个三行循环值得单独背下来。语言基础见早年的 JavaScript 笔记)。底注 10、初始筹码 100,赢牌赔付 20(净赚 10,即 1:1),平局退注。
对局里你只有要牌和停牌两个动作。庄家翻完暗牌后按「不足 17 就要牌」机械行动。源码的循环条件是点数小于 17,所以软 17 也停。
双方发到手就 21 点时立即结算,双方都是 21 点则平局。
庄家优势从哪来?拆成三笔账。
第一笔,顺序。玩家先行动,爆牌即输,筹码当场易主。庄家根本不需要掏牌就赢了。庄家哪怕用最差的策略,「等你先爆」这件事本身就值回一大半票价。
第二笔,决策不对称。庄家策略公开、固定、零发挥空间。玩家拥有自由,也就拥有犯错的空间。赌场不怕高手,怕的是不会算的人,第二笔账的大头其实是「对手会犯错」的期望值。
第三笔,赔率。玩家两张 21 点(blackjack)在赌场赔 3:2,站内实现赔 1:1。
玩家拿到 BJ 的概率是 ,赔付从 3:2 降到 1:1,仅此一项每手期望就白损约 2.4 个百分点。组件的意义是练策略,不是模拟赌场赔率,这一点玩之前要心里有数。
还有一个对玩家有利的小口径:单副牌。
赌场通常 6–8 副牌混洗,玩家拿到 blackjack 的概率随牌副数增加而下降(小牌堆里 A 与 10 类「同行」的概率更大)。站内用单副牌把这份补贴还给了玩家,又用「每手重洗」把数牌的空间清零。
一手一洗,历史不携带任何信息,每一手都是纯粹的独立试验。
另外站内没有保险(insurance)这类侧注动作,教练的四种主动作:要牌、停牌、加倍、分牌,就是它的全部策略空间。先把主干打熟,枝叶以后再说。
爆牌风险可以直接算。
把牌值分布近似成「面值 2–9 各占 1/13、10 类(10/J/Q/K)占 4/13、A 记 1」,硬牌点数 再要一张的爆牌概率是「抽到超过 的牌」的概率:
| 硬牌点数 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
|---|---|---|---|---|---|---|---|---|---|
| 爆牌概率 | 30.8% | 38.5% | 46.2% | 53.8% | 61.5% | 69.2% | 76.9% | 84.6% | 92.3% |
比如硬 16,爆牌概率是 。六成的概率当场死亡。
这张表解释了为什么 12–16 被称为「烂手」(stiff):每一张都悬在爆牌线上。
这张表只对硬手成立。A 能当 11 也能当 1,所以软手永远不会一次爆。软 17 抽到 A 是软 18,抽到 10 是硬 17,软牌的爆牌率是零,直到它退化成硬牌为止。这正是「软」字的含义。
另一侧的教科书统计是:庄家明牌 5 或 6 时最终爆牌率约四成,明牌 A 时最低,只有一成多。
这两组数字相向而行,基本策略的全部逻辑就藏在它们的夹缝里。
基本策略
基本策略(basic strategy)的定义极简。对每一个「玩家手牌 × 庄家明牌」的组合,比较要牌、停牌、加倍、分牌四种动作的期望收益,取最大者。
上世纪五十年代起人们用计算机把这张表算到了穷尽。它是概率论里「期望最大化」最民主的应用:不需要天赋,背下来就能把庄家优势从瞎打的 2%–4% 量级压到 0.5% 上下(赌场标准规则下)。
硬牌的骨架最好记。
17 点及以上停牌。
13–16 对明牌 2–6 停、对 7–A 要。
12 只对 4–6 停。
11 永远加倍。
10 对 9 及以下加倍。
9 对 3–6 加倍。
8 及以下要牌。
背后的物理很直白。明牌 2–6 时庄家爆牌率高,你手里 12–16 的烂手不需要冒险改善,等它爆就行。明牌 7–A 时庄家大概率收到 17 以上,你必须把自己也改善到 17+,爆牌风险只能硬吃。硬 16 对明牌 10 之所以是 21 点最著名的难题,就是因为两个选项都疼,而查表给出的答案是要牌。
查表里最反直觉的一行是硬 12:对明牌 2、3 要牌,对 4–6 停。
12 的爆牌率只有 30.8%,看似可以放心停。但明牌 2、3 时庄家的爆牌率不够高,它还会一路抽到 17 以上,与其指望它自己爆,不如趁爆牌率尚低主动改善。到明牌 4–6,庄家「憋死」的概率已经足够大,烂手原地不动就是最优解。
同样是 12 点,两种活法,分界线恰好画在 3 与 4 之间。
软牌和对子的分支同样有理可循。
软 18(A,7)分三种处理。明牌 3–6 时加倍(庄家最脆弱,加注扩战果)、明牌 2/7/8 时停牌、明牌 9/10/A 时要牌。
软 13–16 则只在明牌 5–6 时冲加倍窗口。
对子方面。A,A 和 8,8 永远分。前者冲锋 21,后者把「硬 16」这张最烂的手牌拆成两次机会。5,5 永远不分,10 点是好手,当硬 10 打并伺机加倍。9,9 除明牌 7/10/A 外都分。7,7 对 7 及以下分。4,4 只对 5–6 分。
每一条都能用期望差解释,没有一条是玄学。
站内的策略教练把这堂课做成了判卷游戏。
21 点页面对局之外还有一个标着 coach 的标签页。随机发一手牌,亮出庄家明牌和你的手牌,先直接给出「建议:某动作」,你选择要牌、停牌、加倍或分牌后立刻判卷,答错会告诉你最佳打法是什么。
加倍按钮只在两张牌时出现,分牌按钮只在对子时出现。
教练还有一条宽容规则。当建议是「加倍」而你选了「要牌」时也算对。两张牌之后加倍本来就不合法,实战里「想加倍而不能」的次优选择正是要牌。
另外站内每手重洗、每手独立,数牌在这里无效,教练练的是查表本身的条件反射,不是记忆牌堆。
德州扑克
站内 德州扑克是个「没有下注的扑克」。
三家(你加两家),从底牌、翻牌三张、转牌、河牌一路发到摊牌。摊牌时每人从自己的 2 张底牌加 5 张公共牌共 7 张里选最佳 5 张,实现方式是把 C(7,2) = 21 种「弃两张」的组合全部枚举逐一评型。牌型判定连 A-2-3-4-5 轮子这种最小顺子和多人平局分池都处理了。
没有下注,它就不是扑克,而是发牌与评型的试验台。正好用来练两件纯数学的事:数 outs 和算赔率。
outs 的定义:还没出现、能把你变成赢家的牌的数量。
翻牌之后未知牌有 52 − 2 − 3 = 47 张。
数 outs 有一条基本卫生要求:不重复计数。听同花又带两头顺的复合听牌,9 张花色补牌里有几张同时能凑顺,直接 9+8 就把同一张牌数了两遍。outs 是「能救你的不同牌张数」,不是两个愿望相加。
翻牌后听同花(手里两张、桌上两张同花色),补牌是剩下的 9 张同花色,两街内至少来一张的概率是:
只看转牌一张则是 9/47 ≈ 19.1%。
两头顺听牌有 8 个 outs,同样的算法约 31.5%。卡顺只有 4 个 outs,约 16.5%。
实战没人现场算组合数,口算用的是「2/4 法则」。还有两街时 outs 乘 4,还有一街时乘 2。
对照精确值:9 个 outs 乘 4 得 36%,真实 35.0%,略高估一点。一街乘 2 得 18%,真实 19.1%,略低估一点。
误差控制在一两个百分点以内,换来的是牌桌上三秒钟的判断速度。近似的误差方向都清楚,用起来才有底。
底池赔率把概率换算成价格。
你面对的决策是「跟注 X 才能继续」,则需要的最低胜率是 。
例:底池已有 100,对手下注 50,你跟注 50,跟注成本占最终底池的比例是 50/200 = 25%。
此时同花听牌(35%)远高于门槛,跟注期望为正。卡顺(16.5%)低于门槛,弃牌。
这就是「价格与概率」的对照实验。同一个听牌,在便宜的底池里是资产,在昂贵的底池里是负债。
至于隐含赔率。后续街还能骗到的钱会放宽门槛。它是这个模型的合理外推,但把它当每次跟注的万能借口,就是把外推当成了定理。
在组件里做这个实验的姿势:反复发牌,专门盯听牌局。翻牌落下后先数 outs、先估概率,再看转牌与河牌揭晓答案,拿现实校准直觉。
没有筹码的德州恰好把心理全部剥离,剩下一台纯粹的频率校准器。等这些数字长进直觉,带上下注的牌局里你才有资格谈「感觉」。
Yahtzee
Yahtzee 的概率结构完全不同于对抗性牌局。没有对手施压,你是独自面对 5 颗骰子和一张只能用一次的记分单。
站内 Yahtzee的规则口径。每轮 3 掷、首掷后可点选锁骰,13 轮填完 13 个类别。上区 1–6 按同面点数求和。三条、四条按全部五颗骰子的点数和计分。葫芦 25 分(站内实现把五条也算作葫芦,源码里有一个专门的五条分支)。小顺 30 分(排序去重后含 1234、2345、3456 任一段)。大顺 40 分。五条 50 分。chance 记总点数。
记分单会在每次掷完实时预览「现在记进每个空类能拿几分」。这个预览把期望计算从脑内搬到了界面上,等于游戏替你完成了半个查表。
单掷概率是所有规划的地基。5 颗骰子共 种等可能结果:
| 牌型 | 组合数 | 概率 |
|---|---|---|
| 五条 | 6 | 0.077%(1/1296) |
| 四条及以上 | 156 | ≈ 2.0% |
| 三条及以上 | 1656 | ≈ 21.3% |
| 葫芦(站门口径) | 306 | ≈ 3.9% |
| 小顺 | 1200 | ≈ 15.4% |
| 大顺 | 240 | ≈ 3.1% |
大顺为什么是 240?两条序列(1–5、2–6)各 5! 种排列,。概率低到一整局也未必见一次,这正是它值 40 分的原因。
而五条的 1/1296 也有翻身的办法。三掷连抽、每掷锁住最多的同面骰,最优策略下掷出五条的整体概率约 4.6%。机会次数是最好的杠杆,三掷不是三次独立的 1/1296,而是一棵带决策的树。
期望最大化在这个游戏里有一个极干净的样本。
首掷出 4,4,4,x,y。锁住三条、只掷剩下两颗,「三条」类按总点数计分,期望恒等于 (两颗骰子的期望和是 7)。也就是说这个动作在三条类别上期望一分不降,却白赚了升级为四条的概率 和五条的 1/36。
期望不降、上行的方差白送,这种「免费期权」在骰子游戏里随处可捡,前提是你会算。
反过来,若三掷已尽,4,4,4 记进三条就是实打实的 12 + x + y,没有期望可言,只有账面。
同一个思路换把骰子再验一遍。锁住 (6,6) 重掷三颗,chance 类无论掷出什么都是 12 加那三颗的和,期望恒为 。「按总点数计分」把运气在期望里平均掉了。
而三条类有 的概率从 0 升起,四条与五条是更远的尾部。
同一把骰子落在不同记分类别里就是不同的随机变量,选类别就是选随机变量的形状。
还有一个规则塑造策略的绝佳案例。
经典 Yahtzee 规则里,上区(1–6 六类)总分达到 63 分奖励 35 分,于是「保六冲奖」成为最优策略的一部分,早期甚至要为凑 63 牺牲下区的机会。
站内实现没有这条奖励,于是每一类直接按期望分贪心即可。
规则删掉一条,最优策略的结构就换一套。期望问题最迷人的地方在于它对规则字眼极端敏感。
同类思路还有机会成本。13 个类别每类只能记一次,「把 9 分记进六点类」还是「记进 chance」不是 9 对 9 的比较,而是 9 对 chance 剩余轮次的期望。
记分单是资源,填错格子比掷出坏骰更伤。
双陆棋
双陆棋是五款游戏里唯一「掷骰 × 走子」交织的。
白方从 24 走向 1,黑方从 1 走向 24。每掷两骰各驱动一步(一子走两步或两子各走一步,顺序自选),掷出对子走四步同点。
落单的棋子(blot)被对方落到同一点就命中,回中柱重来。中柱上的子必须先重新进入对方内区,其他什么都不能动。
本方棋子全部进入内区后才能按骰点出场,且有一条苛刻的收尾规则:目标点位无子、更高点位仍压着本方棋子时,不能跳过出场。这条「必须先清高位」的规则是残局最大的坑,源码里的拒绝逻辑写得明明白白。
组件还内置了一份四卡片的玩法帮助(棋盘方向、走子、击子与中柱、出场),新手可以直接照着下。
骰子给了 36 种等可能的组合,对子占 6/36 = 1/6,却给四步。把方差极大化的设计:对子约等于一次免费的双倍行动。
分支爆炸发生在走子上。两骰先后顺序不同、中间落点不同,局面就完全不同。
组件的黑方内置两套引擎。默认一档按「能击子就击子」的贪心走子。代码里另有一档启发式引擎,把所有可行走子序列整棵枚举,对每个终局面打分后选最优执行。
它的评估函数值得抄一遍。pip 差(进度)+ 15 × 已出场棋子(接近胜利)+ 对方每颗中柱子 20(重击对手)− 自己每颗 blot 的直接命中数 × 4(风险)+ 内区每个成对点 3(防御),五项加总。
把「进度」和「风险」换算成同一货币,这就是局面评估的全部秘密。
直接命中表是双陆棋概率论的入口。距离为 d 的落单子,下一掷能被命中的骰型数(共 36):
| 距离 d | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| 命中骰型 | 11/36 | 12/36 | 14/36 | 15/36 | 15/36 | 17/36 |
这张表不是单调的。d=2 比 d=1 更危险,因为 (1,1) 也能凑出 2。d=3 多出 1+2 两种顺序和 (1,1) 走三步。d=6 凑到 17/36 靠的是 3+3、2+2+2 与直接的 6。组合方式越多,命中越容易。
距离 7 以上两骰必须搭配使用,概率骤降到 6/36 及以下。
所以「blot 放在离对手 6 格以内」是明码标价的风险。而评估函数里那个 ×4 的权重,正是这笔期望账的粗化系数。被击回中柱的一颗子要按 pip 计数重走 25 步(pip 计数里中柱记 25,即离出口最远的距离),它的期望代价 ≈ 被击概率 × 它已走过的路程。
风险定价不必精确,量级对了就够赢。
两个没搬进组件的规则也值得一提。
其一是倍数方块(doubling cube)。现实赌局里最深的一层博弈。用「你敢不敢提议加倍」把竞速的 pip 差升级成心理战,站内实现没有它,AI 评估也就只剩走子这一层。
其二是中柱封锁。中柱有子时必须先从对方内区重新进场,若对方把六个进入点位全部筑成对子,重进无门、整轮作废。源码在找不到任何可行步时会直接跳过回合,极端局面下你会在中柱上看完对手清盘。
纸牌接龙
纸牌接龙是五款游戏里唯一「单人 + 完全隐藏信息」的。
站内是标准 Klondike 阵型。7 列牌堆,牌堆每次翻一张,弃牌堆翻完后整叠翻回继续(不限轮数)。列内红黑交替降序,空列只收 K。基础堆按花色从 A 垒到 K,双击一张明牌可自动送入基础堆。四堆各满 13 张获胜。
随机发牌的接龙引出一个微妙的问题:一局牌「理论上可解」与「你能不能解出来」是两回事。
把暗牌全部翻开、让程序带着完美信息去解,统计上大约八成的局面有解。真实玩法里,24 张暗牌和牌堆顺序对你完全不可见,普通玩家的长期胜率往往只有一两成。
输给的不是运气,是信息。接龙本质是「部分可观测」下的决策:每一步都要在「走得更顺」和「看清更多」之间定价。
这也解释了那条最硬的启发式:优先翻暗牌。翻开一张暗牌同时增加信息和可行步,是把隐藏信息变公开的直接投资。反之,在已翻开的牌之间来回倒腾只是消耗随机性,不产生任何认知收益。
无限回收听起来宽厚,其实牌堆的顺序从头到尾没变。翻完翻回、再翻还是同一列。回收是循环队列,不是重新洗牌,转完一圈之后不再产生任何新信息。
这也让记牌在接龙里格外值钱:完整看过一遍牌堆的人,第二轮是在读一本已经读过的书。
站内实现还有一处与传统 Klondike 的关键差异,玩之前必须知道:基础堆只进不出。源码里不存在任何从基础堆取回牌的路径。
传统规则允许把基础堆的牌拿回来垫用,许多局面靠这一手续命。这里不行。
于是「双击自动送上基础堆」这个顺手的操作,变成了一个需要过脑子的承诺:这张 3 以后还垫不垫得到?
规则的一个小改动,直接反转了贪心的方向。
加上「空列只收 K」这条硬约束(没有 K 候补就开列等于白送一格。空列是稀缺资源,K 是它的唯一门票,这也是新手爱开空列、老手惜列如金的原因),站内这副牌虽然用了翻一张加无限回收的宽松变体,该卡死的局面照样卡死。
约束才是这个游戏真正的骨相。
骰子和纸牌之外,一张桌游规则表本身就是概率的分配方案:有的游戏把命运整个交给骰子,有的只给骰子留一条窄缝。下一章把五个桌游组件摆上同一根「运气数轴」。
运气曲线:桌游里的概率分配
骰子是桌游的心脏,但每款游戏给这颗心脏留的血管粗细不同。有的游戏里骰子就是命运本身(蛇梯棋),有的游戏里骰子只是发牌器,真正比的是你如何应对(海战棋)。
「运气占比」不是玄学,它可以从规则里读出来,也直接决定一款游戏的重开欲望。本章用站内五个桌游组件把这根数轴摊开:两个掷骰竞速、一个信息搜索、一个博弈论纯品、一个记忆游戏。规则口径全部取自源码。
运气占比
一款桌游里「运气说了算」的成分有多大,可以用一个可操作的定义量出来:随机事件落定之后,玩家还剩多少决策能改变结果的分布。
蛇梯棋在这根数轴的最左端。掷完骰子没有任何选择,棋子自动走、梯子自动爬、蛇自动滑,运气占比是百分之百。
飞行棋往右挪一格。骰子照样主导,但四枚棋子选哪枚走是一个真实决策。
海战棋再往右。骰子干脆退场,比的是搜索效率。
石头剪刀布最特殊。决策自由度极大,可在特定对手面前期望恰好归零。
西蒙说没有对手、没有对抗性随机,随机只藏在系统逐轮追加的序列里,剩下的全是记性。
运气占比直接决定一款游戏的重开欲望结构。
全运气游戏输了不内伤。输给的是骰子不是自己,重开没有心理成本,但代价是单局必须极短。蛇梯棋一分钟一局正相配。
高决策游戏输了会复盘,重开之前先难受一会儿,可它用「下一局我能打得更好」的信念把玩家留得更久。
有一个思想实验可以给这根数轴定标:把同一局的所有随机结果原样录下来,换一个玩家重放,结局会变吗?
蛇梯棋的结局一模一样,随机结果唯一地决定了结局。飞行棋会变,走子选择不同结果可能翻转。海战棋几乎面目全非,每一炮都换了一个世界。西蒙说的序列不变,但人类记忆的上限就是关卡的天花板。
结局对随机种子的敏感度,就是运气占比的定义。
牌桌是概率最老的实验室。上一章把 21 点与德州扑克拆过一遍(牌桌概率),这一章把骰子、棋盘和猜拳搬上同一张解剖台。
蛇梯棋
蛇梯棋的规则一句话说完:掷骰子从起点走向 100,落在梯子底往上爬,落在蛇头往下滑,超出 100 的步数作废,恰好站上 100 获胜。
源码里默认棋盘摆了 8 架梯子、8 条蛇。最长的一架梯从 42 直上 63(+21),最毒的一条蛇从 95 垂到 72,离终点 5 格的地方被拽回去 23 格。还有一架梯子从 88 到 97,让你贴着终点线起跳。
玩家唯一能做的事是点「掷骰」。经典模式下蓝方(电脑)甚至自动掷骰。
把这个游戏数学化,它是一条吸收马尔可夫链。状态就是当前格子的编号,下一步转移到哪只依赖当前格子,也就是马尔可夫性。掷出 1 到 6 各以 1/6 的概率前进,落到梯脚或蛇头立刻被确定性重映射到另一格。100 是唯一的吸收态。
整条链没有一个决策点,于是所有经典问题都变成纯计算:平均几局能到 100、方差多大、哪条蛇对局长的贡献最大。一阶分析列出百来个未知数的线性方程组即可解出。
蛇梯棋在概率课堂上的角色从来不是「游戏」,是「带确定性重映射的随机游走」的教具。
不过有一个角落值得手算:终盘的几何分布。
站在 99 时,只有掷出 1 才能获胜,其余五个骰面全部「超出回退」原地踏步,命中率 1/6。站在 98 时只有 2 点能赢,同样 1/6。
位置越贴近 100,浪费的骰面越多。最后一格的等待和后面飞行棋里等六点是同一条分布。区别在于蛇梯棋里你连「等」都只是看着:运气曲线左端的游戏,连等待都是全自动的。
两个具体细节可以再抠一抠。
其一,开局反而藏着一手好运。站在 0 时掷出 3 恰好踩中 3 号梯脚,一步跳到 22。第一掷就有 1/6 的概率白赚 22 格。
其二,源码里的重映射只走一层。落点被换到新格之后,不再检查那格是不是又一个梯脚或蛇头。默认棋盘的布线也刻意避开了连环:没有一架梯子把你送进蛇嘴,也没有一条蛇把你吐到梯子上。转移矩阵因此是「一步式」的,整个过程的数学保持干净。
大方差则是蛇梯棋娱乐性的来源。期望局长不长,方差却巨大。95 → 72 一条蛇足以让临门一脚变成马拉松,落后一整圈的玩家也等得到翻盘的两架梯子。运气占比 100% 的游戏里,「永不绝望」是数学保证的。
进阶模式是这个组件真正有意思的地方。双人热座之外附送一个棋盘编辑器:点任意格子放置或移除梯子与蛇(梯子向上 10 到 29 格,蛇向下 10 到 29 格),一键随机布局,改完点「开始游戏」按自定义棋盘竞速。
这等于把马尔可夫链的转移矩阵交给玩家改写。放一条 95 → 72 的毒蛇,还是把出口改成贴着终点的直达梯?改规则比玩游戏更上头。它把「运气曲线」本身变成了创作材料。
飞行棋
飞行棋沿环形跑道竞速:52 格环形轨道,四方各 4 枚棋子,红方由玩家操作,其余三方是 AI。
源码规则里最值得算的是起飞条件:棋子必须掷出 6 才能离开基地。
这立刻给出一条几何分布。第 次掷骰才首次出现 6 的概率是 ,期望恰好 6 次。
一枚待命棋子的平均起飞等待是 6 次掷骰。四枚棋子全压在基地里的开局,玩家大概率要连吃几轮「无子可动,已跳过」。飞行棋前几分钟的煎熬不是体验问题,是这条分布的直接投影。
6 还有第二个身份:掷出 6 额外奖励一次掷骰。连锁的「再掷一次」本身也服从几何分布。6 在规则里同时扮演钥匙、燃料和彩票。
终点前还有一道精确闸门。每方在环上走 51 格后转入己方 6 格终点跑道,必须恰好点数进巢,超出不算。终盘的「等一个点」和蛇梯棋的终盘等待又一次共享了同一条分布。
但飞行棋和蛇梯棋有一处本质区别:多子结构稀释了运气占比。骰子掷完之后,走哪枚棋子是真决策。
源码里环上摆着 8 个星形安全格(四方起点都在其中),站在安全格上的棋子不会被吃。踩到非安全格上与敌子同格,则把对方整枚送回基地、从头再来。
要不要冒险贴着前方的敌子落点?该催促进最多的那枚还是先起飞新子?这些选择每个回合都真实存在。
三个 AI 的决策则是公开的固定贪心:优先吃子(计 10 分)、其次起飞(1 分)、再比谁最靠前,按分排序取最高。可预测的对手让玩家的每一步选择都有的放矢。
同样一颗骰子,在蛇梯棋里是命运,在飞行棋里只是发牌器。
棋盘的微观规则里还有几处值得留意的取舍。
星形安全格一共 8 个,间隔在 5 到 8 格之间交替。无论棋子绕到环上哪一段,附近总有一处避难所。
吃子必须与敌子恰好同格,差一格都算擦肩而过。
终点跑道里掷大了不能硬闯,超出的点数直接让这步作废。四枚棋子挤在门口等同一个点数的画面,是飞行棋最诚实的运气教学。
源码也没有实现「叠子挡路」的变体规则,同色棋子可以安然叠在同一格。
每一条删繁就简的规则都在做同一件事:压缩棋盘上可争论的巧合,把对抗集中到「吃与被吃」这一个焦点上。
AI 的回合里还藏着一道保险丝:连掷六点的循环带上限,一口气最多连走六次,防止极端的手气把对局节奏撕碎。
海战棋
海战棋里骰子彻底消失,运气的残余只剩开局那一注。
双方舰队自动随机布置:5 艘舰,长度 5、4、3、3、2,共占 18 格,随机横竖、只禁重叠、不禁相邻。此后整局是一个纯粹的不完全信息搜索问题。10×10 的敌阵在你眼里是一张均质的灰图,每一炮只返回「命中」或「未中」两个字的答案。
先看对手。源码里的 AI 开炮是均匀随机撒点。在你的盘面上随机挑格子、避开打过的,仅此而已:没有命中后的追击逻辑,没有概率密度图,击沉一艘舰也不会自动排除周边格。
均匀随机的对手意味着它的「策略」是固定的,整局游戏的全部方差都来自你的搜索质量。这就是运气数轴最右端的形态,运气只剩布阵那一注。
玩家的策略有一半可以严格证明。
最小的舰长 2 格,而任何横竖摆放的 2 格以上的舰必然同时覆盖棋盘黑白相间染色中的一黑一白。所以只打单一色系(至多 50 格),就保证每艘舰至少挨一弹。这一刀把最坏情况下的首次触舰成本从 100 格砍到 50 格。
命中之后策略切换为延伸:沿行列方向继续试探,确定舰的朝向,打到击沉为止。击沉之后再把这艘舰可能占据的周边格从心里划掉。
还要注意回合结构:双方每轮各打一炮,命中不加时。海战棋没有「乘胜追击」的额外奖励,优势只能一轮一轮攒。
每一次射击的答案都让信息集收缩。海战棋的教学价值在于把「问对问题比问得多重要」做成了可玩的界面:同样是一炮一个比特,打在可能性集中的格子上,这一个比特就值半支舰队。
这笔搜索优势还能定量算出来。
AI 不重复地均匀开炮,等于把你的 100 个格子排成随机序列逐个翻开。你共有 18 个舰格,AI 要全部找齐才算赢。随机排序里最大位置的期望是 ,代进去得 炮。
也就是说,均匀随机的对手平均要打满 96 炮才能全歼你。而采用视差加延伸的玩家,通常几十炮就能收工。
这近百炮的差距不是运气好,是信息集管理的复利。每一炮都比随机少浪费一点可能性。
石头剪刀布与西蒙说
数轴上最后两个点放在一起讲,它们各自代表一种极限。
石头剪刀布的电脑每局从三种拳里均匀随机出一个。这恰好就是猜拳的混合策略纳什均衡 。
均衡的含义在博弈论里有精确表述:面对均衡策略,对手采用任何纯策略或混合策略的期望收益都相同(这里是零),无法被剥削。
所以这个组件是一场注定打平的对话。你读不到电脑的「心理」,因为它没有心理。你的任何套路——连出三把石头、赢了就换、输了不换——期望值都是零。
连胜计数器于是成了纯随机涨落的展示窗。对均匀随机的对手,单局胜、负、平各占 1/3,三连胜的概率是 ,连赢五把约 。屏幕上那个数字没有任何技术含量,但它确实让人心跳加速。
真正有营养的猜拳只发生在两个人类之间。人不是好的随机源,回避连出、交替过多,偏离均衡才会被剥削。均匀随机的 AI 恰好把「读心」这门手艺整个没收了。
反过来推也成立。一旦对手偏离均匀,比如出石头的概率涨到 ,最优应对立刻存在且立刻赚钱。改出布的期望是 ,只要 就是正收益。
均匀是猜拳唯一的不可剥削点,偏一点都不行。组件把 AI 钉死在这个点上,等于宣告这场对话在数学上已经结束,剩下的只是抽签仪式。
连胜计数器还有个小性格:平局不清零,只有输才清零。连胜纪录对「平」格外宽容,仿佛它也知道平局不欠谁一个交代。
西蒙说是另一种极限:没有对手、没有回合制对抗,随机只出现在一个地方——每轮往序列尾部追加一个四选一的颜色。玩家复述整段序列,错一个即终局。
盲目乱猜能活过第 轮的概率是 。序列空间 按轮指数膨胀,每加一个颜色恰好多压 2 比特进你的脑子。
人类的对策是组块化:把颜色串按节奏切成短组再存储。经典心理学结论(短时记忆约 7±2 个组块)在这里直接变成关卡天花板。
这个组件还藏着一条平方时间税。每轮都要完整重播序列,每个颜色亮 400 毫秒、步进 650 毫秒。打到第 轮,光看重播就累计花了约 秒。第 12 轮时约 51 秒,而此刻一次手滑就把这一切清零。
时间成本随深度平方增长,记忆压力线性增长,失败却瞬时结清。西蒙说的重开欲望在五个组件里反而最强,因为它把失败的归因百分之百留给了你一个人。
四个色块各配了一个音高(E4、A4、C#5、E5),重播时视觉与听觉同步输入。同样的序列多走了一条感官通道,组块化的效率因此更高。多通道编码是免费的记忆增幅器,组件用了它,却从不在界面上解释。
先手、重开与那条窄带
五个组件还共享一个容易被忽略的参数:谁先动手。
蛇梯棋红方先掷,飞行棋红方先行,海战棋你先开炮。三个竞速或搜索类游戏全把先手送给了玩家。
先手在竞速型游戏里是真实优势,而且算得清楚。设两家的完赛轮数是同分布的随机变量 ,先手胜率为 ,即 ,永远严格过半。
连完全对称的蛇梯棋,先手也白赚半步。
商业桌游为此发明了一整套补偿机制:后手补偿资源、轮换先手、竞标买先手。站内组件选的是最省事也最讨喜的方案:先手直接给你,输了你也没借口。
「洗牌」在数轴上的位置同样讲究。
海战棋把布阵整个交给随机数。你连布阵的偏方都无法带进局,双方在期望上拿到等价的舰队分布。
蛇梯棋进阶模式的「随机布局」按钮则更进一步,把洗牌扩大到整个转移矩阵。
洗牌的数学意义是把运气从过程噪声前移成初始条件的抽样。开局期望公平了,但策略深度也被削掉一层。布阵时的每一个偏方,原本都是老玩家的私有财产。
蛇梯棋一分钟一局,输了只怪骰子,「再来」没有心理成本。
飞行棋输了可以怪骰子也可以怪走子,复盘有一半素材。
海战棋输了几乎全是自己搜索得不够好。
运气占比越低,单局的心理重量越大,游戏就越依赖「我能变强」的信念留人。石头剪刀布把胜负整个交给噪声,却用连胜计数器给噪声编了剧情。西蒙说没有骰子也没有对手,却让人一遍遍重来,因为每一条失败都指向唯一的责任人。
骰子承担得越多,玩家越轻松;玩家自己承担得越多,游戏越上头。
以上全在分配和消耗随机;信息熵回答的是更基本的问题:不确定性本身值多少。下一章用同一把熵尺量两个方向相反的问题——压缩与猜谜。
信息熵:压缩与猜谜
信息熵 H 衡量不确定性:一个随机变量的每个可能取值贡献”惊喜程度” log₂(1/p),按概率加权平均就是熵——平均而言,你最少需要多少 bit 才能表达或确定这个变量。
这把尺子量得出两个方向相反的问题:一个是压缩,文本的分布是已知的,怎么把编码长度压到贴近熵的下界(霍夫曼编码);另一个是猜谜,答案是未知的,怎么用最少的提问把它逼出来(Wordle 的最优策略)。本章把两个应用一次讲完。
上半场:霍夫曼编码
从定长编码的浪费说起
ASCII 给每个字符 8 bit,等长、整齐,但对自然语言是浪费。
一篇文章里 ‘e’ 出现几十万次,‘z’ 出现三次,它们却占同样多的空间。
压缩的根本思路就一句话:让常见的字符用短编码,罕见的用长编码。
但”长短搭配”立刻撞上一个致命问题:解码时怎么知道边界?
如果 A=0、B=1、C=10,收到 “10” 是 B+? 还是 C?
编码 A=0、B=01 时 “01” 永远悬空。
要正确解码,编码集必须满足一个性质:任何编码都不是另一个编码的前缀(前缀码性质)。
这样从左到右读,每读到一个完整编码就无歧义地切分。
贪心构造
1951 年,MIT 的研究生 David Huffman 在一门课的期末作业里(据传是为了免考试)发明了这个构造,从此”频率高码长短”从直觉变成了可证明的最优解。
今天 ZIP、JPEG、MP3 的内层都有它的身影。
构造简单到可以口述:
- 统计每个字符的频率,各自作为单节点树入队列;
- 取出频率最小的两棵树,合并成新树(频率 = 二者之和),新树放回队列;
- 重复直到只剩一棵树。这棵树的每个叶子是一个字符,左走 0 右走 1,从根到叶子的路径就是该字符的编码。
为什么这个贪心是对的?
核心是交换论证,两步:
- 最稀有的两个字符一定在最深层。假设最优树里频率最小的字符 x 不在最深层,而某个高频字符 y 在深层。把 x 和 y 的编码对调,总长度只会更小或不变,与最优矛盾;
- 最深的两个叶子是兄弟。若最深层只有一个叶子,删掉它的兄弟层、把该叶子上移一层,编码更短,仍与最优矛盾。所以最优树的最深层至少有两个叶子,把它们配成兄弟不损失最优性。
“合并最小的两个”恰好强制实现了这两条,于是每一步都维持最优子结构。
贪心成立。
这是贪心算法证明的标准范型:交换论证 + 最优子结构。
一个最小的例子
拿四个字符把流程走一遍。
设频率 A=8、B=4、C=2、D=1(合计 15):
- 最小的两个是 D(1) 和 C(2),合并成 3;队列里剩 A(8)、B(4)、新(3);
- 最小的两个是新(3) 和 B(4),合并成 7;队列里剩 A(8)、新(7);
- 合并 A(8) 和新(7),树成型。
每条左边标 0、右边标 1,得到的码表:A=0、B=10、C=110、D=111。
频率最高的 A 只花 1 bit,最稀有的 D 花 3 bit。
平均码长 (8×1 + 4×2 + 2×3 + 1×3) ÷ 15 = 25/15 ≈ 1.67 bit。
定长编码四个字符要 2 bit,这里省了约 17%。
树本身也值得看一眼:两个稀有字符被推到最深层共享一棵子树,交换论证的两条结论(最稀有的在最深、最深的成兄弟)在结果里肉眼可见。
顺手算一下这个分布的熵:H = −Σp·log₂p ≈ 1.64 bit。
霍夫曼的 1.67 只比物理下界多 0.03。小字母表上整数取整的代价微不足道。
分布越丰富、字符越多,“每个码长必须是整数”浪费的相对占比越大,这正是大字母表上算术编码/ANS 的优势来源。
与熵的下界对账
信息论给出了压缩的物理下界:二进制前缀码的平均码长 L̄ 满足
H 是字符分布的香农熵。
霍夫曼编码在这个区间内。它是”按整字符编码”的世界里的最优,但一般到不了 H,因为:
- 霍夫曼码长必须是整数。熵的最优分配常是分数(比如某字符理想码长 2.3 bit),霍夫曼只能给它 2 或 3,每次取整都漏一点;
- 它只看单个字符的频率,字符间的相关性(‘q’ 后面几乎必是 ‘u’)完全没利用。
补齐这两块的正是后续技术:算术编码允许分数码长,把 L̄ 推到几乎等于 H。上下文建模(PPM、LZ 系列)利用相关性。
现代压缩器(如 DEFLATE)是”LZ77 找重复 + 霍夫曼编码剩余”的组合拳。
霍夫曼不单独打天下,但始终在场。
这条下界线的近况:算术编码早年间因专利问题普及受限,专利到期后又遇到了更新的对手。
**ANS(不对称数系)**以接近算术编码的压缩率、低得多的计算开销,成了 Zstandard 等现代压缩器的核心熵编码。
霍夫曼没被挤走的原因很朴素:码表小、解码快、实现简单,在”够用且快”的场合仍然是最顺手的那把刀。
工程细节
树形解码对硬件和高速解码器不友好(要逐位走树、缓存不友好)。
工程上实际部署的是范式霍夫曼码(Canonical Huffman):不存树,只存每个字符的码长,然后按固定规则(同码长按字典序连续分配)重排编码。
解码器用一张小的”首码-偏移”表逐级推进,分支预测友好,这也是 DEFLATE 在 zlib 里采用的形式。
另一个工程点:码表本身要随数据传输。
对短报文,码表的开销可能吃掉压缩收益。
这就是为什么几十字节的字符串压缩后反而更大(还有头部开销),压缩算法都有”最小划算长度”。
码表从哪来也有一层取舍。
最直接的是两遍扫描:第一遍统计频率,第二遍编码,代价是文件得先整个过一遍,流式场景不友好。
自适应霍夫曼(FGK、Vitter 算法)改成单遍:编解码双方同步从一棵初始树出发,每处理一个字符就同步更新树形状,省掉预先统计,代价是树更新逻辑复杂得多。
DEFLATE 选了折中。LZ77 先把重复消灭大半,剩下的字节流按块统计频率、逐块出霍夫曼表,统计窗口小到可以接受。
动手实现要点
自己写一遍(百行以内的事)会踩到四个具体问题,先打预防针:
- 用堆,别用数组扫最小。每次取两个最小树用小顶堆 O(log n),全程 O(n log n)。数组线性扫是 O(n²)。对几万字符的统计也能跑,但没必要;
- 频率相同的并列怎么排。合并顺序不同会得到形状不同、平均码长相同的树(最优树不唯一),但编解码双方必须用同一棵。规则要写死(比如并列时按字符序),并在码表里带上足够信息让解码方重建出同一棵。这也是下一个要点的动机;
- 码表存”码长”而不是”码”。存每个字符的码长(范式霍夫曼),解码方按固定规则重建编码,码表从”字符 + 二进制码串”缩到”字符 + 一个小整数”。这就是范式霍夫曼在工程里无可替代的原因;
- 位打包与尾部填充。码流是位流,落盘前要按字节打包。最后一个字节的空位要么记录有效位数,要么约定填充值。漏了这步,解码端会在文件末尾多读出”幽灵字符”。DEFLATE 把码长限制在 15 位以内,保证任意码都能塞进半字节级的字段。
拿一段真实文本(比如本文)输入霍夫曼树可视化,看三件事:高频字母的码长是不是最短、树的形状是不是”频率越深越靠下”、以及把文本换成分布越平均的文本。熵越高、压缩率越差。
压缩能赚的,只有分布里”不均匀”的那部分钱。
下半场:Wordle
压缩是把分布的”不均匀”花在编码长度上;猜谜是反过来,主动制造提问,把答案的不确定性一口一口吃掉。
Wordle 的规则:六个机会,猜一个五字母单词,每次猜测后用三种颜色反馈。绿色表示字母正确且位置正确,黄色表示字母存在但位置不对,灰色表示字母不在答案里。
规则简单,但”下一个词该猜什么”是个严肃的数学问题。
先分清两个池子
策略空间的一半来源在于:答案池和猜测池不是同一个池子。
原版 Wordle 的答案表约 2300 个常用词,但”允许输入的合法猜测”约 13000 个。
你可以猜任何合法五字母词,答案只会从中选常用词。
于是”拿一个不像答案的词去换信息”(后文的域外词打法)天然合法:猜测词只要切得碎,不需要像答案。
很多手工玩家不知道这一点,一直把候选当猜测、白白放弃了近 6 倍的提问空间。
把猜测看作一次”提问”
关键视角转换:一次猜测不是在猜答案,而是在向答案池提一个问题。
反馈的颜色组合就是这个问题的答案,它会把 2000 多个候选答案切成若干堆。你下次只需要在反馈对应的那一堆里继续找。
比如答案池里有 guess 和 guest 两个词,你猜 gauge:如果答案是 guess,反馈是”绿绿灰灰灰”式的某种组合;如果答案是 guest,反馈会不同。
猜得好,意味着这个”问题”能把答案池切得越均匀、越碎越好。每一堆都小,下一轮选择就少。
信息熵:衡量”切得多碎”的尺子
信息熵恰好就是干这个的。
对某个候选猜测 g,设它把答案池切成 k 堆,第 i 堆里有 nᵢ 个词,池子总数 N,那么这次猜测的期望信息量(以 bit 计)是:
直觉读法:如果一次猜测把 2000 个词平均切成两半,每堆 1000 个,熵是 1 bit。你只获得了”二选一”的信息量。
如果切成 100 堆且每堆大小接近,熵就接近 log₂100 ≈ 6.6 bit,一步就把候选从上千压到大约 20 个。
五字母反馈总共只有 3⁵ = 243 种模式(三种颜色、五个位置),所以理论单次熵上限是 log₂243 ≈ 7.9 bit,实际远达不到,因为字母模式不是均匀分布的。
拿”总量”对一下账更能看出游戏的宽裕程度:候选池约 2300 个词,全部不确定性是 log₂2315 ≈ 11.2 bit。
好起手一次拿走 5 bit 上下,第二步按池计算再拿 4 bit 左右。理论上两次猜测后剩余候选就应该是个位数。
实际会有波动,但这就是为什么公开的求解器成绩普遍稳定在平均 3.5 步左右解出:六个机会不是六次运气,是三倍于需求的信息预算。
写反馈比较逻辑时,重复字母是最常见的 bug 源。
答案里只有一个 R 时,猜测里有两个 R,第二个 R 不能标黄。
Wordle 官方规则按”从左到右消耗绿色、剩余按次数给黄色”处理,手写时要专门构造带重复字母的用例测一遍。
暴力算一遍:起手词排行榜
计算流程是机械的:对每一个合法猜测词 g,对每一个候选答案 a,计算 g 对 a 的反馈模式,按模式分桶,套上面的公式。
这个计算量大约是 13000 × 2300 次反馈比较,任何脚本语言几秒钟就能跑完。
公开的各类分析(不同作者用的答案表大小略有差异,具体数值会上下浮动)结论是一致的:
- 好起手词的期望剩余候选大约在 60–100 个的量级,对应约 5 bit 上下的信息量。一步就砍掉了 95% 以上的可能;
- 排名靠前的常见词包括 SLATE、CRANE、SALET、TRACE、SOARE、RAISE 这一类,它们的共同点非常明显:覆盖高频字母,且字母互不重复。英语里 E、A、R、O、T、S、I、L、N 的出现频率最高,五个位置全是高频字母、没有重复浪费的词天然占优;
- 反例也一致:像 JUJUS、FUFFY 这种重复字母加低频字母的词,熵值低得可怜。它们对绝大多数答案只能切出很小的信息量。
需要强调:“哪个词第一”没有唯一答案。
答案表差几百个词,排序就会重排;把”猜中答案”和”缩小候选”两个目标加权不同,结果也不同。
但”高频字母、无重复”这个方向是稳定的。
两词连招 vs 按反馈计算
一种流行策略是固定两词连招:比如先 SLATE 再 ROUND(或者 CRANE 加 POILS),两步覆盖十来个高频字母。
这种打法的好处是省脑子。不用真的计算,期望两次之后剩余候选通常也能压到几十个以内。
但它不是最优的。
信息熵框架下的严格做法是:每一步都根据当前剩余候选池重新计算熵最大的下一个猜测。
固定连招的问题在于浪费。如果第一词已经排除了所有含 R 的答案,第二个词里的 R 就纯属于信息量浪费。
实测差距通常在零点几步以内,所以对普通玩家来说,连招的性价比其实很高。
对追求平均步数极限的人来说,按池计算才有意义。
还有一个反直觉的推论:当你已经把候选缩小到两三个时,熵最大的猜测未必是候选之一。
比如剩余候选是 MATCH、CATCH、PATCH。它们只在首字母不同,此时猜一个含 M、C、P 的”域外词”一次分辨三者,比在其中赌一个的三分之一概率更划算(代价是多花一步)。
一局推演(示意)
把框架串成完整的对局流程。数字是示意性的,用来说明节奏而非真实某局。
起手 SLATE,假设反馈砍掉九成候选、剩约 60 个词。
第二跳按剩余池重算熵,选一个在”剩下的 60 个词”上切得最碎的词。它很可能不是任何候选词。反馈之后剩个位数。
第三跳要么已经是显性唯一,要么在两三个候选里做一次域外分辨。
四个跳内收工,这正是 11.2 bit 总账预言的节奏。
实战中的两条纪律值得强调:每一步都按当前池重算(第一跳的最优词,第二跳几乎必然不是),以及反馈一到手先手动划掉不可能的词(人脑的”池”要和理论的池同步)。
否则算出来的熵再漂亮也是对着一个过期的池子算的。
硬模式
硬模式(Hard Mode)要求已确认的提示必须全部用上。
这大大限制了”域外词”打法。你不能再自由地提信息量最大的问题,只能在满足约束的词里挑。
所以硬模式下:
- 起手词的选择更重要,因为回旋余地变小;
- 遇到”确认了两个元音但候选很多”的局面,容易被迫一步步碰运气;
- 一个实用的折中:在非硬模式时也尽量选不制造零碎约束的猜测,保留自己的自由度,这样切回硬模式不会水土不服。
想上手验证结论的话,直接开一局:Wordle。
熵给不确定性定了价,而素数是数论里确定性最强的对象——可把它们撒进一张网格,居然也长出统计结构。最后一个实验来自一场学术报告上的走神涂鸦。
乌拉姆螺旋:素数的条纹
1963 年,数学家乌拉姆(Stanisław Ulam)在一场冗长的学术报告里随手涂画:把 1、2、3……按方形螺旋一圈圈排开,把素数圈出来。他原以为会看到一片均匀的噪声,看到的却是对角线——大量素数沿着斜线排成清晰的条纹。素数由整除性定义,和「怎么摆放」毫无关系,却在一种最朴素的二维排列里露出了结构。本章把这个现象讲清楚:对角线从哪里来、和二次多项式 有什么关系、生成这张图需要什么工程,以及这个玩具式的可视化为什么能持续生产严肃的数论猜想。
构造规则
方形螺旋的构造规则一句话说完。从中心 1 出发,向右一格、向上一格、向左两格、向下两格、向右三格……同一长度走两段再加一。
于是行进段长是 1, 1, 2, 2, 3, 3, 4, 4……以中心为原点,这样排下去,每个自然数都占一个确定的格子。
前 25 个数排出来是这样(星号标出素数):
* 16 15 14 * 18 5 4 * 12 * 6 1 * * 20 * 8 9 10 21 22 * 24 2525 个数里 9 个素数,肉眼已经开始见到斜线。3 与 13 连成一条,5 与 17 连成另一条。数多了以后这些线会一直延伸下去。
这个构造立刻给出几条可以证明的几何事实,它们是后面全部讨论的地基。
第 圈(第 个环)恰好装下从 到 的所有数,环的边长是 。
奇数的平方严格落在同一条对角线上。 在 , 在 , 在 ……即 位于射线 上。
把这条对角线穿过中心延伸到另一侧,得到射线上的数列 ,通项正好是:
第 3 条值得亲手验证一遍。 时是 7(左下角), 时是 21, 时是 43, 时是 73。按构造规则逐格数过去,它们确实严格共线。
这不是巧合而是必然,下一节把必然性讲出来。
每条对角线都是一个多项式
为什么「对角线」与「二次多项式」会一一对应?先看坐标怎么算。
按段长 1, 1, 2, 2, 3, 3 的节奏沿「右、上、左、下」循环行进,坐标与数值可以同步生成:
x = 0, y = 0, n = 1, seg = 1方向按 右→上→左→下 循环每个方向走 seg 步,每步记下 (x, y, n) 并令 n 增一每走完两个方向,seg 加一现在盯着一条从中心出发的射线看。第 格必然落在第 个环上,而第 个环上的数全部夹在 与 之间。也就是 附近、跨度不足 的区间。
于是射线上第 格的数值必然形如:
的整系数二次式。主项 被环结构锁死, 和 由射线的方向决定。
螺旋有多少条射线,就有多少个这样的多项式。乌拉姆螺旋本质上是一台「把整系数二次多项式的取值摊在格子上」的机器,而素数标记让我们直接看见每个多项式的素数产出。
这个视角立刻给出两个可以验证的推论。
其一, 为奇数的射线(比如正东方向那一条 :2, 11, 28, 53, 86……)取值随 奇偶交替,一半的值是偶数,素数密度天然折半。图上表现为这些射线整体偏暗。
其二, 合适的射线整条数列有固定因子。比如穿过 的那条线除 1 以外全是平方数,一条线黑到底。
明线暗线,各自都有代数解释。
对角线富集
为什么一条对角线可能特别亮
「富集」的参照系是素数定理给的背景密度。大小在 附近的数,是素数的概率约 ,随 增大缓慢衰减。
如果多项式取值「表现像随机数」,它落在对角线上的素数频率就该贴着这条背景线。富集意味着偏离随机,而偏离的原因恰恰藏在模算术里。
直觉算法是这样的。一个随机整数不被素数 整除的概率是 。多项式 在模 下的取值只踩部分剩余类,设它避开 0 的剩余类比例是 ,那么 不被 整除的概率就是 。
把所有小素数的修正因子相乘,就得到 相对随机数的密度倍率:
倍率大于 1 的多项式在图上是亮线,小于 1 的是暗线。
这个猜想有更早的源头。Bunyakovsky 在 1857 年就提出,若整系数多项式首项系数为正、在正整数上不可约、且没有固定素因子(所有取值没有公共的素数约数),它就应当取到无穷多个素数。
恰好是三条前提全满足的最简单样例, 也在前提之内。于是它们成了整张猜想网的中心结,一百六十余年悬而未决。
Hardy 与 Littlewood 在 1923 年的猜想 F 把它推进成定量版本:二次多项式的素数密度 = 背景密度 × 一个由系数决定的常数。连这个常数怎么按模算术连乘都给出了公式。
乌拉姆在 1964 年与同事发表的统计也确认:对角线富集是可测量的真实现象,不是视觉错觉。
拿 做解剖
就用中心对角线这条多项式练手。列出前几项并分解:
| 分解 | ||
|---|---|---|
| 1 | 7 | 素 |
| 2 | 21 | |
| 3 | 43 | 素 |
| 4 | 73 | 素 |
| 5 | 111 | |
| 6 | 157 | 素 |
| 7 | 211 | 素 |
| 8 | 273 |
立刻看到一个结构性规律。模 3 下 等价于 ,而 时它恒等于 0。每第三个值必然被 3 整除(表中第 2、5、8 行整齐地体现了这个周期)。
有意思的是,随机数不被 3 整除的概率也是 ,这条线在 3 因子上并不吃亏。亏不亏要逐个素数算:模 下把 的每个剩余类代进去,数一数 落在 0 类的比例 ,再用 当修正因子连乘。
对这条线,3 的因子恰好打平,真正抬密度的是它在若干素数上「多避开」的份额。
拿素数 5 完整走一遍。 取 0 到 4, 模 5 依次得 ,零类一次没踩到。等价的代数判据是方程 的判别式 ,而 3 不是模 5 的二次剩余,方程无解。
于是 ,修正因子 ,这条线在素数 5 上白赚四分之一的密度。
把模 2、模 7 以及更大的素数一路算完,累乘出这条线的总倍率,就能预告它在图上的明暗档位。
每条对角线的亮度,原则上都能这样用纸笔算出档位。视觉上的「神秘条纹」,翻译成代数语言就是一族可计算的模周期性。连「哪些位置必然是合数」都带着周期,也就是条纹看起来「有秩序」的根源。
极端案例
富集的极端是欧拉在 1772 年发现的 。 从 0 取到 39,全是素数。
它为什么这么强?现代的解释连着代数数论。41 属于与「类数等于 1」的虚二次域相关的 Heegner 数序列(1, 2, 3, 7, 11, 19, 43, 67, 163), 的判别式 正对应最后一个 Heegner 数。
这个多项式的取值在模每个小素数 下都恰好避开 0。障碍被整体压到了 40 之后,前 40 项一个合数都没有。
把 41 放在螺旋中心重新铺开,这条多项式的值严格落在一条对角线上,构成全图最亮的线之一。
与整数只差 量级这个著名的「拉马努金近似」,背后也是同一个 163。
一条乌拉姆螺旋上最亮的对角线,和虚二次域类数问题共享同一个深层参数。玩具玩具着就摸到了教科书末尾的章节。
生成这张图的工程
可视化另一半的功夫在工程。几万到几百万个格子,怎么高效判定素数并画出来。
素数判定一条条做试除是不合格的。对 个数判定到 ,总代价 ,百万格子已经卡顿。
标准答案是埃拉托斯特尼筛。开一个长度为 的布尔数组,从 2 起,把每个素数的倍数批量划掉,总代价 ,百万规模毫秒级完成。内存紧张时用位集,每个数只占一比特,百万数也就一百多 KB。
需要判定「超大数」本身(比如某个具体的多项式值是不是素数)时,再换米勒-拉宾这类概率判定。筛法管「批量标格子」,概率判定管「单点复核」,两件工具各管一段。
米勒-拉宾的原理一句话版:利用费马小定理的逆否命题,随机选一个底数 做若干次模幂检验。数若是合数,每次检验有至少四分之三的概率当场拆穿它。跑 k 轮,漏判概率不超过 ,三十轮的误判率已经小于硬件翻转一位的概率。
工程上更常用的确定性变体是:对 64 位以内的整数,固定用前若干个小素数(2 到 37 的十二个底数)各验一轮,即可给出严格结论,复杂度是每轮一次模幂。
算法边界上还有一条实用分界:几百位的大数直接上米勒-拉宾,64 位以内确定性底数轮, 以内的批量格子老老实实筛。选择判定器之前,先看输入是「一个数」还是「一段连续区间」。
渲染侧的核心决策是别给每个数字建一个 DOM 节点。上万个 div 就能把页面拖进卡顿,百万个直接白屏。
正确姿势是画到 Canvas 上。 个数对应 个像素,一百万个素数格子就是一张 1000×1000 的位图,放大缩小交给坐标变换,浏览器的 2D 光栅化轻松扛住。
交互上把「拖动滑块调范围、参数一变即重绘」做成状态驱动,图像本身永远是纯函数式的「数据到位图」映射。
这套「筛法出数据、Canvas 出图」的组合,几乎是所有数论可视化的通用底座,换一种螺旋只需换坐标函数。
可视化作为猜想发生器
从「看着像」到「可以证伪」
乌拉姆螺旋真正的方法论价值在于:它把「素数分布有没有规律」这个大而无当的问题,压成了一个个具体、可证伪的小命题。
「对角线亮」是视觉印象。翻译成数学语言是「多项式 的素数密度超过背景」。再往下追问一步,就撞进未解难题的核心。 是否产生无穷多个素数,这是 1912 年 Landau 提出的四大问题之一,至今悬而未决。
可视化没有解决任何问题,但它完成了同样重要的事:把注意力对准了正确的对象。
这几乎是所有成功可视化的共同配方。找一个天然把结构「摆到脸上」的坐标系,让眼睛替注意力做初筛,剩下的问题自然变成可以动手验证的命题。
对照组
同一批素数,换一种摆法,图案完全不同。
Sacks 螺旋(1994 年)把 放在半径 、角度 的阿基米德螺旋上。效果是完全平方数严格排成一条射线。 时半径恰为 、角度恰为 整圈,方向归位。而素数则聚集在一族向内卷的曲线上,每条曲线又对应一个二次多项式的取值。
两个坐标系里的图案没有对错,它们是同一批算术事实在不同投影下的影子。乌拉姆螺旋擅长暴露「线性排布」的规律,Sacks 螺旋擅长暴露「平方律」的规律。
想认真从图里读出东西,多准备几个坐标系是基本素养。一条线只在一种摆法里出现,说明你看到的是摆法。在多种摆法里以不同面目反复出现,才更可能是数本身的性质。
警惕反向错误
反过来也要防一件事。人眼对直线和连续性的敏感度被演化调得极高,会在噪声里本能地「连线」。
乌拉姆螺旋的对角线富集是统计上可检验的真实现象(沿对角线数素数、与同长度随机线段对比,比值显著大于 1)。但富集幅度并不像截图给人的感觉那么夸张。最亮的对角线密度通常也只有背景的一到两倍,是视觉系统把差异放大成了「条纹」。
所以严谨的姿势是:眼睛提出候选,计数器做判决。可视化负责猜想,统计负责定案,两步不可互相替代。
一点历史
乌拉姆自己对这个发现的叙述相当克制。1963 年在一次学术会议的报告上,他因为走神把整数按螺旋涂开、随手标出素数,对角线「立刻显形」。
他和同事斯坦、威尔斯很快做了定量工作。按角度区间统计素数密度,确认富集超出随机涨落。成果在 1964 年发表成论文,标题朴素得像实验记录。
真正让这个图案进入大众视野的是马丁·加德纳。1964 年 3 月他的《科学美国人》「数学游戏」专栏用整期介绍了螺旋,附上数百页计算的插图,读者来信随即涌向编辑部。
这段传播史有个值得记取的细节:乌拉姆的涂鸦、斯坦和威尔斯的定量工作、加德纳的专栏介绍,三个环节缺了任何一个,这个图案都只是某个人的走神。
今天你在工具里拖一下范围滑块就能看到的画面,1964 年要用纸笔算数百个格子才能画出一角。算力把「看见结构」的门槛降到了零,但「解释结构」的门槛半个多世纪纹丝未动。
螺旋之外
讨论「素数看起来像什么」,绕不开一个更根本的问题:随机分布长什么样?
1936 年克拉默给出过一个著名的零模型(现在叫克拉默模型)。假设每个整数 以独立概率 是「素数」,这样生成的人造素数集在统计上应该重现素数定理。
把克拉默模型的素数同样铺进乌拉姆螺旋,你会得到一张几乎以假乱真的图。有斑点、有短条纹,唯独没有乌拉姆螺旋那种横贯全图的长对角线。
这个对照实验把「富集」的定义精确化了:结构不是「看起来不乱」,而是「比随机模型多出的长程相关性」。而长程相关性的来源恰恰是模算术(多项式在模 p 下的周期行为),克拉默模型里没有模算术,所以画不出条纹。
同一套思路也解释了孪生素数为什么常被当作螺旋的「进阶观察对象」。
孪生素数对(p 与 p+2 同为素数)的候选密度由孪生素数常数 修正。又是一个由模算术乘积定义的常数,与二次多项式的密度常数出自同一台机器。
把「孪生素数中较小的那个」标进螺旋,条纹比素数本身更锐利。原因朴素:除 (3, 5) 以外的孪生素数对,成员必然形如 与 ,模 6 的位置被完全锁死。
看懂这一层,再看螺旋上任何明暗,你都会先问一句:这个图案的「随机基线」是什么,偏离基线的部分由哪个模决定。这个问题意识比任何单个结论都值钱。
亲手玩
在素数螺旋里调大范围,先整体看条纹,再挑一条最亮的对角线把它的数值序列抄下来,试着做三件事。猜通项(首项系数几乎必然是 4)、对小素数逐个验模、找那条「每 k 个值被 p 整除」的周期规律。
这条流程走一遍,Hardy–Littlewood 的启发式就从名词变成了手感。
下面三条猜想难度递进,全部可以手工或写十行脚本验证。
暗线的极端样例:穿过奇数平方的那条对角线(),除中心 1 外每个值都是完全平方。这条线应当黑到底。上图为证。
中心线的模 3 节拍: 在 时被 3 整除。把这条线前 50 项列出来,每三个位置标一次「必然合数」,再与它邻居 (暗点在 )对比节拍相位。
奇系数射线折半:任选一条 为奇数的射线,统计前 500 项的素数个数,与 为偶数的邻居对比。比值应当接近 1/2(奇偶交替吃掉一半候选)。
还有一个观念值得在动手前钉牢:素性与进制无关,但图案只认数本身。
有人怀疑对角线是十进制记法的巧合。不是。把格子上每个数换一种进制书写(用进制转换工具在二进制、八进制、十六进制之间任意切换),数还是那个数,整除关系一个不变,螺旋图案也就一个不变。
可视化暴露的是数字本身的性质,也就是它区别于「数字规律迷信」的地方。
从舍入误差到素数条纹,这条长文跨了八章,讲的是同一件事:确定性的答案可以从大量随机抽样中可靠地涌现,误差与不确定性的代价则一直明码标价。