freeCodeCamp 每日编码挑战 Challenge 84 "Infected" 题解:指数传播与周期性修补的 JavaScript 模拟
【免费下载链接】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 每日编码挑战的第 84 道题 "Infected"(题目文件)展开:给定自蠕虫(internet worm)发布以来经过的天数,模拟"每天感染数翻倍、每第 3 天打补丁削减 20% 且向上取整"的动态过程,返回最终感染总数。读完本文,你将掌握题目规则、完整推导过程、官方解答的逐行解析、边界条件与复杂度分析,并了解这道题在 freeCodeCamp 课程体系中的定位与测试基建。
挑战背景:1988 年互联网蠕虫与指数增长模型
题目描述引用了一个真实历史事件:
On November 2nd, 1988, the first major internet worm was released, infecting about 10% of computers connected to the internet after only a day.
1988 年 11 月 2 日发布的首个大型互联网蠕虫(即历史上著名的 Morris 蠕虫),在一天之内感染了约 10% 接入互联网的计算机。题目正是以此为背景,将蠕虫的传播抽象为一个简洁的数学模型:
- 第 0 天:第一台计算机被感染。
- 此后每天:被感染计算机的数量翻倍。
- 每第 3 天:病毒传播之后会应用一次补丁,将感染数削减 20%,削减的数量向上取整到最接近的整数(即对补丁数量
Math.ceil,而不是对结果向下取整)。
这道题的核心价值在于:用一个简短的可迭代模拟,同时练习了指数增长、取模判断周期与Math.ceil向上取整三个 JavaScript 基础能力,是非常典型的 daily coding challenge 难度与风格。
题目规则与数学模型
需要实现一个函数infected(days),输入为经过的天数(非负整数),输出为经过该天数后总计仍处于感染状态的计算机数量。
规则的形式化表述如下:
- 初始条件:
day === 0时感染数为1。 - 传播阶段:从第 1 天起,每一天先执行
infected *= 2。 - 补丁阶段:若
day % 3 === 0,则在传播完成后执行削减——补丁数量 =Math.ceil(infected * 0.2),然后infected -= patched。
需要注意的关键细节:补丁是在当天病毒传播之后才应用的,因此第 3 天是先由第 2 天的 4 翻倍到 8,再削减;而不是先削减再翻倍。这一顺序直接决定了所有测试用例的结果。
官方逐步示例推导
题目给出了第 0~3 天的完整示例,逐日验证如下:
| 天数 | 翻倍后感染数 | 是否打补丁 | 补丁数量(向上取整) | 当日结束后感染总数 |
|---|---|---|---|---|
| Day 0 | 1(初始) | 否 | — | 1 |
| Day 1 | 1 × 2 = 2 | 否 | — | 2 |
| Day 2 | 2 × 2 = 4 | 否 | — | 4 |
| Day 3 | 4 × 2 = 8 | 是(3 % 3 === 0) | 8 × 20% = 1.6 →Math.ceil(1.6)= 2 | 8 − 2 = 6 |
其中第 3 天是理解本题的枢纽:8 * 0.2 = 1.6,向上取整得2,于是8 - 2 = 6。这里必须使用Math.ceil,如果误用Math.floor或Math.round,结果会分别变成8 - 1 = 7或8 - 2 = 6(Math.round(1.6)恰好也为 2,但其他天数会偏离),无法稳定通过全部测试。
种子代码与函数签名
题目提供的初始代码(--seed-contents--)如下,学习者需要补全函数体:
function infected(days) { return days; }函数接收一个参数days(非负整数),返回数值类型的感染总数。题目没有要求处理负数或非整数输入——从测试用例看,入参均为1、3、8、17、25这样的非负整数。
测试用例一览
仓库题目文件中的--hints--部分给出了 5 个测试用例,它们既是指标,也是推导答案的依据:
assert.equal(infected(1), 2); assert.equal(infected(3), 6); assert.equal(infected(8), 152); assert.equal(infected(17), 39808); assert.equal(infected(25), 5217638);逐条验证:
infected(1):第 1 天只翻倍不打补丁 →1 × 2 = 2✓infected(3):如上表推导 →6✓infected(8):在第 3、6 天各打一次补丁,连续迭代结果为152✓infected(17):在第 3、6、9、12、15 天共打 5 次补丁,结果为39808✓infected(25):补丁发生在 3、6、9、12、15、18、21、24 天共 8 次,最终为5,217,638✓
可以观察到:尽管每 3 天削减一次,但由于削减比例(20%)远小于翻倍比例(100%),感染数整体仍呈指数级上升——这正是题目想传达的"蠕虫传播的可怕速度"。
官方解答逐行解析
题目自带的--solutions--给出了参考实现:
function infected(days) { let infected = 1; for (let day = 1; day <= days; day++) { infected *= 2; if (day % 3 === 0) { let patched = Math.ceil(infected * 0.2); infected -= patched; } } return infected; }逐行解读:
let infected = 1;—— 对应"第 0 天第一台计算机被感染"的初始状态。for (let day = 1; day <= days; day++)—— 从第 1 天迭代到第days天,正好执行days次循环;当days === 0时循环体一次都不执行,直接返回1,与"第 0 天返回 1"的语义一致。infected *= 2;—— 每天先翻倍(传播阶段)。if (day % 3 === 0)—— 用取模判断"每第 3 天",覆盖第 3、6、9、12…天。let patched = Math.ceil(infected * 0.2);—— 补丁数量按当前感染数的 20% 计算并向上取整。Math.ceil是本题最容易遗漏或误用的 API。infected -= patched;—— 削减感染数,进入下一天。
该实现的时间复杂度为 O(days),空间复杂度为 O(1),只需维护单个数字变量即可完成模拟,无需任何数组或额外数据结构。
常见陷阱
- 取整方向错误:必须用
Math.ceil。题目明确要求"Round the number of patched computers up to the nearest whole number"(将补丁数量向上取整),即补丁数偏多、削减更激进。 - 补丁与翻倍的顺序:必须"先传播、后打补丁"。若颠倒顺序(如第 3 天先 4−补丁再翻倍),结果将完全不同。
- 循环边界:
for (let day = 1; day <= days; day++)与day < days相差一次循环,直接导致所有测试用例偏移一天。 - 运算精度:
infected * 0.2会产生浮点数(如1.6),Math.ceil负责将其归一为整数;削减后infected应始终是整数。
变体与思考:Python 版本与扩展练习
freeCodeCamp 的每日编码挑战同时维护了 JavaScript 与 Python 两个版本(对应挑战类型dailyChallengeJs = 28与dailyChallengePy = 29,见 challenge-types.ts)。如果你想用 Python 复现同一逻辑,思路完全一致,只是把取模与向上取整换成 Python 语法(如math.ceil);仓库的配对测试保证了两种语言版本在题目 ID、标题、描述与测试数量上完全一致。
此外可以自行扩展思考:
- 若补丁改为每 4 天一次,感染数曲线如何变化?
- 若削减比例改为 30%,
infected(25)会变成多少? - 如果允许
days很大(如 10^6),O(days) 是否仍然可行?能否推导出闭式公式?
该挑战在 freeCodeCamp 仓库中的工程定位
"Infected" 并非孤立题目,它在整个仓库中有完整的工程化支撑,这些结构可以帮助你理解每日编码挑战的运转方式:
- 挑战文件本身:curriculum/challenges/english/blocks/daily-coding-challenges-javascript/68e39ed6106dac2f0a98fd66.md 采用 freeCodeCamp 标准的多段落 Markdown 格式:front-matter(
id、title、challengeType、dashedName)、--description--、--hints--、--seed--与--solutions--。 - 挑战类型:
challengeType: 28对应共享配置中的dailyChallengeJs(packages/shared/src/config/challenge-types.ts),其视图类型为classic,提交类型为tests——也就是说,学习者在经典编辑器中编写代码,由内置测试断言(即--hints--)判定是否通过。 - 所属块(block)配置:daily-coding-challenges-javascript.json 表明该块使用多文件编辑器(
usesMultifileEditor: true)、帮助分类为 JavaScript,并设置了disableLoopProtectTests: true(即不施加"循环保护"测试限制,允许自由使用for循环)——这正是本解答可以放心使用for循环的工程依据。 - Schema 校验:challenge-schema.js 要求
challengeType为0~33的整数,28 在其合法范围内,保证此类题目的元数据合法性。 - 每日挑战 API:前端通过 daily-coding-challenge 路由 获取每日题目,提供
/daily-coding-challenge/date/:date、/day/:day、/today、/month/:month、/all、/newest等公开 GET 端点,响应 schema 见 schemas/daily-coding-challenge.ts。 - 配对测试:daily-challenges.test.js 会分别加载 JavaScript 与 Python 每日挑战,断言两者数量一致,且同序号题目的 ID、标题、描述与测试数量完全匹配,保证两种语言版本同步维护。
如果你想在本地查看这道题,只需阅读上述题目文件即可;若想运行其测试,可参考 curriculum/src/test 下的 Vitest 配置与测试工具链(test-challenges.js、vitest.config.mjs)了解如何按 block 加载并校验挑战。
小结
Challenge 84 "Infected" 是一道小而精的模拟题:用for循环驱动指数增长,用% 3触发周期性补丁,用Math.ceil处理向上取整。它的工程价值不仅在于练习三个基础语法点,更在于让你体会"以文档规则为唯一事实来源"的解题方式——题目描述、测试用例与官方解答三者互相印证,构成了 freeCodeCamp 每日编码挑战可自动化判定的完整闭环。
【免费下载链接】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),仅供参考