news 2026/9/9 22:09:57

五子棋胜负判断:方向数组与矩阵遍历的核心解法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
五子棋胜负判断:方向数组与矩阵遍历的核心解法

2023B卷的这道“五子棋迷”,我第一眼看到题目名字的时候,还以为是让写一个能自己下棋的AI。结果读完题面才发现,它只是让你判断一个已经摆好的棋盘上,黑棋还是白棋已经连成了五个子。题面本身不算复杂,但如果你没把矩阵遍历、方向数组、边界条件想清楚,写出来的代码很容易在斜线和棋盘边缘上翻车。

这道题设置得很典型:输入一个m行n列的棋盘,0表示空位,1表示黑子,2表示白子,然后输出当前是黑胜还是白胜。它没有要求你用蒙特卡洛树搜索,也没有要求你设计什么评估函数,核心就是一个胜负判断逻辑。对我来说,这种题反而是最应该拿满分的,因为考点非常明确,就是基础数据结构加逻辑判断。

如果你正在准备笔试、软考或者校招机考,这篇文章的思路可以直接拿来当模板。我会从题目拆解开始,一步步讲到代码实现、测试用例,最后再延伸出怎么在这个基础上做一个简单落子推荐,让这道题在面试时也能变成你的加分项。

1. 拿到题目先别急着写代码:先拆清楚需求

1.1 这道题到底在考什么

先说结论:这道题考的是二维矩阵遍历和“连续状态判断”,不是五子棋AI,也不是什么高深的博弈算法。你只需要回答一个问题——当前棋盘上,有没有任意一个方向连续出现5个相同的非空棋子。

很多同学一看到“五子棋”三个字,脑子里立刻飘过alpha-beta剪枝、必胜开局、活四冲四这些概念。其实出了考场你就会发现,这类题绝大多数只是在考你能不能把一个“判断连续5颗同色棋子”的需求,用代码准确、高效地写出来。它真正想检验的是三件事:第一,你懂不懂方向数组;第二,你处理边界条件是否细心;第三,你在多个输入样例下能不能稳定输出正确答案。

所以拿到题之后,不要着急动键盘,先把题目里隐藏的需求拆出来。这里有几个关键词要highlight一下:“m行n列”说明棋盘不一定正方形,矩阵遍历要按行列来;“0、1、2”分别代表空、黑、白;“连续五个或以上”意味着存在长连也要判定获胜;输出时通常要区分黑胜、白胜、未分胜负。

1.2 输入输出规格要一秒钟看清

笔试中最常见的坑不是算法不会,而是输入输出格式理解错。这类题的输入格式一般有两种。

第一种是标准矩阵式输入:第一行两个整数m和n,接下来m行每行有n个整数,数字之间用空格隔开。这种最直观,直接把二维数组填进去就可以。

第二种是紧凑字符串式输入:棋盘每一行是一串像“00112”这样的字符,没有空格,你需要自己拆成单字符再转成数字。这种格式有时候会在“编程题”里出现,因为它传输起来更省空间,但读起来并不费劲。

输出格式就更要看清楚了。有的判题系统要求输出“black”和“white”,有的要求输出“1”和“2”,还有的干脆让你输出获胜棋子的坐标。题目名“五子棋迷”下面可能还会有一行说明:如果双方都未连成五子,输出“0”或“none”。我在模拟题里遇到的是输出小写字符串,所以代码里专门做了映射,方便随时改。

我的建议是:读完题先用注释在代码开头把输入输出格式写下来,免得写了一半忘了。比如:

# 输入: # 3 5 # 1 1 1 1 1 # 0 2 0 0 0 # 0 0 2 2 2 # 输出:black

这个习惯能帮你省下至少15分钟的调试时间。

1.3 别把简单题做成困难题

我在刷题群里见过有人一上来就写了一个完整的五子棋AI,带UI、带音乐、带悔棋功能,最后在主函数里只调用了一个接口。想法很好,但不是做题。笔试限时,判题系统只认答案,你堆再多搜索树都不会加分。

正确思路是:先写一个最朴素的判断逻辑,保证样例能过,再慢慢优化。甚至不需要优化空间复杂度,只要你把矩阵扫描和方向判断写对,O(mn)级别的复杂度在比赛环境里完全够用。

我当时给自己定的目标是:15分钟内写完主流程,10分钟做自测,5分钟整理注释。后面如果还有时间,再考虑扩展成一个简易落子推荐器。这个节奏可以帮你避免“想太多、写太少”的尴尬。

2. 核心算法设计与关键取舍

2.1 方向数组:一条路径管住横竖斜

五子棋的胜负判断,本质上是判断某个位置是否存在四条直线之一上的连续五子。四系直线分别是水平方向、垂直方向、主对角线方向(从左上到右下)、副对角线方向(从右上到左下)。

如果不用方向数组,你会写出四段几乎一样的循环代码,复制粘贴一时爽,一旦改逻辑就要改四个地方。我建议直接用方向数组统一处理,把四个方向写成坐标偏移量:

DIRS = [ (0, 1), # 水平:行不变,列+1 (1, 0), # 垂直:行+1,列不变 (1, 1), # 主对角线:行+1,列+1 (1, -1), # 副对角线:行+1,列-1 ]

五子棋判断为什么只需要四个方向而不是八个?因为连成五个的方向可以看作一条无向线段,比如从左到右和从右到左本质是同一条线段。我们只要固定沿“向右、向下、右下、左下”四个方向检查,就能覆盖所有可能连成五子的情况,而且不会重复判断到相反方向。

有了这个方向数组,后续所有逻辑都只需写一份通用代码。这个技巧在“岛屿数量”“扫雷游戏”这类矩阵题里面也很好用,属于通用套路。

2.2 用“窗口检查”代替双向扩展

我第一次写五子棋判断时,用的是“从每个棋子出发,沿两个方向数连续同色棋子”的方法。代码大概长这样:从当前点往正方向数,再往反方向数,加起来看是否大于等于5。现在回头想,这个写法虽然也能过,但有几个隐患。

第一,双向扩展会产生大量重复计算。假如一整行都是黑子,你对每个黑子都把它所在行从头到尾数一遍,复杂度会退化成O(mn*max(m,n)),棋盘一大就容易超时。

第二,边界条件变多。你要同时判断正方向和反方向,一旦漏掉某个方向的越界检查,程序可能直接崩溃。

更好的方案是“窗口检查”:对棋盘的每一个非空格点,以它作为一段连续5个子中的起点,沿四个方向检查接下来的4个位置是否和它同色。只要有一个方向满足,就说明存在五连。

为什么只需要检查4个位置?因为当前点本身就是一颗棋子,所以再数4颗就够了。例如水平方向,你要看位置(x, y)右侧连续4个位置是否和当前棋子同色,即(x, y+1)到(x, y+4)是否都等于board[x][y]。

这种写法的复杂度是多少?对每个格子最多检查4个方向,每个方向最多看4步,所以总操作次数大约是mn44,也就是常数级的O(mn)。即使棋盘是10001000,也只需要遍历4000万个格子,几毫秒就能跑完,笔试完全没压力。

代码上也很省心,因为每次都是从当前点往单一方向走,不会出现“先向右数了多少又向左数多少”的混乱。我用这个方法重写了一遍,原来那些隐形bug基本都消失了。

2.3 扫描顺序和提前返回

判断胜负时,可以按行从上到下、从左到右扫描棋盘。找到某个非空格点,然后尝试四个方向检查;一旦发现五连,马上返回当前棋子对应的颜色,不需要继续扫描后面的位置。这种“提前返回”的好处是:对于已经分出胜负的用例,耗时会更短,而且代码逻辑也更清楚。

有一个细节容易争执:如果黑棋和白棋都在棋盘上形成了五连怎么办?严格来说,合法对局中不可能出现双方同时连成五子的局面,因为每次落子只会新增一个棋子。但判题系统偶尔会构造这种“非法状态”来测试你的鲁棒性。我的处理策略是优先返回黑棋(先手)胜利,或者按题目要求输出第一个获胜方。这类特殊情况下不要过度设计,用简单的顺序判断即可。

从工程角度看,提前返回还能帮你省掉不必要的方向扩展。比如某一行中间位置已经判断出黑棋五连,直接跳出双层循环,后续棋子根本不用检查。

3. 完整代码实现与逐段解说

3.1 Python版完整代码

下面是我最后提交的Python版本,代码里加了详细注释,方便你对照理解:

import sys DIRS = [ (0, 1), # 水平向右 (1, 0), # 垂直向下 (1, 1), # 主对角线方向 (1, -1), # 副对角线方向 ] def check_win(board, m, n): def in_board(x, y): return 0 <= x < m and 0 <= y < n for x in range(m): for y in range(n): if board[x][y] == 0: continue color = board[x][y] # 1 黑,2 白 for dx, dy in DIRS: # 从当前点作为起点,检查后续4个位置 cnt = 1 nx, ny = x + dx, y + dy for _ in range(4): if not in_board(nx, ny) or board[nx][ny] != color: cnt = 0 break cnt += 1 nx += dx ny += dy if cnt >= 5: return color return 0 def main(): data = sys.stdin.read().strip().split() if not data: return it = iter(data) m = int(next(it)) n = int(next(it)) board = [] for _ in range(m): row = [] for _ in range(n): row.append(int(next(it))) board.append(row) result = check_win(board, m, n) if result == 1: print("black") elif result == 2: print("white") else: print("none") if __name__ == "__main__": main()

这段代码看起来不长,但已经覆盖了绝大多数测试点。每次检查时,只要后续4个位置里有任何一个越界或者颜色不同,就直接把cnt置为0,表示这个方向不存在以当前点为起点的五连。如果4个位置全部同色,cnt会在循环结束时是5,满足胜利条件。

3.2 核心函数拆开讲

check_win函数是整套代码的核心。它遍历棋盘上每个非空位置,然后尝试四个方向。在方向循环里,我用一个cnt变量记录连续相同棋子的数量,初始为1,因为当前点已经算一个。接着循环4次,每次都判断下一个位置是否存在并且颜色相同。

有同学会问:为什么要从当前点作为起点,而不是从棋盘左上角往右下角滑一个“长度为5的窗口”?其实这两种思路等价。从当前点作为起点,本质上就是让每一个可能的长度为5的水平、垂直、斜线窗口的左端点(或者上端点)都能被遍历到。这样实现起来更直观,也不容易漏。

这里有一个优化点:其实你可以不用cnt变量,直接用一个布尔值标记是否满足条件,但cnt在调试时有一个好处,就是能打印出一段连续到底有多长,方便排查。

3.3 C++版核心代码参考

如果你用的是C++,需要注意二维vector的传参方式。建议写成常量引用,避免拷贝整个棋盘。核心判断函数可以参考:

#include <vector> using namespace std; const int dx[4] = {0, 1, 1, 1}; const int dy[4] = {1, 0, 1, -1}; int checkWin(const vector<vector<int>>& board, int m, int n) { for (int x = 0; x < m; ++x) { for (int y = 0; y < n; ++y) { if (board[x][y] == 0) continue; int color = board[x][y]; for (int d = 0; d < 4; ++d) { int cnt = 1; int nx = x + dx[d]; int ny = y + dy[d]; for (int k = 0; k < 4; ++k) { if (nx < 0 || nx >= m || ny < 0 || ny >= n) { cnt = 0; break; } if (board[nx][ny] != color) { cnt = 0; break; } cnt++; nx += dx[d]; ny += dy[d]; } if (cnt >= 5) return color; } } } return 0; }

C++版本的核心逻辑和Python一致。这里唯一要提醒的是,方向数组最好定义成全局常量,或者在函数内部用static修饰。这样编译器优化起来更容易,代码也更干净。

很多初学者容易在C++里犯一个错误:直接复制Python的for循环,忘记vector下标要用整数,导致编译报错。其实只要把逻辑理清,C++写起来非常快,特别是IO方面直接用cin读入就行。

4. 测试用例与边界验证

4.1 我用来验证的测试用例表

代码写完,我拿下面这组用例测了一遍。建议你也准备几组类似的,覆盖率越高越好。

用例编号棋盘描述预期输出验证点
13x5全0none空棋盘不误判
2一行5个1black水平五连
3一列5个2white垂直五连
4主对角线5个1black斜线胜利
5副对角线5个2white另一个斜线方向
64x4棋盘,4连none不足5个不误判
7棋盘左上角横着5个1black起点在边界
8棋盘右下角斜着5个2white终点在边界
96个1连在一起black长连也判胜

第6组特别重要,因为很多人会把“恰好5个”误判为“超过4个就算”,结果4连也报胜利。第9组长连则是考验你的逻辑是否用了>=5,而不是==5。

4.2 小棋盘和边界用例容易漏

如果棋盘是1x1或者2x2,理论上不可能出现五连,你的代码要能正常返回none,而不是因为越界崩溃。这里的关键点就是in_board函数,只要有这个保护,小棋盘就不会出问题。

边界用例最容易被忽略的场景是:胜利线段紧贴棋盘边缘。比如黑棋在棋盘第一行连续5个,起点的y可能是0,往右数4个之后恰好到边界。如果你只检查了“下一个位置是否越界”,却没在for循环里连续检查4次,很容易漏判。

我当时就吃过一个亏:用了一个临时变量ny,结果在循环里更新了ny之后,下一次循环忘了重新从y+dy开始,导致四个方向都混在一起。后来我改成每轮循环都基于x + dx和y + dy重新计算起点,问题立刻消失。

4.3 多组输入和“双方都赢”的情况

有些判题系统会在一道题里塞多组测试用例,循环处理直到EOF。这种情况下,你的board数组必须在每组输入后重新创建,不能复用上一个用例的状态,否则上一轮的棋子会串到下一轮。

我在支持多组输入时,习惯把主循环写成这样:

while True: line = sys.stdin.readline() if not line: break # 解析当前用例

但有些题目明确说只有一组用例,就不要画蛇添足。先看题目描述里有没有“多组输入”字样,如果有,再改写输入逻辑。

关于“双方都赢了”的情况,按我的保守做法:先判断黑棋是否五连,再判断白棋。因为黑棋先行,既然五子棋本身是先手游戏,这种约定在大多数题目里都能得分。

5. 从判断胜负到简单AI:题目之外还能做点啥

5.1 先做一个“一步获胜”的探测器

如果笔试时间富余,面试官大概率会追问:你能不能让程序自己找地方落子?这时候你就不是单纯改卷,而是要把题目往AI方向延伸一步。

最简单的落子策略是“一步获胜检测”。遍历棋盘上所有空位,临时把这个位置改成当前玩家的颜色,然后调用刚才写好的check_win函数。如果返回胜利,说明这个点就是必杀点。

关键代码如下:

def can_win_in_one(board, m, n, color): for x in range(m): for y in range(n): if board[x][y] != 0: continue board[x][y] = color if check_win(board, m, n) == color: board[x][y] = 0 return (x, y) board[x][y] = 0 return None

注意:临时落子之后,无论是否找到获胜点,都要立刻把board[x][y]恢复为0,否则棋盘会被污染。

5.2 防守逻辑:先堵对面再说

有进攻就得有防守。防守很简单:模拟对方继续落子。如果在某个空位放上对手的棋子后,对手能立刻五连,那你必须优先封堵这个点。

实战中,攻防判断的顺序一般是:先看自己有没有一步获胜的棋,如果有就立刻获胜;再看对手有没有一步获胜的棋,如果有就堵住;最后再按评分函数选一个相对好的位置。

这个逻辑虽然只是贪心,但已经能撑起一个最简单的命令行五子棋AI。面试时能现场写出这个,通常会被认为是思路清晰的。

5.3 凑一个命令行五子棋小游戏

如果你想再做得多一点,可以写一个20x20棋盘,玩家输入坐标,AI用上面“进攻优先、防守其次”的策略应对。不需要做界面,就用终端打印棋盘,已经足够展示能力。

这种扩展的价值在于:它把一道“判断函数”的笔试题变成了一个可以演示的完整程序。面试官考察的点不再是孤立的算法,而是你从需求到实现的工程能力。我当时就在白板上画了个棋盘,演示了两三回合,面试效果比单纯讲代码要好。

6. 常见问题与排查技巧实录

6.1 我在调试时的三个翻车现场

第一个翻车现场:方向数组写错。我最开始用了8个方向,结果同一水平线段会被两个方向重复检测,代码逻辑并没有错,但是输出结果出现了“黑棋明明五连却返回none”的诡异情况。原因是我在一个方向数组里混入了(0,-1),然后从当前点向左数,导致起点选在了线段中间,后续检查向左的4个位置时不够5个。后来我固定只检查四个单向方向,问题立刻解决。

第二个翻车现场:边界条件忘写。测试小棋盘时,程序直接数组越界崩溃。我加了一个is_valid函数,并且在循环里同步判断x和y是否越界,崩溃问题才消失。

第三个翻车现场:多组输入没清空。上一组用例中board残留了棋子,导致下一组用例误判为黑胜。后来我每次读入新用例都重新初始化board,或者直接用局部变量,再没出现过这类问题。

6.2 快速定位问题的土办法

遇到输出不符合预期时,先别急着看算法,直接在check_win函数里加一段打印逻辑。比如把当前扫描到的坐标、方向和计数cnt打出来:

debug_info = f"x={x}, y={y}, dx={dx}, dy={dy}, cnt={cnt}"

然后构造一个只有一行黑棋的简单棋盘,运行一次,你就能清楚地看到每个起点沿方向扩展的过程。这个方法虽然土,但比单纯看代码更高效。

还有一个技巧:把棋盘打印成二维表格,手动模拟一次。比如用“1 1 1 1 1”这个数据,你要能自己数出第0列到第4列是黑棋五连,然后对照程序输出,如果输出是none,那问题一定出在方向或者循环次数上。

6.3 一点应试建议

如果让我重新做一次这道2023B卷的“五子棋迷”,我会比第一次更快,因为我已经把这类题归纳成了一个固定套路:读题确认输入输出,定义方向数组,遍历棋盘并检查连续5个位置,最后按题目要求输出。

你不需要背代码,但需要理解每一行是怎么来的。尤其要明白为什么是“检查4个后续位置”,而不是“检查5个后续位置”。当你能给别人讲清楚这一点,这道题才算真正吃透。最后再送一个小建议:笔试前把方向数组和矩阵遍历的模板默写几遍,五子棋、岛屿数量、扫雷这类题目基本都能稳稳拿下。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 22:09:38

NeMo Voice Agent 实战指南:3步在本地跑通全开源语音助手

NeMo Voice Agent 实战指南&#xff1a;3步在本地跑通全开源语音助手 【免费下载链接】Speech A scalable generative AI framework built for researchers and developers working on Large Language Models, Multimodal, and Speech AI (Automatic Speech Recognition and Te…

作者头像 李华
网站建设 2026/9/9 22:09:36

如何用 Ant Design CLI 离线查询组件 API、Demo 与 Design Token

如何用 Ant Design CLI 离线查询组件 API、Demo 与 Design Token 【免费下载链接】ant-design An enterprise-class UI design language and React UI library 项目地址: https://gitcode.com/GitHub_Trending/an/ant-design 在编写或维护基于 Ant Design&#xff08;an…

作者头像 李华
网站建设 2026/9/9 22:07:09

OpenSim外部几何模型导入:从STL预处理到XML挂载全指南

简介&#xff1a;面向OpenSim生物力学建模初学者与研究人员&#xff0c;这份资源演示了在OpenSim 4.1环境下为leg6dof9musc腿部六自由度九肌肉模型添加外部几何模型的具体过程。压缩包共3个文件&#xff0c;包括原始OSIM模型文件、STL格式的示例外部几何体&#xff0c;以及用于…

作者头像 李华
网站建设 2026/9/9 22:05:43

Android三维模型加载实战:用JPCT-AE快速渲染OBJ模型

简介&#xff1a;这是一份面向Android开发者的JPCT三维模型加载示例工程&#xff0c;围绕OpenGL ES渲染、GLSurfaceView视图与JPCT API展开&#xff0c;详细覆盖从模型导入、场景构建、渲染循环到触摸交互的完整链路。项目自带名为“3DTest”的完整实例&#xff0c;可直接查看W…

作者头像 李华
网站建设 2026/9/9 22:05:03

Material UI 如何用 NumberField 组件实现带步进按钮的数字输入?

Material UI 如何用 NumberField 组件实现带步进按钮的数字输入&#xff1f; 【免费下载链接】material-ui Material UI: Comprehensive React component library that implements Googles Material Design. Free forever. 项目地址: https://gitcode.com/GitHub_Trending/ma…

作者头像 李华
网站建设 2026/9/9 22:03:56

AT24C64驱动详解:从I2C时序到页写跨页处理的完整实现

简介&#xff1a;AT24C64驱动文件是一份面向嵌入式开发者的EEPROM驱动代码包&#xff0c;解决微控制器通过IC总线读写AT24C64的常见需求&#xff0c;适用于STM32、51等单片机项目的配置存储与设备信息读取场景。资源共3个文件&#xff0c;压缩包仅1KB&#xff0c;其中C源码文件…

作者头像 李华