Facebook Pixel

348. Design Tic-Tac-Toe 🔒

MediumDesignArrayHash TableMatrixSimulation
Leetcode Link

Problem Description

This problem asks you to implement a Tic-Tac-Toe game for an n x n board that can determine the winner after each move.

You need to create a TicTacToe class with two methods:

  • TicTacToe(int n): Initializes a game board of size n x n
  • int move(int row, int col, int player): Places a mark for the given player (1 or 2) at position (row, col) and returns:
    • 0 if there's no winner yet
    • 1 if player 1 wins
    • 2 if player 2 wins

A player wins when they successfully place n of their marks in:

  • Any complete row
  • Any complete column
  • The main diagonal (from top-left to bottom-right)
  • The anti-diagonal (from top-right to bottom-left)

The solution uses a counting approach where it tracks how many marks each player has placed in each row, column, and diagonal. The key insight is to use a dictionary to count marks:

  • Rows are indexed as 0 to n-1
  • Columns are indexed as n to 2n-1
  • Main diagonal is indexed as n << 1 (which is 2n)
  • Anti-diagonal is indexed as n << 1 | 1 (which is 2n + 1)

For each move, the solution increments the appropriate counters for that player's row, column, and potentially diagonals (if the move is on a diagonal). If any counter reaches n, that player has won.

The main diagonal contains cells where row == col, and the anti-diagonal contains cells where row + col == n - 1. This clever indexing scheme allows checking for a win in O(1) time after each move.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that we don't need to store the entire board state to determine if someone has won. Instead, we only need to track how many marks each player has placed in each possible winning line.

Think about what constitutes a win in Tic-Tac-Toe: a player needs to fill an entire row, column, or diagonal with their marks. So rather than checking the entire board after each move, we can maintain counters for each possible winning line.

For an n x n board, there are exactly 2n + 2 possible winning lines:

  • n rows
  • n columns
  • 1 main diagonal
  • 1 anti-diagonal

The challenge is how to efficiently track and update these counters. We can assign a unique index to each winning line:

  • Rows get indices 0 to n-1
  • Columns get indices n to 2n-1
  • The main diagonal gets index 2n (written as n << 1 using bit shift)
  • The anti-diagonal gets index 2n + 1 (written as n << 1 | 1)

When a player makes a move at position (row, col), we know exactly which lines are affected:

  • The row at index row
  • The column at index n + col
  • If row == col, the move is on the main diagonal
  • If row + col == n - 1, the move is on the anti-diagonal

By incrementing the appropriate counters for the current player and checking if any counter has reached n, we can determine in constant time whether this move results in a win. This avoids the need to scan through rows, columns, or diagonals after each move, making the solution very efficient.

Solution Approach

The implementation uses a counting strategy with dictionaries to track the number of marks each player has placed in each winning line.

Data Structure:

  • self.cnt: A list containing two dictionaries, one for each player. Each dictionary maps a line index to the count of marks that player has in that line.
  • self.n: Stores the board size for easy reference.

Initialization (__init__):

self.cnt = [defaultdict(int), defaultdict(int)]

We create two defaultdict(int) objects, one for player 1 (at index 0) and one for player 2 (at index 1). Using defaultdict allows us to automatically initialize counts to 0 when accessing a new key.

Move Implementation (move):

  1. First, we get the current player's counter dictionary:

    cur = self.cnt[player - 1]

    Since players are numbered 1 and 2, but list indices are 0-based, we use player - 1.

  2. Update the row counter:

    cur[row] += 1

    The row index directly corresponds to the row number.

  3. Update the column counter:

    cur[n + col] += 1

    Columns are offset by n to avoid collision with row indices.

  4. Check and update diagonal counters:

    • Main diagonal (top-left to bottom-right):

      if row == col:
          cur[n << 1] += 1

      The main diagonal uses index 2n (written as n << 1 using left bit shift).

    • Anti-diagonal (top-right to bottom-left):

      if row + col == n - 1:
          cur[n << 1 | 1] += 1

      The anti-diagonal uses index 2n + 1 (written as n << 1 | 1 using bitwise OR).

  5. Check for a win:

    if any(cur[i] == n for i in (row, n + col, n << 1, n << 1 | 1)):
        return player

    We check if any of the affected lines (current row, current column, and possibly diagonals) now have n marks. If so, the current player wins.

  6. If no win is detected, return 0.

Time Complexity: O(1) per move - we only update and check a constant number of counters.

Space Complexity: O(n) - we store at most 2n + 2 counters for each player.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a 3x3 Tic-Tac-Toe game to illustrate how the solution works.

Initial Setup:

  • Board size n = 3
  • Player 1's counter: cnt[0] = {}
  • Player 2's counter: cnt[1] = {}
  • Index mapping:
    • Rows: 0, 1, 2
    • Columns: 3, 4, 5
    • Main diagonal: 6 (n << 1)
    • Anti-diagonal: 7 (n << 1 | 1)

Move 1: Player 1 places at (0, 0)

X . .
. . .
. . .
  • Update row 0: cnt[0][0] = 1
  • Update column 0: cnt[0][3] = 1 (3 = n + 0)
  • Cell (0,0) is on main diagonal (row == col), so: cnt[0][6] = 1
  • Check counters: all are 1, no winner yet
  • Return: 0

Move 2: Player 2 places at (1, 1)

X . .
. O .
. . .
  • Update row 1: cnt[1][1] = 1
  • Update column 1: cnt[1][4] = 1 (4 = n + 1)
  • Cell (1,1) is on main diagonal (row == col), so: cnt[1][6] = 1
  • Check counters: all are 1, no winner yet
  • Return: 0

Move 3: Player 1 places at (0, 1)

X X .
. O .
. . .
  • Update row 0: cnt[0][0] = 2 (was 1, now 2)
  • Update column 1: cnt[0][4] = 1
  • Cell (0,1) is not on any diagonal
  • Check counters: row 0 has 2 marks, still need 3 to win
  • Return: 0

Move 4: Player 2 places at (2, 0)

X X .
. O .
O . .
  • Update row 2: cnt[1][2] = 1
  • Update column 0: cnt[1][3] = 1
  • Cell (2,0) is on anti-diagonal (2 + 0 = 2 = n - 1), so: cnt[1][7] = 1
  • Check counters: all are 1, no winner yet
  • Return: 0

Move 5: Player 1 places at (0, 2)

X X X
. O .
O . .
  • Update row 0: cnt[0][0] = 3 (was 2, now 3)
  • Update column 2: cnt[0][5] = 1
  • Cell (0,2) is on anti-diagonal (0 + 2 = 2 = n - 1), so: cnt[0][7] = 1
  • Check counters: row 0 has 3 marks = n, Player 1 wins!
  • Return: 1

Final state of counters:

  • Player 1: {0: 3, 3: 1, 6: 1, 4: 1, 5: 1, 7: 1}
    • Row 0 has 3 marks (winning line)
    • Column 0, main diagonal, column 1, column 2, anti-diagonal each have 1 mark
  • Player 2: {1: 1, 4: 1, 6: 1, 2: 1, 3: 1, 7: 1}
    • All counters have only 1 mark

The key insight: instead of scanning the entire board, we only needed to check the counters for the lines affected by the current move. When Player 1's row 0 counter reached 3, we immediately knew they won.

Solution Implementation

1from collections import defaultdict
2from typing import Dict
3
4class TicTacToe:
5  
6    def __init__(self, n: int):
7        """
8        Initialize the TicTacToe board.
9      
10        Args:
11            n: Size of the board (n x n)
12        """
13        self.board_size = n
14        # Two dictionaries to track counts for each player (player 1 and player 2)
15        # Each dictionary maps position identifiers to count of marks
16        self.player_counts = [defaultdict(int), defaultdict(int)]
17  
18    def move(self, row: int, col: int, player: int) -> int:
19        """
20        Make a move on the board and check if the player wins.
21      
22        Args:
23            row: Row index (0-indexed)
24            col: Column index (0-indexed)
25            player: Player number (1 or 2)
26      
27        Returns:
28            The player number if they win, otherwise 0
29        """
30        # Get the count dictionary for the current player (player-1 for 0-indexing)
31        current_player_counts = self.player_counts[player - 1]
32        n = self.board_size
33      
34        # Increment count for the row where the move was made
35        # Row identifiers: 0 to n-1
36        current_player_counts[row] += 1
37      
38        # Increment count for the column where the move was made
39        # Column identifiers: n to 2n-1 (offset by n to distinguish from rows)
40        current_player_counts[n + col] += 1
41      
42        # Check if the move is on the main diagonal (top-left to bottom-right)
43        if row == col:
44            # Main diagonal identifier: 2n (n << 1)
45            current_player_counts[n << 1] += 1
46      
47        # Check if the move is on the anti-diagonal (top-right to bottom-left)
48        if row + col == n - 1:
49            # Anti-diagonal identifier: 2n + 1 (n << 1 | 1)
50            current_player_counts[n << 1 | 1] += 1
51      
52        # Check if any line (row, column, or diagonal) has n marks from this player
53        # If so, the player has won
54        lines_to_check = [
55            row,           # Current row
56            n + col,       # Current column
57            n << 1,        # Main diagonal
58            n << 1 | 1     # Anti-diagonal
59        ]
60      
61        if any(current_player_counts[line_id] == n for line_id in lines_to_check):
62            return player
63      
64        return 0
65
66
67# Your TicTacToe object will be instantiated and called as such:
68# obj = TicTacToe(n)
69# param_1 = obj.move(row,col,player)
70
1class TicTacToe {
2    private int boardSize;
3    private int[][] playerCounts;  // Track counts for each player's marks in rows, columns, and diagonals
4
5    /**
6     * Initialize the TicTacToe board with size n x n
7     * @param n The size of the board
8     */
9    public TicTacToe(int n) {
10        this.boardSize = n;
11        // For each player (2 players total), track counts for:
12        // - n rows (indices 0 to n-1)
13        // - n columns (indices n to 2n-1)
14        // - main diagonal (index 2n)
15        // - anti-diagonal (index 2n+1)
16        playerCounts = new int[2][(n * 2) + 2];
17    }
18
19    /**
20     * Make a move on the board
21     * @param row The row index (0-indexed)
22     * @param col The column index (0-indexed)
23     * @param player The player making the move (1 or 2)
24     * @return The winner (1 or 2) if there is one, otherwise 0
25     */
26    public int move(int row, int col, int player) {
27        // Get the count array for the current player (player-1 for 0-indexed array)
28        int[] currentPlayerCounts = playerCounts[player - 1];
29      
30        // Increment count for the current row
31        currentPlayerCounts[row]++;
32      
33        // Increment count for the current column (offset by boardSize to avoid row indices)
34        currentPlayerCounts[boardSize + col]++;
35      
36        // If the move is on the main diagonal (top-left to bottom-right)
37        if (row == col) {
38            currentPlayerCounts[boardSize * 2]++;
39        }
40      
41        // If the move is on the anti-diagonal (top-right to bottom-left)
42        if (row + col == boardSize - 1) {
43            currentPlayerCounts[boardSize * 2 + 1]++;
44        }
45      
46        // Check if the current player has won by filling any row, column, or diagonal
47        if (currentPlayerCounts[row] == boardSize ||                    // Won by filling the row
48            currentPlayerCounts[boardSize + col] == boardSize ||         // Won by filling the column
49            currentPlayerCounts[boardSize * 2] == boardSize ||           // Won by filling main diagonal
50            currentPlayerCounts[boardSize * 2 + 1] == boardSize) {       // Won by filling anti-diagonal
51            return player;
52        }
53      
54        // No winner yet
55        return 0;
56    }
57}
58
59/**
60 * Your TicTacToe object will be instantiated and called as such:
61 * TicTacToe obj = new TicTacToe(n);
62 * int param_1 = obj.move(row,col,player);
63 */
64
1class TicTacToe {
2private:
3    int boardSize;
4    // Store count of marks for each player
5    // Each player has counters for: rows[0..n-1], cols[n..2n-1], main diagonal[2n], anti-diagonal[2n+1]
6    vector<vector<int>> playerCounts;
7
8public:
9    TicTacToe(int n) : boardSize(n), playerCounts(2, vector<int>(2 * n + 2, 0)) {
10        // Initialize board size and player count arrays
11        // playerCounts[0] for player 1, playerCounts[1] for player 2
12        // Each player needs n rows + n cols + 1 main diagonal + 1 anti-diagonal = 2n + 2 counters
13    }
14
15    int move(int row, int col, int player) {
16        // Get reference to current player's count array (player-1 because player is 1 or 2, but index is 0 or 1)
17        vector<int>& currentPlayerCounts = playerCounts[player - 1];
18      
19        // Increment count for the current row
20        ++currentPlayerCounts[row];
21      
22        // Increment count for the current column (offset by boardSize to avoid overlap with row indices)
23        ++currentPlayerCounts[boardSize + col];
24      
25        // Check if move is on main diagonal (top-left to bottom-right)
26        if (row == col) {
27            ++currentPlayerCounts[boardSize * 2];
28        }
29      
30        // Check if move is on anti-diagonal (top-right to bottom-left)
31        if (row + col == boardSize - 1) {
32            ++currentPlayerCounts[boardSize * 2 + 1];
33        }
34      
35        // Check if any line (row, column, or diagonal) has been completely filled by current player
36        if (currentPlayerCounts[row] == boardSize ||                    // Row win
37            currentPlayerCounts[boardSize + col] == boardSize ||         // Column win
38            currentPlayerCounts[boardSize * 2] == boardSize ||           // Main diagonal win
39            currentPlayerCounts[boardSize * 2 + 1] == boardSize) {       // Anti-diagonal win
40            return player;  // Current player wins
41        }
42      
43        return 0;  // No winner yet
44    }
45};
46
47/**
48 * Your TicTacToe object will be instantiated and called as such:
49 * TicTacToe* obj = new TicTacToe(n);
50 * int param_1 = obj->move(row,col,player);
51 */
52
1// Global variables for TicTacToe game state
2let boardSize: number;
3// Track count of marks for each player
4// First dimension: player index (0 for player 1, 1 for player 2)
5// Second dimension: rows (0 to n-1), columns (n to 2n-1), main diagonal (2n), anti-diagonal (2n+1)
6let playerMarkCounts: number[][];
7
8/**
9 * Initialize the TicTacToe game with given board size
10 * @param n - The size of the board (n x n)
11 */
12function initializeTicTacToe(n: number): void {
13    boardSize = n;
14    // Create count arrays for both players
15    // Each player needs to track n rows + n columns + 2 diagonals
16    const totalCounters = (n << 1) + 2; // 2n + 2 counters
17    playerMarkCounts = [
18        Array(totalCounters).fill(0), // Player 1 counters
19        Array(totalCounters).fill(0)  // Player 2 counters
20    ];
21}
22
23/**
24 * Make a move on the TicTacToe board
25 * @param row - The row index of the move (0-indexed)
26 * @param col - The column index of the move (0-indexed)
27 * @param player - The player making the move (1 or 2)
28 * @returns The winner (1 or 2) if there is one, otherwise 0
29 */
30function move(row: number, col: number, player: number): number {
31    // Get the current player's count array (player-1 for 0-based indexing)
32    const currentPlayerCounts = playerMarkCounts[player - 1];
33  
34    // Increment row counter for this player
35    currentPlayerCounts[row]++;
36  
37    // Increment column counter for this player (offset by boardSize to avoid row indices)
38    currentPlayerCounts[boardSize + col]++;
39  
40    // Check if move is on main diagonal (top-left to bottom-right)
41    if (row === col) {
42        currentPlayerCounts[boardSize << 1]++; // Index: 2n
43    }
44  
45    // Check if move is on anti-diagonal (top-right to bottom-left)
46    if (row + col === boardSize - 1) {
47        currentPlayerCounts[(boardSize << 1) | 1]++; // Index: 2n + 1
48    }
49  
50    // Check if current player has won
51    // Win condition: any row, column, or diagonal has n marks
52    if (
53        currentPlayerCounts[row] === boardSize ||                    // Row win
54        currentPlayerCounts[boardSize + col] === boardSize ||        // Column win
55        currentPlayerCounts[boardSize << 1] === boardSize ||         // Main diagonal win
56        currentPlayerCounts[(boardSize << 1) | 1] === boardSize      // Anti-diagonal win
57    ) {
58        return player; // Current player wins
59    }
60  
61    return 0; // No winner yet
62}
63

Time and Space Complexity

Time Complexity: O(1) per move operation. Each move involves:

  • Incrementing counters for the row (cur[row] += 1)
  • Incrementing counters for the column (cur[n + col] += 1)
  • Conditionally incrementing the diagonal counter (cur[n << 1] += 1)
  • Conditionally incrementing the anti-diagonal counter (cur[n << 1 | 1] += 1)
  • Checking at most 4 values to determine if there's a winner

All these operations take constant time regardless of the board size n.

Space Complexity: O(n) where n is the side length of the board. The space is used by:

  • Two dictionaries (one for each player) in self.cnt
  • Each dictionary can store at most 2n + 2 entries:
    • n entries for rows (keys 0 to n-1)
    • n entries for columns (keys n to 2n-1)
    • 1 entry for the main diagonal (key n << 1)
    • 1 entry for the anti-diagonal (key n << 1 | 1)

Since the number of possible keys is linear with respect to n, the overall space complexity is O(n).

Common Pitfalls

1. Incorrect Diagonal Check Logic

A frequent mistake is checking if a position is on a diagonal after already incrementing the counter, or using the wrong conditions for diagonal membership.

Pitfall Example:

# Wrong: Checking diagonals for every move
current_player_counts[n << 1] += 1  # Always incrementing
if row == col:  # Checking condition after increment
    # ...

Solution: Always check the diagonal condition BEFORE incrementing the counter:

if row == col:  # Check first
    current_player_counts[n << 1] += 1  # Then increment

2. Index Collision Between Rows and Columns

Without proper offset, row and column indices can overlap, causing incorrect win detection.

Pitfall Example:

# Wrong: Using same index range for rows and columns
current_player_counts[row] += 1
current_player_counts[col] += 1  # col could equal row for different lines!

Solution: Use distinct index ranges by offsetting column indices:

current_player_counts[row] += 1      # Rows: 0 to n-1
current_player_counts[n + col] += 1  # Columns: n to 2n-1

3. Checking All Lines Instead of Affected Lines

Checking every possible winning line after each move is inefficient and can lead to false positives if not careful.

Pitfall Example:

# Inefficient: Checking all possible lines
for i in range(n):
    if current_player_counts[i] == n:  # Checking all rows
        return player
for i in range(n, 2*n):
    if current_player_counts[i] == n:  # Checking all columns
        return player

Solution: Only check the lines affected by the current move:

lines_to_check = [row, n + col, n << 1, n << 1 | 1]
if any(current_player_counts[line_id] == n for line_id in lines_to_check):
    return player

4. Anti-Diagonal Condition Error

The anti-diagonal condition is often confused with the main diagonal or calculated incorrectly.

Pitfall Example:

# Wrong anti-diagonal conditions
if row == n - col:  # Incorrect formula
if row - col == n - 1:  # Also incorrect

Solution: The correct anti-diagonal condition is when the sum of row and column equals n-1:

if row + col == n - 1:  # Correct: top-right to bottom-left diagonal
    current_player_counts[n << 1 | 1] += 1

5. Player Index Mapping Error

Since players are numbered 1 and 2 but arrays are 0-indexed, off-by-one errors are common.

Pitfall Example:

# Wrong: Direct player index usage
current_player_counts = self.player_counts[player]  # Index out of bounds for player=2

Solution: Always subtract 1 from the player number to get the correct array index:

current_player_counts = self.player_counts[player - 1]  # Correct: maps 1->0, 2->1
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the maximum element can be found in:


Recommended Readings

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

Load More