Skip to content
JZLeetCode
Go back

LeetCode 289 Game of Life

Table of contents

Open Table of contents

Description

Question Links: LeetCode 289

According to Wikipedia’s article: “The Game of Life, also known simply as Life, is a cellular automaton devised by the British mathematician John Horton Conway in 1970.”

The board is made up of an m x n grid of cells, where each cell has an initial state: live (represented by a 1) or dead (represented by a 0). Each cell interacts with its eight neighbors (horizontal, vertical, diagonal) using the following four rules:

  1. Any live cell with fewer than two live neighbors dies as if caused by under-population.
  2. Any live cell with two or three live neighbors lives on to the next generation.
  3. Any live cell with more than three live neighbors dies, as if by over-population.
  4. Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.

The next state is created by applying the above rules simultaneously to every cell in the current state. Given the current state of the m x n grid board, return the next state.

Example 1:

Input: board = [[0,1,0],[0,0,1],[1,1,1],[0,0,0]]
Output: [[0,0,0],[1,0,1],[0,1,1],[0,1,0]]

Example 2:

Input: board = [[1,1],[1,0]]
Output: [[1,1],[1,1]]

Constraints:

Follow up:

Idea 1: In-place State Encoding

The key challenge is that all cells must update simultaneously — we cannot overwrite a cell’s value before its neighbors have read it. The trick is to encode both the current and next state in the same integer using two bits:

Encoding scheme:

  Value  |  Bit 1 (next)  |  Bit 0 (current)  |  Meaning
  -------+----------------+--------------------+------------------
    0    |       0        |        0           |  dead  → dead
    1    |       0        |        1           |  alive → dead
    2    |       1        |        0           |  dead  → alive
    3    |       1        |        1           |  alive → alive

Pass 1: For each cell, count live neighbors using & 1 (reads only the current state, ignoring any next-state bit already set on processed neighbors). If the cell should be alive next generation, set bit 1 with |= 2.

A cell lives in the next generation if:

The Python solution includes board[r][c] in count (it counts itself). So the condition becomes: count == 3 (dead cell with 3 neighbors, or alive with 2 neighbors) or count - board[r][c] == 3 (alive cell with 3 neighbors).

Pass 2: Right-shift every cell by 1 to move the next state into the current-state position.

Complexity: Time O(mn)O(mn) — two passes over the board, each cell checks at most 8 neighbors. Space O(1)O(1) — in-place, no extra board needed.

Idea 2: Copy Board

Make a full copy of the board. Read neighbor values from the copy (which never changes), write updates to the original board. Straightforward application of the four rules.

Complexity: Time O(mn)O(mn) — one pass, each cell checks 8 neighbors. Space O(mn)O(mn) — the copy.

Java

// Solution 1: in-place state encoding. O(mn) time, O(1) space.
public void gameOfLife(int[][] board) {
    int m = board.length, n = board[0].length;
    for (int r = 0; r < m; r++) {
        for (int c = 0; c < n; c++) {
            int count = 0;
            for (int i = Math.max(r - 1, 0); i < Math.min(r + 2, m); i++) // O(1), at most 3
                for (int j = Math.max(c - 1, 0); j < Math.min(c + 2, n); j++) // O(1), at most 3
                    count += board[i][j] & 1;
            // [2nd bit, 1st bit] use 2nd bit to store next state
            if (count == 3 || count - board[r][c] == 3) board[r][c] |= 2; // rules 2,4
        }
    }
    for (int r = 0; r < m; r++)
        for (int c = 0; c < n; c++)
            board[r][c] >>= 1;
}

Python

# Solution 1: in-place state encoding. O(mn) time, O(1) space.
def gameOfLife(self, board: list[list[int]]) -> None:
    m, n = len(board), len(board[0])
    for r in range(m):  # O(m)
        for c in range(n):  # O(n)
            count = 0
            for i in range(max(r - 1, 0), min(r + 2, m)):  # O(1), at most 3
                for j in range(max(c - 1, 0), min(c + 2, n)):  # O(1), at most 3
                    count += board[i][j] & 1
            # count includes board[r][c] itself
            if count == 3 or count - board[r][c] == 3:
                board[r][c] |= 2  # set next state to alive
    for r in range(m):  # O(m)
        for c in range(n):  # O(n)
            board[r][c] >>= 1
# Solution 2: copy board. O(mn) time, O(mn) space.
def gameOfLife(self, board: list[list[int]]) -> None:
    m, n = len(board), len(board[0])
    copy = [row[:] for row in board]  # O(mn) space
    dirs = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
    for r in range(m):  # O(m)
        for c in range(n):  # O(n)
            count = sum(
                copy[r + dr][c + dc]
                for dr, dc in dirs  # O(1), 8 directions
                if 0 <= r + dr < m and 0 <= c + dc < n
            )
            if board[r][c] == 1 and (count < 2 or count > 3):
                board[r][c] = 0
            elif board[r][c] == 0 and count == 3:
                board[r][c] = 1

C++

// Solution 1: in-place state encoding. O(mn) time, O(1) space.
void gameOfLife(vector<vector<int>>& board) {
    int m = board.size(), n = board[0].size();
    int dirs[8][2] = {{-1,-1},{-1,0},{-1,1},{0,-1},{0,1},{1,-1},{1,0},{1,1}};
    // O(mn) first pass: compute next state and store in 2nd bit
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            int live = 0;                           // O(1) neighbor count
            for (auto& d : dirs) {                  // O(8) = O(1) check all 8 neighbors
                int ni = i + d[0], nj = j + d[1];
                if (ni >= 0 && ni < m && nj >= 0 && nj < n)
                    live += board[ni][nj] & 1;      // read current state from 1st bit
            }
            // Cell lives if: exactly 3 neighbors, or alive with exactly 2 neighbors
            if (live == 3 || (live == 2 && (board[i][j] & 1)))
                board[i][j] |= 2;                   // set 2nd bit for next state
        }
    }
    // O(mn) second pass: shift to get next state
    for (int i = 0; i < m; ++i)
        for (int j = 0; j < n; ++j)
            board[i][j] >>= 1;
}
// Solution 2: copy board. O(mn) time, O(mn) space.
void gameOfLife(vector<vector<int>>& board) {
    int m = board.size(), n = board[0].size();
    vector<vector<int>> copy = board;               // O(mn) space for board copy
    int dirs[8][2] = {{-1,-1},{-1,0},{-1,1},{0,-1},{0,1},{1,-1},{1,0},{1,1}};
    // O(mn) iterate every cell
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            int live = 0;                           // O(1) neighbor count
            for (auto& d : dirs) {                  // O(8) = O(1) check all 8 neighbors
                int ni = i + d[0], nj = j + d[1];
                if (ni >= 0 && ni < m && nj >= 0 && nj < n)
                    live += copy[ni][nj];
            }
            if (copy[i][j] == 1 && (live < 2 || live > 3))
                board[i][j] = 0;                    // under/over-population: dies
            else if (copy[i][j] == 0 && live == 3)
                board[i][j] = 1;                    // reproduction: becomes alive
        }
    }
}

Rust

// Solution 1: in-place state encoding. O(mn) time, O(1) space.
pub fn game_of_life(board: &mut Vec<Vec<i32>>) {
    if board.is_empty() || board[0].is_empty() { return; }
    let m = board.len() as i32;
    let n = board[0].len() as i32;
    for i in 0..m {
        for j in 0..n {
            let mut live = 0;
            for di in -1..=1 {
                for dj in -1..=1 {
                    if di == 0 && dj == 0 { continue; }
                    let ni = i + di;
                    let nj = j + dj;
                    if ni >= 0 && ni < m && nj >= 0 && nj < n {
                        live += board[ni as usize][nj as usize] & 1;
                    }
                }
            }
            let cur = board[i as usize][j as usize] & 1;
            if (cur == 1 && (live == 2 || live == 3)) || (cur == 0 && live == 3) {
                board[i as usize][j as usize] |= 2;
            }
        }
    }
    for row in board.iter_mut() {
        for cell in row.iter_mut() {
            *cell >>= 1;
        }
    }
}
Share this post on:

Previous Post
System Design - How Unicode and UTF-8 Encoding Work
Next Post
LeetCode 983 Minimum Cost For Tickets