freeCodeCamp Daily Coding Challenge 301 "Last Load" 全解析:用平均值预测洗衣粉剩余天数
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
导读
本文围绕 freeCodeCamp 开源课程体系中daily-coding-challenges-javascript模块的第 301 道每日编程挑战"Last Load",完整拆解其题目语义、输入输出契约、五个官方测试用例的推导过程,以及官方标准解法的核心思路(历史均值 + 向下取整)。同时结合仓库中的块结构定义、API 路由实现、数据库种子脚本与前端组件,说明这道题如何在 freeCodeCamp 的每日编程挑战体系中从 Markdown 题目文件流转到真实产品页面,帮助你既掌握算法本身,也理解整套基础设施的运行机制。
一、题目速览:问题陈述与函数契约
题目原文位于 6a0dcc730cb92a616f86f0c5.md,属于daily-coding-challenges-javascript块(块结构定义见 daily-coding-challenges-javascript.json)。
题目描述(原文直译):
给定你剩余的洗衣粉勺数(scoops),以及一个数组,记录你在过去每天使用的勺数(usage),返回你还能使用洗衣粉的完整天数。
从使用历史中计算你的平均每日用量,并假设未来每天都是这个用量。
函数契约:
| 项 | 说明 |
|---|---|
| 函数名 | lastLoadDate(scoops, usage) |
| 参数 1 | scoops:剩余洗衣粉的总勺数(Number) |
| 参数 2 | usage:过去若干天每日用量的数组(Number[]) |
| 返回值 | 剩余可用洗衣粉的完整天数(Number,向下取整) |
这个函数命名看起来略有歧义——lastLoadDate字面含义是"最后一次洗衣的日期",但题目实际要求的是"还能洗多少次完整的衣服"(即剩余天数)。理解这一点是解题的关键:返回值是天数而非日期。
二、官方测试用例逐条推导
题目通过# --hints--段给出 5 个官方断言,源码中每条断言都使用assert.equal精确校验返回值,我们逐条验证解题逻辑的正确性。
用例 1:稳定用量
assert.equal(lastLoadDate(10, [2, 2, 2, 2, 2, 2, 2]), 5);- 每日用量:
[2, 2, 2, 2, 2, 2, 2],平均每日用量 =14 / 7 = 2 - 剩余天数 =
10 / 2 = 5,向下取整后仍为5✓
用例 2:含零用量的波动数据
assert.equal(lastLoadDate(16, [2, 3, 0, 3, 4, 2, 1]), 7);- 总用量 =
2 + 3 + 0 + 3 + 4 + 2 + 1 = 15,平均每日用量 =15 / 7 ≈ 2.1429 - 剩余天数 =
16 / 2.1429 ≈ 7.467,向下取整为7✓
注意:即使某一天用量为0(没洗衣服),该天依然计入分母usage.length,这保证了平均值能真实反映"摊到每一天"的用量。
用例 3:非整除场景
assert.equal(lastLoadDate(33, [5, 0, 4, 3, 3, 2]), 11);- 总用量 =
5 + 0 + 4 + 3 + 3 + 2 = 17,平均每日用量 =17 / 6 ≈ 2.8333 - 剩余天数 =
33 / 2.8333 ≈ 11.647,向下取整为11✓
用例 4:大数值混合波动
assert.equal(lastLoadDate(50, [2, 0, 2, 9, 12, 0, 2]), 12);- 总用量 =
2 + 0 + 2 + 9 + 12 + 0 + 2 = 27,平均每日用量 =27 / 7 ≈ 3.8571 - 剩余天数 =
50 / 3.8571 ≈ 12.963,向下取整为12✓
用例 5:高消耗场景下取整的重要性
assert.equal(lastLoadDate(20, [13, 9, 12, 10, 8]), 1);- 总用量 =
13 + 9 + 12 + 10 + 8 = 52,平均每日用量 =52 / 5 = 10.4 - 剩余天数 =
20 / 10.4 ≈ 1.923,若不取整会得到1.923,但题目要求"完整天数",因此向下取整为1✓
最后一个用例最能体现Math.floor的必要性:1.92 天意味着第 2 天的用量不足,所以只能算 1 个完整天。
三、官方解法:一行核心计算 + 向下取整
题目文件末尾的# --solutions--段给出了官方标准解:
function lastLoadDate(scoops, usage) { const avg = usage.reduce((sum, n) => sum + n, 0) / usage.length; return Math.floor(scoops / avg); }解法拆解
- 求和:
usage.reduce((sum, n) => sum + n, 0)遍历数组累加所有历史用量,0作为初始值保证空数组安全; - 求均值:除以
usage.length得到平均每日用量avg。这一行同时隐含了题目的核心建模——用历史均值代表未来的恒定日耗; - 预测天数:
scoops / avg计算出按当前均值能支撑的天数(可能为小数); - 向下取整:
Math.floor()只保留"完整"的天数,舍弃不足以支撑一整天的零头,严格符合题目"number offulldays"的要求。
选手可能写出的等价实现
function lastLoadDate(scoops, usage) { const total = 0; for (const n of usage) { total += n; } const avg = total / usage.length; return Math.floor(scoops / avg); }两种写法在语义上等价,官方解法更依赖reduce,体现函数式风格;无论哪种,avg的计算与Math.floor的取整是得分的关键。
四、边界情况与陷阱分析
从测试用例和官方实现中可以提炼出以下容易踩坑的点:
| 边界/陷阱 | 分析 |
|---|---|
| 必须向下取整 | 题目明确要求 "full days"。Math.floor是标准做法;Math.round或Math.ceil都会在用例 3、4、5 上得到错误答案 |
| 均值可能不是整数 | 用例 2 中均值 ≈ 2.14,用例 3 中 ≈ 2.83,直接除会产生小数,取整时机要在最后一步 |
| 0 值天不能丢弃 | 用例 2、4 都包含0,若先过滤 0 再求平均会得到偏高的均值、偏少的天数 |
| 空数组 | 题目未提供空数组用例;若usage为空,avg会得到NaN。官方用例未覆盖,但严谨实现可先做防御(不过按题目语境,历史记录始终存在) |
| 除法结果为 1 以下 | 用例 5 中20 / 10.4 ≈ 1.92,取整后为1,验证了"连 2 天都不够"的语义 |
另外注意scoops的单位是"勺"(scoop),usage数组元素同为"勺/天",单位一致才能直接相除得到天数。
五、仓库中的挑战工程化:从 Markdown 到产品页
这道题并非孤立存在的习题文件,它在 freeCodeCamp 仓库中属于一套完整的每日编程挑战基础设施,理解这套链路能帮你更好地把握题目在整个平台中的定位。
5.1 块结构与 365 天挑战序列
块元数据 daily-coding-challenges-javascript.json 中:
- 该块的
helpCategory为JavaScript,usesMultifileEditor为true(多文件编辑器),blockLayout为legacy-challenge-list; challengeOrder数组按顺序登记了365 道挑战(Challenge 1: Vowel Balance 至 Challenge 365: Bucket Fill 3),Challenge 301 "Last Load" 的 id6a0dcc730cb92a616f86f0c5与题目文件 frontmatter 中的id一一对应;- 365 这个数字与种子脚本的校验值一致(见下文),恰好构成一整年的每日一题。
5.2 种子脚本:题目如何进入数据库
tools/daily-challenges/seed-daily-challenges.ts 负责把挑战写入 MongoDB 的DailyCodingChallenges集合:
- 通过 GraphQL 从 dev-playground superblock 拉取 JavaScript 与 Python 各 365 道题,数量不匹配即抛错;
- 起始日期硬编码为
2025-08-11T00:00:00.000Z(UTC),并带有一个防误改的 sanity check——一旦起始日期被意外改动,脚本会直接抛出异常,保护已发布的生产数据; - 第 i 道题的日期 = 起始日期 +
i * 24h,即每天一道,正好铺满一年; - 使用
replaceOne+upsert的bulkWrite实现幂等写入。
5.3 API 路由:按日期/天/月查询挑战
后端 api/src/daily-coding-challenge/routes/daily-coding-challenge.ts 暴露了 6 个公开 GET 端点:
| 端点 | 说明 |
|---|---|
/daily-coding-challenge/date/:date | 按YYYY-MM-DD获取某日挑战,晚于当日(美中时间)返回 404 |
/daily-coding-challenge/day/:day | 按MM-DD获取,内部通过getSourceDate映射回原始挑战年份 |
/daily-coding-challenge/today | 返回今天(美中时区)的挑战 |
/daily-coding-challenge/month/:month | 按YYYY-MM返回整月挑战列表(id、challengeNumber、date、title) |
/daily-coding-challenge/all | 返回全部已发布挑战的摘要列表 |
/daily-coding-challenge/newest | 返回最新一条挑战的日期 |
其中日期解析与"未来不可见"逻辑依赖 helpers.ts:getNowUsCentral基于date-fns-tz计算America/Chicago时区偏移,getUtcMidnight将任意时间归一到 UTC 零点;getSourceDate则把任意年份的MM-DD映射回2025-08-11 ~ 2026-08-10这一年的原始挑战日期(2 月 29 日被映射到 2 月 28 日),从而支持每年循环复用同一批题目。
5.4 客户端组件与数据校验
前端页面通过 widget.tsx 展示入口按钮("今天的每日挑战"与"挑战存档"),跳转到/learn/daily-coding-challenge/{MM-DD}与/learn/daily-coding-challenge/archive。
数据库返回的数据会先经过 daily-coding-challenge-validator.ts 的 Joi 校验,该文件约束了每个挑战的结构:
{ id: string, challengeNumber: number(integer, min 1), title: string, date: string, description: string, javascript: { tests: [{ text, testString }], challengeFiles: [{ fileKey, contents }] }, python: { tests: [{ text, testString }], challengeFiles: [{ fileKey, contents }] } }也就是说,"Last Load"这道题在数据库中以javascript+python双语言形式存储,testString正是本文开头那 5 条assert.equal断言,用户提交代码后由平台在浏览器端(或服务端)运行这些断言来判定对错。
六、本地动手:如何验证你的解法
由于挑战断言独立于平台,你完全可以在本地 Node.js 环境中直接验证:
function lastLoadDate(scoops, usage) { const avg = usage.reduce((sum, n) => sum + n, 0) / usage.length; return Math.floor(scoops / avg); } const cases = [ [10, [2, 2, 2, 2, 2, 2, 2], 5], [16, [2, 3, 0, 3, 4, 2, 1], 7], [33, [5, 0, 4, 3, 3, 2], 11], [50, [2, 0, 2, 9, 12, 0, 2], 12], [20, [13, 9, 12, 10, 8], 1] ]; for (const [scoops, usage, expected] of cases) { const actual = lastLoadDate(scoops, usage); console.log( `lastLoadDate(${scoops}, [${usage}]) => ${actual} (expected ${expected})`, actual === expected ? 'PASS' : 'FAIL' ); }运行后 5 条用例应全部输出PASS。你也可以用这个脚手架继续做变体实验,例如:
- 把
Math.floor换成Math.ceil/Math.round,观察用例 3、4、5 的断言如何失败,从而加深对"完整天数"语义的理解; - 给函数传入空数组,观察
avg变为NaN的行为,并思考是否需要防御式编程; - 改用
for...of循环累加,体会与reduce的等价性。
七、总结
"Last Load"(Challenge 301)是一道典型的"均值预测 + 取整"应用题:用reduce求历史均值、用scoops / avg预测天数、用Math.floor满足"完整天数"的语义约束,官方解法仅三行却覆盖了取整、零值、非整除等全部陷阱。而在 freeCodeCamp 仓库中,这道题以 Markdown 题目文件为源,经由challengeOrder编号、种子脚本入库、REST API 暴露、Joi 校验与前端组件渲染,最终成为每日编程挑战页面上的可提交题目——读懂这一整条链路,既是一次算法练习,也是一次对开源平台内容管线的微观观察。
延伸阅读:同一模块的其他题目可继续翻阅 curriculum/challenges/english/blocks/daily-coding-challenges-javascript/ 目录;API 层设计见 api/src/daily-coding-challenge/;种子脚本与客户端组件分别位于 tools/daily-challenges/seed-daily-challenges.ts 与 client/src/components/daily-coding-challenge/。
【免费下载链接】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),仅供参考