A clear explanation of validating whether a Tic-Tac-Toe board can occur in a legal game.
Problem Restatement
We are given a Tic-Tac-Toe board as a list of three strings.
Each cell contains one of:
| Character | Meaning |
|---|---|
"X" | First player’s mark |
"O" | Second player’s mark |
" " | Empty square |
We need to return True if this board position could happen during a valid Tic-Tac-Toe game.
The rules are:
Xalways moves first.- Players alternate turns.
- A move can only be placed in an empty square.
- The game ends when a player gets three marks in a row, column, or diagonal.
- No more moves can be played after the game ends.
Input and Output
| Item | Meaning |
|---|---|
| Input | board, a 3 x 3 string array |
| Output | True if the position is reachable, otherwise False |
| First player | X |
| Second player | O |
| Empty cell | Space character " " |
Function shape:
class Solution:
def validTicTacToe(self, board: list[str]) -> bool:
...Examples
Example 1:
board = ["O ", " ", " "]This is invalid because O cannot move first.
The answer is:
FalseExample 2:
board = ["XOX", " X ", " "]Count the marks:
| Mark | Count |
|---|---|
X | 3 |
O | 1 |
X has moved two more times than O, which is impossible because players alternate turns.
The answer is:
FalseExample 3:
board = ["XOX", "O O", "XOX"]This board can be reached by alternating moves.
The answer is:
TrueFirst Thought: Simulate All Games
One idea is to generate every possible Tic-Tac-Toe game from an empty board.
Then we check whether the given board appears among those states.
This is possible because the board is only 3 x 3, but it is unnecessary.
A valid board can be checked using a few rules about move counts and winners.
Key Insight
There are only two things that matter:
- The number of
XandOmarks. - Whether
XorOalready has a winning line.
Since X starts first and players alternate:
| Valid count relation | Meaning |
|---|---|
x_count == o_count | It is X’s turn next, or O just moved |
x_count == o_count + 1 | It is O’s turn next, or X just moved |
Any other count relation is invalid.
Then handle winning boards:
| Winner | Required count relation |
|---|---|
X wins | x_count == o_count + 1 |
O wins | x_count == o_count |
Both players cannot win in a valid final state, because the game stops as soon as one player wins.
Algorithm
- Count how many
XandOmarks appear. - Check if
Xhas a winning line. - Check if
Ohas a winning line. - Validate the count relation:
- If
o_count > x_count, returnFalse. - If
x_count > o_count + 1, returnFalse.
- If
- Validate winner rules:
- If
Xwins andx_count != o_count + 1, returnFalse. - If
Owins andx_count != o_count, returnFalse. - If both win, return
False.
- If
- Return
True.
Correctness
Since X always moves first and players alternate turns, at any reachable state X must have either the same number of moves as O, or exactly one more move. The algorithm rejects every board that violates this condition.
A player wins only immediately after making a move. Therefore, if X wins, the last move must have been made by X, so X must have exactly one more mark than O. If O wins, the last move must have been made by O, so the counts must be equal.
The algorithm checks every row, column, and diagonal for winning lines. If a winning line exists with an impossible move count, the board is rejected.
If both players are winners, the board is rejected. In a valid game, play stops when the first winner appears, so the other player cannot later create a winning line.
If none of these invalid conditions hold, then the mark counts and winner status are consistent with a legal sequence of moves. Therefore, the board is valid.
Complexity
The board size is fixed at 3 x 3.
| Metric | Value | Why |
|---|---|---|
| Time | O(1) | We inspect a constant-size board |
| Space | O(1) | Only counters and booleans are stored |
Implementation
class Solution:
def validTicTacToe(self, board: list[str]) -> bool:
x_count = 0
o_count = 0
for row in board:
x_count += row.count("X")
o_count += row.count("O")
def wins(player: str) -> bool:
for i in range(3):
if all(board[i][j] == player for j in range(3)):
return True
if all(board[j][i] == player for j in range(3)):
return True
if all(board[i][i] == player for i in range(3)):
return True
if all(board[i][2 - i] == player for i in range(3)):
return True
return False
x_wins = wins("X")
o_wins = wins("O")
if o_count > x_count:
return False
if x_count > o_count + 1:
return False
if x_wins and o_wins:
return False
if x_wins and x_count != o_count + 1:
return False
if o_wins and x_count != o_count:
return False
return TrueCode Explanation
First, count both marks:
x_count += row.count("X")
o_count += row.count("O")The helper checks whether a player has any winning line:
def wins(player: str) -> bool:Rows and columns are checked together:
for i in range(3):
if all(board[i][j] == player for j in range(3)):
return True
if all(board[j][i] == player for j in range(3)):
return TrueThen the two diagonals are checked:
if all(board[i][i] == player for i in range(3)):
return True
if all(board[i][2 - i] == player for i in range(3)):
return TrueThe move count must be consistent with alternating turns:
if o_count > x_count:
return False
if x_count > o_count + 1:
return FalseIf both players win, the board cannot be legal:
if x_wins and o_wins:
return FalseA winning board must match the player who just moved:
if x_wins and x_count != o_count + 1:
return False
if o_wins and x_count != o_count:
return FalseTesting
def run_tests():
s = Solution()
assert s.validTicTacToe(["O ", " ", " "]) is False
assert s.validTicTacToe(["XOX", " X ", " "]) is False
assert s.validTicTacToe(["XOX", "O O", "XOX"]) is True
assert s.validTicTacToe([" ", " ", " "]) is True
assert s.validTicTacToe(["X ", " ", " "]) is True
assert s.validTicTacToe(["XX ", "OO ", " "]) is True
assert s.validTicTacToe(["XXX", "OO ", " "]) is True
assert s.validTicTacToe(["XXX", "O O", "OO "]) is False
assert s.validTicTacToe(["OOO", "XX ", "XX "]) is True
assert s.validTicTacToe(["OOO", "X ", "XX "]) is False
print("all tests passed")
run_tests()Test coverage:
| Test | Why |
|---|---|
O moves first | Rejects invalid turn order |
Too many X marks | Rejects invalid count relation |
| Valid full board | Accepts reachable draw-like state |
| Empty board | Valid initial state |
Single X | Valid first move |
X wins with correct count | Accepts valid win |
X wins with extra moves | Rejects moves after game end |
O wins with correct count | Accepts valid O win |
O wins with wrong count | Rejects impossible winner count |