794. Valid Tic-Tac-Toe State
Problem Description
This problem asks you to validate whether a given Tic-Tac-Toe board configuration could be reached through a valid game sequence.
You're given a 3×3 board represented as a string array where:
'X'
represents player 1's marks'O'
represents player 2's marks' '
(space) represents empty squares
The rules of Tic-Tac-Toe that must be followed are:
- Player 1 (
'X'
) always goes first, followed by Player 2 ('O'
) taking turns - Players can only place marks in empty squares
- The game ends when either:
- A player gets three marks in a row (horizontally, vertically, or diagonally)
- All squares are filled
- No moves can be made after the game ends
Your task is to determine if the given board state is valid - meaning it could actually occur during a legitimate game following these rules.
For example, a board with 5 'X'
marks and 2 'O'
marks would be invalid since players alternate turns (so 'X'
can have at most one more mark than 'O'
). Similarly, if both players have winning positions simultaneously, that would be impossible in a real game since the game ends as soon as one player wins.
The solution needs to check several validity conditions:
- The count of
'X'
marks should equal the count of'O'
marks, or be exactly one more (since'X'
goes first) - If
'X'
has won, they must have exactly one more mark than'O'
(the game ended on X's turn) - If
'O'
has won, they must have the same count as'X'
(the game ended on O's turn) - Both players cannot have won simultaneously
Intuition
To determine if a board state is valid, we need to think about what makes a Tic-Tac-Toe game invalid. Since we're working backwards from a final board state, we need to verify that this state could have been reached through legal moves.
The key insight is that there are only a few ways a board can be invalid:
-
Turn count violation: Since
'X'
always goes first and players alternate, the number of'X'
marks should either equal the number of'O'
marks (when'O'
just played) or be exactly one more (when'X'
just played). Any other difference means someone played out of turn. -
Playing after winning: If a player has won, the game should have ended immediately. So we need to check:
- If
'X'
won, it must have been on'X'
's turn, meaningx_count = o_count + 1
- If
'O'
won, it must have been on'O'
's turn, meaningx_count = o_count
- Both players cannot win (the game would have ended when the first player won)
- If
The approach becomes clear: first count the marks for each player, then check if any player has won, and finally validate these results against the game rules.
For checking wins, we need to examine all possible winning configurations: 3 rows, 3 columns, and 2 diagonals. A player wins if they have all three positions in any of these lines.
The validation logic follows naturally:
- If the count difference is wrong (
x != o
andx - 1 != o
), it's invalid - If
'X'
won but doesn't have exactly one more mark than'O'
, it's invalid (game should have ended on X's turn) - If
'O'
won but doesn't have equal marks with'X'
, it's invalid (game should have ended on O's turn)
This gives us a clean, systematic way to validate any board configuration by checking these necessary conditions.
Solution Approach
The solution implements the validation logic through a systematic check of game rules:
1. Win Detection Helper Function
First, we create a helper function win(x)
that checks if player x
(either 'X'
or 'O'
) has won:
- Check all 3 rows:
all(board[i][j] == x for j in range(3))
for each rowi
- Check all 3 columns:
all(board[j][i] == x for j in range(3))
for each columni
- Check main diagonal:
all(board[i][i] == x for i in range(3))
- Check anti-diagonal:
all(board[i][2 - i] == x for i in range(3))
The function returns True
if any winning condition is met.
2. Count the Marks
Count the total number of 'X'
and 'O'
marks on the board:
x = sum(board[i][j] == 'X' for i in range(3) for j in range(3))
o = sum(board[i][j] == 'O' for i in range(3) for j in range(3))
This uses nested list comprehension to iterate through all 9 positions and count occurrences.
3. Validate Turn Count
Check if the difference in counts is valid:
if x != o and x - 1 != o: return False
Since 'X'
goes first, valid states are:
x == o
:'O'
just playedx == o + 1
:'X'
just played
Any other difference means someone played out of turn.
4. Validate Win Conditions
Check if wins are consistent with turn counts:
if win('X') and x - 1 != o: return False
If 'X'
won, the game ended on 'X'
's turn, so x
must be exactly o + 1
.
return not (win('O') and x != o)
If 'O'
won, the game ended on 'O'
's turn, so x
must equal o
. The expression returns False
if 'O'
won but the counts don't match.
Time and Space Complexity:
- Time:
O(1)
- We check a fixed 3×3 board with constant operations - Space:
O(1)
- Only using a few variables regardless of input
The solution elegantly handles all invalid cases through these conditional checks, ensuring the board represents a reachable game state.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through validating this board configuration:
["XOX", "O O", "XOX"]
Step 1: Count the marks
- Count X's: positions (0,0), (0,2), (2,0), (2,2) → x = 4
- Count O's: positions (0,1), (1,0), (1,2) → o = 3
- Difference check: x - o = 1 ✓ (valid since X goes first)
Step 2: Check if X has won
- Row 0: X-O-X (no)
- Row 1: O-' '-O (no)
- Row 2: X-O-X (no)
- Column 0: X-O-X (no)
- Column 1: O-' '-O (no)
- Column 2: X-O-X (no)
- Main diagonal: X-' '-X (no)
- Anti-diagonal: X-' '-X (no)
- Result: X has not won
Step 3: Check if O has won
- Using same checks: O has not won
Step 4: Apply validation rules
- Turn count valid? x = o + 1 ✓
- If X won, is x = o + 1? Not applicable (X didn't win)
- If O won, is x = o? Not applicable (O didn't win)
Result: Valid board ✓
Now let's try an invalid example:
["XXX", "OOO", " "]
Step 1: Count the marks
- X count: 3, O count: 3
Step 2: Check wins
- X has won (top row: XXX)
- O has won (middle row: OOO)
Step 3: Validation fails Both players cannot win in a valid game since the game ends immediately when one player wins. This board is invalid ✗.
Solution Implementation
1class Solution:
2 def validTicTacToe(self, board: List[str]) -> bool:
3 def check_winner(player: str) -> bool:
4 """Check if the given player has won the game."""
5 # Check horizontal wins (all cells in a row)
6 for row in range(3):
7 if all(board[row][col] == player for col in range(3)):
8 return True
9
10 # Check vertical wins (all cells in a column)
11 for col in range(3):
12 if all(board[row][col] == player for row in range(3)):
13 return True
14
15 # Check main diagonal (top-left to bottom-right)
16 if all(board[i][i] == player for i in range(3)):
17 return True
18
19 # Check anti-diagonal (top-right to bottom-left)
20 return all(board[i][2 - i] == player for i in range(3))
21
22 # Count the number of X's and O's on the board
23 x_count = sum(board[row][col] == 'X' for row in range(3) for col in range(3))
24 o_count = sum(board[row][col] == 'O' for row in range(3) for col in range(3))
25
26 # Valid game states: X plays first, so X count should equal O count or be one more
27 if x_count != o_count and x_count - 1 != o_count:
28 return False
29
30 # If X won, the game should have ended with X's turn (X count = O count + 1)
31 if check_winner('X') and x_count - 1 != o_count:
32 return False
33
34 # If O won, the game should have ended with O's turn (X count = O count)
35 return not (check_winner('O') and x_count != o_count)
36
1class Solution {
2 private String[] board;
3
4 /**
5 * Validates if a Tic-Tac-Toe board state is valid.
6 * A valid board must satisfy:
7 * 1. X plays first, so X count equals O count or X count = O count + 1
8 * 2. If X wins, X must have played last (X count = O count + 1)
9 * 3. If O wins, O must have played last (X count = O count)
10 *
11 * @param board 3x3 Tic-Tac-Toe board represented as array of strings
12 * @return true if the board state is valid, false otherwise
13 */
14 public boolean validTicTacToe(String[] board) {
15 this.board = board;
16
17 // Count the number of X's and O's on the board
18 int xCount = count('X');
19 int oCount = count('O');
20
21 // Check if the turn order is valid
22 // X goes first, so X count should equal O count or be one more than O count
23 if (xCount != oCount && xCount - 1 != oCount) {
24 return false;
25 }
26
27 // If X wins, X must have made the last move (xCount = oCount + 1)
28 if (win('X') && xCount - 1 != oCount) {
29 return false;
30 }
31
32 // If O wins, O must have made the last move (xCount = oCount)
33 return !(win('O') && xCount != oCount);
34 }
35
36 /**
37 * Checks if the specified player has won the game.
38 * Checks all possible winning conditions: rows, columns, and diagonals.
39 *
40 * @param player the character representing the player ('X' or 'O')
41 * @return true if the player has won, false otherwise
42 */
43 private boolean win(char player) {
44 // Check all rows and columns for a win
45 for (int i = 0; i < 3; i++) {
46 // Check row i for three in a row
47 if (board[i].charAt(0) == player &&
48 board[i].charAt(1) == player &&
49 board[i].charAt(2) == player) {
50 return true;
51 }
52
53 // Check column i for three in a column
54 if (board[0].charAt(i) == player &&
55 board[1].charAt(i) == player &&
56 board[2].charAt(i) == player) {
57 return true;
58 }
59 }
60
61 // Check main diagonal (top-left to bottom-right)
62 if (board[0].charAt(0) == player &&
63 board[1].charAt(1) == player &&
64 board[2].charAt(2) == player) {
65 return true;
66 }
67
68 // Check anti-diagonal (top-right to bottom-left)
69 return board[0].charAt(2) == player &&
70 board[1].charAt(1) == player &&
71 board[2].charAt(0) == player;
72 }
73
74 /**
75 * Counts the occurrences of a specific character on the board.
76 *
77 * @param target the character to count ('X' or 'O')
78 * @return the number of occurrences of the character
79 */
80 private int count(char target) {
81 int count = 0;
82
83 // Iterate through each row of the board
84 for (String row : board) {
85 // Check each character in the row
86 for (char cell : row.toCharArray()) {
87 if (cell == target) {
88 count++;
89 }
90 }
91 }
92
93 return count;
94 }
95}
96
1class Solution {
2public:
3 bool validTicTacToe(vector<string>& board) {
4 // Lambda function to count occurrences of a specific character on the board
5 auto countPieces = [&](char piece) {
6 int count = 0;
7 for (const auto& row : board) {
8 for (const auto& cell : row) {
9 count += (cell == piece);
10 }
11 }
12 return count;
13 };
14
15 // Lambda function to check if a specific player has won
16 auto hasWon = [&](char piece) {
17 // Check all rows for a win
18 for (int i = 0; i < 3; ++i) {
19 if (board[i][0] == piece && board[i][1] == piece && board[i][2] == piece) {
20 return true;
21 }
22 }
23
24 // Check all columns for a win
25 for (int i = 0; i < 3; ++i) {
26 if (board[0][i] == piece && board[1][i] == piece && board[2][i] == piece) {
27 return true;
28 }
29 }
30
31 // Check main diagonal (top-left to bottom-right)
32 if (board[0][0] == piece && board[1][1] == piece && board[2][2] == piece) {
33 return true;
34 }
35
36 // Check anti-diagonal (top-right to bottom-left)
37 return board[0][2] == piece && board[1][1] == piece && board[2][0] == piece;
38 };
39
40 // Count X's and O's on the board
41 int xCount = countPieces('X');
42 int oCount = countPieces('O');
43
44 // Rule 1: X plays first, so X count must be equal to O count or one more than O count
45 if (xCount != oCount && xCount - 1 != oCount) {
46 return false;
47 }
48
49 // Rule 2: If X has won, X must have played last (X count must be one more than O count)
50 if (hasWon('X') && xCount - 1 != oCount) {
51 return false;
52 }
53
54 // Rule 3: If O has won, O must have played last (X count must equal O count)
55 return !(hasWon('O') && xCount != oCount);
56 }
57};
58
1/**
2 * Validates if a Tic-Tac-Toe board state is valid
3 * @param board - 3x3 board represented as array of strings
4 * @returns true if the board state is valid, false otherwise
5 */
6function validTicTacToe(board: string[]): boolean {
7 /**
8 * Counts occurrences of a specific character on the board
9 * @param target - Character to count ('X' or 'O')
10 * @returns Number of occurrences
11 */
12 function countOccurrences(target: string): number {
13 let count = 0;
14 for (const row of board) {
15 for (const char of row) {
16 if (char === target) {
17 count++;
18 }
19 }
20 }
21 return count;
22 }
23
24 /**
25 * Checks if a player has won the game
26 * @param player - Player character to check ('X' or 'O')
27 * @returns true if the player has won, false otherwise
28 */
29 function hasWon(player: string): boolean {
30 // Check horizontal wins
31 for (let i = 0; i < 3; i++) {
32 if (board[i][0] === player && board[i][1] === player && board[i][2] === player) {
33 return true;
34 }
35 }
36
37 // Check vertical wins
38 for (let i = 0; i < 3; i++) {
39 if (board[0][i] === player && board[1][i] === player && board[2][i] === player) {
40 return true;
41 }
42 }
43
44 // Check main diagonal (top-left to bottom-right)
45 if (board[0][0] === player && board[1][1] === player && board[2][2] === player) {
46 return true;
47 }
48
49 // Check anti-diagonal (top-right to bottom-left)
50 return board[0][2] === player && board[1][1] === player && board[2][0] === player;
51 }
52
53 const xCount = countOccurrences('X');
54 const oCount = countOccurrences('O');
55
56 // X plays first, so X count must equal O count or be one more than O count
57 if (xCount !== oCount && xCount - 1 !== oCount) {
58 return false;
59 }
60
61 // If X won, the game should have ended with X's turn (X count = O count + 1)
62 if (hasWon('X') && xCount - 1 !== oCount) {
63 return false;
64 }
65
66 // If O won, the game should have ended with O's turn (X count = O count)
67 return !(hasWon('O') && xCount !== oCount);
68}
69
Time and Space Complexity
Time Complexity: O(1)
The time complexity is constant because:
- The board size is fixed at 3×3, so all loops iterate a constant number of times
- The
win
function checks 8 possible winning conditions (3 rows + 3 columns + 2 diagonals), each requiring at most 3 comparisons - Counting 'X' and 'O' pieces requires iterating through 9 cells exactly once
- All operations are performed on a fixed-size board, making the total number of operations constant regardless of input
Space Complexity: O(1)
The space complexity is constant because:
- Only a fixed number of integer variables (
x
ando
) are used to store counts - The
win
function uses no additional data structures - No recursive calls are made that would consume stack space
- The input board is not copied or modified, only read
- All operations use a constant amount of extra memory independent of the input size (which is already fixed)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting that Both Players Can't Win Simultaneously
One of the most common mistakes is not checking whether both players have winning positions on the board. In a valid Tic-Tac-Toe game, once a player wins, the game immediately ends - so it's impossible for both players to have three in a row.
Incorrect approach:
def validTicTacToe(self, board: List[str]) -> bool:
# ... count X's and O's ...
# Only checking individual win conditions separately
if check_winner('X') and x_count - 1 != o_count:
return False
if check_winner('O') and x_count != o_count:
return False
return True
The problem: This code would incorrectly return True
for a board where both X and O have winning positions, as long as the count conditions are met individually.
Correct solution:
def validTicTacToe(self, board: List[str]) -> bool:
# ... count X's and O's ...
x_wins = check_winner('X')
o_wins = check_winner('O')
# Both players cannot win
if x_wins and o_wins:
return False
# Check win conditions with proper counts
if x_wins and x_count - 1 != o_count:
return False
if o_wins and x_count != o_count:
return False
return True
2. Misunderstanding Turn Count Logic
Another frequent error is incorrectly validating the relationship between X and O counts. Remember that X always goes first, so the valid count differences are limited.
Incorrect approach:
# Wrong: allowing X to be ahead by more than 1 if x_count - o_count > 1: return False # Wrong: allowing O to have more marks than X if o_count > x_count: return False
The problem: The first check alone doesn't catch when O has more marks than X. The second check alone doesn't catch when X is too far ahead.
Correct solution:
# X can only be equal to O or exactly one ahead if x_count != o_count and x_count - 1 != o_count: return False
This single condition handles both cases: it ensures O never has more marks than X, and X is never more than one mark ahead of O.
3. Off-by-One Errors in Diagonal Checks
When checking diagonals, it's easy to make indexing mistakes, especially for the anti-diagonal.
Incorrect approach:
# Wrong anti-diagonal check
if all(board[i][3 - i] == player for i in range(3)): # Index out of bounds!
return True
# Or using wrong formula
if all(board[i][2 - i - 1] == player for i in range(3)): # Wrong indices!
return True
Correct solution:
# Anti-diagonal: indices should be (0,2), (1,1), (2,0)
if all(board[i][2 - i] == player for i in range(3)):
return True
The formula 2 - i
correctly generates the column indices 2, 1, 0 as row index i goes from 0, 1, 2.
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!