What is the Tic-tac-toe problem?

Leetcode 348. Design a Tic-tac-toe game that is played between two players on a n x n grid.

You may assume the following rules:

  • A move is guaranteed to be valid and is placed on an empty block.
  • Once a winning condition is reached, no more moves is allowed.
  • A player who succeeds in placing n of their marks in a horizontal, vertical, or diagonal row wins the game.

Example:

Given n = 3, assume that player 1 is "X" and player 2 is "O" in the
board.

TicTacToe toe = new TicTacToe(3);

toe.move(0, 0, 1); -> Returns 0 (no one wins)
|X| | |
| | | |    // Player 1 makes a move at (0, 0).
| | | |

toe.move(0, 2, 2); -> Returns 0 (no one wins)
|X| |O|
| | | |    // Player 2 makes a move at (0, 2).
| | | |

toe.move(2, 2, 1); -> Returns 0 (no one wins)
|X| |O|
| | | |    // Player 1 makes a move at (2, 2).
| | |X|

toe.move(1, 1, 2); -> Returns 0 (no one wins)
|X| |O|
| |O| |    // Player 2 makes a move at (1, 1).
| | |X|

toe.move(2, 0, 1); -> Returns 0 (no one wins)
|X| |O|
| |O| |    // Player 1 makes a move at (2, 0).
|X| |X|

toe.move(1, 0, 2); -> Returns 0 (no one wins)
|X| |O|
|O|O| |    // Player 2 makes a move at (1, 0).
|X| |X|

toe.move(2, 1, 1); -> Returns 1 (player 1 wins)
|X| |O|
|O|O| |    // Player 1 makes a move at (2, 1).
|X|X|X|

Follow up:

  • Could you do better than O(n^2) per move() operation?

Hint:

  • Could you trade extra space such that move() operation can be done in O(1)?
  • You need two arrays: int rows[n], int cols[n], plus two variables: diagonal, anti_diagonal.

Define the game framework

First, we describe the game’s APIs as follows:

class TicTacToe
-----------------------------------------------------------------
      TicTacToe(int n)          create a game given a n x n board
int   move(x, y, id)            a player id makes a move at (x,y)
-----------------------------------------------------------------

Next, we write the game framework in Python:

class TicTacToe:
    def __init__(self, n):
        self.board = [[None for j in range(n)] for i in range(n)]

    def move(self, x, y, id):
        pass

Then, write the test cases:

toe = TicTacToe(3)
assert toe.move(0, 0, 1) == 0
assert toe.board[0][0] == 1

assert toe.move(0, 2, 2) == 0
assert toe.board[0][2] == 2

assert toe.move(2, 2, 1) == 0
assert toe.board[2][2] == 1

assert toe.move(1, 1, 2) == 0
assert toe.board[1][1] == 2

assert toe.move(2, 0, 1) == 0
assert toe.board[2][0] == 1

assert toe.move(1, 0, 2) == 0
assert toe.board[1][0] == 2

assert toe.move(2, 1, 1) == 1
assert toe.board[2][1] == 1

A O(n^2) solution

The simplest solution is to count the number of cells for each player on every row and column, plus diagonal and anti-diagonal. If it is empty, we do nothing. If the cell is occupied by the first player, we increase 1. Otherwise, for the second player, we decrease 1. Once we find a count that equals to n, there is a winner.

class TicTacToe:
    def __init__(self, n):
        self.n = n
        self.board = [[None for j in range(n)] for i in range(n)]

    def val(self, row, col):
        v = self.board[row][col]
        if not v:
            return 0
        elif v == 1:
            return 1
        else:
            return -1

    def win(self):
        """
        Check every row and column, diagonal, anti-diagonal
        """
        rows = [0 for i in range(self.n)]
        cols = [0 for i in range(self.n)]
        for row in xrange(self.n):
            for col in xrange(self.n):

                rows[row] += self.val(row, col)
                cols[col] += self.val(row, col)

                if abs(rows[row]) == self.n or abs(cols[col]) == self.n:
                    # print "row and col check!"
                    return True

        diagonal = 0
        anti_diagonal = 0
        for row, col in zip(range(self.n), range(self.n)):
            diagonal += self.val(row, col)
            anti_diagonal += self.val(row, self.n-col-1)

        # print "diagonal and anti-diagonal!", diagonal, anti_diagonal
        return abs(diagonal) == self.n or abs(anti_diagonal) == self.n

    def move(self, x, y, id):
        if self.board[x][y] != None:
            raise Exception("The cell is not empty!")

        self.board[x][y] = id

        # print self.board
        # print "win?", self.win()

        return 1 if self.win() else 0

An improved O(n) solution

We can create sentinel for each row, column, diagonal, and anti_diagonal. When we add a new move, we update these sentinels. For any new move, we just need to check the max value of these sentinels. If we scan all sentinels, the time complexity is O(n).

class TicTacToe:
    def __init__(self, n):
        self.n = n
        self.board = [[None for j in range(n)] for i in range(n)]
        self.rows = [0 for i in range(self.n)]
        self.cols = [0 for i in range(self.n)]
        self.diagonal = 0
        self.anti_diagonal = 0

    def move(self, x, y, id):
        if self.board[x][y] != None:
            raise Exception("The cell is not empty!")

        self.board[x][y] = id

        inc = 1 if id == 1 else -1

        self.rows[x] += inc
        self.cols[y] += inc
        if x == y:
            self.diagonal += inc
        if x == self.n-1-y:
            self.anti_diagonal += inc

        max_row = max(map(abs, self.rows))
        max_col = max(map(abs, self.cols))

        max_count = max([max_row, max_col, abs(self.diagonal), abs(self.anti_diagonal)])

        return 1 if max_count == self.n else 0

An ultimately improved O(1) solution

Why do we need to scan the sentinels? We only need to check updated sentinels when adding a new move. This reduces the time to O(1).

def move(self, x, y, id):
    ...
    vals = [self.rows[x], self.cols[y], self.diagonal, self.anti_diagonal]
    vals = map(abs, vals)
    max_count = max(vals)

    return 1 if max_count == self.n else 0

The whole procedure is as follows: