五子棋的胜负判定:只查刚落下的那一点就够
每次落子后全盘扫描是浪费:胜负只可能因为最新一手而改变。沿四个方向从落子点向两侧延伸计数,判定成本与棋盘大小无关。
写完 五子棋 的棋盘之后,第一个想到的判胜写法是全盘扫描:对每个格子检查四个方向有没有五连。它能用,但如果每落一子都做一次,就是把 O(棋盘) 的成本放进了最热的路径。
胜负只可能因为最后一手改变
在一手落子之前,棋盘上不存在五连(否则游戏已经结束了)。所以新出现的五连一定包含刚落下的那颗子。只需要检查它所在的四条线:
const DIRECTIONS = [[0, 1], [1, 0], [1, 1], [1, -1]];
function checkWin(board, row, col, size) {
const who = at(board, row, col, size);
if (!who) return null;
for (const [dr, dc] of DIRECTIONS) {
const cells = [index(row, col, size)];
for (const sign of [1, -1]) {
let r = row + dr * sign;
let c = col + dc * sign;
while (inBounds(r, c, size) && at(board, r, c, size) === who) {
cells.push(index(r, c, size));
r += dr * sign;
c += dc * sign;
}
}
if (cells.length >= 5) return { who, cells };
}
return null;
}
cells 顺带把连子的坐标收集起来,界面高亮胜利的五颗子时不用再找一遍。
成本与棋盘大小无关
四个方向,每个方向最多向两侧各走 4 步多一点就够判断(找到第五颗就可以停),所以是常数级。15×15 还是 19×19,判定的循环次数几乎一样。
| 写法 | 每次落子成本 |
|---|---|
| 全盘扫描 | O(size²),15×15 是 225 次起步 |
| 只查落子点 | 四个方向各 O(1),与棋盘大小无关 |
边界与规则的两个决定
- 越界当成「不是同色」:不要单独写判断,把
inBounds和「颜色相同」写在一个条件里,边界就自然终止了延伸。 - 长连算不算胜:不同规则不同(有的禁手规则里六连反而算输)。本站按「五颗及以上」判胜,写在
cells.length >= 5里 —— 一句话就是规则,改规则也只改这一处。
位棋盘版本
如果把 15×15 的棋盘塞进 64 位整数(每行用若干位表示),判胜可以变成移位与按位与:把棋盘朝某个方向平移一格再与原盘相与,重复四次,还剩下的位就是四连。这是 popcount 那篇 里提到的同类思路。本站没有这么做:15×15 有 225 格,位棋盘要拆成四个 64 位字,中间的对角线跨越字边界,代码复杂度换来的收益在这个规模上不明显。
增量判定是个通用原则:全量重算之前先问「这次变化可能影响什么」。

评论
…