2. 候选数到底怎么算才对:核心逻辑拆解
2.1 排除法的本质:一行、一列、一宫,三个候选池取交集
任何数独格子的可选数,本质上就是“1到9这些数字里,去掉本行出现过的数字,再去掉本列出现过的数字,再去掉本宫出现过的数字,剩下没被淘汰的就是候选数”。这句话理解起来不难,但写成代码时要特别注意边界。
我把这个需求原样丢给DeepSeek,它的第一版代码长这样:
python复制def get_candidates(board, row, col):
if board[row][col] != 0:
return set()
used = set()
# 同行
for j in range(9):
if board[row][j] != 0:
used.add(board[row][j])
# 同列
for i in range(9):
if board[i][col] != 0:
used.add(board[i][col])
# 同宫
start_row = 3 * (row // 3)
start_col = 3 * (col // 3)
for i in range(start_row, start_row + 3):
for j in range(start_col, start_col + 3):
if board[i][j] != 0:
used.add(board[i][j])
return set(range(1, 10)) - used
这段代码核心思路是对的,但放在真实项目里有两个隐患。第一是边界索引问题,第二是它只计算了单个格子的候选数,没有一次性返回全部空格的结果。我稍后会讲怎么逐步逼着DeepSeek把这些隐患解决掉,这里先把候选数计算本身讲透。
2.2 宫范围的起点为什么是 3 * (row // 3)
很多初学者甚至AI生成的代码,最容易栽在“宫”的计算上。为什么是 3 * (row // 3) 而不是 row // 3 * 3 或者别的写法?
数独盘是9x9,宫是3x3。第0、1、2行属于第0个宫带,第3、4、5行属于第1个宫带,第6、7、8行属于第2个宫带。row // 3 做的就是把行号压缩到0、1、2这三个档位,再乘以3,就得到了当前宫带起始行号。列方向同理。
DeepSeek第一版代码也是这么写的,没有踩坑,但我后来测试时故意换了好几种边界输入,发现了一个隐蔽问题:它检查宫的时候,循环范围写成 range(start_row, start_row + 3) 没问题,但有的版本会写成 range(row, row + 3),这种只要格子不在宫带第一行,就会扫描到别的宫去,候选数直接错乱。
所以拿到AI生成的代码,我第一件事永远是拿几个手工可推算的盘面去验证宫的范围,而不是直接信任。
2.3 从单格函数到全盘候选数映射
单个格子的候选数算出来了,整个棋盘的可选数就是一个字典:
python复制def calc_candidates_map(board):
candidates = {}
for r in range(9):
for c in range(9):
if board[r][c] == 0:
candidates[(r, c)] = get_candidates(board, r, c)
return candidates
这里只对空格做计算,已填数字的格子直接跳过。我让DeepSeek生成这个映射函数时,特意加了条件:返回值必须是字典,key是二元组,value必须是set类型。为什么要强调类型?因为AI有时候会下意识地返回列表,而后续做排除法时,集合的删除操作(比如 candidates[(r,c)].remove(x))比列表操作安全得多。
从这段开始,项目才算真正进入了“能做事”的阶段。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
3. 把AI代码变成可信代码:测试、重构、优化
3.1 手工推演棋盘:验证AI代码的“照妖镜”
AI生成的代码“看起来能跑”和“真的算得对”是两码事。我当时的做法是手工构造了一个只挖几个洞的棋谱,每个格子的候选数都能靠人脑算出来,然后拿AI的输出和手工结果对比。
我用的测试盘面是这样的(0 代表空格):
python复制test_board = [
[5, 3, 0, 0, 7, 0, 0, 0, 0],
[6
