1. 题目背景与解题意义华为OD机试双机位C卷黑白棋这个题目确实在很多考生的题库名单里出现过难度算不上顶尖但思路很典型属于那种看着简单、写起来考细节的题。尤其近几年OD机试慢慢从“背题就能过”变成“理解才能过”说实话单纯刷题但不动手把代码跑通、跑对边界的人很容易在类似题目上翻车。黑白棋也叫Reversi、Othello本身是个非常经典的棋类游戏。规则一句话就能概括双方轮流落子落子位置必须能夹住对方至少一颗棋子夹住的棋子全部翻转成己方颜色最后棋盘上谁的棋子多谁赢。听起来不难但一旦要求你实现“给定局面判断合法位置”或者“对某个位置落子并更新棋盘”的时候一大堆边界问题就冒出来了。这些边界问题恰恰是机试最喜欢埋坑的地方。这道题能被放进双机位C卷说明出题人想考察的不是你会不会背某个算法模板而是你的代码能不能在时间限制内把规则理清楚、把复杂逻辑拆成可维护的模块。C/C、Java、Python、Go、JS五种语言都能写但推荐优先选择自己最熟、提交时间最可控的一门。这篇就专门拆解黑白棋题目的常见变体、怎么做题解分析、怎么把规则翻译成代码、五门语言各自怎么写最省事最后附上我从实际刷题和模拟考试里总结的排查经验。无论你是第一次考OD还是二战三战这篇都能直接当参考。2. 常见题目变体与解题思路拆解2.1 双机位C卷中的常见考法黑白棋在OD机试里很少让你写一个完整的对战AI更多地是把规则拆成小任务来考。我归纳了一下目前见到的考法主要集中在三类第一类是“合法性判断”。给你一个棋盘状态告诉你当前轮到黑棋或白棋下要求找出所有可以落子的合法位置或者判断某个指定位置是否合法。这个考的是对“夹住对方棋子”这一核心规则的深刻理解。第二类是“落子后棋盘更新”。给你一个棋盘和一步落子要求模拟翻转后的结果输出新棋盘。这种考法看着功能简单但对八个方向的遍历逻辑要求很高稍不留神就会在边角方向上漏掉棋子。第三类是“终局判胜负”。给定一个中间局面或终局局面统计黑白棋子数量并判断谁赢有时还会附加“有效棋步数”之类的条件。这种相对简单但会结合前面两类逻辑一起出现形成一道综合题。坦白讲我当年在准备这类题时最大的感受是题目本身不难难在你急了之后忽略的细节。比如用Java写的时候二维数组的边界判断写错了运行时抛一次ArrayIndexOutOfBoundsException整道题就白费了。这种坑谁踩谁知道。2.2 解题思路的通用框架不管题目具体怎么出核心逻辑都绕不开三个环节判断合法位置、执行翻转、更新计分如果需要的话。我在做题时习惯把这三步拆成独立的函数然后用一个主流程去串起来。这样做的好处是每一步都能单独调试出错了也容易定位。判断合法位置是整个题目的地基。从规则出发一个合法的落子必须满足两个条件第一该位置当前必须是空格第二从这个位置向上下左右和两条对角线共八个方向延伸至少要有一个方向能形成“己方棋子-连续若干对方棋子-己方棋子”的格局。有了这个判断逻辑后续的“翻转”其实就是把判断过程中找到的满足条件的棋子全部变色。这里有个细节判断和翻转最好是分开实现不要混在一起因为判断时你可能只想验证合法性并不想真的改变棋盘状态而翻转时需要明确知道哪些棋子需要变色两者的逻辑侧重点不同。另外在很多题目里边界条件会直接影响解法。比如有的变体规定棋盘是4x4有的是8x8有的甚至给一个非方形的棋盘。这时候如果你直接把方向常量写死后面想改就麻烦了。所以我的习惯是把棋盘大小作为参数传入所有方向计算都基于这个参数动态判断这样无论题目怎么变代码都能复用。2.3 为什么这道题值得重点准备从功利的角度说黑白的棋性价比很高。它考察的是二维数组操作、方向遍历、边界判断这些能力几乎是所有算法题的基础练好它对之后处理矩阵类题目比如岛屿数量、八皇后变种都有直接帮助。从应试的角度说黑白棋的规则固定、变体有限只要你把代码模板吃透考场上遇到类似题目基本就是“换汤不换药”。很多考生喜欢到处收集押题其实与其押一百道不一样的题不如把黑白棋这种高频题型完整啃下来吃透一个比模糊地见过十个有效得多。我个人的建议是笔试前至少亲手写两遍这道题第一遍允许查资料、慢慢调第二遍就得限时40分钟内独立完成并跑通所有自测样例。达到这个水平后考场上遇到同类问题的把握会大很多。3. 核心规则解析与代码实现要点3.1 棋盘表示与基本数据结构做黑白棋第一步就是选好棋盘的数据结构。最常见的做法是用二维数组值0表示空格1表示黑子2表示白子或反过来看你心情但一定要统一。如果你用C可以直接用vectorvector Java用int[][]Python用list of listGo用[][]intJS直接用二维数组这些都没问题。选二维数组的原因很简单棋盘本来就是二维格子结构数组天然适合随机访问写方向遍历时也直观。虽然用一维数组加坐标换算也能做但可读性和可维护性都会差很多考试时间紧张时没必要给自己找麻烦。棋盘的初始化也很讲究。标准黑白棋开局的中心四格是固定的左上黑、右上白、左下白、右下黑4x4或8x8都一样规则就是这么定的。不过OD机试里很少让你初始化棋盘多数是给你一个现成的局面让你处理所以这里只需要保证你读入数据的方式没问题就好。3.2 八方向遍历的正确姿势八个方向的遍历看起来很简单但不小心就出bug。我推荐用一个方向数组来统一处理避免写八个if-else。这里以C为例你可以定义const int dx[8] {-1, -1, -1, 0, 0, 1, 1, 1}; const int dy[8] {-1, 0, 1, -1, 1, -1, 0, 1};然后统一遍历这8个方向对每个方向做延伸判断。这个写法最大的好处是代码精简、不易遗漏方向而且后续要调整方向顺序或者做对称处理都很方便。判断某个方向是否“能翻转”的核心逻辑是从落子位置出发沿当前方向走第一步必须遇到对方棋子然后继续沿同方向走直到遇到己方棋子则这个方向合法如果先遇到空格或出界则这个方向不合法直接短路。这里有个容易忽略的点第一步必须是对方棋子如果你第一步就踩到空格或己方棋子那么这个方向直接作废。很多新手会在这一步踩坑因为规则字面上是“夹住对方棋子”但并没有强调这个“对方棋子”紧邻落子位置。实际上规则就是要求紧邻的因为棋子翻转只能翻转连续的对方棋子中间不能有空格。3.3 合法性判断与翻转的统一实现判断合法位置时不需要真正翻转棋子只需要判断是否存在至少一个合法方向。但翻转棋子时需要真正把对应方向的棋子全部变色。怎么让这两者共享一套逻辑呢我的做法是写一个函数传入参数指定“只判断”还是“执行翻转”bool checkDirection(int x, int y, int dx, int dy, vectorvectorint board, int curColor, bool doFlip) { int nx x dx, ny y dy; bool hasOpposite false; while (nx 0 nx n ny 0 ny n) { if (board[nx][ny] 0) return false; if (board[nx][ny] curColor) { if (doFlip) { int fx x dx, fy y dy; while (fx ! nx || fy ! ny) { board[fx][fy] curColor; fx dx; fy dy; } } return hasOpposite; } hasOpposite true; nx dx; ny dy; } return false; }这个函数简洁地把“判断”和“翻转”合并成了一个流程。当doFlip为false时它只检查能否走到己方棋子并返回bool当doFlip为true时它在确认条件成立后再次从起点沿该方向走到己方棋子处把所有中间的棋子翻成当前颜色。注意我第二次遍历时是从起点的下一个格子开始的终止条件是走到刚才找到的己方棋子位置这样避免把起点和终点也翻转了。3.4 五种语言实现时的注意事项C的特点是指针和引用灵活但也最容易在边界上翻车。建议用vector而不是裸数组因为vector自带size方法避免越界访问。Java写这题时最烦的是二维数组的边界判断。推荐在核心循环里每次都判断是否在界内不要预先做可能漏判的简化。然后Java的int[][]默认值是0如果你用0表示空格那么在读入棋盘前不要额外初始化否则可能覆盖掉真正需要的默认值。Python的优势是写起来快劣势是慢。好在黑白棋棋盘不大即使双重循环也完全不会超时。这里要注意Python的深拷贝与浅拷贝如果你需要保存棋盘状态做回溯一定要用copy.deepcopy或者自己手动逐行复制直接list复制会共享内部引用改一个就全改了。Go的数组类型比较严格[8][8]int和[][]int是不同类型建议直接用切片切片即[][]int这样在函数间传递时更灵活。Go的越界不会像Java那样抛异常而是直接panic排查起来更难所以边界判断一定不能省。JS写这题很顺手数组就是天生动态的。但JS里0和空数组在布尔判断中容易混建议比较时都用全等避免隐形类型转换带来的诡异行为。4. 完整实现流程与核心环节拆解4.1 我直接给出一个能跑的C参考实现下面这段代码我实测过能够处理“给定棋盘、当前下棋方、输出所有合法位置并统计翻转后的棋子数”这类核心需求。你只要根据题目输入格式稍作调整就能用#include iostream #include vector using namespace std; const int dx[8] {-1, -1, -1, 0, 0, 1, 1, 1}; const int dy[8] {-1, 0, 1, -1, 1, -1, 0, 1}; int n; vectorvectorint board; bool isValidMove(int x, int y, int curColor) { if (board[x][y] ! 0) return false; int oppColor 3 - curColor; for (int k 0; k 8; k) { int nx x dx[k], ny y dy[k]; bool hasOpp false; while (nx 0 nx n ny 0 ny n) { if (board[nx][ny] 0) break; if (board[nx][ny] curColor) { if (hasOpp) return true; break; } hasOpp true; nx dx[k]; ny dy[k]; } } return false; } void applyMove(int x, int y, int curColor) { board[x][y] curColor; int oppColor 3 - curColor; for (int k 0; k 8; k) { int nx x dx[k], ny y dy[k]; bool hasOpp false; while (nx 0 nx n ny 0 ny n) { if (board[nx][ny] 0) break; if (board[nx][ny] curColor) { if (hasOpp) { int fx x dx[k], fy y dy[k]; while (fx ! nx || fy ! ny) { board[fx][fy] curColor; fx dx[k]; fy dy[k]; } } break; } hasOpp true; nx dx[k]; ny dy[k]; } } } int main() { cin n; board.assign(n, vectorint(n)); for (int i 0; i n; i) { for (int j 0; j n; j) { cin board[i][j]; } } int curColor; cin curColor; vectorpairint,int moves; for (int i 0; i n; i) { for (int j 0; j n; j) { if (isValidMove(i, j, curColor)) { moves.push_back({i, j}); } } } int blackCnt 0, whiteCnt 0; for (auto p : moves) { applyMove(p.first, p.second, curColor); } for (int i 0; i n; i) { for (int j 0; j n; j) { if (board[i][j] 1) blackCnt; if (board[i][j] 2) whiteCnt; } } cout moves.size() endl; for (auto p : moves) { cout p.first p.second endl; } cout blackCnt whiteCnt endl; return 0; }这里用了3 - curColor来求对方的颜色前提是你约定黑子1、白子2这样1的对手是22的对手是1刚好用3减就行。这是个很实用的小技巧省得每次写ifelse判断。4.2 函数拆解与主流程设计思路上面这段代码我拆成了三个核心函数isValidMove负责判断某个点是否可下applyMove负责落子并翻转主函数负责读入、遍历所有位置、统计输出。这个拆分思路本身比具体代码更值得借鉴。拆分函数不是为了让代码看起来高级而是为了让你在考试时能快速定位bug。假如最后输出结果不对你只需要判断是isValidMove写错了还是applyMove写错了定位范围缩小一大半。如果你把逻辑全堆在main里排错时每个循环、每个边界条件都可能是凶手排查效率太低了。主流程里有一个容易漏掉的细节如果你需要统计所有合法位置并且要求把落子后的棋盘状态作为最终输出那么你必须对每个合法位置都真实调用applyMove。有些同学只统计了合法位置数忘了还要翻转棋子更新棋盘结果后面统计黑白棋子数量时发现棋盘还是原样那就白费了大半功夫。4.3 边界条件与样例设计是考试的关键OC考试给的样例通常不会覆盖所有边界情况所以你自己得学会构造几个边界样例来验证代码。我常用的几个测试思路如下第一测角点。把棋盘的四个角作为落子位置重点看左上角(0,0)、右下角(n-1,n-1)这些位置。角点只有一个或两个方向可以延伸最容易暴露方向遍历不完整的问题。第二测空格判断。落子位置必须为空如果棋盘上已经没有空格应该输出0个合法位置并且不改变棋盘状态。这个很容易在题目变体中出现要特别小心。第三测双方无棋可下的局面。黑白棋有个规则如果没有合法位置就得跳过回合。虽然OD机试里很少直接考这个规则但如果题目暗示了“无棋可下则输出0”你的代码得能正确处理空棋盘或满棋盘的极端情况。第四测全同色棋盘。比如棋盘上全是黑子当前轮到白子下那么合法位置必然是0。这个看似简单但有些代码在判断时会把“找不到对方棋子”误判为“合法”从而输出错误的坐标实测中很容易漏掉。4.4 其他语言快速调整的思路如果你用Python上面C的主逻辑完全可以直接翻译但Python的while循环稍微注意下缩进即可。Python版本可以更简洁因为语言本身更灵活比如可以用for循环加break代替繁琐的while边界判断。我把核心逻辑简写如下def is_valid(x, y, board, cur): if board[x][y] ! 0: return False opp 3 - cur for dx, dy in dirs: nx, ny x dx, y dy has_opp False while 0 nx n and 0 ny n: if board[nx][ny] 0: break if board[nx][ny] cur: if has_opp: return True break has_opp True nx dx ny dy return FalseJava的实现大体和C一致只是语法略啰嗦。需要注意Java的int数组默认值是0如果你用0表示空格那么读入时不要手动初始化成其他值。Go的实现基本一致只是方向数组的声明稍微繁琐一点。JS则要注意可读性函数不要写得太长因为JS调试时很难看出具体哪里出了问题。5. 常见问题、排查心得与避坑技巧5.1 最容易踩的坑方向数组与边界判断我刷题时遇到最多的问题就是方向数组漏了方向。有些人喜欢手写八个方向的if-else结果写着写着就漏了一个对角线。所以一定要用方向数组把上下左右和四个对角线统一放进一个循环里。第二个常见问题是把“边界判断”和“内容判断”顺序写反了。比如先访问board[nx][ny]再判断nx是否在界内这会导致越界访问。正确方式是先用条件判断nx和ny是否在界内再访问数组两者顺序不能颠倒。还有一个我一再强调的细节第一步必须是对方棋子。有的实现里循环逻辑先遇到己方棋子就返回false这其实是对的但如果你把hasOpp的判断写在前面可能会出现“第一步是己方棋子但也算合法”的bug。这个时候一定要把第一步的身份判断和中间过程分开理解不能混为一谈。5.2 考试时的时间分配与调试策略OD机试的时间是有限的所以做题节奏很重要。我建议拿到题目后先花5分钟看清输入输出格式尤其注意棋盘尺寸的输入方式。有些题目固定是8x8不输入n直接给8行数据有些则输入n和n行数据。这两种情况处理方式不同先搞清楚再动手写代码。然后花10到15分钟把主体逻辑写完剩下来的时间全部用来跑测试。不要着急提交机试成绩只看结果你提前交卷不会加分。测试时除了示例外把前面提到的角点、空棋盘、全同色这些边界样例都跑一遍能有效降低翻车概率。如果中途发现结果不对不要盲目重写先用打印语句cout/print/console.log追踪中间状态。最常见的问题是缺少关键打印信息导致排查困难。比如你先打印一下isValid在几个具体点上的返回值就能快速判断是判断逻辑错还是翻转逻辑错。5.3 多语言混用时的额外注意如果你平时用的是C但考试允许用Java我建议不要临时换语言除非你非常确定自己的Java水平不比C差。考试不是炫技的地方用你最稳的语言写比什么都强。我在真实考试中见过有人因为Java的二维数组语法不熟在分配数组时浪费了好几分钟这是完全可以避免的。如果你确实要换语言提前练习一下数组声明和读入方式。拿Java来说int[][] board new int[n][n]这句要写熟练Scanner怎么读二维数组也要记熟。Python则要注意input().split()返回的是字符串列表需要逐个转成int这些问题都很基础但考场上一次性写对的人不一定多。Go语言的切片默认值是nil不像数组有零值初始化。所以如果你用make([][]int, n)之后还要逐行make一定要记住这个坑。JS则要注意数组的map方法返回的是新数组如果你直接对原数组map修改可能会搞混深拷贝和浅拷贝。5.4 从真题场景中总结的经验我在练习和复盘时发现黑白棋这类“规则题”最忌讳的就是死记模板。比如你把8x8写死在代码里题目突然给4x4就直接越界。更好的做法是永远把棋盘大小作为变量n传入所有循环都用n做边界这样任意尺寸都能跑通。另外建议养成“随手写辅助函数”的习惯。判断某个点是否在棋盘内这种小函数哪怕只有一行也值得单独抽出来。因为如果你在多个方向循环里反复写nx0 nxn不仅容易出错还很难一眼看出逻辑问题。抽成函数后用起来又清晰又不容易错。最后一个小技巧把棋盘输出做成一个可选的debug函数。考试时如果需要调试可以直接打印当前棋盘状态一眼看出翻转之后颜色是否对。这个函数平时练习时也很有用尤其是你处理完多个合法落子后检查棋盘是否被正确更新debug函数能省下大量无意义的盯代码时间。黑白棋这道题真正拉开差距的往往不是算法难度而是你是否能在限定时间内把规则准确翻译成代码。把本文的核心逻辑吃透、边界样例跑熟考场上遇到它你就有充足的底气去拿分。