第 5 章 设计算法 — 递归、动态规划、蛮力与贪心
第 5 章 设计算法 — 递归、动态规划、蛮力与贪心
这一章讲什么: 面对一个全新问题,怎么想出解法。书里给了五类策略: 递归(以及它长出的分治/减治)、回溯、动态规划、蛮力搜索、贪心算法, 每类配 2–3 个完整例子。它在全书链条里的位置:工具箱章—— 第 6–9 章的具体算法(排序、选择、洗牌、搜索)全是这些策略的实例, 第 12–17 章的数据结构算法里也到处是本章的手艺。
1. 顶层全景
一个递归函数的两类情况:基础情况(直接解) + 递归情况(用更小的同类问题)
│
├─ 减治(decrease-and-conquer):只解一个更小版本 → 阶乘、二分搜索
├─ 分治(divide-and-conquer):解多个更小版本 → 斐波那契、汉诺塔、归并/快排
├─ 回溯(backtracking):试错+撤销 → 迷宫、凑数谜题
├─ 动态规划(DP):子问题存起来不重算 → 斐波那契加速、断行
├─ 蛮力(brute force):全排列试个遍 → 重言式、字母算式(阶乘级,慎用)
└─ 贪心(greedy):每步取当前最优 → 找零、最近邻旅行商(可能不最优!)
图说:左边三个是「递归家族」,右边三个是「搜索家族」。
全书后文反复回到这张表:快排是分治、堆调整是减治、图遍历是回溯、拓扑排序接贪心。2. 主线例子:斐波那契数,从朴素递归到动态规划的完整改造
选斐波那契当主线,因为原书拿它把「递归 → 发现性能缺陷 → 动态规划修复」 走成了完整一圈1:
数列定义:F₀=0,F₁=1,之后每一项是前两项之和:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55…2
第 ① 步:朴素递归版。 直接翻译定义:
const fibo = (n) => {
if (n === 0) { return 0; }
else if (n === 1) { return 1; }
else { return fibo(n - 1) + fibo(n - 2); }
};对、短、不可能写错——但慢。算 fibo(7) 的调用树里,fibo(5) 算了两次, fibo(3) 算了五次,fibo(2) 算了八次3。 总调用数 C(n) 满足 C(n) = C(n−1) + C(n−2) + 1,解出来 C(n) = 2·Fibo(n+1) − 1 (习题 5.4 的答案)——指数级。算 fibo(50) 要万亿次级调用,天荒地老。
第 ② 步:记忆化(memoization)——给纯函数装缓存。 这是第 2 章高阶函数
- 闭包的直接应用,全书最美的 8 行代码之一4:
const memoize = (fn) => {
const cache = {};
return (...args) => {
const strX = JSON.stringify(args);
return strX in cache
? cache[strX]
: (cache[strX] = fn(...args));
};
};
const fibo = memoize((n) => { /* 原样递归体 */ });走一遍 fibo(7):第一次调用一路递归下去,但每个 fibo(k) 算出后都存进
cache(键是参数序列化成的字符串,值是结果);之后任何重复的 fibo(k)
都是一次查表。每个值只真正计算一次——时间从指数级降到线性级,
fibo(100) 立即返回5。
缓存放在闭包里,外面看不见、也碰不到(第 1 章闭包一节的实战)。
第 ③ 步:制表(tabulation)——自底向上,连缓存表都省了。
const fibo = (n) => {
if (n < 2) { return n; }
let a = 0, b = 1;
while (n > 1) { [a, b] = [b, a + b]; n--; }
return b;
};从最小的值往上滚:[a, b] = [b, a+b] 是解构交换的推广(第 1 章笔法)。
注意它根本不存历史值——因为斐波那契只需要最近两个数。
这暴露了记忆化与制表的真正分工(书里的原话要点):记忆化严格只算需要的
但有递归开销;制表可能算多了但没有递归、通常更快6。
第 ④ 步:同一招用到「区间求和」。 断行问题需要频繁问「第 x 到 y 个块共多宽」。
三个版本递进:朴素循环 O(n) → 记忆化递归(「0 到 q 的和 = 0 到 q−1 的和 + arr[q]」,
多次询问后摊还 O(1))7 → 制表预计算前缀和,
IIFE + 闭包把 partial 表藏起来,查询恒 O(1)8。
第 4 章「预计算换查询」在这里有了第三个名字:自底向上动态规划。
3. 核心原理
3.1 递归的四步构造法
写递归函数不靠灵感,靠流程。书里给的流程看似循环论证,实为最强心法9:
- 假设你已经有一个能解这个问题的函数(就是你要写的这个);
- 找出能直接解的基础情况(不必再递归的小输入);
- 想出怎么把原问题化解成一个或多个更小的同类问题;
- 调用 ① 假设的函数解决 ③ 的小问题,小到头就直接解。
阶乘是样板:n! = n × (n−1)!,基础情况 0! = 1(「书架空着的排法只有一种」)。 代码与定义逐字对应,所以「很难写错」10。 书里补了递归的「全能性」:任何能用循环写的都能用递归写,反过来也行; Haskell 之类的语言甚至没有循环、只有递归。
3.2 减治与分治:递归的两个量级
两者都是「化小」,区别在化几个:
- 减治(decrease-and-conquer):只用一个更小版本——阶乘、二分搜索、 遍历任务清单;书里也叫它「分治的特例」11。
- 分治(divide-and-conquer):用多个更小版本——斐波那契(两个)、 归并排序与快排(第 6 章)、汉诺塔(下面走一遍)。
汉诺塔走查(3 根柱 A/B/C,n 个盘从 A 移到 C,大盘不能压小盘):
const towers = (disks, origin, extra, destination) => {
if (disks > 0) {
towers(disks - 1, origin, destination, extra); // ① 上面 n−1 个挪去中转柱
console.log(`Move disk ${disks} from ${origin} to ${destination}`); // ② 最大的挪去目标柱
towers(disks - 1, extra, origin, destination); // ③ n−1 个再挪回来压住
}
};towers(4, "A", "B", "C") 输出 15 步(2⁴−1)。为什么是 2ⁿ−1?
递推 M(n) = 2M(n−1) + 1:两次搬 n−1 个盘加搬一次最大的12。
书里附了那个著名的尾声:64 个金盘的版本,一秒一步要 2⁶⁴−1 秒,
五千八百亿年以上——指数级的直观尺度13。
3.3 回溯:选一条路,死路就撤销重选
回溯(backtracking) 的模式:面临多个选项时选一个先试; 成功就收工,失败就回到当初做选择的地方换下一个;选项耗尽则整体无解14。
迷宫的伪代码骨架(第 17 章会给完整图算法):到目标?成功。 否则标记当前格已访问,对每格邻居递归试;哪个邻居返回了路径就返回; 全失败返回 null——null 一路冒泡上去,就是「撤销」的实现15。
Sam Loyd「海滩打娃娃」谜题(击倒几个编号娃娃凑满 50 分)是完整代码版:
const solve = (goal, standing, score = 0, dropped = []) => {
if (score === goal) { return dropped; } // 正好达标
else if (score > goal || standing.length === 0) { return null; } // 超了/没选项了:失败
else {
const chosen = standing[0];
const others = standing.slice(1);
return (
solve(goal, others, score + chosen, [...dropped, chosen]) // 先试:选第一个娃娃
|| solve(goal, others, score, dropped) // 失败回溯:不选它
);
}
};
solve(50, [15, 9, 30, 21, 19, 3, 12, 6, 25, 27]);|| 串联就是回溯的骨骼:左边(选了它)返回 null 才轮到右边(不选它)。
习题 5.3 的变体(射箭可重复命中同一环)只要在递归里加一支「再试同一选项」的递归
或把选项列表复制多份,骨架原封不动16。
3.4 蛮力搜索:全排列的两张脸
蛮力 = 系统性地试所有组合。问题只有一个,但致命:组合数爆炸。
脸一:重言式判定(指数级 2ⁿ)。 n 个布尔变量的表达式是否恒真? 递归地对每个变量先试 false 再试 true,全真才算重言式; 一旦发现一个反例立刻短路返回17。
脸二:字母算式(阶乘级 n!)。 经典题 SEND + MORE = MONEY: 每个字母对应一个不同数字,求使等式成立的对应。10 个数字的全排列共 10! = 3,628,800 种, 逐个检验。检验函数直白:拼出 SEND、MORE、MONEY 三个数,首位为 0 直接拒, 否则看等式18。解:S=9, E=5, N=6, D=7, M=1, O=0, R=8, Y=2 (9567 + 1085 = 10652)。
全排列的生成本身是个 14 世纪 Narayana Pandita 的四步算法: 从右找第一个「上坡」d[p] < d[p+1] → 从右找比 d[p] 大的最小值 d[q] → 交换 → 翻转 p 之后。 书上拿 8403976521 → 8405123679 完整走了一遍19。
边界(书里明写):这个类别的算法「阶数落入指数或阶乘级,输入稍大就不可用, 所以本书其余部分会避开这类代码」——蛮力在这本书里的角色是反面教材20。
3.5 贪心算法:每步最优,全局可能不最优
贪心(greedy) 的规矩:每到一个需要做决定的地方,选当下看起来最好的选项, 不回头、不试探。快,但可能翻车——「贪心算法可能(也可能不)达成目标」是定义的一部分21。
三个例子画出它的适用边界:
- 找零(成立但有条件):每次尽量用最大面额。$229.60 = 2×$100 + $20 + $5
- 4×$1 + 2×quarter + 1×dime,张数最少,成立。 但换一个币制就翻车:有 $9/$8/$1 的国家,付 $16,贪心给 1×$9+7×$1(8 张), 最优是 2×$8(2 张)22。
- 旅行商(通常不最优):每步去最近的未访问城市。快,多数时候不错, 但不保证最短——这是 NP 难问题,精确解要蛮力全排列23。
- 最小生成树(成立且可证):给若干房子沿已有道路铺电缆,总成本最小。 Kruskal 算法(第 17 章实现)也是贪心——每次选最便宜的、不成环的路段, 而这个贪心保证最优24。
适用边界的规律(补充,不在书里明说,来自通用知识):贪心能赢的问题都有 「贪心选择性质 + 最优子结构」;书里用三个例子的事实差异(找零看币制、 旅行商看运气、生成树保证)让读者自己摸到这条线,但没给判别定理。
3.6 断行问题:动态规划的实战规模
值得单独走一遍,因为它是「DP 不只是斐波那契」的证据:给宽度不定的表单字段断行, 要求右对齐且空白惩罚最小25:
- 输入:块宽
[7, 2, 5, 3, 6],行宽上限 MW=10; - 行成本 = 行尾空白²(平方让「分散的小空白」优于「集中的大空白」: 两个空格放一行 2²=4,分放两行 1²+1²=2);
- 递归:
costOfFragment(p, q)= 若 p..q 放得下则成本 (10−宽)², 否则在 p..q 间枚举断点 r,取左右成本之和的最小值; - 结果:成本 19,断点 [0, 2, 4]——即
[7] / [2,5] / [3,6],验证: (10−7)² + (10−7)² + (10−9)² = 9+9+1 = 19; 对照另两种三行排法 21 和 25,确实最优26。
和斐波那契同病:子段成本被重复计算(如 (5,3,6) 出现多次),
套一层 memoize 即修复27。习题 5.5 的优化更妙:循环里一旦
累计宽度超过 MW 就 break——越往右只会更宽28。
书里注明这是 TeX 排版算法(Knuth-Plass)的同族问题。
4. 作者的判断与证据
- 「递归实现很难写错」是反复声明的经验判断(阶乘、斐波那契两处都说了), 依据是「逻辑与定义逐字对应」;反例(忘写基础情况)本章未讨论,见局限10。
- 蛮力被明确定位为反面教材:「结果算法的阶数通常落入指数或阶乘级…… 本书其余部分会避开这类代码」——少见的、旗帜鲜明的「别学我」20。
- 贪心的边界用三个例子的事实差异呈现,不给判别定理——这是教学取舍: 记住三个案例的三种结局,比记住一个定理对初学者更有用,但形式化工具缺失。
- DP 两路线的取舍有明确口径:记忆化严格按需但有递归开销,制表可能多做但更快 6;断行题里还演示了「记忆化能套在任何纯函数上」的通用性。
- 习题答案补充了硬事实:fibo(7) 共 41 次调用、C(n)=2Fibo(n+1)−129; 汉诺塔人手解法(最小盘循环移动)30。
判断(我们的,不是书里的): 本章的编排顺序(递归 → 回溯 → DP → 蛮力 → 贪心) 是一条精心设计的性能焦虑曲线:先给你最优雅的武器,再让你亲眼看到它爆炸 (fibo 调用树),然后递上修复工具(DP),最后把「不惜代价试个遍」和 「赌一把当前最优」放在光谱两端示众。这个顺序比按字母表讲策略高明得多。 如果错,会错在: 焦虑曲线对自学者有效,但对已经见过这些策略的读者是冗余; 且本章每个策略都浅尝辄止,读者若只读本章会产生「我懂了」的错觉—— 真正的掌握要看第 6–17 章里这些策略反复出场后的复盘。
5. 边界与局限
- 尾递归、递归深度限制没讲:JS 引擎对调用栈深度有限制(深递归会爆栈), 本章所有递归都很浅,问题没暴露;也没有讲尾调用优化(ES2015 规定了但引擎普遍未实现) ——这是补充(不在书里,来自通用知识)。
- 蛮力与贪心的理论边界没给:没有 NP 完全性的概念框架,读者不知道 「旅行商为什么不能贪心解决」是结构性原因而非运气。
- 动态规划没有系统性框架:没有「状态定义 → 转移方程 → 边界 → 顺序」的标准四步, 靠三个例子悟;习题答案里的优化(break 剪枝)也只给代码不给原则。
- 回溯的复杂度没分析:迷宫与谜题代码都正确,但最坏情况多慢没量化。
6. 可带走的
- 递归四步(递归 = 函数自己调用自己):假设函数已存在 → 找基础情况(小到不必再递归的情形)→ 化解为更小同类问题 → 调用假设的函数。
- 减治解一个更小版(阶乘),分治解多个(斐波那契/汉诺塔);汉诺塔 n 盘 = 2ⁿ−1 步,指数级。
- 朴素递归斐波那契是指数级(fibo(7) 就要 41 次调用);记忆化让每个值只算一次,变线性。
- memoize 八行:闭包存缓存、
JSON.stringify(把参数序列化成字符串的标准格式)做键、查表优先——可套在任何纯函数上。 - 记忆化(自顶向下,严格按需)vs 制表(自底向上,可能多做但更快);前缀和 + IIFE = 查询 O(1)。
- 回溯 = 试错 +
||串联的撤销;「可重复选」的变体只需加一支递归或复制选项。 - 蛮力属于指数/阶乘级,现实中别用——本书自己就这么宣布;全排列四步算法(Narayana Pandita)留档备用。
- 贪心每步取当前最优、可能拿不到整体最优:找零看币制,旅行商看运气,最小生成树保证最优。
- DP 实战不止数列:断行问题的成本定义(空白²)与「超宽即 break」剪枝是可直接搬运的套路。
7. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 递归四步 | Recursion | text/15-fm-recursion.txt:61(搜「four-step」) |
| 减治/分治 | Recursion | text/15-fm-recursion.txt:84(搜「decrease-and-conquer」) |
| 阶乘 | Recursion | text/15-fm-recursion.txt:92(搜「factorial」) |
| 斐波那契朴素版 | Recursion | text/15-fm-recursion.txt:145(搜「F0」) · text/15-fm-recursion.txt:148(搜「fibo」) |
| 汉诺塔代码与 2ⁿ−1 | Recursion | text/15-fm-recursion.txt:181(搜「towers」) · text/15-fm-recursion.txt:217(搜「584」) |
| 回溯定义与迷宫 | Recursion | text/15-fm-recursion.txt:229(搜「Backtracking」) · text/15-fm-recursion.txt:248(搜「solveMaze」) |
| 打娃娃谜题 | Recursion | text/15-fm-recursion.txt:292(搜「solve」) |
| DP 两种路线 | Recursion | text/15-fm-recursion.txt:321(搜「top down」) · text/15-fm-recursion.txt:324(搜「tabulation」) |
| memoize 实现 | Recursion | text/15-fm-recursion.txt:370(搜「memoize」) |
| 断行问题 | Recursion | text/15-fm-recursion.txt:417(搜「7, 2, 5」) · text/15-fm-recursion.txt:453(搜「costOfFragment」) · text/15-fm-recursion.txt:499(搜「19 [0, 2, 4]」) |
| 自底向上斐波那契 | Recursion | text/15-fm-recursion.txt:538(搜「[a, b]」) |
| 区间和三版本 | Recursion | text/15-fm-recursion.txt:585(搜「totalWidth2」) · text/15-fm-recursion.txt:608(搜「partial = [0]」) |
| 蛮力与重言式 | Recursion | text/15-fm-recursion.txt:629(搜「combinatorial」) · text/15-fm-recursion.txt:663(搜「isTautology」) |
| 字母算式与全排列 | Recursion | text/15-fm-recursion.txt:700(搜「Dudeney」) · text/15-fm-recursion.txt:729(搜「Narayana」) · text/15-fm-recursion.txt:799(搜「sendMoreMoney」) |
| 贪心定义与找零 | Recursion | text/15-fm-recursion.txt:825(搜「may not always work」) · text/15-fm-recursion.txt:842(搜「$9」) |
| 旅行商与生成树 | Recursion | text/15-fm-recursion.txt:852(搜「Hamiltonian」) · text/15-fm-recursion.txt:871(搜「Kruskal」) |
| 答案 5.1–5.7 | Chapter 5 | text/68-ch05-chapter-5.txt:7(搜「one-liner」) · text/68-ch05-chapter-5.txt:14(搜「odd steps」) · text/68-ch05-chapter-5.txt:66(搜「2Fibo」) · text/68-ch05-chapter-5.txt:96(搜「reduce」) |
Footnotes
-
出处:「Recursion」第 145 段(
text/15-fm-recursion.txt:145,搜「F0」)。原文:F₀=0,F₁=1,n>1 时 Fₙ=Fₙ₋₁+Fₙ₋₂。 ↩ -
出处:「Recursion」第 142 段(
text/15-fm-recursion.txt:142,搜「Leonardo」)。原文给出数列 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55;注脚补历史:以比萨的列奥纳多(斐波那契)命名,他因兔子问题得名,数列更早出现于诗歌韵律。 ↩ -
出处:「Recursion」第 361 段(
text/15-fm-recursion.txt:361,搜「repeating」)。原文:fibo(7) 隐含 fibo(5) 重复计算,「fibo(3) 或 fibo(2) 被调用多少次?」;调用树图 5-6 见第 357 段。 ↩ -
出处:「Recursion」第 370–378 段(
text/15-fm-recursion.txt:370,搜「memoize」)。原文:memoize 是高阶函数,用闭包维护缓存对象;返回函数先把参数 JSON.stringify 成字符串当键,strX in cache成立则直接返回缓存值,否则计算、存缓存、返回。 ↩ -
出处:「Recursion」第 388 段(
text/15-fm-recursion.txt:388,搜「immediate」)。原文:「如果你现在试 fibo(100),结果会立刻出来……0 到 100 之间每个斐波那契数都会被计算,但只计算一次。算法从指数级变成了线性级。」 ↩ -
出处:「Recursion」第 324 段(
text/15-fm-recursion.txt:324,搜「tabulation」)。原文:制表可能更快(无需递归)但可能解了并不需要的子问题;记忆化因递归略慢但「严格只算需要的」。 ↩ ↩2 -
出处:「Recursion」第 585–594 段(
text/15-fm-recursion.txt:585,搜「totalWidth2」)。原文:记忆化版把区间和定义为更小区间和的组合;「你仍有一个 O(n) 算法,但随时间推移变成 O(1) 过程;初始延迟被摊还。」 ↩ -
出处:「Recursion」第 607–614 段(
text/15-fm-recursion.txt:608,搜「partial = [0]」)。原文:IIFE + 闭包预计算前缀和表partial[k]= 前 k 项和(partial[0]=0);查询 =partial[to + 1] - partial[from],O(1)。 ↩ -
出处:「Recursion」第 61–73 段(
text/15-fm-recursion.txt:61,搜「four-step」)。原文四步:假设已有解题函数 → 找基础情况 → 想出如何先解一个或多个更小版本 → 用假设的函数解小问题。 ↩ -
出处:「Recursion」第 101–108 段(
text/15-fm-recursion.txt:101,搜「factorial」)。原文:n=0 返回 1,否则 n×factorial(n−1);「递归实现很难出错,因为逻辑与定义紧密对应」。 ↩ ↩2 -
出处:「Recursion」第 84 段(
text/15-fm-recursion.txt:84,搜「decrease-and-conquer」)。原文:只递归解「仅一个更小版本」的策略叫减治,与分治同属一个思想。 ↩ -
出处:「Recursion」第 181–191 段(
text/15-fm-recursion.txt:181,搜「towers」)。原文:先递归把上面 n−1 个挪到中转柱,搬最大的,再递归挪回来;towers(4,"A","B","C") 输出 15 步。 ↩ -
出处:「Recursion」第 217 段(
text/15-fm-recursion.txt:217,搜「584」)。原文:M(n)=2M(n−1)+1、M(0)=0,故 M(n)=2ⁿ−1;64 盘「以每秒一步计需 2⁶⁴−1 秒,超过 5840 亿年,所以我们很安全」。 ↩ -
出处:「Recursion」第 229 段(
text/15-fm-recursion.txt:229,搜「Backtracking」)。原文:选一个选项试;成功则完,失败则「回溯到做选择的点换另一个」;选项耗尽则无解。 ↩ -
出处:「Recursion」第 248–261 段(
text/15-fm-recursion.txt:248,搜「solveMaze」)。原文伪代码:到达目标返回 path;否则标记已访问,对所有邻格递归,任一成功即返回;全试过返回 null。第 270 段(搜「guaranteed」)说明「只要存在路径,算法最终必能找到」。 ↩ -
出处:「Chapter 5」第 23–59 段(
text/68-ch05-chapter-5.txt:23,搜「solve」)。答案原文:先递归「再试同一环」,失败再走原逻辑;第二法把环按可命中次数复制进列表(40、39 各两次,23、24 各四次,17 五次,16 六次);解为 16,16,17,17,17,17。 ↩ -
出处:「Recursion」第 663–675 段(
text/15-fm-recursion.txt:663,搜「isTautology」)。原文:参数凑齐则求值,得 false 打印 Failed at 并短路返回;否则对 [...args, false] 与 [...args, true] 递归且取 &&。需 2ⁿ 次测试,第 657 段(搜「2n tests」)。 ↩ -
出处:「Recursion」第 799–813 段(
text/15-fm-recursion.txt:799,搜「sendMoreMoney」)。原文:S 或 M 为 0 拒;拼 SEND/MORE/MONEY 三数判等式;解输出9 5 6 7 1 0 8 2 3 4,即 9567+1085=10652。组合数 10! 见第 708 段(搜「3,628,800」)。 ↩ -
出处:「Recursion」第 729–752 段(
text/15-fm-recursion.txt:729,搜「Narayana」)。原文四步与 8403976521 → 8405123679 的走查;称该算法可能由 14 世纪印度数学家 Narayana Pandita 发现。 ↩ -
出处:「Recursion」第 632 段(
text/15-fm-recursion.txt:632,搜「avoid」)。原文:「鉴于这类算法的结果阶数,毫不奇怪本书其余部分会避开这种代码。」 ↩ ↩2 -
出处:「Recursion」第 825 段(
text/15-fm-recursion.txt:825,搜「may not always work」)。原文:「贪心算法可能(也可能不)达成目标」;基本定义里算法应保证解题,贪心是个特例。 ↩ -
出处:「Recursion」第 842 段(
text/15-fm-recursion.txt:842,搜「$9」)。原文:$16 在有 $9/$8/$1 的国家,贪心得 1×$9+7×$1,而最优是 2×$8;「贪心算法可能(也可能不)成功,视情况而定」。 ↩ -
出处:「Recursion」第 852–858 段(
text/15-fm-recursion.txt:852,搜「Hamiltonian」)。原文:精确解需全排列,城市一多就不可行;贪心版「每步访问最近的未访城市」,「不保证找到最优,但在某些条件下能找到」。 ↩ -
出处:「Recursion」第 868–871 段(
text/15-fm-recursion.txt:871,搜「Kruskal」)。原文:Kruskal 算法是贪心且「保证找到最优解」——每次选最便宜的、不成环的路段。 ↩ -
出处:「Recursion」第 417 段(
text/15-fm-recursion.txt:417,搜「7, 2, 5」)。原文:五个块宽 7、2、5、3、6,行宽 10;TeX 用 Knuth-Plass 算法解同族问题(第 414 段注释)。 ↩ -
出处:「Recursion」第 444 段(
text/15-fm-recursion.txt:444,搜「squaring」)与第 500 段(搜「19 [0, 2, 4]」)。原文:平方成本使分散小空白占优;三种三行排法成本 21、25、19;算法输出 19 [0, 2, 4]。 ↩ -
出处:「Recursion」第 503–514 段(
text/15-fm-recursion.txt:503,搜「multiple times」)。原文:(5,3,6)、(2,5,3)、(7,2) 等被重复计算;套 memoize 后灰色块免算,多处以箭头标出「无需递归」。 ↩ -
出处:「Chapter 5」第 79–87 段(
text/68-ch05-chapter-5.txt:80,搜「totalWidth(p, r)」)。答案原文:循环开头if (totalWidth(p, r) > MW) break;——「p 到 r 宽度一超 MW 就停,后面的只会更宽」。 ↩ -
出处:「Chapter 5」第 66 段(
text/68-ch05-chapter-5.txt:66,搜「2Fibo」)。答案原文:C(n)=C(n−1)+C(n−2)+1,显式解 C(n)=2·Fibo(n+1)−1;C(7)=41 见题干(第 928 段)。 ↩ -
出处:「Chapter 5」第 14 段(
text/68-ch05-chapter-5.txt:14,搜「odd steps」)。答案原文:奇数步最小盘循环移(A→B→C→A),偶数步做唯一不涉及最小盘的移动;奇数盘时最小盘反向循环。 ↩