
前阵子帮人看一段训练代码loss 跑到一半突然变成 nan一层层打印中间量最后定位到一个归一化常数算出来是 inf。追下去问题出在一行math.factorial(n)——那个 n 只有 80 出头但 80! 这个数早就超出 float64 的表达范围了。另一个更隐蔽的例子是排序型损失函数对一个 batch 内的样本做全排列枚举样本数从 6 涨到 9运行时间从毫秒级直接跳到分钟级。这两个现象看着一个属于数值精度、一个属于算法复杂度但在根子上是同一件事对阶乘这个函数的增长速度缺少肌肉记忆对阶数这个词在不同语境下的确切含义没有分清。这篇接着前面几篇的线索往下走专门聊阶乘与阶数。阶乘是组合数学的入口也是微积分里泰勒展开、Gamma 函数、高斯矩计算的骨架阶数则是判断一个算法、一个近似、一个模型能不能落地的标尺。我会把阶乘的三种理解路径拆开讲把阶数这个词在机器学习语境下的四种不同含义逐个辨析然后用三个可以直接复制运行的小实验把阶乘的崩溃边界、斯特林公式的误差曲线、排列枚举的爆炸点全部量化出来。不管你是刚补微积分基础的新人还是已经写了几年模型代码、但对lgamma这个函数一直半懂不懂的从业者这篇应该都能挖到点东西。1. 阶乘的三种理解路径走错哪一条后面都别扭阶乘这个概念出现得太早导致很多人对它的印象停留在一句从 1 乘到 n。这句话不算错但它只描述了阶乘的一个侧面。我在带新人的时候习惯让他们从三条不同的路径各走一遍因为这三条路径分别对应机器学习里三种完全不同的问题路径一对应计数与组合路径二对应代码实现与数值溢出路径三对应连续化与可微性。三条路走通阶乘就不再是一个孤立的算式而是一个能自由调用的工具。1.1 路径一排列数阶乘的原始语义先从最朴素的问题入手n 个互不相同的对象排成一列有多少种排法第一个位置有 n 种选择选完之后第二个位置剩 n−1 种一直到最后只剩 1 种乘起来就是 n!。这个逐步消耗选择的过程就是阶乘的原始语义它天生就是一个计数结果而不是一个算术结果。从这里可以直接理解为什么 0! 定义成 1 而不是 0。n0 时没有任何对象空排列这件事只有一种做法——什么都不做。这跟空乘积等于 1是同一个道理的两种说法。有人觉得这是硬凑的约定其实不是如果 0! 不取 1二项式系数 C(n,0) 和 C(n,n) 都会变成未定义泰勒展开的常数项会崩指数分布的归一化也会崩。这个约定的价值不在数学美感而在于它是唯一能让上百个公式保持自洽的选择。顺着排列往下走一步就是带重复的排列。如果 n 个对象分成若干组组内元素彼此不可区分总排列数要除掉各组的内部排列数写成 n! / (n₁! n₂! … nₖ!)。这就是多项式系数语言模型里对一条 token 序列做多重集计数、做重复惩罚时的归一化项用的就是它。我在做样本均衡的时候经常要用这个数来估某个组合出现的理论概率直接一把math.factorial除下去往往是 inf/inf必须换成对数形式。1.2 路径二递推式写代码时真正用得上的形式写进代码里的阶乘几乎从来不是从 1 乘到 n而是递推形式 n! n × (n−1)!边界 0! 1。递推形式的价值在于它把阶乘从一个一次性算完的大整数变成了可以逐步累积的状态这让它能够被塞进循环、塞进动态规划、塞进流式计算。def fact_iter(n: int) - int: if not isinstance(n, int) or n 0: raise ValueError(n 必须是非负整数) r 1 for i in range(2, n 1): r * i return r这段代码有两个细节值得说。一是循环从 2 开始而不是 1n0 和 n1 时直接返回 1省掉一次无意义的乘法——单次省不了什么但在内层循环里跑上百万次就不是小事了。二是用迭代而不是递归。递归写return n * fact(n-1)更贴近数学定义但阶乘的递归深度等于 nn 上千就会撞到递归栈限制而且函数调用开销远大于循环。我见过有人在解析树上用递归算阶乘树深一过 800 就直接崩了报的还是递归深度超限跟阶乘本身一点关系都没有排查了半天。真正要警惕的不是写法而是类型。Python 的 int 是任意精度math.factorial(1000)能老老实实吐出 2568 位的大数这给人一种错觉以为阶乘很安全。一旦切换到 numpy、C、Java情况立刻变了32 位有符号整数在 13! 就溢出12! 479001600 还行13! 6227020800 就出去了64 位有符号整数在 21! 溢出20! ≈ 2.43×10¹⁸ 刚好卡住21! ≈ 5.11×10¹⁹ 就爆了float64 撑到 170! 左右170! ≈ 7.26×10³⁰⁶171! 直接变 inf。这三个数字建议直接背下来比任何调试技巧都省时间。1.3 路径三Gamma 函数让阶乘在实数轴上活着前两条路径都局限在整数上。一旦你需要对阶乘求导、需要把它嵌进连续优化目标、需要在 n 不是整数时也算一个阶乘就必须换成 Gamma 函数Γ(z) ∫₀^∞ t^(z−1) e^(−t) dt且对正整数 n 有 Γ(n) (n−1)!。这里有第一个高频坑错位一格。Γ(n) 等于 (n−1)!不是 n!。所以当你想算 n! 的时候写的是 Γ(n1)。scipy.special.gamma、math.gamma、torch.lgamma全都是这个约定第一次用必踩。我的习惯是干脆不记这个关系直接记要算 n! 就用 lgamma(n1)把错位消化在写法里不给自己犯错的机会。Gamma 函数在机器学习里出现得比想象中频繁。Beta 分布、Dirichlet 分布的归一化常数里全是 Gamma 的比值t 分布、卡方分布的概率密度里也有变分推断里算 ELBO 的解析式KL 散度项经常化简成 lgamma 的组合。它之所以重要是因为它把离散计数和连续积分缝在了一起高斯积分 ∫ x^(2k) e^(−x²/2) dx 这类看起来跟阶乘毫无关系的式子算出来的结果恰好是双阶乘的形式。1.4 顺手补一个双阶乘双阶乘记作 n!!表示隔一个乘一个5!! 5×3×1 156!! 6×4×2 48。它跟普通阶乘的关系是 (2k−1)!! (2k)! / (2^k · k!)(2k)!! 2^k · k!。这个式子在推导高斯分布各阶矩的时候是核心工具——标准正态分布的 2k 阶矩等于 (2k−1)!!一阶矩方差是 1四阶矩是 3六阶矩是 15。做高阶矩估计、做峰度检验的时候如果不认识双阶乘看到 3、15、105 这串数会一头雾水。2. 阶数四种含义混着用一定出事中文里的阶是个筐什么都往里装。在机器学习的技术讨论里阶数至少承担了四种互不相同的含义而且这四种含义之间没有任何换算关系。我看到过有人在讨论二阶优化器的时候把它跟阶乘的二阶项扯到一起那纯粹是被字面误导了。2.1 含义一增长阶比的是谁跑得快增长阶是算法分析里的那套语言。说 f(n) 是 O(g(n))意思是存在常数 C 和 n₀使得 n ≥ n₀ 时 |f(n)| ≤ C·|g(n)|。它衡量的是当输入变大时代价按什么形状上升跟具体快多少倍完全无关。这套语言最好用的地方是它给了一个严格的排序。从慢到快O(1) O(log n) O(√n) O(n) O(n log n) O(n²) O(n³) O(2ⁿ) O(n!) O(nⁿ)。这个链条里n! 的位置很特殊——它比任何指数函数都快但比 nⁿ 慢因为 n! 1×2×…×n ≤ n×n×…×n nⁿ。准确地说log(n!) Θ(n log n)也就是说阶乘的对数是 n log n 量级这正是比较排序下界 log₂(n!) ≈ n log₂n − 1.44n 的来源。很多人背排序下界是 n log n但不知道这个下界是从阶乘推出来的知道了之后会发现它其实是一个信息论结论n 个元素的排列有 n! 种要区分这么多种可能至少需要 log₂(n!) 次二选一。增长阶最大的陷阱是常数被吃掉了。O(n log n) 的归并排序在小规模数据上打不过 O(n²) 的插入排序因为前者的常数项里有额外的内存分配和拷贝。我在实际工程里做排序、做近邻检索n 小于 32 的时候一律切插入排序这不是玄学是常数项在主导。所以看到这个算法阶更低就无脑换是典型的只学了半套。2.2 含义二多项式的阶也就是泰勒截断到第几项这里的阶数指的是最高次幂。一次近似是直线二次近似是抛物线阶数越高逼近越准但代价是需要的导数信息越多。梯度是一阶信息Hessian 是二阶信息再往上就是三阶张量、四阶张量存储成本按维度指数爆炸——一个 d 维参数的 Hessian 是 d² 个数d10⁶ 时就是 10¹²完全不可能显式存下来。这个语境下的阶跟阶乘的联系在系数上。泰勒展开的每一项都带着一个 1/k!这个 1/k! 不是装饰它是保证高阶项衰减得足够快的关键。下一节会专门算这件事。2.3 含义三导数阶数、差分阶数二阶导数里的阶就是求导次数。这个概念在数值计算里延伸出二阶差分二阶中心差分在微分方程里延伸出二阶常系数线性方程。做时间序列、做物理仿真、做神经 ODE 的时候天天见。需要注意的是离散差分和连续导数的阶数含义一致但误差阶不同。一阶前向差分的误差是 O(h)一阶中心差分是 O(h²)二阶中心差分也是 O(h²)。这里的 h 是步长O(h²)说的是步长减半误差降到四分之一。我在调数值梯度的时候会先用这个规律验一下实现对不对把 h 从 1e-4 改成 5e-5如果误差没按预期缩小那一定是差分格式写错了而不是精度问题。2.4 含义四张量的阶也就是有几个轴这个用法在深度学习框架里最普遍标量是 0 阶张量向量是 1 阶矩阵是 2 阶一张 RGB 图片是 3 阶C,H,W一个 batch 的 RGB 图片是 4 阶N,C,H,W。这个阶英文原文是 rank 或者 order跟线性代数里的矩阵秩rank容易撞车看英文文档时要格外小心。之所以要专门列出来是因为这四个含义经常在同一个段落里同时出现。比如谈二阶优化可能同时涉及增长阶Hessian 求逆是 O(d³)、多项式阶二次近似、导数阶二阶导、张量阶Hessian 是 2 阶张量。四件事全对但彼此不能互相替换。3. 阶乘到底涨得多快把它压到对数轴上再看人对指数的直觉本来就不准对阶乘的直觉基本等于零。100! 和 100¹⁰⁰ 哪个大很多人会犹豫。答案是后者大得多。想建立直觉唯一的办法是把阶乘取对数看它在对数轴上长什么样。3.1 一张表建立数量级概念nn! 的近似值ln(n!)log₂(n!)103.63×10⁶15.1021.79202.43×10¹⁸42.3461.08503.04×10⁶⁴148.48214.211009.33×10¹⁵⁷363.74524.761707.26×10³⁰⁶706.571019.310004.02×10²⁵⁶⁷5912.138530看第三列和第四列感受完全不一样n 从 10 涨到 1000涨了 100 倍而 ln(n!) 只从 15 涨到 5912涨了不到 400 倍。这就是对数的威力——它把一个天文数字压成了一条平缓得多的曲线。但注意5912 这个数依然很大只是从无法书写变成了可以书写。这张表最实用的地方在于判断我能不能直接存下来。170 那一行是 float64 的天花板超过它直接 inf这是硬边界。1000 那一行虽然溢出了但 5912 作为对数值完全存得下所以凡是涉及大阶乘的计算一律走对数域把乘除法变成加减法。3.2 斯特林公式不算就知道有多大斯特林公式是阶乘的渐近展开三个版本按精度需求取用粗略版n! ≈ (n/e)ⁿ。只保留主导项适合做复杂度估算。标准版n! ≈ √(2πn) · (n/e)ⁿ。多加了一个 √n 的修正误差降到 1/(12n) 量级。对数版ln(n!) ≈ n·ln n − n ½·ln(2πn) 1/(12n)。这是工程上真正用的形式因为它不会溢出。第三版值得多说一句。它的误差项是 1/(12n) − 1/(360n³) …也就是说 n10 时相对误差约 0.83%n100 时约 0.083%n1000 时约 0.0083%。每把 n 放大十倍误差降一个数量级。这个精度对于绝大多数模型里的计数、归一化、信息量估算都远远够用而且计算成本是 O(1)——不管 n 是 10 还是 10⁹算一次只需要一次 log 运算。我自己的经验是n 20 时直接用斯特林近似对数版误差小于 0.4%n ≤ 20 时查表或者直接精确算因为小 n 时误差反而大n1 时相对误差接近 8%。这条经验值在写性能敏感的内层循环时很关键——你要知道什么时候可以省掉一次lgamma调用。3.3 一个快速判断n! 夹在 2ⁿ 和 nⁿ 之间记不住具体数值的时候用这个夹逼关系现场推n! 显然小于 nⁿ每一项都不超过 n也显然小于等于 nⁿ而 n! 跟 2ⁿ 的比较在 n4 处翻转。n2ⁿn!nⁿ121124243862741624256532120312566472046656n ≤ 3 时 2ⁿ 更大n ≥ 4 之后 n! 全面胜出而且差距越拉越大。这个翻转点特别有用当你看到某个算法的时间复杂度写着 O(2ⁿ) 觉得还好的时候想想它对应的问题规模如果换成枚举排列变成 O(n!)在 n20 时已经是 2.4×10¹⁸ 次操作——按每秒十亿次算要跑 77 年。4. 阶乘在机器学习里的落点比你以为的多阶乘在机器学习里不是一道课后习题它有明确的使用位置。我按出现频率从高到低排一下这几个位置覆盖了我日常 90% 以上用到阶乘的场景。4.1 组合数与二项式系数一定要走对数域二项式系数 C(n,k) n! / (k! (n−k)!)这是阶乘最常见的直接用法。它的坑非常经典n1000、k500 时C(1000,500) ≈ 2.7×10²⁹⁹勉强还能存但如果你按字面先算 n! 再算 k! 再相除中间的 1000! 早就溢出了得到的是 inf/inf nan。正确写法是全程取对数from math import lgamma def log_comb(n: int, k: int) - float: if k 0 or k n: return float(-inf) return lgamma(n 1) - lgamma(k 1) - lgamma(n - k 1)lgamma是 ln Γ(x) 的实现直接返回对数值不经过中间的大数。这个函数在 scipy 里叫gammaln在 torch 里叫torch.lgamma在 numpy 早期版本里没有很多人就是在这里踩坑然后自己写了个math.log(math.factorial(n))——那个写法在 n 稍大时先溢出再看 log 就是 log(inf) inf等于白干。顺带一提多项式系数 n! / (n₁!…nₖ!) 的写法完全一样把连乘的 lgamma 项加起来减掉就行。做序列生成时算一条候选序列在多项分布下的对数概率用的就是这个。4.2 泰勒展开里的 1/k! 到底在配什么eˣ Σ x^k / k!这个式子里 1/k! 的来源经常被含糊带过。实际逻辑是我们要找一个幂级数 Σ aₖx^k 来表示 eˣ而 eˣ 的每一阶导数都等于它本身在 x0 处全是 1。同时单项式 x^k 求 k 次导得到 k!常数再求一次就变 0。所以为了让第 k 项在求 k 次导后恰好贡献 1系数必须取 1/k!。换句话说1/k! 是抵消掉求导产生的阶乘的补偿因子。这个理解对做数值近似很关键。你截断到第 K 项误差量级大致是 x^(K1)/(K1)!。注意分母是阶乘所以误差衰减得极快——这就是为什么 eˣ 在 x 不太大时用七八项就能算到机器精度。反过来如果 x 很大比如 x20那 x^K/K! 这个比值会先涨后跌前几项反而越来越大出现严重的相消误差。这就是泰勒展开的收敛半径是无穷但数值可用的范围是有限的这句话的由来。我在实现自定义激活函数、自定义 loss 的时候凡是涉及 exp、log1p、sin 这类函数的近似第一件事就是估一下自变量的取值范围。如果范围超过 10就别自己写泰勒了老老实实调库函数用多项式近似补精度这件事在宽范围内基本是徒劳。4.3 高斯矩与双阶乘概率模型里的隐形阶乘前面提过标准正态分布的 2k 阶矩等于 (2k−1)!!。这个结论在变分推断、矩匹配、分数匹配score matching里到处都是。举个具体场景用二阶泰勒近似去逼近一个期望 E[f(X)]其中 X 服从高斯分布展开后会出现 E[X²]、E[X⁴] 这些项系数就是 1、3、15、105 这串双阶乘数。反过来如果你在做重要性采样或者计算高阶矩估计发现权重里出现 1/3、1/15 这样的分母基本可以确定是某个高斯假设下的解析结果而不是代码写错了。认识这串数能省下大量这个系数哪来的的排查时间。4.4 排列、行列式与排序型损失n! 就是复杂度本身行列式的莱布尼茨定义是det(A) Σ_σ sgn(σ) ∏ᵢ A(i,σ(i))求和遍历所有 n! 个排列。这是一个极好的教学例子定义上需要 n! 项但实际计算用 LU 分解只要 O(n³)。n10 时 n!3638800 还算得动n20 时 n!2.43×10¹⁸直接不可能。这说明定义上的复杂度和实际可算的复杂度之间可以差出天文数字而填平这个差距的正是数学结构在这里是行列式的多重线性。排序型损失也踩同一个坑。如果你要对一个 batch 内的样本做全排列找出最优排序或者是给所有可能的排序分配概率这类模型在某些推荐、检索场景里确实存在那归一化常数就是 n!。batch size10 时是 363 万batch size15 时是 1.3 万亿。我见过有人在 batch8 上开发测试一切正常上线把 batch 调到 12服务直接被打挂——因为 n! 从 4 万涨到了 4.8 亿。5. 三个实验把上面的结论全部验证一遍光看结论记不住动手跑一遍才能形成肌肉记忆。这三个实验都很短加起来不到五十行代码建议直接复制到本地跑。环境只需要 Python 标准库加 numpy。5.1 实验一找出阶乘的崩溃边界import math import numpy as np def fact_dtype(n, dtype): r dtype(1) for i in range(2, n 1): r dtype(r * i) return r for n in (12, 13, 20, 21): exact math.factorial(n) safe fact_dtype(n, np.int64) print(fn{n:2d} exact{exact} int64{safe} match{exact int(safe)})跑起来会看到n12 和 n20 时 int64 结果和精确值一致n13 时如果换成np.int32就会翻转成负数n21 时 int64 会给出负数或者触发溢出警告。这一步的目的是让你亲眼看到溢出不是报错而是悄悄给出一个错误答案——这是最危险的一类 bug因为程序不崩只是结果错了。再加一段测浮点边界for n in (170, 171): try: v float(math.factorial(n)) print(n, v) except OverflowError as e: print(n, overflow:, e)Python 的 int 转 float 在超出范围时会抛异常这是好事但如果你用numpy.float64的累积乘法它会直接变成 inf 而不报错。5.2 实验二斯特林近似的误差曲线import math def log_stirling(n): return (n 0.5) * math.log(n) - n 0.5 * math.log(2 * math.pi) 1.0 / (12 * n) def log_fact(n): return math.lgamma(n 1) print(f{n:6} {log差:12} {相对误差:12}) for n in (1, 2, 5, 10, 20, 50, 100, 1000, 10000): diff log_stirling(n) - log_fact(n) print(f{n:6} {diff:12.6e} {math.expm1(diff):12.6%})这里有一个必须注意的点不要用非对数的斯特林公式去算大 n。math.sqrt(2*math.pi*n) * (n/math.e)**n这个写法在 n1000 时(1000/e)^1000 已经是 10²⁵⁶⁵ 量级直接 OverflowError。我早期就写过这个版本n 小时测试全过n 一大就崩还以为是公式推导错了折腾了半天才发现是中间量溢出。走对数域之后log_stirling在 n10⁶ 时依然安稳。输出的规律应该是相对误差约等于 −1/(12n)。n10 时约 −0.83%n100 时约 −0.083%n1000 时约 −0.0083%。如果你的输出不服从这个规律那一定是常数写错了比如 1/(12n) 写成了 1/12n 或者漏了 ½ln(2πn)。这个用理论误差率反查代码的技巧比盲猜快得多。5.3 实验三排列枚举的爆炸点在哪里import itertools, time for n in range(6, 11): data list(range(n)) t0 time.perf_counter() cnt sum(1 for _ in itertools.permutations(data)) dt time.perf_counter() - t0 print(fn{n:2d} count{cnt:9d} time{dt:.3f}s)预期结果大致是n6 时 720 个、n8 时 40320 个都在毫秒级n9 时 362880 个开始上百毫秒n10 时 3628800 个基本到秒级。这还只是生成一遍不做事如果每个排列后面还要算一次损失、算一次梯度时间要再乘上一个常数。这个实验的价值是给你一个具体的手感数字。我现在的判断标准是n ≤ 8 可以暴力枚举n ≤ 10 需要谨慎评估且必须有超时保护n 10 一律换算法。这条线不是拍脑袋来的就是跑这个实验跑出来的。你在自己的机器上重新跑一遍得到的是你自己机器的线比任何二手经验都准。6. 踩过的坑和一张速查表这一节是我这几年在阶乘和阶数上真实踩过的坑的汇总按出现频率排序。大部分问题不是数学不会而是没想到。6.1 五类高频错误第一类是Gamma 的错位。把 Γ(n) 当成 n!结果所有组合数都差一个因子 n。这个错误的隐蔽性在于小 n 时误差看起来像那么回事n5 时 Γ(5)24 而 5!120差了 5 倍但如果你在算一个比值分子分母同时错位误差可能正好抵消测试全绿。我的做法是永远写lgamma(n 1)把 1 焊死在调用里。第二类是中间量溢出。算 C(n,k) 时先算 n!、算概率时先把所有因子乘起来。只要任何一步涉及大于 170 的阶乘float 就废了大于 20 的整数阶乘int64 就废了。原则很简单只要式子里有阶乘、Gamma、指数、连乘超过十项一律先取对数。第三类是把 log(n!) 写成 n·log(n)。这不是笔误而是真的有人把斯特林公式的主导项当成精确式在用。n·log n − n 才是主导漏掉 −n 这一项在 n100 时误差 100相对误差超过 27%。别小看这 −n它是熵、是信息量、是自由能的一部分物理意义明确。第四类是误差阶和经验规律对不上就归咎于精度。前面说的差分步长实验就是典型如果步长减半误差没按 h² 缩小说明格式写错了不是精度不够。同理斯特林误差不按 1/(12n) 走说明公式抄错了。这类用理论反查实现的习惯比单步调试高效得多。第五类是把 O(n!) 当成小规模还能接受。这条最要命因为 n 小的时候确实能接受而小的边界在 n8 到 n10 之间非常窄。我现在的做法是任何代码里出现排列枚举强制加一个 n 的上限断言超了直接报错绝不允许它悄悄跑到线上。6.2 一张速查表场景危险写法安全写法备注大 n 阶乘math.factorial(n)转 floatmath.lgamma(n1)n170 必溢出组合数fact(n)/(fact(k)*fact(n-k))lgamma三项相减中间量必爆概率连乘p1*p2*...*pksum(log p)下溢比溢出更常见斯特林近似sqrt(2πn)*(n/e)**n对数版n1000 直接溢出排列枚举itertools.permutations无保护加 n 上限断言建议上限 8 或 10张量阶数判断用len(shape)猜打印x.ndim跟矩阵秩不是一回事收敛阶验证只看误差绝对值看误差随步长的缩放比例比值才是阶表里下溢比溢出更常见这一条值得单独说一句。概率连乘时几十个小数乘起来很容易变成 0float64 的下界约 5×10⁻³²⁴这时候日志里全是 0 或者 −inf但不会报任何错。这跟阶乘溢出是镜像问题处理方式一样走对数域。6.3 我自己的排查顺序遇到结果不对但程序不崩的情况我现在固定按这个顺序过一遍通常三分钟内能定位先打印所有中间量的最大值和最小值看有没有 inf、nan、0。这一步能抓掉一半问题。检查所有涉及阶乘、Gamma、组合数的调用确认有没有错位、有没有中间溢出。把可疑的乘法链改写成对数求和看结果是否恢复正常。如果涉及近似用理论误差阶反推把规模翻倍看误差是否按预期规律变化。最后才怀疑浮点精度因为真正由精度导致的问题比前面四类少得多。这个顺序背后的逻辑是先抓量级错误再抓实现错误最后才谈精度。很多人一上来就怀疑浮点精度调了半天 eps 参数其实问题在量级上差了几个数量级那根本不是精度能解释的。我自己在这个顺序上吃过亏所以现在把它当成硬流程。最后分享一个我日常用的小习惯凡是看到代码里出现factorial、gamma、permutations、comb这几个词不管三七二十一先在上面加一行注释写明输入上限是多少、超了会怎样。这个注释不需要给任何人看纯粹是为了三个月后的自己。阶乘这个东西的危险性不在于难而在于它太简单简单到你不会为它多停留两秒——而那两秒往往就是 nan 和正常结果之间的距离。