编程语言研究书库

第 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:

  1. 假设你已经有一个能解这个问题的函数(就是你要写的这个);
  2. 找出能直接解的基础情况(不必再递归的小输入);
  3. 想出怎么把原问题化解成一个或多个更小的同类问题;
  4. 调用 ① 假设的函数解决 ③ 的小问题,小到头就直接解。

阶乘是样板: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。

三个例子画出它的适用边界:

  1. 找零(成立但有条件):每次尽量用最大面额。$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。
  2. 旅行商(通常不最优):每步去最近的未访问城市。快,多数时候不错, 但不保证最短——这是 NP 难问题,精确解要蛮力全排列23。
  3. 最小生成树(成立且可证):给若干房子沿已有道路铺电缆,总成本最小。 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. 可带走的

  1. 递归四步(递归 = 函数自己调用自己):假设函数已存在 → 找基础情况(小到不必再递归的情形)→ 化解为更小同类问题 → 调用假设的函数。
  2. 减治解一个更小版(阶乘),分治解多个(斐波那契/汉诺塔);汉诺塔 n 盘 = 2ⁿ−1 步,指数级。
  3. 朴素递归斐波那契是指数级(fibo(7) 就要 41 次调用);记忆化让每个值只算一次,变线性。
  4. memoize 八行:闭包存缓存、JSON.stringify(把参数序列化成字符串的标准格式)做键、查表优先——可套在任何纯函数上。
  5. 记忆化(自顶向下,严格按需)vs 制表(自底向上,可能多做但更快);前缀和 + IIFE = 查询 O(1)。
  6. 回溯 = 试错 + || 串联的撤销;「可重复选」的变体只需加一支递归或复制选项。
  7. 蛮力属于指数/阶乘级,现实中别用——本书自己就这么宣布;全排列四步算法(Narayana Pandita)留档备用。
  8. 贪心每步取当前最优、可能拿不到整体最优:找零看币制,旅行商看运气,最小生成树保证最优。
  9. DP 实战不止数列:断行问题的成本定义(空白²)与「超宽即 break」剪枝是可直接搬运的套路。

7. 原文地图

主题原书章原文位置
递归四步Recursiontext/15-fm-recursion.txt:61(搜「four-step」)
减治/分治Recursiontext/15-fm-recursion.txt:84(搜「decrease-and-conquer」)
阶乘Recursiontext/15-fm-recursion.txt:92(搜「factorial」)
斐波那契朴素版Recursiontext/15-fm-recursion.txt:145(搜「F0」) · text/15-fm-recursion.txt:148(搜「fibo」)
汉诺塔代码与 2ⁿ−1Recursiontext/15-fm-recursion.txt:181(搜「towers」) · text/15-fm-recursion.txt:217(搜「584」)
回溯定义与迷宫Recursiontext/15-fm-recursion.txt:229(搜「Backtracking」) · text/15-fm-recursion.txt:248(搜「solveMaze」)
打娃娃谜题Recursiontext/15-fm-recursion.txt:292(搜「solve」)
DP 两种路线Recursiontext/15-fm-recursion.txt:321(搜「top down」) · text/15-fm-recursion.txt:324(搜「tabulation」)
memoize 实现Recursiontext/15-fm-recursion.txt:370(搜「memoize」)
断行问题Recursiontext/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]」)
自底向上斐波那契Recursiontext/15-fm-recursion.txt:538(搜「[a, b]」)
区间和三版本Recursiontext/15-fm-recursion.txt:585(搜「totalWidth2」) · text/15-fm-recursion.txt:608(搜「partial = [0]」)
蛮力与重言式Recursiontext/15-fm-recursion.txt:629(搜「combinatorial」) · text/15-fm-recursion.txt:663(搜「isTautology」)
字母算式与全排列Recursiontext/15-fm-recursion.txt:700(搜「Dudeney」) · text/15-fm-recursion.txt:729(搜「Narayana」) · text/15-fm-recursion.txt:799(搜「sendMoreMoney」)
贪心定义与找零Recursiontext/15-fm-recursion.txt:825(搜「may not always work」) · text/15-fm-recursion.txt:842(搜「$9」)
旅行商与生成树Recursiontext/15-fm-recursion.txt:852(搜「Hamiltonian」) · text/15-fm-recursion.txt:871(搜「Kruskal」)
答案 5.1–5.7Chapter 5text/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

  1. 出处:「Recursion」第 145 段(text/15-fm-recursion.txt:145,搜「F0」)。原文:F₀=0,F₁=1,n>1 时 Fₙ=Fₙ₋₁+Fₙ₋₂。 ↩

  2. 出处:「Recursion」第 142 段(text/15-fm-recursion.txt:142,搜「Leonardo」)。原文给出数列 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55;注脚补历史:以比萨的列奥纳多(斐波那契)命名,他因兔子问题得名,数列更早出现于诗歌韵律。 ↩

  3. 出处:「Recursion」第 361 段(text/15-fm-recursion.txt:361,搜「repeating」)。原文:fibo(7) 隐含 fibo(5) 重复计算,「fibo(3) 或 fibo(2) 被调用多少次?」;调用树图 5-6 见第 357 段。 ↩

  4. 出处:「Recursion」第 370–378 段(text/15-fm-recursion.txt:370,搜「memoize」)。原文:memoize 是高阶函数,用闭包维护缓存对象;返回函数先把参数 JSON.stringify 成字符串当键,strX in cache 成立则直接返回缓存值,否则计算、存缓存、返回。 ↩

  5. 出处:「Recursion」第 388 段(text/15-fm-recursion.txt:388,搜「immediate」)。原文:「如果你现在试 fibo(100),结果会立刻出来……0 到 100 之间每个斐波那契数都会被计算,但只计算一次。算法从指数级变成了线性级。」 ↩

  6. 出处:「Recursion」第 324 段(text/15-fm-recursion.txt:324,搜「tabulation」)。原文:制表可能更快(无需递归)但可能解了并不需要的子问题;记忆化因递归略慢但「严格只算需要的」。 ↩ ↩2

  7. 出处:「Recursion」第 585–594 段(text/15-fm-recursion.txt:585,搜「totalWidth2」)。原文:记忆化版把区间和定义为更小区间和的组合;「你仍有一个 O(n) 算法,但随时间推移变成 O(1) 过程;初始延迟被摊还。」 ↩

  8. 出处:「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)。 ↩

  9. 出处:「Recursion」第 61–73 段(text/15-fm-recursion.txt:61,搜「four-step」)。原文四步:假设已有解题函数 → 找基础情况 → 想出如何先解一个或多个更小版本 → 用假设的函数解小问题。 ↩

  10. 出处:「Recursion」第 101–108 段(text/15-fm-recursion.txt:101,搜「factorial」)。原文:n=0 返回 1,否则 n×factorial(n−1);「递归实现很难出错,因为逻辑与定义紧密对应」。 ↩ ↩2

  11. 出处:「Recursion」第 84 段(text/15-fm-recursion.txt:84,搜「decrease-and-conquer」)。原文:只递归解「仅一个更小版本」的策略叫减治,与分治同属一个思想。 ↩

  12. 出处:「Recursion」第 181–191 段(text/15-fm-recursion.txt:181,搜「towers」)。原文:先递归把上面 n−1 个挪到中转柱,搬最大的,再递归挪回来;towers(4,"A","B","C") 输出 15 步。 ↩

  13. 出处:「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 亿年,所以我们很安全」。 ↩

  14. 出处:「Recursion」第 229 段(text/15-fm-recursion.txt:229,搜「Backtracking」)。原文:选一个选项试;成功则完,失败则「回溯到做选择的点换另一个」;选项耗尽则无解。 ↩

  15. 出处:「Recursion」第 248–261 段(text/15-fm-recursion.txt:248,搜「solveMaze」)。原文伪代码:到达目标返回 path;否则标记已访问,对所有邻格递归,任一成功即返回;全试过返回 null。第 270 段(搜「guaranteed」)说明「只要存在路径,算法最终必能找到」。 ↩

  16. 出处:「Chapter 5」第 23–59 段(text/68-ch05-chapter-5.txt:23,搜「solve」)。答案原文:先递归「再试同一环」,失败再走原逻辑;第二法把环按可命中次数复制进列表(40、39 各两次,23、24 各四次,17 五次,16 六次);解为 16,16,17,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」)。 ↩

  18. 出处:「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」)。 ↩

  19. 出处:「Recursion」第 729–752 段(text/15-fm-recursion.txt:729,搜「Narayana」)。原文四步与 8403976521 → 8405123679 的走查;称该算法可能由 14 世纪印度数学家 Narayana Pandita 发现。 ↩

  20. 出处:「Recursion」第 632 段(text/15-fm-recursion.txt:632,搜「avoid」)。原文:「鉴于这类算法的结果阶数,毫不奇怪本书其余部分会避开这种代码。」 ↩ ↩2

  21. 出处:「Recursion」第 825 段(text/15-fm-recursion.txt:825,搜「may not always work」)。原文:「贪心算法可能(也可能不)达成目标」;基本定义里算法应保证解题,贪心是个特例。 ↩

  22. 出处:「Recursion」第 842 段(text/15-fm-recursion.txt:842,搜「$9」)。原文:$16 在有 $9/$8/$1 的国家,贪心得 1×$9+7×$1,而最优是 2×$8;「贪心算法可能(也可能不)成功,视情况而定」。 ↩

  23. 出处:「Recursion」第 852–858 段(text/15-fm-recursion.txt:852,搜「Hamiltonian」)。原文:精确解需全排列,城市一多就不可行;贪心版「每步访问最近的未访城市」,「不保证找到最优,但在某些条件下能找到」。 ↩

  24. 出处:「Recursion」第 868–871 段(text/15-fm-recursion.txt:871,搜「Kruskal」)。原文:Kruskal 算法是贪心且「保证找到最优解」——每次选最便宜的、不成环的路段。 ↩

  25. 出处:「Recursion」第 417 段(text/15-fm-recursion.txt:417,搜「7, 2, 5」)。原文:五个块宽 7、2、5、3、6,行宽 10;TeX 用 Knuth-Plass 算法解同族问题(第 414 段注释)。 ↩

  26. 出处:「Recursion」第 444 段(text/15-fm-recursion.txt:444,搜「squaring」)与第 500 段(搜「19 [0, 2, 4]」)。原文:平方成本使分散小空白占优;三种三行排法成本 21、25、19;算法输出 19 [0, 2, 4]。 ↩

  27. 出处:「Recursion」第 503–514 段(text/15-fm-recursion.txt:503,搜「multiple times」)。原文:(5,3,6)、(2,5,3)、(7,2) 等被重复计算;套 memoize 后灰色块免算,多处以箭头标出「无需递归」。 ↩

  28. 出处:「Chapter 5」第 79–87 段(text/68-ch05-chapter-5.txt:80,搜「totalWidth(p, r)」)。答案原文:循环开头 if (totalWidth(p, r) > MW) break;——「p 到 r 宽度一超 MW 就停,后面的只会更宽」。 ↩

  29. 出处:「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 段)。 ↩

  30. 出处:「Chapter 5」第 14 段(text/68-ch05-chapter-5.txt:14,搜「odd steps」)。答案原文:奇数步最小盘循环移(A→B→C→A),偶数步做唯一不涉及最小盘的移动;奇数盘时最小盘反向循环。 ↩

On this page