freeCodeCamp 每日编码挑战解析:Connect 3 三连棋检测算法(Challenge 322)
【免费下载链接】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 板块第 322 题 "Connect 3" 为对象,完整还原题目规则、输入输出格式、全部测试用例与官方参考解答,并结合仓库源码讲解其运行机制与算法原理。读完本文,你将掌握矩阵方向枚举、边界检查、坐标规范化等二维数组遍历的核心技巧,并了解该挑战在 freeCodeCamp 课程体系与后端 API 中的实际位置。
挑战背景:freeCodeCamp 的每日编码挑战体系
本挑战文档位于curriculum/challenges/english/blocks/daily-coding-challenges-javascript/6a19b1062d1b153d8ac76d70.md,归属于 JavaScript 每日编码挑战块(block)。从 块结构配置 可以看到,整个块共包含 365 道挑战,按天数编号(Challenge 1 至 Challenge 365),这正是"每日一题"的设计初衷——一年 365 天每天一道编码练习题。
从仓库结构看,该功能在项目中是完整闭环的:
- 课程内容:挑战文档以 Markdown 形式存放在
curriculum/challenges/english/blocks/daily-coding-challenges-javascript/目录下,每份文档包含题目描述(--description--)、测试用例(--hints--)、初始代码(--seed--)与参考解答(--solutions--)四部分; - 数据落地:种子脚本 seed-daily-challenges.ts 从 GraphQL 拉取挑战数据并写入 MongoDB 的
DailyCodingChallenges集合,起始日期被固定为2025-08-11T00:00:00.000Z,此后每个挑战顺延一天,脚本还内置了数量校验(期望 365 道)与起始日期校验,防止发布后误改; - 后端服务:daily-coding-challenge.ts 提供了按日期、按天、按月份、全部以及最新等 6 个只读 GET 接口,供前端按日拉取题目信息;
- 前端展示:客户端通过
/learn/daily-coding-challenge/路由渲染当日挑战(见 show-daily-coding-challenge.tsx),并用 daily-coding-challenge-validator.ts 中的 Joi schema 校验后端返回的数据结构。
Connect 3(Challenge 322)正是这 365 道题中的一道二维数组遍历类题目,位于该块的后期部分。
问题陈述精读
输入
函数connectThree(matrix)接收一个二维数组(矩阵)作为输入,矩阵中的每个单元格是以下三种值之一:
| 取值 | 含义 |
|---|---|
"R" | 红色玩家的棋子 |
"Y" | 黄色玩家的棋子 |
"" | 空字符串,表示该格为空 |
判定规则
"三连"(three in a row)的定义是:水平、垂直或对角线上存在三个连续的、类型相同的非空单元格。也就是说,三个"R"或三个"Y"必须紧挨着排成一条直线。
返回值
函数需要返回两种结果之一:
- 有赢家时:返回一个扁平数组,格式为
["R", [0,2], [1,3], [2,4]],即依次是赢家标识("R"或"Y")和三个获胜格子的坐标。坐标以[行, 列]形式给出,且必须按"从上到下、从左到右"的顺序排列(top-to-bottom, then left-to-right); - 无赢家时:返回空数组
[]。
测试用例逐条剖析
原文档共给出 5 组测试用例,覆盖了水平、垂直、正对角线、反对角线和无赢家五种典型场景,是理解题意的关键素材。
用例 1:水平三连
connectThree([["", "", "", ""], ["", "", "", ""], ["", "Y", "", ""], ["Y", "R", "R", "R"]]) // 应返回 ["R", [3, 1], [3, 2], [3, 3]]棋盘第 4 行(索引 3)从左到右是["Y", "R", "R", "R"],其中R在列索引 1、2、3 上连续出现三个,构成水平三连。赢家为"R",坐标按从左到右返回。
用例 2:垂直三连
connectThree([["", "", "", ""], ["", "Y", "Y", ""], ["", "Y", "R", "R"], ["", "Y", "R", "R"]]) // 应返回 ["Y", [1, 1], [2, 1], [3, 1]]观察列索引 1:行 1、2、3 的取值分别是"Y"、"Y"、"Y",构成垂直三连。赢家为"Y",坐标按从上到下返回。
用例 3:正对角线三连
connectThree([["", "", "Y", "R"], ["", "Y", "R", "Y"], ["", "R", "Y", "R"], ["", "R", "Y", "R"]]) // 应返回 ["R", [0, 3], [1, 2], [2, 1]](0,3)、(1,2)、(2,1)三个格子的值依次是"R"、"R"、"R",沿"右上到左下"的对角线方向(即行递增、列递减)连成一线。
用例 4:反对角线三连
connectThree([["", "Y", "", ""], ["", "Y", "Y", ""], ["", "R", "R", "Y"], ["R", "R", "Y", "R"]]) // 应返回 ["Y", [0, 1], [1, 2], [2, 3]](0,1)、(1,2)、(2,3)三个格子均为"Y",沿"左上到右下"方向(行、列同时递增)连成一线。
用例 5:无赢家
connectThree([["Y", "R", "R", "Y"], ["R", "Y", "Y", "R"], ["Y", "R", "R", "Y"], ["R", "Y", "Y", "R"]]) // 应返回 []该棋盘是一个交替排列的Y/R棋盘,任何方向上都找不到三个连续同类棋子,因此返回空数组。
初始代码与求解思路
题目给出的种子代码(--seed--部分)非常简单:
function connectThree(matrix) { return matrix; }它原样返回矩阵,显然不满足任何测试用例,需要你补全算法。整体求解思路分三步:
第一步:枚举四个方向
三连只可能沿四个方向出现,因此只需检查四种"方向向量":
| 方向 | 行增量 (dr) | 列增量 (dc) | 说明 |
|---|---|---|---|
| 水平向右 | 0 | 1 | 检查同一行 |
| 垂直向下 | 1 | 0 | 检查同一列 |
| 右下对角线 | 1 | 1 | 主对角线方向 |
| 左下对角线 | 1 | -1 | 副对角线方向 |
由于"水平向左"是"水平向右"的反方向,检查方向向量的正方向即可覆盖所有情况,无需重复枚举。
第二步:以每个非空格为起点延伸
遍历矩阵中的每一个格子,跳过空字符串。对每个非空格,沿四个方向依次检查第 1、2 个后续格(共 3 格),一旦遇到越界或类型不一致就中断该方向的检查。
第三步:命中后规范化坐标
如果某个方向连续收集到 3 个相同棋子,就对这 3 个坐标排序,使结果满足"从上到下、从左到右"的输出要求,再组装返回数组。
官方参考解答逐行解析
原文档--solutions--部分给出了完整的参考实现,逐段解析如下:
function connectThree(matrix) { const rows = matrix.length; const cols = matrix[0].length; const directions = [[0, 1], [1, 0], [1, 1], [1, -1]]; for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { const piece = matrix[r][c]; if (!piece) continue; for (const [dr, dc] of directions) { const cells = [[r, c]]; for (let i = 1; i < 3; i++) { const nr = r + dr * i; const nc = c + dc * i; if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) break; if (matrix[nr][nc] !== piece) break; cells.push([nr, nc]); } if (cells.length === 3) { cells.sort(([r1, c1], [r2, c2]) => r1 !== r2 ? r1 - r2 : c1 - c2); return [piece, ...cells]; } } } } return []; }- 第 1-2 行:读取矩阵的行数
rows与列数cols,用于后续边界判断; - 第 4 行:定义四个方向向量。这里选择向右、向下、右下、左下四个方向,恰好覆盖水平、垂直、主对角线、副对角线;
- 第 6-7 行:双重循环遍历所有格子;
- 第 8-9 行:取出当前格子的棋子,若为空字符串(
!piece为真,注意""是假值)则跳过。这一行是性能关键:空单元格既不可能成为任何三连的起点,也无需作为终点检查,直接剪枝; - 第 11-21 行:对每个方向,从当前格开始沿方向延伸。
cells数组初始只含起点;循环i从 1 到 2,检查第 2、3 个格子是否越界(第 14 行)或与起点棋子不同(第 15 行),任一条件不满足就break放弃该方向; - 第 23-26 行:
cells.length === 3意味着成功收集到三连。此时先对坐标排序:优先按行号升序(从上到下),行号相同时按列号升序(从左到右),最后用展开语法组装成[piece, ...cells]扁平数组并立即返回; - 第 30 行:全部遍历结束仍未找到三连,返回
[]。
注意排序回调中的解构写法:cells.sort(([r1, c1], [r2, c2]) => r1 !== r2 ? r1 - r2 : c1 - c2),它直接对坐标数组对[行, 列]解构,比较规则完全对应题目要求的输出顺序。
算法复杂度与正确性分析
- 时间复杂度:最坏情况下需要遍历全部
rows × cols个格子,每个格子最多检查 4 个方向、每个方向最多延伸 2 步,因此总复杂度为O(rows × cols × 4 × 2),即O(rows × cols)的线性级别。对于 4×4 这样的小棋盘,性能开销可以忽略; - 空间复杂度:除
cells数组外没有额外的大型数据结构,为O(1)(不计入返回值); - 正确性论证:任意一条长度为 3 的直线段,其"最靠上(行号最小)、同行时最靠左(列号最小)"的端点一定会在遍历时先被访问,且该方向向量会被枚举到,因此所有潜在三连都会被检测,不会遗漏;反之,
break条件保证了只有严格同类型且连续的格子才会被计入,不会产生误报。
实战验证:在本地运行测试
若想在本地验证你的实现,可以按如下步骤在 freeCodeCamp 仓库环境中操作:
- 确保已按 README.md 安装好 pnpm 依赖;
- 将上述参考解答放入 Node.js 环境(或直接在浏览器控制台)执行;
- 依次运行文档中的 5 组
assert.deepEqual断言:
assert.deepEqual(connectThree([["", "", "", ""], ["", "", "", ""], ["", "Y", "", ""], ["Y", "R", "R", "R"]]), ["R", [3, 1], [3, 2], [3, 3]]); assert.deepEqual(connectThree([["", "", "", ""], ["", "Y", "Y", ""], ["", "Y", "R", "R"], ["", "Y", "R", "R"]]), ["Y", [1, 1], [2, 1], [3, 1]]); assert.deepEqual(connectThree([["", "", "Y", "R"], ["", "Y", "R", "Y"], ["", "R", "Y", "R"], ["", "R", "Y", "R"]]), ["R", [0, 3], [1, 2], [2, 1]]); assert.deepEqual(connectThree([["", "Y", "", ""], ["", "Y", "Y", ""], ["", "R", "R", "Y"], ["R", "R", "Y", "R"]]), ["Y", [0, 1], [1, 2], [2, 3]]); assert.deepEqual(connectThree([["Y", "R", "R", "Y"], ["R", "Y", "Y", "R"], ["Y", "R", "R", "Y"], ["R", "Y", "Y", "R"]]), []);全部通过即代表实现正确。该挑战对应的块配置位于 daily-coding-challenges-javascript.json,其中challengeType: 28标记了这类题目的类型,而挑战文档本身存放在 challenge 文件 中。
拓展思考:从 Connect 3 到 Tic-Tac-Toe 与四子棋
Connect 3 的解法框架具有极强的可扩展性,可以推广到一系列同类博弈判定问题:
- 推广连子数:将内层循环的
i < 3改为i < n,即可检测任意长度的连子,例如五子棋的五连; - 推广到 Tic-Tac-Toe 完整判定:本块中的 Challenge 153: Tic-Tac-Toe 就是同族题目,需要同时处理"胜负判定"与"平局判定";
- 方向向量表驱动:把
directions数组抽象为可配置参数,能让同一套遍历逻辑复用于国际象棋、四子棋等不同棋类的攻击范围检查,例如本块中的 Challenge 243: Rook Attack 与 Challenge 217: Captured Chess Pieces 都涉及沿方向向量的棋盘扫描。
掌握"方向向量 + 边界剪枝 + 坐标规范化"这一组合拳,你就拥有了解决一大类棋盘/网格类算法题的通用的底层能力。
【免费下载链接】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),仅供参考