news 2026/10/31 21:49:33

华为OD机试黑白棋:八方向扫描与翻转规则详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
华为OD机试黑白棋:八方向扫描与翻转规则详解

华为OD机试的C卷里,最容易被低估的题目之一就是【黑白棋】。这题看着像博弈题,实际上考的是最朴素的八方向扫描,Python和JS都能写,但很多人第一次做的时候连题目都没读懂——落子之后到底哪些棋子要翻转,边界怎么处理,坐标是从0开始还是从1开始,每个环节都能让人卡住。这篇文章就把这道题从规则建模、完整实现到踩坑点全部拆开讲清楚,适合正在刷OD机试真题、尤其是C卷Python或JS方向的读者。黑白棋本身不难,难的是把生活化的棋类规则翻译成代码时,脑子里必须有一个清晰的扫描模型,否则写出来的循环要么越界,要么漏翻棋子。

1. 黑白棋机试题的底层逻辑:8方向扫描与状态翻转

1.1 题目到底长什么样

华为OD机试里的【黑白棋】不是要求你写一个能下棋的AI,而是给定一个8乘8的棋盘状态,棋盘上用*表示空位,X表示黑棋,O表示白棋。输入前8行是棋盘,接下来是一个落子坐标,再接下来是一个要落的棋子颜色。题目要求你模拟这次落子,把该翻转的棋子全部翻转,最后输出新的棋盘状态;如果这个位置不能落子,就按题目约定输出指定错误信息。

这才是真题的真实考法,棋盘上可能已经有了很多棋子,初始布局不是固定的。你只需要处理一步落子,不需要考虑轮流下棋、终局判断、胜负统计这些额外的东西。很多人第一眼看到"黑白棋"三个字,脑子里自动浮现出棋类AI、博弈树、评估函数,直接吓住,其实完全跑偏了。

1.2 把"夹住"翻译成代码

黑白棋的核心规则一句话就能讲完:落子之后,从落子位置向八个方向看,如果某个方向上连续是一个或多个对方棋子,再往后遇到了己方棋子,那么中间夹住的这一串对方棋子全部变成己方棋子。

用生活化的类比,这就像做一个三明治,你手里拿着的棋子是上面那层面包片,棋盘上已经有的己方棋子是最底下那层,中间夹着的火腿肠就是对方棋子。火腿肠被夹住了,就变成你的。

更严格一点说,判定某个方向是否合法需要三步。第一步,落子位置本身必须是空位,这一点最容易被忽略。第二步,从落子位置沿某个方向走一格,这一格必须是对方棋子,如果这一格是空的或者己方棋子,这个方向直接作废。第三步,沿着这个方向继续走,中间每一步都得是对方棋子,直到遇到棋盘边界或者遇到己方棋子。如果先遇到己方棋子,中间这些对方棋子全部翻转;如果先遇到边界或者空格,这个方向什么也不发生。

1.3 这题不是博弈,是模拟

理解这道题的正确姿势,是把心态从"下棋"切换到"状态更新"。黑白棋每一步落子对棋盘的影响是局部的,只影响落子位置所在的八个方向延伸出去的那几条线。这种题目本质上就是一个矩阵遍历加条件判断的问题,跟你平时刷的岛屿数量、扫雷游戏、迷宫寻路属于同一类,区别只是每个方向的判断逻辑稍微绕一点。

OD机试之所以喜欢出这类题,是因为它同时考察了读题能力、边界处理和代码组织能力。一个简单的规则,如果脑子里的方向模型不清晰,写出来的代码很容易出现重复逻辑或者漏判。

2. 方向数组与合法落子判定:这道题的心脏

2.1 为什么八个方向一定要用增量数组

新手最容易写出的代码是这样的:判断上面怎么写一遍,判断下面再复制一遍,判断左上再复制一遍,代码十分钟写完,却要花四十分钟排查某个方向的坐标算错了。方向增量数组就是为了消灭这种重复。

八个方向对应八组行和列的偏移量,按顺序写在数组里:

DIRS = [ (-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1) ]

这样写的好处有两个。第一,逻辑只写一遍,八个方向通过循环遍历,不会漏方向。第二,合法判定和翻转操作可以共用同一个方向循环框架,后续维护时只需要改一处逻辑,不会出现"判定能过但翻转错误"这种见了鬼的问题。

2.2 合法落子判定三步法

我在做题时习惯把判定拆成三个明确步骤,每一步都有对应的代码形态。第一步检查当前位置是不是空位,不是的话直接返回不可落;第二步初始化八个方向的循环,每个方向先把坐标移动到紧邻格,检查它是不是对方棋子,不是就跳过这个方向;第三步进入while循环,一直沿着方向前进,只要还在棋盘内且是对方棋子就继续走,最后判断停下来的位置是不是己方棋子,是的话说明这个方向能够夹住对方棋子,整个落子合法。

第三步里有三个细节必须同时满足:停下来的坐标必须在棋盘内、该位置的棋子颜色必须是己方、中间至少有一颗对方棋子。有人会把"停下来的坐标在棋盘内"这一条漏掉,导致数组越界,这是最常见的运行时错误。

2.3 翻转操作和判定共用一个循环框架的原因

翻转的本质是"先标记,后确认"。落子后,从落子点向某个方向走,一边走一边把沿途的对方棋子坐标记录到一个临时列表里。走完之后如果发现这个方向封口了,也就是遇到了己方棋子,就把列表里记录的所有坐标全部改成己方颜色;如果没封口,这个列表直接丢掉,继续检查下一个方向。

判定和翻转的循环结构几乎一模一样,区别只在于判定只需要返回一个布尔值,翻转需要真正执行修改。所以我把这两部分拆成两个函数,但保持相同的命名和遍历风格。有些人喜欢把判定和翻转合并,落子前先判定,判定通过了再落子,这样也可以,但代码会绕一些。我推荐先判定后翻转,逻辑更直白,也方便你在打印日志时确认每一步的状态。

3. Python实现:完整代码与逐段拆解

3.1 棋盘读入技巧

Python里读机试的输入,最稳的方式是sys.stdin把所有行一次性读进来,然后用列表推导过滤空行。棋盘每一行是一个8位字符串,直接用list(line)把它变成可修改的字符列表,这样落子时才能改棋盘,字符串本身是不可变的。

坐标行的处理有个隐藏细节。真题给的坐标可能是"第几行第几列",行列之间有空格,也可能用逗号分隔。我建议统一用split()按空白切割,拿到两个整数后再判断是否需要减一。很多题目描述里说左上角是(1,1),那么数组下标必须做减一转换,这一步做错整个答案就是零分。

3.2 两个核心函数:could_place 与 place_and_flip

could_place负责回答"这个位置能不能落子",它只做判断不修改棋盘。place_and_flip负责真正落子并完成翻转。我单独把它们拆开,是因为机试调试时经常需要单独验证"某个位置是否合法",合在一起每次都要打印棋盘状态,很麻烦。

import sys DIRS = [ (-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1) ] def in_board(r, c): return 0 <= r < 8 and 0 <= c < 8 def opp_color(color): return 'O' if color == 'X' else 'X' def can_place(board, r, c, color): if board[r][c] != '*': return False opp = opp_color(color) for dr, dc in DIRS: nr, nc = r + dr, c + dc if not in_board(nr, nc) or board[nr][nc] != opp: continue while in_board(nr, nc) and board[nr][nc] == opp: nr += dr nc += dc if in_board(nr, nc) and board[nr][nc] == color: return True return False def place_and_flip(board, r, c, color): board[r][c] = color opp = opp_color(color) for dr, dc in DIRS: nr, nc = r + dr, c + dc to_flip = [] while in_board(nr, nc) and board[nr][nc] == opp: to_flip.append((nr, nc)) nr += dr nc += dc if in_board(nr, nc) and board[nr][nc] == color: for fr, fc in to_flip: board[fr][fc] = color return board def main(): lines = [line.strip() for line in sys.stdin if line.strip()] board = [list(line) for line in lines[:8]] parts = lines[8].split() r, c = int(parts[0]), int(parts[1]) color = lines[9].strip() r -= 1 c -= 1 if not can_place(board, r, c, color): print("invalid") else: board = place_and_flip(board, r, c, color) for row in board: print(''.join(row)) if __name__ == "__main__": main()

这段代码的main函数里,r -= 1和c -= 1默认针对坐标从1开始的题目。如果题目示例明确说坐标从0开始,就把这两行删掉。我建议考试时先用题目给的示例输入试一次,输出对不上就立刻怀疑这里。

3.3 用一组实际用例验证

给你一组可以手算验证的输入:

******** ***O*X** ***O*O** ***O*O** ***X*O** ******** ******** ******** 1 4 X

坐标1 4转成数组下标是(0, 3),这里本来是空位。从它向下看,(1,3)是O,(2,3)是O,(3,3)是O,(4,3)是X,中间三颗白子被夹住,全部翻成黑子。输出应该变成:

***X**** ***X*X** ***X*O** ***X*O** ***X*O** ******** ******** ********

你可以自己拿笔在纸上推一遍。特别注意(1,4)这个位置是X,它属于另一条线,不参与这次翻转,棋盘上的其它棋子一个都不能动。这种"只翻转夹住的一串"的要求,靠的就是to_flip列表的精确记录。

4. JavaScript实现:同样的逻辑,不同的语法坑

4.1 二维数组创建最容易踩的坑

JS里很多人一上来就写Array(8).fill(Array(8).fill('*')),然后惊悚地发现改一个格子,整列都跟着变。原因在于fill传入的是一个数组引用,每一行其实都是同一个数组。正确写法是用Array.from配合箭头函数:

let board = Array.from({ length: 8 }, () => Array(8).fill('*'));

如果是从输入字符串转换,就用line.split('')生成每一行的字符数组,这种方式不会有引用共享问题。我见过好几个把机试当练习的开发者,在这上面浪费了二十分钟,拿到题目后先把这个语法点绕过去,后面就顺畅了。

4.2 输入输出处理的固定模板

Node.js环境下的标准输入用readline逐行读,我会把所有非空行收集到一个数组里,close事件触发后再统一解析。字符串数组要先通过split('')转成可修改的二维数组,输出时用row.join('')转回字符串。

const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const DIRS = [ [-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1] ]; function inBoard(r, c) { return r >= 0 && r < 8 && c >= 0 && c < 8; } function opp(color) { return color === 'X' ? 'O' : 'X'; } function canPlace(board, r, c, color) { if (board[r][c] !== '*') return false; const op = opp(color); for (const [dr, dc] of DIRS) { let nr = r + dr, nc = c + dc; if (!inBoard(nr, nc) || board[nr][nc] !== op) continue; while (inBoard(nr, nc) && board[nr][nc] === op) { nr += dr; nc += dc; } if (inBoard(nr, nc) && board[nr][nc] === color) return true; } return false; } function placeAndFlip(board, r, c, color) { board[r][c] = color; const op = opp(color); for (const [dr, dc] of DIRS) { let nr = r + dr, nc = c + dc; const toFlip = []; while (inBoard(nr, nc) && board[nr][nc] === op) { toFlip.push([nr, nc]); nr += dr; nc += dc; } if (inBoard(nr, nc) && board[nr][nc] === color) { for (const [fr, fc] of toFlip) { board[fr][fc] = color; } } } } let inputLines = []; rl.on('line', (line) => { if (line.trim().length > 0) inputLines.push(line.trim()); }); rl.on('close', () => { let board = inputLines.slice(0, 8).map((line) => line.split('')); const parts = inputLines[8].trim().split(/\s+/); let r = parseInt(parts[0], 10) - 1; let c = parseInt(parts[1], 10) - 1; const color = inputLines[9].trim(); if (!canPlace(board, r, c, color)) { console.log('invalid'); return; } placeAndFlip(board, r, c, color); for (const row of board) { console.log(row.join('')); } });

4.3 两种语言选谁更稳

如果你Python和JS都还凑合,我建议Python优先。Python的代码量更短,列表推导、字符串转字符数组、逐行拼接输出都更直觉。JS的优势在于如果你平时做前端多,readline模板已经背下来了,那用JS也没问题。机试判分看的是输出结果,不是代码风格,选自己最熟的语言才是对的。

无论用哪种语言,核心逻辑完全一致:方向数组、边界检查、先判定后翻转。语言差异只体现在语法上。JS里const [dr, dc] of DIRS这种解构写法很漂亮,但如果你用的Node版本比较老,记得改成for (let i = 0; i < DIRS.length; i++)这种传统写法,不要因为解构不兼容导致运行时错误。

5. 双机位C卷实战:输入解析、边界条件和时间分配

5.1 坐标偏移是零分重灾区

真题里的坐标描述通常有两种。一种是"第1行第1列用0表示",另一种是"左上角坐标为(1,1)"。后者需要转成数组下标时减一,非常容易错。我的建议是不要死记,考试时先看题目给的输入示例,用示例数据跑一遍自己的程序,发现输出和示例不一致,检查范围优先排序是:坐标偏移、颜色字符大小写、棋盘读入是否多读少读。

这个题目还有一个容易忽略的点:输入里可能混有空格和空行。有些样例在棋盘和坐标之间留了一个空行,如果你用lines[8]直接取坐标,空行被过滤掉之后坐标的下标会变。我统一用"过滤非空行"的方式来处理,这样不管样例里有没有空行都一样。

5.2 最容易翻车的四个测试用例

我用一个表格把这几个必须自测的场景列出来,省得临场抓瞎。

场景输入特点预期行为对应代码位置
落子位置有棋子坐标处是X或O输出invalidcan_place第一步
落子位置被己方棋子包围周围没有对方棋子输出invalid各方向扫描都作废
落子位置在角落左上角有对方棋子斜线只翻转对角线方向越界跳过
中间有多个对方棋子一条线上三颗对方棋子整串全部翻转while收集列表

每个方向独立判断,一个方向合法就落子,不要把八个方向的结果做逻辑与。有学员写过"把所有方向翻转完才算合法"的代码,方向之间互相污染,结果一个方向合法但另一个方向不合法时把不该翻的也翻了。每个方向是独立的事件。

5.3 机试节奏和时间分配

C卷的题量通常不小,留给每道题的时间有限。我建议黑白棋这类保姆级模拟题控制在20分钟以内完成。时间分配是这样:读题加画图确认规则5分钟、写核心逻辑10分钟、用两个样例验证5分钟。如果超过30分钟还没写对,建议先跳过做后面的题,最后再回来调试。

双机位考试还有一层空间上的约束:你所在的物理环境被两个摄像头盯着,桌面尽量清爽,不要放纸质笔记。平时刷题时就要练习在纯编辑器环境下不看自己的笔记写代码,很多人的真实问题是考场环境下鼠标键盘的位置都变了,手速和节奏全乱。这不是技术问题,但确实影响发挥。

6. 从黑白棋到方向数组系列题:机试的举一反三

6.1 方向数组是机试算法题的万能钥匙

黑白棋里用到的方向数组,几乎原封不动可以复用到其它高频题。比如岛屿数量用四方向的(1,0),(-1,0),(0,1),(0,-1)做DFS;扫雷游戏数雷就是八方向计数,和黑白棋的方向判定几乎一模一样;迷宫问题做BFS时,方向数组配合visited数组就是完整模板。

所以刷这道题的重点不是背答案,而是把"方向数组加边界检查"这个范式焊在脑子里。我每次做矩阵类题目,第一件事就是把方向数组写出来,然后写一行边界判断函数,剩下的都是业务逻辑。

6.2 机试阅卷是黑盒,输出格式比算法更值钱

OD机试的判分方式是黑盒测试,跑用例比对输出。这意味着算法再漂亮,输出格式和预期差一个空格都是零分。最后一行的换行符通常无所谓,但中间每一行结尾不能有多余空格,不能输出调试信息。这是我反复强调的点,也是很多人栽跟头的地方。

我把通用的方向数组搜索模板贴在这里,你可以直接抄走作为自己的代码片段:

def in_board(r, c, rows, cols): return 0 <= r < rows and 0 <= c < cols def scan(board, r, c): rows, cols = len(board), len(board[0]) # 八方向,四方向时只保留上下左右四个 for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (-1, 1), (1, -1), (1, 1)]: nr, nc = r + dr, c + dc if not in_board(nr, nc, rows, cols): continue # 按题目要求处理

6.3 把这道题练成条件反射

我个人建议的做法是把黑白棋手写出完整代码,然后第二天不看书再写一遍。这个过程比你盯着题解看三遍有用得多,因为你会发现在第一遍里觉得理所当然的边界处理,第二遍时仍然会错,错的地方就是你的思维盲区。

如果你能逼自己在20分钟内闭卷写完Python版和JS版各一次,再回到真题环境里,这道题对你来说就是送分题。方向数组的套路一旦熟练,后面遇到矩阵旋转、八皇后攻击范围检查、象棋棋子走法判断,你会觉得都是同一道题换了层皮。

黑白棋这道题真正想考察的不是你会不会下棋,而是你有没有把复杂规则抽象成可遍历的代码结构的能力。认真完成一次从规则到代码的转换,再遇到任何类似的模拟类题目,你心里就有底了。

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

扫描线+离散化+线段树+二分:从矩形面积并到卡常优化实战

1. 整体思路拆解&#xff1a;扫描线、离散化和线段树是怎么凑到一起的先把这个标题拆开来看。扫描线、离散化、线段树、二分、卡常&#xff0c;这五个词几乎是每一位打算法竞赛的选手都绕不开的坎。尤其是"扫描线|离散化|seg二分"这种组合&#xff0c;一看就是那种矩…

作者头像 李华
网站建设 2026/10/22 23:41:31

900个DrawCall为啥不卡?渲染耗时的真实瓶颈与优化实战

干过游戏性能优化的人应该都有过这种经历&#xff1a;拿Unity Profile一看&#xff0c;当前帧DrawCall飙到了900&#xff0c;心里咯噔一下——完了&#xff0c;这帧不得卡死&#xff1f;结果切到Game视图一看&#xff0c;帧率纹丝不动&#xff0c;帧时间也就七八毫秒。我当时第…

作者头像 李华
网站建设 2026/10/17 13:55:32

JavaWeb学生成绩系统实战:Servlet+JSP+MySQL+ECharts全链路解析

简介&#xff1a;本资源是一套基于ServletJSPMySQL开发的JavaWeb学生成绩管理系统升级版&#xff0c;面向Java初学者与Web开发入门者&#xff0c;解决传统成绩管理中数据可视化薄弱、交互性不足等实际教学与实训痛点。项目新增ECharts统计图表模块&#xff0c;支持成绩分布、班…

作者头像 李华
网站建设 2026/10/18 2:50:09

AI写作工具使用声明全拆解:四条硬条款与学术求生欲的风险边界

1. 先别急着点“同意”&#xff1a;一份使用声明到底在声明什么注册“好写作AI”这类工具的时候&#xff0c;你大概率是这样做完整个流程的&#xff1a;输手机号、收验证码、扫一眼用户协议标题&#xff0c;直接拉到最底部&#xff0c;点在“我已阅读并同意”前面那个小圈上。整…

作者头像 李华
网站建设 2026/10/18 13:57:04

DeepSeek Harness桌面端实测:安装、插件、Skill部署与代码回退全解析

DeepSeek Harness 官方桌面端终于来了。作为一个从命令行版一路用到现在的老用户&#xff0c;我第一时间就把手头的项目全部迁了过来。以前跑 Agent 任务最怕两件事&#xff1a;窗口一关任务断掉&#xff0c;十几个会话堆在同一个终端里分不清谁是谁。桌面端把这些历史遗留问题…

作者头像 李华
网站建设 2026/10/18 5:32:47

350亿参数大模型手机端侧部署实战:量化、KV缓存与分层加载

1. 为什么350亿参数塞进手机这件事值得认真聊聊“内存墙”这个词&#xff0c;做端侧部署的人听到都会条件反射地皱眉。简单说&#xff0c;它就是算力还没累&#xff0c;内存先跪了——芯片的计算单元明明能跑得更快&#xff0c;但数据在内存和计算单元之间搬来搬去的带宽和容量…

作者头像 李华