freeCodeCamp 每日编程挑战精讲:Challenge 61「Launch Fuel」的迭代燃料计算与浮点取整
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
本篇以 freeCodeCamp 课程仓库中的真实题目为蓝本,完整拆解 JavaScript 每日编程挑战(Daily Coding Challenge)第 61 题「Space Week Day 7: Launch Fuel」:从题目规则、逐步演算,到抽象建模、参考解法逐行讲解,再结合仓库源码说明该题从 Markdown 源文件到每日挑战系统的完整落地链路。读完你可以独立复现解法,理解「迭代逼近 + 提前终止」这类递归物理模型的编程实现,并掌握toFixed浮点取整在判题场景中的正确用法。
挑战的仓库位置与元信息
这道题的源文件位于 curriculum/challenges/english/blocks/daily-coding-challenges-javascript/68c497f3aaefc9fd9f1b0e25.md,文件头部的 frontmatter 给出了题目的全部元信息:
--- id: 68c497f3aaefc9fd9f1b0e25 title: "Challenge 61: Space Week Day 7: Launch Fuel" challengeType: 28 dashedName: challenge-61 ---从仓库结构看,本题隶属于 JavaScript 的每日编程挑战块(block)。在 curriculum/structure/blocks/daily-coding-challenges-javascript.json 中,该块的元数据记录为dashedName: daily-coding-challenges-javascript、helpCategory: JavaScript、usesMultifileEditor: true、disableLoopProtectTests: true,而它的challengeOrder列表按 id 维护了挑战顺序。
值得注意的是,这道题属于题目描述中所说的「Space Week」(太空周)主题系列,仓库同一目录下的相邻文件记录了完整的一周剧情线,例如 Challenge 55 至 Challenge 59 分别对应「Stellar Classification」「Exoplanet Search」「Phone Home」「Landing Spot」「Goldilocks Zone」,而 68c497f3aaefc9fd9f1b0e24.md 是 Day 6「Moon Phase」。本文要讲的这一份,正是太空周第 7 天(也是最后一天)的题目:计算把有效载荷送入轨道所需的燃料质量。
题目规则逐条拆解
原题描述给出了四条核心规则与一个输出约定:
- 推力比固定为 1/5:火箭每提升 5 kg 质量需要 1 kg 燃料,即「所需燃料 = 当前需提升的总质量 / 5」。
- 燃料自身有质量:每加入一份燃料,总质量就增加,进而产生新的燃料需求——这是一个自我递归的过程。
- 迭代终止条件:从有效载荷质量出发,先算当前所需燃料,把该燃料计入总质量后再算一次;不断重复,直到某一次新增加所需的燃料量小于 1 kg 为止。
- 忽略火箭本体质量:只计算「载荷 + 载荷自身所需燃料」的质量。
最终要求:返回燃料总量,四舍五入保留一位小数。
从工程角度理解,这其实是一个「反馈循环」问题:燃料增加 → 质量增加 → 需要的燃料增加 → 质量再增加……区别只在于它并非无限收敛,而是被「新增量小于 1 kg」这条阈值提前截断。
从示例出发:payload = 50 kg 的完整推演
原题用 50 kg 载荷给出了完整演算,这里把它整理成一张可逐行对照的表格(按参考解法的迭代语义展开):
| 轮次 | 当前总质量(载荷+已计燃料) | 本次新增燃料 | 累计燃料 | 说明 |
|---|---|---|---|---|
| 0 | 50.0 | 50 / 5 = 10.0 | 10.0 | 首轮所需燃料 |
| 1 | 60.0(50+10) | 60 / 5 − 10 = 2.0 | 12.0 | 新增 2 kg |
| 2 | 62.0(60+2) | 62 / 5 − 12 = 0.4 | 12.4 | 新增 0.4 kg < 1,停止 |
最终需要提升的总质量是 62.4 kg,其中 50 kg 是原始载荷、12.4 kg 是燃料;由于最后一次「新增 0.4 kg」已经小于 1 kg,迭代到此为止,答案返回12.4。
注意一个容易误解的细节:终止条件针对的是每一次迭代产生的新增燃料量,而不是累计燃料总量。也就是说,小于 1 kg 的那个尾项依然会计入最终燃料总和,只是它不再触发下一轮计算。
抽象建模:等比级数、固定点与提前截断
把推演过程公式化可以看得很清楚。记载荷质量为M,初始增量d₀ = M / 5。加入d₀后总质量变为M + d₀,此时「为这一新增质量补足的燃料」应为:
d₁ = (M + d₀) / 5 − d₀ = M/5 · (1/5) = d₀ / 5同理可推出d₂ = d₁ / 5、d₃ = d₂ / 5……也就是说,每次迭代的新增燃料构成公比为 1/5 的等比数列。如果让它无限进行下去,累计燃料的和就是等比级数:
F = d₀ + d₀/5 + d₀/25 + … = d₀ / (1 − 1/5) = M / 4这个「理想上限」与直观的物理图像吻合:若加入的燃料质量为 F,总质量变为 M + F,燃料恰好等于 (M + F)/5,解方程 5F = M + F 即得 F = M/4。
而本题实际要求的是:把公比为 1/5 的项逐个累加,直到某一项小于 1 kg 就停(且该尾项计入总和)。例如载荷 500 kg:增量序列为 100、20、4、0.8(<1 停),累计 124.8,与后面的断言一致;而理想上限 500/4 = 125,二者相差正是被截断的无穷小尾巴 0.2。
由此可以把题目重新描述成一个非常干净的算法:*以 M/5 为首项、1/5 为公比累加等比数列的有限前缀,前一项 ≥ 1 时继续取下一项,出现 < 1 的项时累加后停止。*这为写出等价解法提供了数学依据。
判题断言:五组 Hints 的验证
题目源文件的# --hints--部分内嵌了五组assert.equal,它们会成为挑战页面上自动判题系统执行的测试字符串。测试代码暗示函数签名为launchFuel(payload):
assert.equal(launchFuel(50), 12.4); assert.equal(launchFuel(500), 124.8); assert.equal(launchFuel(243), 60.7); assert.equal(launchFuel(11000), 2749.8); assert.equal(launchFuel(6214), 1553.4);| 载荷(kg) | 燃料需求 | 理想上限(载荷/4) | 两者之差 |
|---|---|---|---|
| 50 | 12.4 | 12.5 | 0.1 |
| 500 | 124.8 | 125.0 | 0.2 |
| 243 | 60.7 | 60.75 | ≈0.05(取整后体现) |
| 11000 | 2749.8 | 2750.0 | 0.2 |
| 6214 | 1553.4 | 1553.5 | 0.1 |
观察上表可以验证我们的等比级数模型:所有期望值都略小于「载荷 / 4」的理想上限,差的正是截断掉的无穷尾项,而由于最终结果还要toFixed(1)四舍五入,部分差距会被取整掩盖。这也从侧面印证了「迭代若干次 + 保留一位小数」就是判题点所在。
参考解法逐行讲解
仓库在源文件的# --solutions--中给出了官方参考实现:
function launchFuel(payload) { let totalMass = payload; let additionalFuel = totalMass / 5; let totalFuel = additionalFuel; while (additionalFuel >= 1) { totalMass += additionalFuel; additionalFuel = (totalMass / 5) - totalFuel; totalFuel += additionalFuel; } return Number(totalFuel.toFixed(1)); }逐行对应题目规则来分析:
let totalMass = payload:初始化待提升质量,这里刻意不含火箭本体质量(规则 4)。let additionalFuel = totalMass / 5:第一轮燃料需求,即 1 kg 燃料 / 5 kg 质量(规则 1)。let totalFuel = additionalFuel:累计燃料先记为第一轮结果。while (additionalFuel >= 1):只要上一轮计算出的新增燃料 ≥ 1 kg,就说明还需要继续迭代;这正是「重复直到新增量小于 1 kg」的镜像写法(规则 3)。需要特别说明的是:while 体内的totalFuel += additionalFuel会把每一次算出的增量(包括最后一轮 <1 的那个值)都计入累计燃料,因此最终 totalFuel 已经包含了尾项。additionalFuel = (totalMass / 5) - totalFuel:这是核心递推式——先算「提升当前总质量所需的燃料总量」,再减去已累计的燃料,差值就是本轮需要补足的燃料。加入燃料 → 总质量上升 → 需求上升,循环被正确建模(规则 2)。return Number(totalFuel.toFixed(1)):toFixed(1)负责四舍五入保留一位小数,但它返回的是字符串(例如"12.4"),所以外层用Number()把它转回数值,从而与assert.equal中的数字期望值严格匹配。
我们以内存量逐轮跟踪payload = 500:初始totalFuel = 100;第 1 轮后totalMass = 600、新增 20、累计 120;第 2 轮后totalMass = 620、新增 4、累计 124;第 3 轮算出totalMass = 624时新增 0.8 < 1,totalFuel停在 124.8 退出循环,返回124.8。与断言完全一致。
等价的「等比级数」写法
若按上文建模结论重写,可以得到更直白、也更能体现数学结构的版本:
function launchFuel(payload) { let totalFuel = 0; let increment = payload / 5; // 等比数列首项 d₀ while (true) { totalFuel += increment; // 本项(含 <1 的尾项)计入总和 if (increment < 1) break; // 下一轮将 < 1,停止 increment = increment / 5; // 公比 1/5 } return Number(totalFuel.toFixed(1)); }这个版本与参考解法对同一组载荷产生完全相同的输出,可作为验证参考解法正确性的第二个独立实现——参考解法把「累计燃料加入总质量」和「按 1/5 递推」融合进同一状态,而等比版本直接把比例关系显式化。把它放在脑图中对比,能帮你更深刻地理解两版代码为何等价:totalMass / 5 − totalFuel计算出的增量,本质上就等于上一轮增量的 1/5。
边界情况:极小载荷
当payload < 5时,payload / 5 < 1,首项就已经小于阈值。参考解法中additionalFuel初始即小于 1,while循环一次都不执行,直接返回payload / 5取整的结果(例如payload = 2返回0.4)。这在物理上同样自洽:不足 1 kg 的增量按规则直接停止追加。作为训练,可以自行验证payload = 0与payload = 6214等场景下的行为一致性。
浮点与取整的精度细节
这是本题最容易失分的技术点,值得单列说明:
- JS 中
payload / 5是浮点除法,对243、6214这类值会产生无限循环小数(如6214 / 5 = 1242.8,而 6214 迭代中的中间量如6214/4会出现非精确表示)。好在判题只要求一位小数,浮点误差通常在1e-12量级,toFixed(1)能将其掩蔽。 toFixed(1)执行的是四舍五入语义并返回字符串,因此外面必须套Number()。如果直接return totalFuel.toFixed(1),assert.equal的宽松相等(==)虽然能通过,但严谨的严格相等(===)判题环境下就会因字符串/数值类型不同而失败。- 由于二进制浮点表示,个别恰好落在「.05 边界」的值可能让
toFixed出现与十进制直觉不符的舍入(例如(1.005).toFixed(2)结果为"1.00")。本组测试数据未触及该陷阱,但在工程实践中若要绝对稳妥,可以改用「放大 10 倍取整后再缩小」或引入Number.EPSILON微调。作为扩展练习,不妨思考:如果题目把阈值从「1 kg」改成「1 g」,实现是否需要改动。
从一道题看 freeCodeCamp 的每日挑战落地链路
把这份源文件放回仓库全景,可以看到一条相当完整的「题目 → 服务 → 界面」链路,本题正是其中的一环:
- 题目创作与存储:课程块以 Markdown 形式维护题面、hints 与参考解法,本块(daily-coding-challenges-javascript)还声明了
usesMultifileEditor: true与disableLoopProtectTests: true——后者意味着判题环境不会因解法中的长循环而误杀,while结构可以放心使用。此外该块isUpcomingChange: true,这与 tools/daily-challenges/README.md 中「在本地以upcoming changes模式运行客户端,再执行pnpm seed-daily-challenges把挑战从开发沙箱超级块同步到DailyCodingChallenges集合」的说明相互印证。 - 服务端聚合:进入 api/src/daily-coding-challenge/schemas/daily-coding-challenge.ts 可以看到,一天的挑战被设计成包含
date、challengeNumber、title、description以及javascript/python两组tests与challengeFiles的数据结构——也就是说,同一道题会按语言分别携带测试串和初始代码(本题的 JavaScript 版本即function launchFuel(payload) { return payload; }这段占位种子)。 - 提交与判题:学习者在客户端完成后,前端会把语言选择(见 client/src/templates/Challenges/classic/action-row.tsx 中
javascript/python切换逻辑)与代码一起提交;API 侧的 api/src/routes/protected/challenge.ts 中约 350 行附近定义了/daily-coding-challenge-completed路由用于记录完成状态。端到端行为则由 e2e/daily-coding-challenge.spec.ts 覆盖。 - 判题核心:挑战页面最终执行的,正是本题
# --hints--中那几条assert.equal测试字符串——因此「返回 12.4 而不是'12.4'」「保留一位小数」「正确终止循环」这三点,就是本题能否通过的全部关键。
写在最后:还能往哪里深挖
本题表面是简单的除法循环,背后却浓缩了三个可迁移的编程思想:递归物理量的迭代建模(新增量反哺自身)、几何级数与提前截断(停止条件决定误差),以及浮点取整与类型转换(toFixed返回字符串这个经典陷阱)。如果你对这类题目感兴趣,同目录下的太空周系列是极好的配套练习:Day 6 的月相计算(68c497f3aaefc9fd9f1b0e24.md)同样包含模运算与分类讨论;再向前追溯的 Goldilocks Zone、Stellar Classification 等题则训练不同的数学建模路径。把「读题 → 建模 → 验证 → 对比参考解」这套流程反复跑几遍,正是提升算法编码能力最直接的方式。
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考