freeCodeCamp JavaScript 每日挑战 349:Cell Signal——用三个信号塔坐标在手机网格上定位
【免费下载链接】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 Challenges)第 349 题 “Cell Signal”:给定一个包含三个信号塔读数的网格,如何判定手机所在单元格。你将完整继承原题的题面规则、全部 5 个测试用例与参考实现,并进一步结合仓库中的区块配置文件、挑战类型定义与挑战页面源码,理解这道题在 freeCodeCamp 课程体系中的实际运行形态,最终掌握“约束交集 + 方向距离判定”这一类网格定位题的通用解法。
题目背景:网格中的手机定位
该题目来自 freeCodeCamp 课程仓库的 JavaScript 每日挑战区块,挑战文件为 6a22d77ddf034bc4e35b1d56.md。在区块结构文件 daily-coding-challenges-javascript.json 的challengeOrder中,它被排定为 “Challenge 349: Cell Signal”(相邻题依次为 Challenge 348: Loan Calculator 与 Challenge 350: Letter Distance)。
题面原文如下:
Given a grid containing three cell tower readings, determine the location of the phone.
- Each cell in the grid is either
0(no tower) or a positive integer representing the number of cells to the phone, measured in a straight line: horizontal, vertical, or diagonal.- Return the
[row, col]of the cell that is the correct number of cells from all three towers.- There is always exactly one solution.
翻译成中文:
- 网格中每个单元格要么是
0(无信号塔),要么是一个正整数,表示该信号塔到手机的直线格数——这个直线只能是水平、垂直或 45° 对角线方向; - 要求返回
[row, col](行、列),该单元格必须同时满足全部三个信号塔的距离约束; - 题目保证有且只有一个解,因此找到第一个满足条件的单元格即可返回。
题目的输入输出契约是:
function findSignal(grid) { // grid: 二维整数数组 // 返回值: [row, col] 或 undefined(题面保证一定有解) }初始脚手架(seed)代码如下,读者需要在函数体内完成实现:
function findSignal(grid) { return grid; }核心数学:什么叫“直线距离为 dist”
这是本题唯一的难点,也是最容易写错的地方。所谓“手机在信号塔的dist格之外”,指的是二者位于同一条水平线、同一条垂直线或同一条对角线上,且相隔恰好dist格。
设候选手机位置为(r, c),信号塔位于(tr, tc),记行差dr = |r - tr|、列差dc = |c - tc|,则合法位置等价于以下三种情形之一:
| 方向 | 条件 |
|---|---|
| 垂直方向(同一列) | dc === 0且dr === dist |
| 水平方向(同一行) | dr === 0且dc === dist |
| 对角线方向(45°) | dr === dc === dist |
注意两点:
- 距离不是欧氏距离,也不是曼哈顿距离,而是轴对齐或对角线上的格数;
dist至少为 1(正整数),所以手机不可能恰好落在信号塔格子上——这也意味着所有非零格子都可以直接从候选集中排除。
参考实现用一个布尔函数把上表压缩成一行判定,等价地写成了:
(dr === dist || dc === dist) && (dr === 0 || dc === 0 || dr === dc)拆解来看:前一半(dr === dist || dc === dist)要求至少一个轴方向的差恰好等于dist;后一半(dr === 0 || dc === 0 || dr === dc)要求另一个轴差为 0(水平/垂直)或者两轴差相等(对角线)。两者合起来恰好覆盖且仅覆盖上表的三种合法方向,其余所有位置(例如 L 形偏移)都会被排除。
完整测试用例(原题 5 组断言)
原题提供了 5 组测试,全部采用assert.deepEqual深比较(比较的是[row, col]的精确数值与顺序)。以下 5 组用例与原题文档逐字对应,可直接复制到本地 Node 环境自测:
assert.deepEqual(findSignal([[0, 0, 1], [0, 1, 0], [0, 0, 1]]), [1, 2]);第一个用例中,塔[1,2]的读数为1。答案[1, 2]距塔[0,2](值 1)垂直 1 格,距塔[1,1](值 1)水平 1 格,距塔[2,2](值 1)垂直 1 格,三个约束同时成立。
assert.deepEqual(findSignal([[0, 2, 0], [1, 0, 0], [0, 0, 1]]), [2, 1]);答案[2,1]:距塔[0,1](值 2)垂直 2 格;距塔[1,0](值 1)对角线 1 格(dr=1, dc=1);距塔[2,2](值 1)水平 1 格。这组用例特意验证了对角线方向的判定。
assert.deepEqual(findSignal([[0, 0, 2, 0], [0, 0, 0, 0], [2, 0, 0, 0], [0, 0, 0, 1]]), [2, 2]);答案[2,2]:距塔[0,2](值 2)垂直 2 格;距塔[2,0](值 2)水平 2 格;距塔[3,3](值 1)对角线 1 格。
assert.deepEqual(findSignal([[0, 3, 0, 0, 0], [0, 0, 0, 0, 2], [0, 0, 0, 0, 0], [4, 0, 0, 0, 0], [0, 0, 0, 0, 0]]), [3, 4]);答案[3,4]:距塔[0,1](值 3)对角线 3 格(dr=3, dc=3);距塔[1,4](值 2)垂直 2 格;距塔[3,0](值 4)水平 4 格。
assert.deepEqual(findSignal([[3, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0, 2, 0, 0, 0, 2]]), [3, 3]);答案[3,3]:距塔[0,0](值 3)对角线 3 格;距塔[5,1](值 2)对角线 2 格(dr=2, dc=2);距塔[5,5](值 2)对角线 2 格(dr=2, dc=2)。这是 6×6 网格,三塔两塔共用同一答案,验证了多塔交叉约束收敛到唯一解的情形。
参考实现逐段解析
原题给出的参考解法(原文档# --solutions--部分)完整如下,随后逐段分析:
function findSignal(grid) { const rows = grid.length; const cols = grid[0].length; const towers = []; for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { if (grid[r][c] !== 0) towers.push([r, c, grid[r][c]]); } } function isValid(r, c, tr, tc, dist) { const dr = Math.abs(r - tr); const dc = Math.abs(c - tc); return (dr === dist || dc === dist) && (dr === 0 || dc === 0 || dr === dc); } for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { if (grid[r][c] !== 0) continue; if (towers.every(([tr, tc, dist]) => isValid(r, c, tr, tc, dist))) { return [r, c]; } } } }第一步:预提取信号塔。外层遍历一次网格,把所有非零格子收进towers数组,每个元素是[行, 列, 距离读数]三元组。题目保证恰好三个塔,但代码并不写死为 3——towers.every(...)对任意数量的塔都成立,实现上更具鲁棒性。
第二步:单塔判定函数isValid。如上节分析,它只负责回答“候选点是否恰好位于该塔的水平/垂直/对角线dist格处”。把它抽成函数后,主循环只需对每个候选点调用every做全量约束检查。
第三步:扫描候选点并早退。第二个双重循环遍历所有格子:
if (grid[r][c] !== 0) continue;跳过信号塔格子——因为读数是正整数,手机不可能与塔同格,这个跳过是正确性要求而非单纯的性能优化;towers.every(...)要求候选点同时满足全部塔的约束(约束交集),一旦命中就立即return [r, c]。由于题面保证有且仅有一个解,扫描到第一个合法点即全局解。
复杂度:设网格为rows × cols、塔数为t(本题恒为 3),时间复杂度为O(rows * cols * t),空间复杂度O(t)。对日常挑战的网格规模(用例中最大 6×6)而言完全不是问题。
常见错误与陷阱
结合判定条件(dr === dist || dc === dist) && (dr === 0 || dc === 0 || dr === dc),实现时最容易踩的坑有:
- 误用欧氏/曼哈顿距离:用
Math.sqrt(dr*dr + dc*dc) === dist或dr + dc === dist都无法区分“直线上的点”与“L 形偏移的点”。例如对 45° 方向、dist = 2的塔,(dr=2, dc=2)的欧氏距离是2√2 ≠ 2,会漏掉对角线解。 - 忘记
dr === 0 || dc === 0分支:如果只写dr === dc && dr === dist,就只能找出对角线方向的候选,水平/垂直方向的合法解全部被丢弃。 - 忘记跳过非零格:若不
continue,信号塔自身所在格子(dr = 0, dc = 0)不会恰好满足任何正dist的约束,虽然不会误判,但属于无谓的无效检查;更重要的是在“手机恰好与某塔同格”这类变体中逻辑会崩坏。 - 返回值顺序:断言使用
assert.deepEqual比较[row, col],返回[c, r]会直接失败。 every与some混淆:题目要求“距所有塔都是正确格数”,必须全部满足;用some会导致任一单塔满足即返回,产生假解。
题目在 freeCodeCamp 课程体系中的位置
理解这道题的运行环境,有助于知道它如何被交付给学习者:
挑战类型定义。文件头元数据中challengeType: 28对应共享包 challenge-types.ts 中的dailyChallengeJs = 28(Python 版每日挑战为dailyChallengePy = 29)。同一文件定义了三类映射关系:
viewTypes中[dailyChallengeJs]: 'classic'(见 challenge-types.ts)——每日挑战复用经典(classic)挑战视图渲染编辑器与测试面板;submitTypes中[dailyChallengeJs]: 'tests'(见 challenge-types.ts)——提交时运行的是“跑测试”型判定,即本文的 5 组assert.deepEqual断言;getIsDailyCodingChallenge与getDailyCodingChallengeLanguage两个工具函数把 28/29 归入每日挑战集合,并分别映射到'javascript'/'python'语言标签。
区块配置。daily-coding-challenges-javascript.json 声明了该区块的关键属性:helpCategory: "JavaScript"、usesMultifileEditor: true、disableLoopProtectTests: true、blockLayout: "legacy-challenge-list",并且isUpcomingChange: true表明该区块处于待上线/变更状态。本区块从 “Challenge 1: Vowel Balance” 一直排到 “Challenge 365: The Last Challenge: Bucket Fill 3”,全年 365 天每日一题;本块同时被挂载在dev-playground超级区块下(见 dev-playground.json),供开发者/学习者在练习场中按序刷题。
运行时数据流。在线页面上,每日挑战由客户端组件 show-daily-coding-challenge.tsx 承接:组件在 第 157~163 行 按日期向${apiLocation}/daily-coding-challenge/day/{monthDay}发起请求,先经validateDailyCodingChallengeSchema做 JSON Schema 校验,再由formatChallengeData把数据库返回的 JavaScript 变体拼装成challengeType: 28、helpCategory: 'JavaScript'的挑战节点(见 第 91~114 行),最终注入ShowClassic组件渲染——即学习者看到的正是本挑战文件的题面、测试断言与script.js编辑区。Python 变体(challengeType: 29,编辑区main.py)走同一管线,仅语言与文件键不同。
值得注意的边界:仓库中该挑战文件本身没有tests段落,5 组断言写在# --hints--小节内;而区块 JSON 与在线 API 管线(javascript.tests字段)共同承担了测试的最终交付,这也是该区块disableLoopProtectTests: true与在线校验流程配合的原因。
小结与自测建议
本题的通用解法可以归纳为三步:预提取全部约束源(信号塔)→ 为每个约束源定义单点判定函数(轴对齐/对角线距离)→ 全网格扫描并用every做约束交集,命中即返回。这一模式可迁移到本区块其他同类题目(如 Challenge 243: Rook Attack、Challenge 245: Spiral Matrix 等网格题,均位于 区块目录)。
自测步骤:
- 将本文给出的 5 组
assert.deepEqual断言与你的findSignal实现放入同一个 Node.js 文件; - 用 Node 内置的
node:assert替换assert(或在 freeCodeCamp 编辑器内直接提交),确认全部通过; - 对照“常见错误”一节,逐一验证水平/垂直/对角线三种方向在 5 组用例中的覆盖:用例 1、3 覆盖垂直与水平,用例 2、4、5 覆盖对角线——全部通过说明方向判定逻辑完备。
【免费下载链接】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),仅供参考