348. Design Tic-Tac-Toe 🔒
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 sizen 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 yet1
if player 1 wins2
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
ton-1
- Columns are indexed as
n
to2n-1
- Main diagonal is indexed as
n << 1
(which is2n
) - Anti-diagonal is indexed as
n << 1 | 1
(which is2n + 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.
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
rowsn
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
ton-1
- Columns get indices
n
to2n-1
- The main diagonal gets index
2n
(written asn << 1
using bit shift) - The anti-diagonal gets index
2n + 1
(written asn << 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
):
-
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
. -
Update the row counter:
cur[row] += 1
The row index directly corresponds to the row number.
-
Update the column counter:
cur[n + col] += 1
Columns are offset by
n
to avoid collision with row indices. -
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 asn << 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 asn << 1 | 1
using bitwise OR).
-
-
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. -
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 EvaluatorExample 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
In a binary min heap, the maximum element can be found in:
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!