freeCodeCamp 每日编程挑战 Challenge 362:用 JavaScript 实现 Nonogram(数织)行线索验证器
【免费下载链接】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)中的 Challenge 362「Nonogram Validator」为主体,完整讲解数织线索(nonogram clue)的验证问题:从题目规则、全部测试用例的语义分析,到官方参考解法的单趟游程(run-length)扫描原理,并结合仓库源码(挑战类型定义、区块配置、每日挑战种子脚本与客户端渲染路径)说明该挑战在 freeCodeCamp 课程体系中的定位与运行机制。读完后,你将能够独立写出通过全部测试用例的isValidNonogram实现,并理解这道题背后"先提取游程、再逐项比对"这一可复用的算法模式。
题目描述与规则
该挑战的原始题目定义在 Challenge 362 题目文件 中,要求实现函数isValidNonogram(clue, cells):
给定一个线索数字数组(clue)和一个格子数组(cells),判断格子是否满足该 nonogram 线索。
- 线索是一个数字数组,按顺序表示连续填充格子的长度。例如线索
[3, 2]意味着应有 3 个连续填充的格子,随后是 2 个连续填充的格子,两组之间至少隔开一个空格。- 行(cells)是一个由 1(填充)和 0(空格)组成的数组。
题目附带两个关键约束,是理解全部测试用例的钥匙:
- 顺序性:游程必须按 clue 中数字出现的顺序依次出现,顺序打乱即无效;
- 严格匹配:cells 中出现的每一个 1 都必须恰好被线索覆盖——游程数量、长度、顺序三者必须与 clue 完全一致,不能多、不能少、不能偏长。
这一点由非官方 seed 代码(要求开发者在其基础上补全逻辑)暗示了初始实现骨架:
function isValidNonogram(clue, cells) { // 待补全 return clue; }测试用例全集与逐条解析
题目文件# --hints--部分给出了 6 条assert测试,它们是本题的验收标准。完整继承如下,并逐条说明其考察的边界:
| # | 调用 | 期望 | 考察点 |
|---|---|---|---|
| 1 | isValidNonogram([3, 2], [1, 1, 1, 0, 1, 1]) | true | 基本正例:两个游程3、2精确匹配 |
| 2 | isValidNonogram([3, 2], [0, 1, 1, 1, 1, 1]) | false | 游程合并陷阱:实际只有一个长度 4 的游程,而非[3, 2]之间的分隔 |
| 3 | isValidNonogram([1, 1, 1, 1], [1, 0, 1, 0, 1, 0, 1, 0, 1]) | false | 游程数量不符:实际有 5 个长度为 1 的游程,线索只有 4 个 |
| 4 | isValidNonogram([1, 1, 1, 1], [0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0]) | true | 间距可以大于 1:线索只要求"至少一个空格",多个连续空格完全合法 |
| 5 | isValidNonogram([3, 2, 3], [0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0]) | true | 前后可以有空格:游程前后的连续 0 不构成多余游程 |
| 6 | isValidNonogram([3, 2, 3], [0, 0, 0, 1, 0, 0, 1, 0, 0, 0]) | false | 长度不匹配:游程数量对了(3 个),但每个游程长度都是 1,与[3, 2, 3]不符 |
对应的断言写法(与题目文件一致):
assert.isTrue(isValidNonogram([3, 2], [1, 1, 1, 0, 1, 1])); assert.isFalse(isValidNonogram([3, 2], [0, 1, 1, 1, 1, 1])); assert.isFalse(isValidNonogram([1, 1, 1, 1], [1, 0, 1, 0, 1, 0, 1, 0, 1])); assert.isTrue(isValidNonogram([1, 1, 1, 1], [0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0])); assert.isTrue(isValidNonogram([3, 2, 3], [0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0])); assert.isFalse(isValidNonogram([3, 2, 3], [0, 0, 0, 1, 0, 0, 1, 0, 0, 0]));可以观察到一个设计意图:这 6 条用例分别压住了"游程提取"与"比对逻辑"两个环节的典型错误——用例 2 惩罚"只数 1 的总数"的错误思路([0,1,1,1,1,1]总共有 5 个 1,[3,2]也加起来是 5,但它必须是两个分离的游程);用例 3 惩罚"长度和相等就通过"的思路;用例 6 则惩罚"只比较游程个数、不比较长度"的思路。
参考解法:单趟游程扫描 + 逐项比对
题目文件# --solutions--给出的官方参考解法如下:
function isValidNonogram(clue, cells) { const runs = []; let count = 0; for (let i = 0; i <= cells.length; i++) { if (cells[i] === 1) { count++; } else if (count > 0) { runs.push(count); count = 0; } } if (runs.length !== clue.length) return false; return runs.every((run, i) => run === clue[i]); }其核心是把验证拆成两个阶段:
阶段一:从 cells 中提取游程列表runs。用一个计数器count累计当前连续 1 的长度;每当遇到一个 0(或走到数组末尾)且count > 0时,说明一段游程结束,把count压入runs并清零。这个"遇到分隔符才结算上一段"的写法是典型的游程提取模式,与解析 CSV 分字段、解析词频分词是同一类技巧。
阶段二:与 clue 严格比对。先比长度runs.length !== clue.length,再用Array.prototype.every逐项比较run === clue[i]。长度先行短路,避免在游程数量不同时的无意义逐项比较;every的短路求值也保证了首项不符时立即返回false。
一个值得细究的细节是循环条件写作i <= cells.length而不是惯用的i < cells.length。当i === cells.length时cells[i]为undefined,不等于 1,于是走else if分支:若此时count > 0(即格子数组以 1 结尾),最后一段游程也会被正确结算。若不写这个越界的"哨兵"迭代,[3, 2]对[1, 1, 1, 0, 1, 1]这样以 1 结尾的输入就会漏掉末尾游程、错误地返回false。换言之,这一处<=恰好覆盖了"行尾即隐式空格"的边界,与数织规则中"行边界天然充当分隔"的语义一致。
解法正确性推演
以用例 5([3, 2, 3]对[0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0])走一遍流程:
- 前两个 0:
count保持 0,不触发结算(count > 0不成立),因此行首空格不会产生空游程; - 三个 1 后遇 0:结算
runs = [3]; - 两个 0 后两个 1 再遇 0:结算
runs = [3, 2]; - 三个 1 到末尾哨兵迭代:结算
runs = [3, 2, 3]; runs.length === clue.length === 3,逐项3===3, 2===2, 3===3,返回true。
再看用例 6([3, 2, 3]对[0, 0, 0, 1, 0, 0, 1, 0, 0, 0]):提取出runs = [1, 1, 1],长度虽同为 3,但1 !== 3,第一个元素即短路返回false。
该解法的时间复杂度为 O(n)(n 为 cells 长度),空间复杂度 O(k)(k 为游程数,最坏 O(n)),单次遍历、无回溯,适合在浏览器编辑器中即时运行。
从源码结构看:这道题在 freeCodeCamp 仓库中的位置
题目文件本身是一个"每日挑战块"中的一个挑战条目。围绕它的仓库证据可以回答三个问题:它属于哪个类型、如何被组织、如何被用户看到。
1. 挑战类型:challengeType: 28即 JavaScript 每日挑战。题目文件 frontmatter 中声明challengeType: 28。在 challenge-types.ts 中,常量dailyChallengeJs = 28(同文件还有dailyChallengePy = 29),并且:
viewTypes将 28 映射为'classic',即使用经典单题编辑器界面渲染;submitTypes将 28 映射为'tests',即完成判定依赖题目内嵌的 assert 测试集(也就是上文的 6 条断言);getIsDailyCodingChallenge(challengeType)通过判断类型是否为 28/29 来识别每日挑战。
同时注意 28 不在hasNoSolution的列表中,因此该挑战在界面上允许展示参考答案——这与题目文件自带# --solutions--区块的设计吻合。
2. 区块组织:365 天中的第 362 题。挑战被登记在 daily-coding-challenges-javascript.json 的challengeOrder中(id6a26df95efa55a2524399743,标题 "Challenge 362: Nonogram Validator")。该区块配置了几个与运行行为直接相关的字段:
usesMultifileEditor: true:尽管本题只涉及script.js单文件,编辑器仍按多文件布局挂载;disableLoopProtectTests: true:禁用循环保护类测试钩子,允许答案中包含常规for循环(参考解法正是循环扫描);helpCategory: "JavaScript":求助分类归入 JavaScript。
3. 投放机制:从题库到"每日一题"。从仓库源码看,每日挑战的内容投放链路是:挑战 Markdown 存放在curriculum/challenges/english/blocks/daily-coding-challenges-javascript/下 → seed-daily-challenges.ts 脚本通过 GraphQL 拉取题库(脚本中EXPECTED_CHALLENGE_COUNT = 365,起始日期固定为 2025-08-11,即"第 N 题对应起始日 + N-1 天"),把 JavaScript 与 Python 版本合并后 upsert 进DailyCodingChallenges集合 → 客户端 show-daily-coding-challenge.tsx 按"月-日"路径向 API 请求/daily-coding-challenge/day/{monthDay},经 schema 校验后把数据包装为challengeType: 28的经典挑战(dashedName 形如challenge-${challengeNumber},与题目文件的dashedName: challenge-362规则一致),最终交给ShowClassic渲染并运行测试。本地运行该脚本的流程记录在 tools/daily-challenges/README.md:复制sample.env为.env、安装依赖、启动带 upcoming changes 的主客户端后执行pnpm seed-daily-challenges。
这条链路解释了题目文件中的两处"元数据"约定:id是 MongoDB ObjectId 风格的稳定标识(跨语言版本、跨数据库引用同一题);dashedName: challenge-362则是按题号生成的 URL 友好名。
小结
Challenge 362 表面上是一道数织小验证题,实质考察的是两个通用能力:一是把二值序列"压缩"为游程列表的单趟扫描(含行尾哨兵迭代的边界处理),二是"先比规模、再逐项比较"的严格匹配纪律。官方解法 15 行内完成,O(n) 时间与空间,恰好落在本题区块disableLoopProtectTests: true所允许的常规循环用法内。在仓库中,它作为daily-coding-challenges-javascript区块的第 362 条、challengeType: 28(JavaScript 每日挑战)的一员,经由种子脚本按日期投放、由经典编辑器渲染、以内嵌 assert 测试集判定完成——题目文件(题目与解答)、区块结构(区块配置)、类型系统(challenge-types.ts)与投放脚本(seed-daily-challenges.ts)共同构成了一道"每日一题"从静态内容到线上交付的完整闭环。
【免费下载链接】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),仅供参考