
几何级数这东西几乎每个人在学生时代都背过那个公式 a(1-rⁿ)/(1-r)考完试就还给老师了。我真正把它当回事是几年前在一个对账模块上——业务方要求把一笔分十二期、每期按固定比例递减的账目核到分同事用循环累加写了三十行结果从第九期开始就跟财务的表对不上。查了半天不是业务逻辑错是浮点累加和公式误差在捣鬼。从那以后我养成一个习惯只要看到每项是前一项的固定倍数这种结构先别急着写循环先判断它是不是几何级数收敛条件是什么能不能用闭式解误差怎么估。这篇就把这套东西完整讲一遍几何级数的本质、收敛条件为什么是 |r| 1、以及怎么用 Python 把数值算稳。不管你是刚学完微积分的学生还是每天写业务代码的工程师只要碰到无限累加等比折现误差界这类问题这里面的东西都能直接拿去用。1. 先搞清楚几何级数到底在算什么1.1 一个乘法结构引发的连锁反应几何级数的定义朴素到不能再朴素从首项 a 出发每次乘同一个常数 r把所有项加起来。写成式子就是 a ar ar² ar³ …第 k 项是 ar^(k-1)公比 r 固定不变。它跟等差数列的区别在于等差数列是每次加一个固定量增长是线性的几何级数是每次乘一个固定量增长是指数的。这个差别决定了等差数列求和永远是个二次多项式而几何级数一旦公比绝对值小于 1加无穷多项居然能收敛到一个有限值。我习惯把它理解成衰减链条你手上有一块蛋糕每次切掉固定比例吃掉剩下的继续切。r 0.5 意味着每次吃一半理论上可以切无限次但蛋糕总量永远是原来那一块。这就是收敛的物理直觉——项数无限多但每项衰减得足够快总贡献被压在一个有限上界里。反过来如果每次切掉的比例大于 1那不是在切蛋糕是在滚雪球加得越多越大必然发散。这个乘法结构是识别几何级数的唯一判据。实际工作里它经常伪装成别的东西出现比如每年按 5% 折旧后的残值序列、每次重试成功率下降 10% 的期望次数、递归树每一层的节点数本质都是 ar^k。识别出来之后很多原本要写循环的问题可以直接一行公式解决而且能顺带把误差界算清楚。1.2 三个参数决定了它的全部命运几何级数只有三个参数首项 a、公比 r、项数 n或者无穷。a 只影响整体缩放真正决定收敛与否的只有 r。这一点很多人没意识到——你把 a 从 1 改成 100 万收敛性一点不变只是收敛到的值放大 100 万倍。所以分析几何级数时第一步永远是先把 a 拎出来放一边专心看 r。n 的作用则决定了你是在算有限和还是无穷和。有限和永远有值哪怕 r 10000前 n 项加起来也就是个有限的巨大数字算得出来。无穷和才有收敛问题。工程上绝大多数场景其实是有限和但我们关心的往往是n 取多大才能近似无穷和这时候收敛条件就变成了误差控制的前提。这个区分很关键。我见过有人在 r 1.02 的复利场景里硬套 a/(1-r) 的公式算出来是负数还以为是程序 bug。其实 r 1 时无穷和根本不存在闭式解公式 a/(1-r) 只在 |r| 1 时有意义。有限和要用 a(1-rⁿ)/(1-r)这个公式对 r ≠ 1 的任何值都成立只是当 r 1 时 rⁿ 会爆炸数值上要小心溢出。1.3 它比你想象的更常见几何级数的应用面广得离谱我随手列几个真实碰到的场景。金融里的折现未来每期现金流 C 按折现率折现折现因子 q 1/(1i) 恒定永续年金现值就是标准几何级数。计算机科学里的复杂度分析完全二叉树的节点总数、递归树每层工作量按比例递减的总和都是等比求和。概率论里的几何分布首次成功所需试验次数的期望本质是对几何级数求导。信号处理里的 IIR 滤波器冲激响应、机器学习里带衰减的学习率累计、物理里的阻尼振动能量衰减全都是同一个数学结构。甚至那个经典争论 0.999… 是否等于 1也是几何级数首项 0.9公比 0.1和是 0.9/(1-0.1) 1。有意思的是这些场景的从业者往往不把它叫几何级数。做金融的叫年金公式做算法的叫等比求和做信号的叫几何衰减。名字不同数学是同一个。我个人的经验是一旦你在某个领域看到固定比例这个词反复出现就该条件反射地想到 r想到收敛条件想到闭式解和误差界。这套思维迁移一次能省下大量重复推导。2. 收敛条件的本质为什么偏偏是 |r| 12.1 错位相减部分和公式的来历很多人背得出 a(1-rⁿ)/(1-r)但说不出它怎么来的。推导只用一招错位相减。设 Sₙ a ar … ar^(n-1)两边乘 r 得 rSₙ ar ar² … arⁿ两式相减中间所有项两两抵消只剩 Sₙ - rSₙ a - arⁿ即 Sₙ(1-r) a(1-rⁿ)除过去就得到公式。这个推导有个隐含前提r ≠ 1否则除数为零。r 1 的情形要单独看此时每项都是 aSₙ nan 增大就线性增长无穷和必然发散。这个特例经常被忽略我在代码评审里见过不止一次有人写return a / (1 - r)却忘了拦 r 1结果除零异常或者 inf 直接传到下游。错位相减这个技巧本身也值得记。它的本质是构造一个自指的方程让无限或者冗长的和式在相减后自我消解。同样的思路可以用在等差乘等比比如 Σk·r^k上——先乘 r 再相减会得到一个几何级数加一个修正项这也是后面推几何分布期望要用的方法。2.2 四种情形把 r 的全部分支讲透判断收敛只需要看通项 ar^(n-1) 当 n → ∞ 时是否趋于零。这是级数收敛的必要条件对几何级数来说它也是充分的因为部分和有显式表达式直接取极限就行。按 r 的取值分四类讨论。r 的取值通项行为部分和 Sₙ无穷和结论|r| 1|r|ⁿ → 0项趋于零趋于 a/(1-r)存在等于 a/(1-r)绝对收敛r 1恒等于 a不趋于零na线性增长不存在发散r -1在 a 与 -a 间跳动在 0 与 a 间振荡不存在发散振荡|r| 1|r|ⁿ → ∞爆炸指数增长不存在发散关键洞察在于|r| 1 时 rⁿ 单调趋零部分和被夹在有限区间内取极限得到 a/(1-r)。r -1 特别值得说它是最容易被误判的边界情形——通项的绝对值恒为 |a|根本不趋于零但部分和并不发散到无穷而是在 0 和 a 之间来回跳。这种有界但不收敛的状态在数值计算里表现为结果完全依赖你截断在奇数项还是偶数项非常危险。我在做交替符号的级数近似时踩过一次n 取 1000 和 1001 结果差了整整一个 a排查了半天才反应过来是公比接近 -1。|r| 1 的情形反而好判断指数爆炸任何有限精度都存不下工程上要么报错要么直接判为不可用。真正麻烦的是 |r| 非常接近 1 的情况后面单独讲。2.3 余项公式把差多少算成具体数字工程上我们几乎不可能真的加无穷多项实际做法是取前 n 项然后回答一个问题剩下的尾巴有多大这个尾巴叫余项 Rₙ S - Sₙ。因为 S a/(1-r)、Sₙ a(1-rⁿ)/(1-r)相减后分子上的 1 抵消得到极其干净的结果Rₙ arⁿ/(1-r)。这个公式是整个实战部分的地基。它告诉你两件事余项的大小正比于 rⁿ所以误差随 n 指数衰减衰减速度由 |r| 决定|r| 越小衰减越快。于是要多少项才能达到精度 ε就有了闭式解令 |a||r|ⁿ/|1-r| ≤ ε两边取对数解出 n ≥ ln(ε|1-r|/|a|) / ln|r|。注意分母 ln|r| 是负数因为 |r| 1所以整个式子是正的。举个能建立直觉的例子a 1r 0.99要求绝对误差小于 0.01。余项是 0.99ⁿ/0.01令它等于 0.01 得 0.99ⁿ 1e-4n ≈ ln(1e-4)/ln(0.99) ≈ 458。也就是说公比 0.99 这种看起来衰减很快的情况要四百多项才能压到百分之一的误差。很多人第一次算这个数字都会愣一下。如果 r 0.999同样的精度需要约 9200 项。这就是接近 1 的公比为什么是所有数值问题的重灾区。2.4 从实数轴走到复平面收敛圆盘几何级数的收敛条件在复数域里有个更漂亮的表述。把 r 换成复数 z级数 Σ a·z^k 在 |z| 1 时收敛和是 a/(1-z)|z| ≥ 1 时发散。也就是说复平面上的收敛区域是一个以原点为圆心、半径为 1 的开圆盘叫收敛圆盘。半径为什么是 1因为几何级数的系数全是 a没有衰减收敛半径由系数增长率的倒数决定这里的增长率是常数所以半径恰好是 1。这个视角在学幂级数时会再次遇到——几何级数是所有幂级数里最简单的一个它的收敛半径是 1边界上处处发散不像有些级数在边界部分点收敛。实际写代码验证这件事很有意思。取 z 0.6 0.8i模长正好是 1理论上应该不收敛取 z 0.6 0.6i模长约 0.85应该收敛。跑一下部分和序列模长 1 的那个会在圆上打转部分和像绕圈一样不收敛到定点模长小于 1 的那个会螺旋收敛到 a/(1-z)。这个可视化对理解收敛这个词帮助极大比看公式直观得多。我在带新人时经常让他们先跑这个例子跑完再看公式理解速度明显不一样。3. Python 实战三套写法与各自的适用边界3.1 循环累加最直观也最容易踩坑最朴素的实现是逐项累加维护一个 term 变量每次乘 r。这段代码几乎不会写错逻辑一目了然。def geom_sum_loop(a, r, n): 前 n 项几何级数求和循环累加 total 0.0 term float(a) for _ in range(n): total term term * r return total它的优点是通用r 取任何值都不会崩n 再大也不会溢出除非单项本身溢出。缺点是慢而且误差会累积。每次加法都有舍入误差n 达到千万级时误差累积相当可观。我做过一个实验a 1r 0.5n 10⁷循环累加的结果和精确值比较相对误差在 1e-12 量级但如果把所有项都设成同号的接近值比如 r 接近 1误差会明显放大因为小数被大数吃掉。另一个隐性坑是 term 的初始值类型。如果传进来的是整数 aterm 会先当整数处理直到乘上浮点 r 才转换中途有可能出现整数溢出Python 的 int 不会溢出但 numpy 的定长整型会。所以我习惯开头就 float(a) 强制转换省得后面出莫名其妙的问题。3.2 numpy 向量化快但要防两个溢出数据量大时大家第一反应是向量化。import numpy as np def geom_sum_np(a, r, n): k np.arange(n, dtypenp.float64) terms a * np.power(r, k) return float(np.sum(terms))速度确实快但这段代码有两个雷。第一个是内存n 10⁸ 时光 k 这个数组就要 800MB还没算 terms很容易把机器打爆。第二个是溢出只要 |r| 1 且 n 稍大np.power 直接返回 inf求和之后整个结果变成 inf 或者 nan而且不会有任何警告提示。即使 |r| 1 也有坑。当 r 很小、n 很大时r^k 会下溢到 0这不影响结果后面的项本来就微不足道但如果 r 是负数下溢到 0 的过程中会经历极小值某些实现下可能出现精度异常。更稳妥的写法是用累积乘积配合分块或者干脆用闭式解。我现在的原则是n 小于一万向量化随便用n 超过十万先算一下内存占用再决定r 的绝对值超过 1直接拒绝向量化改用带溢出检查的循环。这个界限不是教条是从几次把测试机内存吃满的经历里总结出来的。3.3 闭式解加误差控制生产环境的首选真正上线跑的代码我一般这么写能闭式解就闭式解但必须先算清楚需要多少项把结果和误差一起返回。import math def geom_sum_closed(a, r): 无穷和闭式解仅 |r| 1 有效 if abs(r) 1.0: raise ValueError(f|r| 1 时级数发散, r{r}) return a / (1.0 - r) def terms_needed(a, r, eps): 估算达到绝对误差 eps 需要的最少项数 ra, rr abs(a), abs(r) if ra 0.0: return 0 if rr 1.0: raise ValueError(只有 |r| 1 才能用截断近似) if rr 0.0: return 1 ratio eps * (1.0 - rr) / ra if ratio 1.0: return 0 return math.ceil(math.log(ratio) / math.log(rr))这里有两个细节值得说。第一ratio 1时必须返回 0否则 log 出来是正数ceil 之后变成负数调用方 range(负数) 直接静默返回空bug 藏得很深。第二所有比较都用绝对值因为 r 为负数时余项公式里的 rⁿ 带符号但误差界只看绝对值。这一点如果写错负公比的情况会算出错误的项数。调用方拿到项数后可以决定是走循环还是走闭式解。我的经验是项数超过一千直接返回闭式解加上理论误差界比真去累加一千次更准也更快。闭式解的误差只来自一次除法和一次减法而循环累加一千次的累积误差可能反而更大。3.4 高精度与补偿求和钱相关的场景别省这一步涉及金额、涉及到分位对账我不用 float一律上 Decimal。from decimal import Decimal, getcontext def geom_sum_decimal(a, r, n): getcontext().prec 50 total Decimal(0) term Decimal(str(a)) ratio Decimal(str(r)) for _ in range(n): total term term * ratio return total注意Decimal(str(a))而不是Decimal(a)直接传 float 会把二进制误差原样带进去0.1 会变成 0.1000000000000000055511…精度白设。这个坑我踩过对账差了 1 分钱查了两小时。如果不想用 Decimal至少要上补偿求和。Kahan 求和能把累加误差从 O(n) 压到 O(1)代价是每次迭代多几次浮点运算。def geom_sum_kahan(a, r, n): total, c 0.0, 0.0 term float(a) for _ in range(n): y term - c t total y c (t - total) - y total t term * r return totalPython 标准库还有个更省事的办法math.fsum它内部用精确累加算法一行就能拿到正确舍入的和。import math def geom_sum_fsum(a, r, n): return math.fsum(a * r**k for k in range(n))代价是要先把所有项生成出来内存占用和向量化版本差不多但精度是当前浮点能给的最高水平。我的选择逻辑是金额场景用 Decimal科学计算用 fsum 或 Kahan普通统计用循环或闭式解看哪个更省事。4. 工程中的真实用法拆解4.1 折现与永续年金公式背后的商业含义永续年金是最典型的几何级数应用。假设某资产每期末给你现金流 C折现率 i那么第一期折到今天的价值是 C/(1i)第二期是 C/(1i)²以此类推。设折现因子 q 1/(1i)显然 q 1整个现值就是 C·q C·q² …首项 a C·q公比 q无穷和是 Cq/(1-q)。代入化简Cq/(1-q) C·[1/(1i)] / [i/(1i)] C/i。结果干净得让人意外——永续年金现值就是年现金流除以折现率。这个公式在估值里天天用但很多人记不住它的前提现金流从第一期期末开始且永远不变。如果现金流从期初开始整个现值要乘 (1i)如果现金流按固定比例 g 增长公比变成 (1g)/(1i)公式变成 C/(i-g)这就是高登增长模型收敛条件变成 g i。这里的关键洞察是所有永续类的金融公式本质都是让公比小于 1 然后把无穷和压成一项。判断一个公式能不能用先看它的公比是不是稳定小于 1。g ≥ i 时高登模型发散对应现实就是增长速度超过折现率的公司无法用这个模型估值这不是数学限制是经济含义。4.2 对几何级数求导期望值是怎么算出来的几何分布的期望推导是对级数求导的经典操作。设每次试验成功概率 p前 k-1 次失败、第 k 次成功概率是 p·q^(k-1)其中 q 1-p。期望 E Σ k·p·q^(k-1)。直接求和不好算但我们可以从已知的几何级数出发。先看 Σ q^k q/(1-q)|q| 1。两边对 q 求导左边变成 Σ k·q^(k-1)右边是 1/(1-q)²。所以 Σ k·q^(k-1) 1/(1-q)²。乘上 p得到 E p/(1-q)² p/p² 1/p。结论期望试验次数是成功概率的倒数p 0.2 时期望 5 次。这个结论直觉上很合理但推导过程用的是逐项求导这个技巧它在幂级数里是合法操作收敛圆盘内部可以逐项求导值得记住。同样的技巧能算二阶矩再求一次导得到 Σ k(k-1)q^(k-2) 2/(1-q)³配合一阶矩算出方差是 q/p²。这个结果在估算重试次数的波动范围时很有用——p 很小时方差极大意味着平均 5 次和可能 50 次同时成立只看均值会严重低估最坏情况。4.3 复杂度分析里那些隐形的等比求和递归算法分析里到处是几何级数。快速排序的平均递归树、归并排序的分层、二分查找的迭代次数都是等比结构。最典型的是完全二叉树的节点计数第 0 层 1 个节点第 1 层 2 个第 h 层 2^h 个总节点数是 2^(h1) - 1这就是首项 1、公比 2、项数 h1 的有限几何和。这个求和在主定理里也藏着。主定理讨论 T(n) aT(n/b) f(n)其中 a 是子问题个数b 是规模缩减比例。当 a/b 1 时递归树每层工作量按 a/b 递减总工作量被根节点支配是 Θ(f(n))当 a/b 1 时每层工作量相同总工作量是 Θ(f(n)·log n)当 a/b 1 时叶子层支配总工作量是 Θ(n^log_b a)。这三种情况的本质就是公比小于、等于、大于 1 的几何级数求和。理解这一层之后很多背结论的东西就变成了推导。比如二分查找为什么是 O(log n) 而不是 O(n)——每次规模减半但只走一条分支a 1、b 2a/b 1/2 1等比递减总工作量被第一层支配所以是 O(1) 次比较乘 log n 层。如果是有两个分支的递归比如朴素斐波那契a 2、b 2a/b 1每层工作量持平总复杂度就是 O(n)这也解释了为什么朴素递归那么慢。4.4 截断采样把无限变成有限的判据工程上经常需要从一个无限序列里采样比如按几何衰减的权重做随机选择。这时候必须回答截断到哪里而这恰好是余项公式的直接应用。假设权重是 w_k a·r^(k-1)要保证被丢弃的总权重占比小于 δ就需要 Rₙ/S δ。因为 S a/(1-r)、Rₙ arⁿ/(1-r)比值就是 rⁿ。所以条件极其简洁rⁿ δn ln δ / ln r。δ 1e-6、r 0.9 时n ≈ 131r 0.99 时n ≈ 1375。同样是衰减 1%公比不同需要的项数差十倍。这个计算我在做推荐系统的候选截断时用过很多次——业务方说权重衰减要平滑实际给的衰减系数如果太接近 1截断长度会爆炸性能扛不住。提前把这笔账算给业务方看比事后优化有效得多。5. 踩坑记录与速查表5.1 三个真实遇到过的 bug第一个是对账模块的浮点累加。分十二期、每期按 0.92 的倍数递减循环累加加了一万多次因为还混了别的计算结果和财务的表格差了几分钱。根因是 float 累加误差在大量迭代后累积加上财务那边用的是高精度十进制。改成 Decimal 之后完全一致。这个教训是涉及金额别用二进制浮点做长链条累加。第二个是 r 接近 1 时的项数爆炸。某个衰减系数设成了 0.9995需要精确到 1e-9按公式算出来要四万多项代码里硬编码的 n 1000 根本不够但误差被其他逻辑掩盖了直到某次数据量变化才暴露。现在我的习惯是项数一律用 terms_needed 动态算绝不硬编码。第三个是负公比的振荡。做交替符号的近似时公比接近 -1n 取奇数还是偶数结果完全不同测试用例里偏偏都是偶数项上线后遇到奇数项就出问题。解决办法是在函数里显式检查 |r| 是否接近 1 且为负如果是要么加大 n 直到余项界满足要么直接报错让调用方处理。现象可能原因处理方式结果为 inf 或 nan|r| 1 且 n 大幂运算溢出检查收敛条件拒绝计算或改用对数域结果在奇偶项间跳动r 接近 -1振荡不发散加大 n 或用余项界判断别只看前几项结果轻微偏差且随 n 增大浮点累加误差累积用 Kahan、math.fsum 或 Decimal除零或得到巨大负数r 1 或 r 1 时误用闭式解在闭式解入口加 |r| 1 断言项数算出来是负数精度要求已满足时 log 值为正加if ratio 1: return 0保护和财务差几分钱Decimal 构造时直接传了 float用Decimal(str(x))而非Decimal(x)5.2 参数敏感性一个容易被忽视的数学事实前面一直在说 r 接近 1 麻烦但具体麻烦到什么程度值得用条件数说清楚。S a/(1-r)对 r 求导得 dS/dr a/(1-r)²相对误差放大倍数是 (dS/S)/(dr/r) r/(1-r)。r 0.99 时放大约 99 倍r 0.999 时放大约 999 倍。这意味着如果 r 本身只有 1e-9 的输入误差比如从配置文件读到 0.999 但实际应该是 0.999000001结果会有约 1e-6 的相对偏差。在需要高精度的场景里这个放大是致命的。解决办法不是提高计算精度而是从源头保证 r 的精度——用字符串传参、避免中途经过浮点、必要时用分数类型存公比。5.3 几条可以直接抄的实操建议第一写任何几何级数求和函数第一行先做收敛判断。|r| ≥ 1 且求无穷和直接抛异常别让它静默返回错误值。异常信息里带上 r 的实际值排查时能省很多时间。第二项数永远动态计算不要硬编码。把精度要求 ε 和参数 a、r 一起传进去用 terms_needed 算算出来的值再取个上限保护比如最多一千万项防止参数异常导致死循环。第三优先闭式解循环只用作验证。写完闭式解之后用循环算前一百项对比一下两者差值应该在余项界内。这个交叉验证我几乎每个数值函数都会做成本很低能挡住绝大多数低级错误。第四负公比和接近 1 的公比单独加测试用例。测试数据里一定要有 r -0.99、r 0.999、r 1、r 1.5、r 0 这几个边界以及 a 0 的情况。我见过 a 0 时 terms_needed 除零的 bug就是测试没覆盖到。最后再分享一个我在实际使用中养成的习惯每次看到比例衰减折现折扣这类词先在纸上写下首项和公比判断 |r| 和 1 的关系再决定是求有限和还是无穷和需要多少精度。这三步花不了一分钟但能避免九成以上的返工。数学公式本身很简单难的是知道什么时候该用它、用的时候边界在哪。