1275. Find Winner on a Tic Tac Toe Game
Problem Description
This problem asks you to determine the outcome of a Tic-tac-toe game given a sequence of moves.
In Tic-tac-toe:
- Two players
A
andB
take turns on a 3×3 grid - Player
A
always goes first and places'X'
characters - Player
B
always places'O'
characters - Players can only place their marks in empty squares
- A player wins by getting 3 of their marks in a row (horizontally, vertically, or diagonally)
- The game ends when someone wins or all 9 squares are filled
You're given a 2D array moves
where moves[i] = [row_i, col_i]
represents the i-th move in the game. The moves alternate between players - moves[0]
is player A's first move, moves[1]
is player B's first move, moves[2]
is player A's second move, and so on.
Your task is to return:
"A"
if player A wins"B"
if player B wins"Draw"
if all 9 squares are filled with no winner"Pending"
if the game hasn't ended yet (there are still empty squares and no winner)
The solution cleverly checks only if the last player to move has won, since the moves are guaranteed to be valid (no one would continue playing after someone already won). It tracks the count of marks in each row, column, and diagonal. If any count reaches 3, that player wins. The solution iterates backwards through moves in steps of 2 to check only the last player's moves, using an array cnt
to track:
cnt[0-2]
: counts for rows 0, 1, 2cnt[3-5]
: counts for columns 0, 1, 2cnt[6]
: count for main diagonal (wherei == j
)cnt[7]
: count for anti-diagonal (wherei + j == 2
)
Intuition
The key insight is that since all moves are valid (meaning no one continues playing after someone has already won), we only need to check if the most recent player has won the game. This dramatically simplifies our approach.
Think about it this way: if player A won on their 3rd move, player B wouldn't have made another move. So if we have a list of moves, and someone has won, it must be the player who made the last move.
Instead of simulating the entire board state, we can work backwards from the last move. We only need to check the positions occupied by the last player who moved. If they have 3 marks in any row, column, or diagonal, they've won.
To track this efficiently, we can use a counting approach. We create an array of 8 counters:
- 3 counters for the 3 rows
- 3 counters for the 3 columns
- 1 counter for the main diagonal (top-left to bottom-right)
- 1 counter for the anti-diagonal (top-right to bottom-left)
By iterating through only the last player's moves (going backwards in steps of 2), we increment the appropriate counters. A cell at position (i, j)
contributes to:
- Row counter
i
- Column counter
j + 3
(offset by 3 to avoid overlap with row counters) - Main diagonal counter (index 6) if
i == j
- Anti-diagonal counter (index 7) if
i + j == 2
If any counter reaches 3, that player has won. The player is determined by checking if the move index is odd or even - even indices belong to player A, odd indices to player B.
If no one has won after checking all moves, we simply check if all 9 squares are filled (n == 9
) to determine if it's a draw or the game is still pending.
Solution Approach
The solution implements the counting strategy to determine if the last player to move has won the game.
Step 1: Initialize the counter array
We create an array cnt
of length 8 to track moves:
cnt[0], cnt[1], cnt[2]
- count moves in rows 0, 1, 2cnt[3], cnt[4], cnt[5]
- count moves in columns 0, 1, 2cnt[6]
- count moves on the main diagonalcnt[7]
- count moves on the anti-diagonal
Step 2: Iterate through the last player's moves
We start from the last move and go backwards in steps of 2 (range(n-1, -1, -2)
). This ensures we only check moves made by the last player who moved.
Step 3: Update counters for each move
For each move at position (i, j)
:
- Increment row counter:
cnt[i] += 1
- Increment column counter:
cnt[j + 3] += 1
(offset by 3 to separate from row counters) - If on main diagonal (
i == j
): incrementcnt[6] += 1
- If on anti-diagonal (
i + j == 2
): incrementcnt[7] += 1
Step 4: Check for a winner
After updating counters for each move, we check if any counter equals 3 using any(v == 3 for v in cnt)
. If true, the last player has won.
- If the move index
k
is odd (k & 1
), player B wins (since B makes odd-indexed moves) - Otherwise, player A wins
Step 5: Determine game status if no winner If we've checked all the last player's moves and found no winner:
- If
n == 9
(all squares filled), return"Draw"
- Otherwise, return
"Pending"
(game still in progress)
The time complexity is O(n)
where n
is the number of moves, and space complexity is O(1)
since we only use a fixed-size array of 8 elements.
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 an example where moves = [[0,0], [1,1], [0,1], [2,2], [0,2]]
.
This represents the following game sequence:
- Move 0: Player A places X at (0,0)
- Move 1: Player B places O at (1,1)
- Move 2: Player A places X at (0,1)
- Move 3: Player B places O at (2,2)
- Move 4: Player A places X at (0,2)
The board after all moves looks like:
X X X . O . . . O
Step 1: Initialize
n = 5
(total moves)cnt = [0, 0, 0, 0, 0, 0, 0, 0]
(8 counters for rows, columns, diagonals)
Step 2: Check last player's moves Since we have 5 moves (odd number), the last move was made by player A. We'll check moves at indices 4, 2, and 0 (going backwards by 2).
Move at index 4: [0,2]
- Position (0,2): row=0, col=2
- Update counters:
cnt[0] += 1
→cnt = [1, 0, 0, 0, 0, 0, 0, 0]
(row 0)cnt[2+3] += 1
→cnt = [1, 0, 0, 0, 0, 1, 0, 0]
(column 2)- Not on main diagonal (0 ≠ 2)
- On anti-diagonal (0+2 = 2):
cnt[7] += 1
→cnt = [1, 0, 0, 0, 0, 1, 0, 1]
- Check if any counter equals 3: No
Move at index 2: [0,1]
- Position (0,1): row=0, col=1
- Update counters:
cnt[0] += 1
→cnt = [2, 0, 0, 0, 0, 1, 0, 1]
(row 0)cnt[1+3] += 1
→cnt = [2, 0, 0, 0, 1, 1, 0, 1]
(column 1)- Not on main diagonal (0 ≠ 1)
- Not on anti-diagonal (0+1 ≠ 2)
- Check if any counter equals 3: No
Move at index 0: [0,0]
- Position (0,0): row=0, col=0
- Update counters:
cnt[0] += 1
→cnt = [3, 0, 0, 0, 1, 1, 0, 1]
(row 0)cnt[0+3] += 1
→cnt = [3, 0, 0, 1, 1, 1, 0, 1]
(column 0)- On main diagonal (0 = 0):
cnt[6] += 1
→cnt = [3, 0, 0, 1, 1, 1, 1, 1]
- Not on anti-diagonal (0+0 ≠ 2)
- Check if any counter equals 3: Yes!
cnt[0] == 3
Step 3: Determine winner Since we found a counter equal to 3 and the move index (0) is even, player A wins.
Result: "A"
The algorithm correctly identifies that player A won by getting three X's in row 0, without needing to simulate the entire board or check player B's positions.
Solution Implementation
1class Solution:
2 def tictactoe(self, moves: List[List[int]]) -> str:
3 """
4 Determine the winner of a Tic-Tac-Toe game based on the moves.
5
6 Args:
7 moves: List of moves where moves[i] = [row, col] represents a move
8 moves[0] is player A's move, moves[1] is player B's move, etc.
9
10 Returns:
11 "A" if player A wins, "B" if player B wins,
12 "Draw" if all 9 moves are made with no winner,
13 "Pending" if game is not finished yet
14 """
15 total_moves = len(moves)
16
17 # Track winning conditions:
18 # Index 0-2: row counts (rows 0, 1, 2)
19 # Index 3-5: column counts (columns 0, 1, 2)
20 # Index 6: main diagonal count (top-left to bottom-right)
21 # Index 7: anti-diagonal count (top-right to bottom-left)
22 win_conditions = [0] * 8
23
24 # Check moves backwards, starting from the last move
25 # Step by -2 to only check moves by the same player
26 for move_index in range(total_moves - 1, -1, -2):
27 row, col = moves[move_index]
28
29 # Increment count for this row
30 win_conditions[row] += 1
31
32 # Increment count for this column (offset by 3 to avoid row indices)
33 win_conditions[col + 3] += 1
34
35 # Check if move is on main diagonal
36 if row == col:
37 win_conditions[6] += 1
38
39 # Check if move is on anti-diagonal
40 if row + col == 2:
41 win_conditions[7] += 1
42
43 # Check if any winning condition is met (3 in a row/column/diagonal)
44 if any(count == 3 for count in win_conditions):
45 # Determine winner based on move index parity
46 # Even index (0, 2, 4, 6, 8) means player A
47 # Odd index (1, 3, 5, 7) means player B
48 return "B" if move_index & 1 else "A"
49
50 # No winner found - check if board is full
51 return "Draw" if total_moves == 9 else "Pending"
52
1class Solution {
2 public String tictactoe(int[][] moves) {
3 int totalMoves = moves.length;
4
5 // Array to track winning conditions:
6 // winConditions[0-2]: count for rows 0-2
7 // winConditions[3-5]: count for columns 0-2
8 // winConditions[6]: count for main diagonal (top-left to bottom-right)
9 // winConditions[7]: count for anti-diagonal (top-right to bottom-left)
10 int[] winConditions = new int[8];
11
12 // Process moves backwards, checking only the current player's moves
13 // k -= 2 ensures we only check moves from the same player
14 for (int moveIndex = totalMoves - 1; moveIndex >= 0; moveIndex -= 2) {
15 int row = moves[moveIndex][0];
16 int col = moves[moveIndex][1];
17
18 // Increment count for the current row
19 winConditions[row]++;
20
21 // Increment count for the current column (offset by 3 to avoid collision with row indices)
22 winConditions[col + 3]++;
23
24 // Check if move is on the main diagonal
25 if (row == col) {
26 winConditions[6]++;
27 }
28
29 // Check if move is on the anti-diagonal (row + col = 2 for a 3x3 board)
30 if (row + col == 2) {
31 winConditions[7]++;
32 }
33
34 // Check if any winning condition is met (3 in a row/column/diagonal)
35 if (winConditions[row] == 3 ||
36 winConditions[col + 3] == 3 ||
37 winConditions[6] == 3 ||
38 winConditions[7] == 3) {
39 // Determine winner based on move index parity
40 // Even index means player A, odd index means player B
41 return moveIndex % 2 == 0 ? "A" : "B";
42 }
43 }
44
45 // If no winner found, check if board is full (9 moves) for draw, otherwise pending
46 return totalMoves == 9 ? "Draw" : "Pending";
47 }
48}
49
1class Solution {
2public:
3 string tictactoe(vector<vector<int>>& moves) {
4 int totalMoves = moves.size();
5
6 // Track winning conditions:
7 // Index 0-2: row counts (row 0, row 1, row 2)
8 // Index 3-5: column counts (col 0, col 1, col 2)
9 // Index 6: main diagonal count (top-left to bottom-right)
10 // Index 7: anti-diagonal count (top-right to bottom-left)
11 int winConditionCounts[8] = {0};
12
13 // Process moves backwards, checking only the last player's moves
14 // Start from the last move and go backwards by 2 to check same player
15 for (int moveIndex = totalMoves - 1; moveIndex >= 0; moveIndex -= 2) {
16 int row = moves[moveIndex][0];
17 int col = moves[moveIndex][1];
18
19 // Increment count for the row this move is in
20 winConditionCounts[row]++;
21
22 // Increment count for the column this move is in (offset by 3)
23 winConditionCounts[col + 3]++;
24
25 // Check if move is on main diagonal (row == col)
26 if (row == col) {
27 winConditionCounts[6]++;
28 }
29
30 // Check if move is on anti-diagonal (row + col == 2 for 3x3 board)
31 if (row + col == 2) {
32 winConditionCounts[7]++;
33 }
34
35 // Check if any winning condition is met (3 in a row/column/diagonal)
36 if (winConditionCounts[row] == 3 ||
37 winConditionCounts[col + 3] == 3 ||
38 winConditionCounts[6] == 3 ||
39 winConditionCounts[7] == 3) {
40 // Determine winner based on move index parity
41 // Even index moves are player A, odd index moves are player B
42 return moveIndex % 2 == 0 ? "A" : "B";
43 }
44 }
45
46 // No winner found - check if board is full (9 moves) or game still pending
47 return totalMoves == 9 ? "Draw" : "Pending";
48 }
49};
50
1/**
2 * Determines the winner of a Tic-Tac-Toe game based on the moves made
3 * @param moves - Array of moves where each move is [row, col]
4 * @returns "A" if player A wins, "B" if player B wins, "Draw" if tie, "Pending" if game not finished
5 */
6function tictactoe(moves: number[][]): string {
7 const totalMoves: number = moves.length;
8
9 // Array to track winning conditions:
10 // Index 0-2: Row counts (rows 0, 1, 2)
11 // Index 3-5: Column counts (columns 0, 1, 2)
12 // Index 6: Main diagonal count (top-left to bottom-right)
13 // Index 7: Anti-diagonal count (top-right to bottom-left)
14 const winConditionCounts: number[] = new Array(8).fill(0);
15
16 // Process moves backwards, checking only the last player's moves
17 // k -= 2 ensures we only check moves from the same player
18 for (let moveIndex: number = totalMoves - 1; moveIndex >= 0; moveIndex -= 2) {
19 const [row, col]: number[] = moves[moveIndex];
20
21 // Increment count for current row
22 winConditionCounts[row]++;
23
24 // Increment count for current column (offset by 3 to avoid overlap with row indices)
25 winConditionCounts[col + 3]++;
26
27 // Check if move is on main diagonal
28 if (row === col) {
29 winConditionCounts[6]++;
30 }
31
32 // Check if move is on anti-diagonal
33 if (row + col === 2) {
34 winConditionCounts[7]++;
35 }
36
37 // Check if any winning condition is met (3 in a row/column/diagonal)
38 if (winConditionCounts[row] === 3 ||
39 winConditionCounts[col + 3] === 3 ||
40 winConditionCounts[6] === 3 ||
41 winConditionCounts[7] === 3) {
42 // Player A plays on even indices (0, 2, 4...), Player B on odd indices (1, 3, 5...)
43 return moveIndex % 2 === 0 ? 'A' : 'B';
44 }
45 }
46
47 // If no winner and all 9 moves made, it's a draw; otherwise, game is still pending
48 return totalMoves === 9 ? 'Draw' : 'Pending';
49}
50
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of moves
. The algorithm iterates through the moves array once in reverse order (from index n-1
to 0
with step -2
), performing constant-time operations for each move. Even though we only process every other move (due to step -2
), this still results in O(n)
iterations in the worst case. Within each iteration, we perform a fixed number of operations: updating counters, checking conditions, and the any()
function which checks at most 8 values (constant time).
The space complexity is O(1)
, not O(n)
as stated in the reference answer. The algorithm uses a fixed-size array cnt
of length 8 to track the state of rows, columns, and diagonals. This array size is constant and does not depend on the input size n
. The only other variables used are loop counters and temporary variables (i
, j
, k
), which also require constant space.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrectly Checking Both Players' Moves
A common mistake is checking all moves sequentially instead of only the last player's moves. Since we only need to know if someone has won (not predict future wins), we should only check if the player who just moved has achieved a winning condition.
Incorrect approach:
# Wrong: Checking all moves mixed together
for i in range(len(moves)):
row, col = moves[i]
win_conditions[row] += 1
# This mixes both players' moves in the same counters!
Correct approach:
# Right: Only check the last player's moves
for move_index in range(len(moves) - 1, -1, -2):
row, col = moves[move_index]
# Now we're only counting one player's moves
2. Forgetting to Reset Counters Between Players
If you decide to check both players separately, forgetting to reset the counters between checking player A and player B will lead to incorrect results.
Incorrect approach:
# Wrong: Using same counters for both players
cnt = [0] * 8
# Check player A
for i in range(0, len(moves), 2):
# update cnt...
# Check player B without resetting!
for i in range(1, len(moves), 2):
# cnt still has player A's counts!
Correct approach:
# Reset counters or use separate arrays for each player cnt_a = [0] * 8 cnt_b = [0] * 8
3. Anti-diagonal Condition Error
The anti-diagonal condition is row + col == 2
for a 3×3 board, not other formulas like row == 2 - col
or abs(row - col) == 2
.
Incorrect:
# Wrong anti-diagonal checks
if row == 2 - col: # This works but less intuitive
if abs(row - col) == 2: # This is wrong!
Correct:
# Correct anti-diagonal check if row + col == 2: win_conditions[7] += 1
4. Misunderstanding Move Index and Player Mapping
The move index determines which player made the move. Even indices (0, 2, 4, 6, 8) are player A's moves, odd indices (1, 3, 5, 7) are player B's moves.
Incorrect:
# Wrong: Confusing the last move index with player
if len(moves) % 2 == 0:
return "A" # Wrong! This checks total moves, not who made the last move
Correct:
# Right: Check the actual move index if move_index & 1: # or move_index % 2 == 1 return "B" else: return "A"
5. Not Handling Edge Cases Properly
Remember that the game can end in multiple states, and checking only for a full board isn't sufficient.
Incorrect:
# Wrong: Only checking if board is full
if len(moves) == 9:
return "Draw"
return "Pending"
Correct:
# Right: First check for winner, then distinguish between Draw and Pending
# After checking for winners...
return "Draw" if len(moves) == 9 else "Pending"
What's the output of running the following function using input 56
?
1KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13 def dfs(path, res):
14 if len(path) == len(digits):
15 res.append(''.join(path))
16 return
17
18 next_number = digits[len(path)]
19 for letter in KEYBOARD[next_number]:
20 path.append(letter)
21 dfs(path, res)
22 path.pop()
23
24 res = []
25 dfs([], res)
26 return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2 '2', "abc".toCharArray(),
3 '3', "def".toCharArray(),
4 '4', "ghi".toCharArray(),
5 '5', "jkl".toCharArray(),
6 '6', "mno".toCharArray(),
7 '7', "pqrs".toCharArray(),
8 '8', "tuv".toCharArray(),
9 '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13 List<String> res = new ArrayList<>();
14 dfs(new StringBuilder(), res, digits.toCharArray());
15 return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19 if (path.length() == digits.length) {
20 res.add(path.toString());
21 return;
22 }
23 char next_digit = digits[path.length()];
24 for (char letter : KEYBOARD.get(next_digit)) {
25 path.append(letter);
26 dfs(path, res, digits);
27 path.deleteCharAt(path.length() - 1);
28 }
29}
30
1const KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13 let res = [];
14 dfs(digits, [], res);
15 return res;
16}
17
18function dfs(digits, path, res) {
19 if (path.length === digits.length) {
20 res.push(path.join(''));
21 return;
22 }
23 let next_number = digits.charAt(path.length);
24 for (let letter of KEYBOARD[next_number]) {
25 path.push(letter);
26 dfs(digits, path, res);
27 path.pop();
28 }
29}
30
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!