Facebook Pixel

794. Valid Tic-Tac-Toe State

MediumArrayMatrix
Leetcode Link

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:

  1. Player 1 ('X') always goes first, followed by Player 2 ('O') taking turns
  2. Players can only place marks in empty squares
  3. The game ends when either:
    • A player gets three marks in a row (horizontally, vertically, or diagonally)
    • All squares are filled
  4. 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
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. 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.

  2. 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, meaning x_count = o_count + 1
    • If 'O' won, it must have been on 'O''s turn, meaning x_count = o_count
    • Both players cannot win (the game would have ended when the first player won)

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 and x - 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 row i
  • Check all 3 columns: all(board[j][i] == x for j in range(3)) for each column i
  • 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 played
  • x == 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 Evaluator

Example 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 and o) 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More