freeCodeCamp 每日编程挑战实战解析:Challenge 290 "Pizza Party" 的向上取整与聚合算法
本篇技术指南以 freeCodeCamp 仓库中的 Challenge 290: Pizza Party(每日编程挑战系列之一)为核心,从题目规则、数学建模、逐行代码实现到测试用例验证,完整剖析这道数组聚合类算法题。读者读完将掌握 Math.ceil 向上取整、Math.max 下限约束、Array.reduce 聚合等 JavaScript 核心技巧,并了解该挑战在 freeCodeCamp 每日编程挑战体系(challengeType 28)中的真实运行机制。
一、题目总览:为披萨派对计算订购数量
挑战文件位于 curriculum/challenges/english/blocks/daily-coding-challenges-javascript/69f8c998d78ad3171a0713bd.md,归属于 daily-coding-challenges-javascript 挑战块。题目场景非常贴近生活:
给定一个数组,表示今天每个人的工作时长(小时),请返回为披萨派对需要订购的披萨数量。
要完成这道题,必须严格遵循以下四条业务规则:
- 切片换算:将每个人的工作时长除以 3,得到其应得的披萨切片数;
- 禁止半片:不能吃半片披萨,因此每个人的切片数需向上取整到最近的整数;
- 最低保障:每个人至少分到 2 片;
- 整披萨订购:每份披萨含 8 片,总切片数换算成披萨后需向上取整到最近的整数个披萨。
这是一个典型的"分步计算 → 逐项聚合 → 最终取整"的数组处理问题,考察的是对 reduce、Math.ceil、Math.max 的组合运用能力。
二、从题目到数学模型
在动手写代码之前,先把文字规则翻译成数学表达式。
设输入数组为 hoursWorked(每人工作时长的数组),那么:
第 1 步:单人的切片数
slices_i = max( ceil(hours_i / 3), 2 )
ceil(hours_i / 3):实现"除以 3 后向上取整",对应规则 1 和规则 2;max(..., 2):实现"每人至少 2 片"的下限约束,对应规则 3。
第 2 步:总切片数
totalSlices = sum(slices_i) // 对每个人累加
第 3 步:最终披萨数
pizzas = ceil(totalSlices / 8)
- 每片披萨 8 片、向上取整,对应规则 4。
值得注意的是,Math.max(Math.ceil(hours / 3), 2) 这一表达式把"取整"和"下限"两个逻辑合并成了一步:先算出按工时换算的切片数并取整,再与 2 比较取较大值。这样即使某人的工时小于 6 小时(ceil(hours/3) < 2),也能保证其至少获得 2 片。
三、种子代码与解决方案逐行拆解
3.1 初始种子代码
挑战为学习者提供了如下函数骨架(位于原文档的 # --seed-- 部分):
function getPizzasToOrder(hoursWorked) {
return hoursWorked;
}
学习者需要把 return hoursWorked 替换为真正的计算逻辑,使函数接收每人工作时长数组、返回订购披萨数。
3.2 官方解决方案
function getPizzasToOrder(hoursWorked) {
const totalSlices = hoursWorked.reduce((sum, hours) => {
return sum + Math.max(Math.ceil(hours / 3), 2);
}, 0);
return Math.ceil(totalSlices / 8);
}
逐行解读:
| 代码片段 | 作用 |
|---|---|
hoursWorked.reduce((sum, hours) => ..., 0) |
遍历数组,以 0 为初始值累加每个人的切片数 |
Math.ceil(hours / 3) |
每小时 3 片换算并向上取整(规则 1、2) |
Math.max(..., 2) |
与 2 比较,保证每人最少 2 片(规则 3) |
Math.ceil(totalSlices / 8) |
每披萨 8 片,总切片换算后向上取整(规则 4) |
整个方案只用一次 reduce 就完成了所有人的切片数聚合,再通过一次 Math.ceil 收尾,时间复杂度和空间复杂度均为 O(n)(仅需遍历一次数组、无额外存储),是这道题在性能上的最优解。
四、测试用例验证:理解边界与陷阱
原文档的 # --hints-- 部分给出了 5 个断言用例,每个都用 assert.equal 校验函数输出。下面逐一演算,帮助理解规则之间的交互:
用例 1:getPizzasToOrder([8, 8, 8]) → 2
每人 8 小时:ceil(8/3) = 3,均大于 2,故每人 3 片,共 9 片;ceil(9/8) = 2。✅
用例 2:getPizzasToOrder([10, 9, 8, 2, 2, 6, 10]) → 3
- 10 小时 →
ceil(10/3) = 4 - 9 小时 →
ceil(9/3) = 3 - 8 小时 →
ceil(8/3) = 3 - 2 小时 →
ceil(2/3) = 1,但max(1, 2) = 2 - 2 小时 → 同上,2 片
- 6 小时 →
ceil(6/3) = 2 - 10 小时 → 4 片
合计 4+3+3+2+2+2+4 = 20 片;ceil(20/8) = 3。✅ 这个用例同时覆盖了"最低 2 片保障"和"整披萨取整"两条规则。
用例 3:getPizzasToOrder([1, 2, 3, 4, 5]) → 2
- 1 小时 →
max(ceil(1/3), 2) = 2 - 2 小时 →
max(1, 2) = 2 - 3 小时 →
max(1, 2) = 2 - 4 小时 →
max(ceil(4/3), 2) = max(2, 2) = 2 - 5 小时 →
max(2, 2) = 2
合计 10 片;ceil(10/8) = 2。✅ 该用例集中考验"工时不足 3 小时也要 2 片"的边界逻辑。
用例 4:getPizzasToOrder([8, 8, 8, 8, 8, 8, 8, 8]) → 3
8 人各 3 片,共 24 片;ceil(24/8) = 3。✅ 恰好整批 3 个披萨,验证无余数时的取整行为。
用例 5:getPizzasToOrder([9, 9, 6]) → 1
- 9 小时 →
ceil(9/3) = 3(两次) - 6 小时 →
ceil(6/3) = 2
合计 8 片;ceil(8/8) = 1。✅ 恰好 1 个披萨,再次验证"整除"场景。
用例 6:getPizzasToOrder([10, 12, 16, 9, 8, 11, 15, 8, 0]) → 5
- 10 → 4、12 → 4、16 →
ceil(16/3)=6、9 → 3、8 → 3、11 →ceil(11/3)=4、15 → 5、8 → 3、0 →max(0, 2) = 2
合计 4+4+6+3+3+4+5+3+2 = 34 片;ceil(34/8) = 5。✅ 该用例包含 0 工时(考验最低切片保障)和较大数值的聚合。
从这 6 组用例可以看出测试设计覆盖了:一般换算、下限约束、整除与不整除、0 工时等典型边界情况。
五、挑战在 freeCodeCamp 体系中的定位
5.1 挑战块与元数据
本挑战的 frontmatter 声明了 challengeType: 28 与 dashedName: challenge-290,并在 curriculum/structure/blocks/daily-coding-challenges-javascript.json 中注册为 "Challenge 290: Pizza Party"(id 为 69f8c998d78ad3171a0713bd),前后分别是 Challenge 289 "Sum of Differences" 与 Challenge 291 "FizzBuzz Count"。
从该块配置可看出其工程特征:
"isUpcomingChange": true:属于进行中的课程变更;"usesMultifileEditor": true:在挑战编辑界面使用多文件编辑器;"helpCategory": "JavaScript":归入 JavaScript 帮助分类;"blockLayout": "legacy-challenge-list":采用传统挑战列表布局。
5.2 每日挑战的 API 支撑
此类挑战的数据由 API 侧的 daily-coding-challenge 模块对外提供。路由实现位于 api/src/daily-coding-challenge/routes/daily-coding-challenge.ts,共注册了 6 个公开 GET 端点:
| 端点 | 说明 |
|---|---|
/daily-coding-challenge/date/:date |
按 YYYY-MM-DD 精确日期获取某天挑战 |
/daily-coding-challenge/day/:day |
按 MM-DD 获取对应日期的挑战(2 月 29 日映射到 2 月 28 日) |
/daily-coding-challenge/today |
获取美国中部时区(America/Chicago)当天的挑战 |
/daily-coding-challenge/month/:month |
按 YYYY-MM 获取整月挑战列表 |
/daily-coding-challenge/all |
获取全部挑战列表(仅返回已发布日期) |
/daily-coding-challenge/newest |
获取最新一期挑战的日期 |
请求/响应结构由 TypeBox schema 定义在 api/src/daily-coding-challenge/schemas/daily-coding-challenge.ts:单个挑战响应包含 id、date、challengeNumber、title、description 以及 javascript / python 两套语言字段(各含 tests 与 challengeFiles)。Pizza Party 这类题目的测试断言正是在此类结构中以 testString 形式交付给前端执行的。
5.3 日期相关的实现细节
源码中有几个值得注意的日期处理细节(见 api/src/daily-coding-challenge/utils/helpers.ts 与路由文件):
- "今天"以美国中部时间为准,由
getNowUsCentral()计算,再经getUtcMidnight归一化为 UTC 零点用于数据库查询; getSourceDate会把MM-DD映射回具体的 UTC 日期,并处理闰年 2 月 29 日这类边界;- 所有查询都会过滤
date <= today US Central的挑战,避免返回未发布的未来题目(路由注释明确指出最后一个挑战日期为 2026 年 8 月 10 日之后将不再返回数据)。
六、举一反三:从本题延伸的实战要点
6.1 取整三件套的选择
Math.ceil(x):向上取整,适合"必须准备足量"的场景(本题的披萨、物资采购、服务器容量规划);Math.floor(x):向下取整,适合"最多能装多少"的场景;Math.round(x):四舍五入,适合统计平均类场景。
6.2 用 Math.max 施加下限约束
Math.max(value, min) 是给计算结果设置下限的惯用写法,等价于 value < min ? min : value。同理,Math.min(value, max) 可以施加上限。本题的"每人至少 2 片"正是这种模式的典型应用。
6.3 reduce 的聚合思维
当需要把数组中的每一项折算成一个数值时,reduce(callback, initialValue) 是最直接的工具。本题把"每人切片数"在回调内即时计算并累加,避免先 map 再 sum 的两趟遍历,代码更紧凑。若希望可读性优先,也可拆成两步:
function getPizzasToOrder(hoursWorked) {
const slicesPerPerson = hoursWorked.map(hours =>
Math.max(Math.ceil(hours / 3), 2)
);
const totalSlices = slicesPerPerson.reduce((sum, s) => sum + s, 0);
return Math.ceil(totalSlices / 8);
}
两种写法结果一致,前者更高效,后者更利于调试中间值。
6.4 可能的陷阱
- 忘记最低切片保障:若直接
sum += Math.ceil(hours / 3),则[1, 2]这类用例会得到ceil(1/3)+ceil(2/3) = 2片、1 个披萨,而正确答案要求每人 2 片,即 4 片、1 个披萨——对于本例恰好一致,但在更大的输入下会显著低估订购量; - 先取整再求和 vs 先求和再取整:必须"每人分别向上取整后再累加",而不是"总工时除以 3 后一次性取整",否则会系统性少算切片;
- 忽略 0 工时成员:
ceil(0/3) = 0,若不做max(..., 2)约束,0 工时者将得不到任何切片。
七、总结
Challenge 290: Pizza Party 是一道短小精悍的数组聚合题:通过"工时 ÷ 3 向上取整、每人至少 2 片、每 8 片 1 个披萨向上取整"三层规则,串起了 Math.ceil、Math.max、Array.prototype.reduce 三个高频 API,并用 6 组覆盖边界条件的断言固化了正确行为。学习者既可以把它当作日常算法练习,也可以顺着本仓库的 challenge 文档、挑战块配置 与 API 路由实现 三条线索,完整理解一道每日编程挑战从题目定义、数据提供到在线判题的全链路工程实现。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00