五子棋的胜负判定:只查刚落下的那一点就够

每次落子后全盘扫描是浪费:胜负只可能因为最新一手而改变。沿四个方向从落子点向两侧延伸计数,判定成本与棋盘大小无关。

写完 五子棋 的棋盘之后,第一个想到的判胜写法是全盘扫描:对每个格子检查四个方向有没有五连。它能用,但如果每落一子都做一次,就是把 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 位字,中间的对角线跨越字边界,代码复杂度换来的收益在这个规模上不明显。

增量判定是个通用原则:全量重算之前先问「这次变化可能影响什么」。

← 返回文章列表

评论

…