1.10 从1加到100:一道简单的数学题挑战一下你的大脑
把从 1 加到 100 拆成循环与公式两条数据通路,比较 CPU、内存、复杂度和定宽溢出。
学习目标
- 能在同一输入上比较循环加法与等差数列公式的 CPU 指令、内存访问和复杂度
- 能在读取 n、执行乘法和检查结果的不同顺序中定位定宽整数溢出
- 能用正常、边界和单一故障样本证明算法选择没有把最终偶然正确当成安全
1.10 从1加到100:一道简单的数学题挑战一下你的大脑
本页依据刘欣《码农翻身》(2018 年第 1 版)及出版社公开书目信息,独立重构 1.10 从1加到100:一道简单的数学题挑战一下你的大脑。正文、代码、图示、实验和练习都是本课程重新设计的教学材料,不复制原书正文、插图、练习答案或代码。
“从 1 加到 100”看似只是小学算术,却能把算法、数据通路和类型边界放在同一张桌上。循环方案重复读取计数器并更新累加器,复杂度是 O(n);公式方案把工作压缩成 n * (n + 1) / 2,但乘法中间值可能先超过整数位宽。少做循环不代表不需要验证。
三个会让简单求和失真的陷阱
五个目录节点到数据通路证据
CPU和内存
↡执行加法、乘法、比较和分支的处理器资源,以及保存输入、累加器、代码和轨迹的存储层次。决定“同一个公式”在机器上如何发生。循环每轮至少更新计数器和累加器,可能反复读取内存;公式减少迭代,却增加乘法、除法、类型转换和范围检查。比较时写清楚是否包含输入解析、缓存未命中与日志成本。
从1加到100
↡以 n=100 为基线的求和问题,也可泛化为正整数 n 的前缀和,并要求结果和中间值可解释。的数学结果是 100 × 101 ÷ 2 = 5050。工程实现不能只抄公式:要定义 n
的类型、允许的最大值、是否允许负数,以及遇到无法表示的结果时返回错误、饱和还是升级类型。
热身
↡在正式比较前固定输入、手算预期、列出循环不变量和可表示范围的准备阶段。先用 n=0、n=1、n=100 三个输入建立基线:循环结束时累加器是前缀和,计数器刚好越过 n,公式与循环结果一致。热身不是降低标准,而是先把 off-by-one 和单位/类型问题暴露出来。
正式出发
↡在固定合同下运行两条算法,记录数据通路、操作次数、范围检查和失败恢复,而不是只看一条最终输出。时同时跑循环和公式。循环的每轮不变量是 sum = 1 + ... + (i - 1);公式要在乘法前确认中间值可表示。若任一路径无法安全表示,就返回明确的
overflow,而不是截断后继续。
1.10 从1加到100:一道简单的数学题挑战一下你的大脑
这个标题的真正问题是:当大脑把数学等式视为永恒真理时,机器怎样把它拆成 CPU 指令、寄存器/内存访问和带宽有限的整数。1.10 从1加到100:一道简单的数学题挑战一下你的大脑用一份输入比较两条通路,再用边界证据决定哪条实现可以交付。
最小求和合同
type SumResult =
| { kind: "ok"; value: bigint; operations: number }
| { kind: "overflow"; stage: "input" | "multiply" | "accumulate" };
function sumLoop(n: bigint, limit: bigint): SumResult {
if (n < 0n) return { kind: "overflow", stage: "input" };
let sum = 0n;
for (let i = 1n; i <= n; i += 1n) {
sum += i;
if (sum > limit) return { kind: "overflow", stage: "accumulate" };
}
return { kind: "ok", value: sum, operations: Number(n) };
}
function sumFormula(n: bigint, limit: bigint): SumResult {
if (n < 0n) return { kind: "overflow", stage: "input" };
const a = n;
const b = n + 1n;
const product = a * b;
if (product > limit * 2n) return { kind: "overflow", stage: "multiply" };
return { kind: "ok", value: product / 2n, operations: 1 };
}合同先选择明确的 bigint 中间值,再把 limit 当成目标机器类型的可表示上限。若真实实现使用定宽整数,必须在每次加法或乘法前做等价检查;不能因为 JavaScript 示例使用宽类型,就假设 C、RISC-V 或数据库中的整数也无限大。
四步复核两条求和通路
1. 热身:固定输入与不变量
用 n=0、1、100 写出预期值、计数器终点、累加器范围和允许的错误。检查循环的初始化、条件和自增各一次,先排除边界索引问题。
Lab
两条求和路径实验
只改变一个条件,观察复杂度、阶段值和溢出判定如何变化。
循环与公式都得到 5050
loop: 100 iterations; formula: 100 × 101 ÷ 2; both within limit
判定
accept:保留两条路径的阶段证据
当前样本:基线 n=100;保存输入、阶段值、操作计数、位宽和重放结果。
正常、边界与单一故障证据
| 样本 | 只改变的变量 | 预期判定 | 必存证据 |
|---|---|---|---|
| 正常 | n=0、1、100,类型和终止条件正确 | 两条路径都得出 5050 或相同基线 | 输入、算法、操作计数、阶段值 |
| 边界 | n 接近位宽上限或结果超过 limit | 在溢出节点停止并返回明确状态 | 中间乘积、累加器、类型和范围检查 |
| 单一故障 | 终止条件、窄转换或一条加法被改动 | 首个不变量破坏被捕获,不能静默通过 | 故障位置、前后轨迹、恢复重放 |
故障诊断:先定位哪条通路先偏离
- 输入节点:确认 n 的解析值、符号、单位和类型;n 小于 0、截断或隐式转换先判为输入问题。
- 执行节点:循环看计数器与累加器不变量,公式看乘法前后的中间值;对每个节点记录可表示范围。
- 结果节点:用独立参考实现或手算值对照,不把同一条有缺陷的公式当作 oracle。
- 恢复节点:选择更宽类型、分解乘法、限制输入或返回 overflow,并从同一输入重新跑,确认轨迹真正改变。
如果循环得到 5050 而公式得到错误值,先查乘法中间值;如果两者都错误,先查 n 和终止条件;如果只在大 n 错误,检查位宽和转换。诊断目标是首个偏离,而不是找到一个能让最终数字“看起来对”的补丁。
最小求和证据包与反例
证据包包含输入 n、整数类型和位宽、初始值、循环不变量、公式中间值、CPU/内存事件口径、操作次数、范围检查、最终状态和重放结果。对性能比较还要固定编译选项、平台和是否包含 I/O,否则数字不能互相比较。
反例可以是 16 位实现计算一个结果尚可表示、但 n*(n+1) 已先溢出的输入;也可以是循环条件写成 < n,只在 n=100 的某次后处理里被偶然修正。保留反例的阶段轨迹,修改一个条件后重新运行,证明通过来自不变量而不是巧合。
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- CPU和内存
执行算术与分支的处理器资源,以及保存输入、累加器和轨迹的存储层次。
- 从1加到100
以 n=100 为基线的前缀和问题,同时要求定义类型和可表示范围。
- 热身
固定小输入、预期值和循环不变量,提前暴露终止条件与类型错误。
- 正式出发
在固定合同下运行并记录两条数据通路及其失败证据。
练习
练习
问题 1: 16 位无符号整数执行 n * (n + 1) / 2。为什么“最终答案能放进 16 位”仍不能证明公式安全?
问题 2: 循环和公式在 n=100 都得到 5050,但 CPU/内存成本要怎么公平比较?
问题 3: 一段循环把 i <= n 改成 i < n 后,某个测试仍然通过。你怎样定位并修复?
本页小结
1.10 从1加到100:一道简单的数学题挑战一下你的大脑的关键不是背出 5050,而是能把循环和公式都还原成 CPU、内存、类型和范围检查。完成标准是先做热身固定不变量,再正式比较两条通路,在定宽边界捕获中间溢出,并用阶段轨迹证明结果不是偶然正确。