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

五子棋胜负判断:方向数组与矩阵遍历的核心解法 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, y1)到(x, y4)是否都等于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 vectorvectorint 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之后下一次循环忘了重新从ydy开始导致四个方向都混在一起。后来我改成每轮循环都基于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 fx{x}, y{y}, dx{dx}, dy{dy}, cnt{cnt}然后构造一个只有一行黑棋的简单棋盘运行一次你就能清楚地看到每个起点沿方向扩展的过程。这个方法虽然土但比单纯看代码更高效。还有一个技巧把棋盘打印成二维表格手动模拟一次。比如用“1 1 1 1 1”这个数据你要能自己数出第0列到第4列是黑棋五连然后对照程序输出如果输出是none那问题一定出在方向或者循环次数上。6.3 一点应试建议如果让我重新做一次这道2023B卷的“五子棋迷”我会比第一次更快因为我已经把这类题归纳成了一个固定套路读题确认输入输出定义方向数组遍历棋盘并检查连续5个位置最后按题目要求输出。你不需要背代码但需要理解每一行是怎么来的。尤其要明白为什么是“检查4个后续位置”而不是“检查5个后续位置”。当你能给别人讲清楚这一点这道题才算真正吃透。最后再送一个小建议笔试前把方向数组和矩阵遍历的模板默写几遍五子棋、岛屿数量、扫雷这类题目基本都能稳稳拿下。