题目描述

✅ 面试题 16.04. 井字游戏

image-20260929011356065

image-20260929011356066

题意分析

判断当前 $n \times n$ 棋盘是 X 获胜、O 获胜、仍可继续的 Pending,还是无人获胜且已经填满的 Draw。获胜要求某一整行、整列、主对角线或副对角线全是同一个非空字符。

题目保证输入符合游戏规则,因此不需要另行判断双方落子数或胜者是否合法。先检查有没有获胜者;只有确定无人获胜后,空格是否存在才用于区分 Pending 和 Draw。

将 X 编码为 1、O 编码为 -1,就能把整条线是否属于同一玩家转成整数和判断,在一次扫描中同时维护全部候选直线。

解法:行列与对角线计数

核心思路

[!blue]

rows[i]、cols[j] 分别保存对应行列中已经扫描部分的编码和,dg 保存主对角线的和,udg 保存副对角线的和。遇到非空格时把当前编码加入所在行列;只有 i == j 才属于主对角线,只有 i + j == n - 1 才属于副对角线。

一条线有 n 个位置,每个位置最多贡献 1,最少贡献 -1。只有全部 n 个位置都为 X 时总和才可能达到 n,全部为 O 时才可能达到 -n;存在空格或混入另一方,绝对值都会小于 n。因此判断绝对值等于 n 就能同时识别两种胜者。

这个判断在扫描过程中也安全:只看了一部分格子时,计数绝对值不可能超过已经处理的位置数,更不可能提前达到 n。首次达到 n 的那条线一定刚被当前非空字符补全,所以直接返回当前字符即可,无需重新遍历这条线。

空格不参与任何编码和,只把 hasEmptyGrid 置为真。扫描结束仍未发现获胜直线,才根据它返回 Pending 或 Draw。如果中途已经获胜,是否还存在空格都不再影响结果。

解题步骤

  • 创建长度为 $n$ 的 rows、cols,并将两个对角线计数初始化为 $0$。
  • 扫描 (i, j):遇到空格只记录 hasEmptyGrid = true;否则把当前字符的 $1$ 或 $-1$ 累加到对应行列。
  • 当 i == j 时更新主对角线;当 i + j == n - 1 时更新副对角线。
  • 任意相关计数的绝对值达到 $n$ 时返回当前字符。扫描结束后,有空格返回 Pending,否则返回 Draw。

中心格可能同时属于两条对角线,应分别更新两份计数,而不是使用互斥分支。n == 1 时,一个非空格就构成完整直线;全空棋盘则没有胜者,最后返回 Pending。

代码实现

// X 记为 1、O 记为 -1;某条线的绝对值达到 n 即出现胜者。
class Solution {
    public String tictactoe(String[] board) {
        int n = board.length;
        int[] rows = new int[n];
        int[] cols = new int[n];
        int dg = 0;
        int udg = 0;
        boolean hasEmptyGrid = false;

        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                char c = board[i].charAt(j);

                if (c == ' ') {
                    hasEmptyGrid = true;
                    continue;
                }

                int v = c == 'X' ? 1 : -1;

                rows[i] += v;
                cols[j] += v;

                if (i == j) {
                    dg += v;
                }

                if (i + j + 1 == n) {
                    udg += v;
                }

                if (Math.abs(rows[i]) == n
                        || Math.abs(cols[j]) == n
                        || Math.abs(dg) == n
                        || Math.abs(udg) == n) {
                    return String.valueOf(c);
                }
            }
        }

        return hasEmptyGrid ? "Pending" : "Draw";
    }
}
// X 记为 1、O 记为 -1;某条线的绝对值达到 n 即出现胜者。
func tictactoe(board []string) string {
    n := len(board)
    rows := make([]int, n)
    cols := make([]int, n)
    dg, udg := 0, 0
    hasEmptyGrid := false
    for i, row := range board {
        for j, c := range row {
            if c == ' ' {
                hasEmptyGrid = true
                continue
            }
            v := 1
            if c == 'O' {
                v = -1
            }
            rows[i] += v
            cols[j] += v
            if i == j {
                dg += v
            }
            if i+j == n-1 {
                udg += v
            }
            if abs(rows[i]) == n || abs(cols[j]) == n || abs(dg) == n || abs(udg) == n {
                return string(c)
            }
        }
    }
    if hasEmptyGrid {
        return "Pending"
    }
    return "Draw"
}

func abs(x int) int {
    if x < 0 {
        return -x
    }
    return x
}

复杂度分析

  • 时间复杂度:$O(n^2)$,每个棋盘格只处理一次。
  • 空间复杂度:$O(n)$,行列计数数组各占 $O(n)$;其余变量为常数空间。

关键点总结

[!green]

  • 编码和的关键不变量是:长度为 $n$ 的直线和为 $\pm n$,当且仅当整条线属于同一玩家。
  • 空格不能参与计数,但要单独记录,用于最终区分 Pending 和 Draw。
  • 面试中要主动说明返回优先级:胜者高于棋盘是否仍有空位。
  • 如果追问“每次落子后都要判断”,可以只更新该落子所在的行、列和对角线,从而把单次操作降为 $O(1)$。

易错点总结

[!yellow]

  • 先看到空格就返回 Pending:后面仍可能发现已经完成的获胜直线,胜负判断优先。
  • 副对角线条件写错:下标从 $0$ 开始时应满足 i + j == n - 1,不是 i + j == n。
  • 只判断计数等于 $n$:这样只能识别 X;应判断绝对值,才能同时识别和为 $-n$ 的 O。
  • 把空格按 O 处理:会伪造负计数,甚至错误判断 O 获胜。

相似题目

题目 难度 关联与区别
348. 设计井字棋 中等 同样使用行列与对角线计数,原题按每次落子增量更新,本题一次扫描整盘。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/63338387
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!