摘要
如果让你实现一个两人对战的井字棋(Tic-Tac-Toe)游戏逻辑,你会怎么做?是不是会直接用二维数组存每个格子的状态,然后在每次落子后从头到尾检查是否赢了?
这种方式虽然直观,但效率并不高,特别是当棋盘扩大到 n×n 的时候。LeetCode 348 这个题目就挑战我们去优化这个过程:每次落子之后都要快速判断胜负,尽可能降低计算开销。
今天我们用 Swift 来实现这个游戏逻辑,看看怎么用一套简洁又高效的数据结构,把这个小游戏做得更聪明些。
描述
这个问题让我们自己“设计”一个井字棋游戏类。也就是说,不是解一个单独的逻辑判断题,而是写出能不断调用和处理的游戏控制结构。
题目需求:
设计一个井字棋游戏类 TicTacToe
:
init(_ n: Int)
- 初始化一个 n x n 的棋盘。
func move(_ row: Int, _ col: Int, _ player: Int) -> Int
-
玩家
player
在(row, col)
落子。 -
玩家编号为 1 或 2。
-
返回值为:
- 0:没有胜者;
- 1:玩家 1 获胜;
- 2:玩家 2 获胜。
示例:
TicTacToe toe = TicTacToe(3)
toe.move(0, 0, 1) // 返回 0(无胜者)
toe.move(0, 2, 2) // 返回 0
toe.move(2, 2, 1) // 返回 0
toe.move(1, 1, 2) // 返回 0
toe.move(2, 0, 1) // 返回 0
toe.move(1, 0, 2) // 返回 0
toe.move(2, 1, 1) // 返回 1(玩家 1 胜出)
题解答案
这道题如果用传统方式去解决,比如在 move()
每次落子后遍历整行、整列、对角线判断是否有玩家胜出,那每次 move 的时间复杂度就是 O(n)。
但我们可以更优化地去思考:我们其实不需要知道整个棋盘状态,只需要知道某一行、某一列或对角线的总和是否等于 n 就可以判断胜负。
于是我们换个角度:
- 为每行、每列维护一个计数数组;
- 为两个对角线也维护两个值;
- 玩家 1 每次落子时计为
+1
; - 玩家 2 落子时计为
-1
; - 当某行、列或对角线的总和变为
+n
或-n
时,就表示某一方胜出。
这样做的核心优势在于:每次落子都是 O(1) 时间复杂度。
题解代码分析
Swift 解法代码:
class TicTacToe {
private var rows: [Int]
private var cols: [Int]
private var diagonal: Int
private var antiDiagonal: Int
private let n: Int
init(_ n: Int) {
self.n = n
self.rows = Array(repeating: 0, count: n)
self.cols = Array(repeating: 0, count: n)
self.diagonal = 0
self.antiDiagonal = 0
}
func move(_ row: Int, _ col: Int, _ player: Int) -> Int {
// 玩家 1 记作 +1,玩家 2 记作 -1
let toAdd = (player == 1) ? 1 : -1
// 更新对应行列
rows[row] += toAdd
cols[col] += toAdd
// 判断对角线
if row == col {
diagonal += toAdd
}
// 判断反对角线
if row + col == n - 1 {
antiDiagonal += toAdd
}
// 胜负判断
if abs(rows[row]) == n || abs(cols[col]) == n || abs(diagonal) == n || abs(antiDiagonal) == n {
return player
}
// 没人赢
return 0
}
}
代码详细解释:
1. 初始化:
self.rows = Array(repeating: 0, count: n)
- 用一个数组来记录每一行的累积值,初始为 0。
2. 落子记录:
let toAdd = (player == 1) ? 1 : -1
- 用不同正负值来区别玩家,从而避免需要分别用两个数组。
3. 判断胜负:
if abs(rows[row]) == n
- 一行里如果都是 +1(或都是 -1),其绝对值就是 n,说明这个玩家在该行获胜。
这种判断方式不仅逻辑清晰,而且效率极高。
示例测试及结果
我们来跑一组测试用例:
let toe = TicTacToe(3)
print(toe.move(0, 0, 1)) // 输出 0
print(toe.move(0, 2, 2)) // 输出 0
print(toe.move(2, 2, 1)) // 输出 0
print(toe.move(1, 1, 2)) // 输出 0
print(toe.move(2, 0, 1)) // 输出 0
print(toe.move(1, 0, 2)) // 输出 0
print(toe.move(2, 1, 1)) // 输出 1,玩家 1 获胜!
输出结果:
0
0
0
0
0
0
1
可以看到,落子之后能快速返回胜负结果,且没有任何冗余遍历。
时间复杂度
每次调用 move()
:
- 更新对应行列、对角线,只涉及常数次操作。
- 所以时间复杂度是 O(1) —— 这是这道题的优化关键。
相比之下,传统方法每次都要 O(n) 遍历行列,性能差很多。
空间复杂度
- 需要用两个数组记录每一行和列:O(n)
- 还需两个整数变量记录对角线:O(1)
- 所以空间复杂度是 O(n)
非常节省空间,且没有使用二维数组模拟棋盘(那样是 O(n²))。
总结
LeetCode 348 虽然看起来是一个游戏类设计题,但它背后真正考验的是:
- 你能不能通过数据结构建模问题状态;
- 能否在落子后常数时间内判断胜负;
- 在 n 比较大时是否还能稳定运行。
通过维护“计数数组”和两个对角线值,我们把传统 O(n) 的遍历过程优化成了 O(1) 的更新和判断。这个思路在很多游戏逻辑、模拟类题目中都非常有价值。
如果你将来要实现一个支持在线对战的 Tic-Tac-Toe 游戏,这套高效结构完全可以用在后台逻辑中,极大提升响应速度。