289. Game of Life
Problem Description
The Game of Life is a cellular automaton where you have an m x n
grid board. Each cell in the grid has an initial state:
- Live cell: represented by
1
- Dead cell: represented by
0
Each cell interacts with its 8 neighbors (horizontal, vertical, and diagonal) according to these rules:
- A live cell with fewer than 2 live neighbors dies (under-population)
- A live cell with 2 or 3 live neighbors survives to the next generation
- A live cell with more than 3 live neighbors dies (over-population)
- A dead cell with exactly 3 live neighbors becomes alive (reproduction)
The key challenge is that all cells must be updated simultaneously - meaning the next state is calculated based on the current state of all cells, not on any partially updated states.
Your task is to update the given board
in-place to reflect its next state after applying these rules to every cell.
The solution uses an in-place marking technique with temporary states:
- State
2
: marks a live cell that will die in the next generation - State
-1
: marks a dead cell that will become alive in the next generation
The algorithm works in two passes:
- First pass: Count live neighbors for each cell and mark cells with temporary states (
2
or-1
) based on the Game of Life rules - Second pass: Convert temporary states to final states (state
2
→0
, state-1
→1
)
This approach allows updating the board in-place while preserving the original state information needed for simultaneous updates.
Intuition
The main challenge in this problem is that all cells need to be updated simultaneously. If we update cells one by one directly (changing 1
to 0
or 0
to 1
), we'll lose the original state information that neighboring cells need for their calculations.
For example, if we change a live cell to dead (1
→ 0
) immediately, when we process its neighbor later, that neighbor won't know this cell was originally alive, leading to an incorrect count of live neighbors.
The key insight is that we need to preserve the original state while marking the future state. We can achieve this by using additional states as temporary markers:
- Instead of directly changing
1
→0
, we mark it as2
(meaning "currently alive but will die") - Instead of directly changing
0
→1
, we mark it as-1
(meaning "currently dead but will become alive")
Why these specific values?
- For state
2
: It's positive (> 0
), so when counting neighbors, we can still recognize it as a currently live cell - For state
-1
: It's non-positive (≤ 0
), so when counting neighbors, we know it's currently a dead cell
This encoding allows us to:
- Read the current state: check if
board[x][y] > 0
(live) orboard[x][y] ≤ 0
(dead) - Mark the future state: use
2
and-1
as intermediate values - Complete the transition: after all cells are marked, convert
2
→0
and-1
→1
This way, we solve the problem in-place with O(1)
extra space, while maintaining the simultaneity requirement of the Game of Life rules.
Solution Approach
The solution implements the in-place marking strategy using two passes through the board.
First Pass - Mark Future States:
We iterate through each cell (i, j)
in the board:
-
Count live neighbors: For the current cell, we examine all 8 surrounding cells within the grid boundaries. The counting logic uses
live = -board[i][j]
initially to exclude the current cell from its own neighbor count, then adds 1 for each neighbor whereboard[x][y] > 0
(currently alive). -
Apply Game of Life rules with marking:
- If the current cell is alive (
board[i][j] == 1
) and has fewer than 2 or more than 3 live neighbors (live < 2 or live > 3
), mark it as2
(will die) - If the current cell is dead (
board[i][j] == 0
) and has exactly 3 live neighbors (live == 3
), mark it as-1
(will become alive) - Cells that don't meet these conditions remain unchanged (live cells with 2-3 neighbors stay
1
, dead cells without exactly 3 neighbors stay0
)
- If the current cell is alive (
Second Pass - Finalize States:
After all cells have been marked, we traverse the board again:
- Convert state
2
(alive but dying) to0
(dead) - Convert state
-1
(dead but becoming alive) to1
(alive) - States
0
and1
remain as they are
Implementation Details:
# First pass: mark transitions
for i in range(m):
for j in range(n):
live = -board[i][j] # Start with negative current cell value
# Check all 8 neighbors
for x in range(i - 1, i + 2):
for y in range(j - 1, j + 2):
if 0 <= x < m and 0 <= y < n and board[x][y] > 0:
live += 1
# Apply rules with marking
if board[i][j] and (live < 2 or live > 3):
board[i][j] = 2 # Live cell dies
if board[i][j] == 0 and live == 3:
board[i][j] = -1 # Dead cell becomes alive
# Second pass: finalize states
for i in range(m):
for j in range(n):
if board[i][j] == 2:
board[i][j] = 0
elif board[i][j] == -1:
board[i][j] = 1
This approach achieves O(mn)
time complexity with two passes through the board and O(1)
space complexity by modifying the board in-place.
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 a small 3x3 grid to illustrate how the solution works:
Initial Board:
[1, 1, 0] [1, 0, 0] [0, 1, 0]
First Pass - Mark Future States:
Processing cell (0,0) with value 1:
- Count live neighbors: Check all 8 directions (only 3 are in bounds)
- (0,1) = 1 ✓, (1,0) = 1 ✓, (1,1) = 0
- Total live neighbors = 2
- Apply rules: Live cell with 2 neighbors survives → Keep as 1
Processing cell (0,1) with value 1:
- Count live neighbors:
- (0,0) = 1 ✓, (0,2) = 0, (1,0) = 1 ✓, (1,1) = 0, (1,2) = 0
- Total live neighbors = 2
- Apply rules: Live cell with 2 neighbors survives → Keep as 1
Processing cell (0,2) with value 0:
- Count live neighbors:
- (0,1) = 1 ✓, (1,1) = 0, (1,2) = 0
- Total live neighbors = 1
- Apply rules: Dead cell with 1 neighbor stays dead → Keep as 0
Processing cell (1,0) with value 1:
- Count live neighbors:
- (0,0) = 1 ✓, (0,1) = 1 ✓, (1,1) = 0, (2,0) = 0, (2,1) = 1 ✓
- Total live neighbors = 3
- Apply rules: Live cell with 3 neighbors survives → Keep as 1
Processing cell (1,1) with value 0:
- Count live neighbors:
- All 8 neighbors: (0,0)=1 ✓, (0,1)=1 ✓, (0,2)=0, (1,0)=1 ✓, (1,2)=0, (2,0)=0, (2,1)=1 ✓, (2,2)=0
- Total live neighbors = 4
- Apply rules: Dead cell with 4 neighbors stays dead → Keep as 0
Processing cell (1,2) with value 0:
- Count live neighbors:
- (0,1) = 1 ✓, (0,2) = 0, (1,1) = 0, (2,1) = 1 ✓, (2,2) = 0
- Total live neighbors = 2
- Apply rules: Dead cell with 2 neighbors stays dead → Keep as 0
Processing cell (2,0) with value 0:
- Count live neighbors:
- (1,0) = 1 ✓, (1,1) = 0, (2,1) = 1 ✓
- Total live neighbors = 2
- Apply rules: Dead cell with 2 neighbors stays dead → Keep as 0
Processing cell (2,1) with value 1:
- Count live neighbors:
- (1,0) = 1 ✓, (1,1) = 0, (1,2) = 0, (2,0) = 0, (2,2) = 0
- Total live neighbors = 1
- Apply rules: Live cell with 1 neighbor dies (under-population) → Mark as 2
Processing cell (2,2) with value 0:
- Count live neighbors:
- (1,1) = 0, (1,2) = 0, (2,1) = 1 ✓ (still counted as 1 even though marked as 2)
- Total live neighbors = 1
- Apply rules: Dead cell with 1 neighbor stays dead → Keep as 0
Board After First Pass:
[1, 1, 0] [1, 0, 0] [0, 2, 0] // Cell (2,1) marked as 2 (will die)
Second Pass - Finalize States:
Convert all temporary states:
- Cell (2,1): 2 → 0 (dying cell becomes dead)
- All other cells remain unchanged
Final Board:
[1, 1, 0] [1, 0, 0] [0, 0, 0]
The key insight demonstrated here is how marking cell (2,1) as 2
preserves its "currently alive" status when cell (2,2) counts its neighbors later, ensuring the simultaneous update requirement is met.
Solution Implementation
1class Solution:
2 def gameOfLife(self, board: List[List[int]]) -> None:
3 """
4 Modify the board in-place according to Conway's Game of Life rules.
5
6 Rules:
7 1. Any live cell with < 2 live neighbors dies (underpopulation)
8 2. Any live cell with 2-3 live neighbors survives
9 3. Any live cell with > 3 live neighbors dies (overpopulation)
10 4. Any dead cell with exactly 3 live neighbors becomes alive (reproduction)
11
12 State encoding:
13 - 0: dead cell staying dead
14 - 1: live cell staying alive
15 - 2: live cell becoming dead (was 1, will be 0)
16 - -1: dead cell becoming alive (was 0, will be 1)
17 """
18 rows, cols = len(board), len(board[0])
19
20 # First pass: Mark cells with their next state using encoded values
21 for row in range(rows):
22 for col in range(cols):
23 # Count live neighbors (including the cell itself if alive)
24 live_neighbors = -board[row][col] # Subtract current cell's value
25
26 # Check all 8 neighboring cells
27 for neighbor_row in range(row - 1, row + 2):
28 for neighbor_col in range(col - 1, col + 2):
29 # Check if neighbor is within bounds and currently alive
30 if (0 <= neighbor_row < rows and
31 0 <= neighbor_col < cols and
32 board[neighbor_row][neighbor_col] > 0):
33 live_neighbors += 1
34
35 # Apply Game of Life rules
36 # Rule 1 & 3: Live cell dies (underpopulation or overpopulation)
37 if board[row][col] == 1 and (live_neighbors < 2 or live_neighbors > 3):
38 board[row][col] = 2 # Mark as "will die"
39
40 # Rule 4: Dead cell becomes alive (reproduction)
41 if board[row][col] == 0 and live_neighbors == 3:
42 board[row][col] = -1 # Mark as "will become alive"
43
44 # Second pass: Update board to final state
45 for row in range(rows):
46 for col in range(cols):
47 if board[row][col] == 2: # Was alive, now dead
48 board[row][col] = 0
49 elif board[row][col] == -1: # Was dead, now alive
50 board[row][col] = 1
51
1class Solution {
2 public void gameOfLife(int[][] board) {
3 int rows = board.length;
4 int cols = board[0].length;
5
6 // First pass: Mark cells that need to change state
7 // Use encoding: 2 = was alive, will die; -1 = was dead, will become alive
8 for (int row = 0; row < rows; row++) {
9 for (int col = 0; col < cols; col++) {
10 // Count live neighbors (excluding current cell)
11 int liveNeighbors = countLiveNeighbors(board, row, col, rows, cols);
12
13 // Apply Conway's Game of Life rules
14 // Rule 1 & 3: Live cell with < 2 or > 3 neighbors dies
15 if (board[row][col] == 1 && (liveNeighbors < 2 || liveNeighbors > 3)) {
16 board[row][col] = 2; // Mark as "will die"
17 }
18 // Rule 4: Dead cell with exactly 3 neighbors becomes alive
19 if (board[row][col] == 0 && liveNeighbors == 3) {
20 board[row][col] = -1; // Mark as "will become alive"
21 }
22 // Rule 2: Live cell with 2-3 neighbors survives (no change needed)
23 }
24 }
25
26 // Second pass: Update all cells to their final states
27 for (int row = 0; row < rows; row++) {
28 for (int col = 0; col < cols; col++) {
29 if (board[row][col] == 2) {
30 board[row][col] = 0; // Cell dies
31 } else if (board[row][col] == -1) {
32 board[row][col] = 1; // Cell becomes alive
33 }
34 }
35 }
36 }
37
38 /**
39 * Counts the number of live neighbors for a given cell
40 * Live cells are represented by positive values (1 or 2)
41 */
42 private int countLiveNeighbors(int[][] board, int row, int col, int rows, int cols) {
43 int liveCount = 0;
44
45 // Check all 8 neighboring cells
46 for (int neighborRow = row - 1; neighborRow <= row + 1; neighborRow++) {
47 for (int neighborCol = col - 1; neighborCol <= col + 1; neighborCol++) {
48 // Skip out-of-bounds cells and the current cell itself
49 if (neighborRow < 0 || neighborRow >= rows ||
50 neighborCol < 0 || neighborCol >= cols ||
51 (neighborRow == row && neighborCol == col)) {
52 continue;
53 }
54
55 // Count cell as live if it's currently alive (value > 0)
56 // This includes cells marked as 2 (will die) since they're currently alive
57 if (board[neighborRow][neighborCol] > 0) {
58 liveCount++;
59 }
60 }
61 }
62
63 return liveCount;
64 }
65}
66
1class Solution {
2public:
3 void gameOfLife(vector<vector<int>>& board) {
4 int rows = board.size();
5 int cols = board[0].size();
6
7 // First pass: Mark cells that need to change state
8 // State encoding:
9 // 0: dead -> dead (stays 0)
10 // 1: live -> live (stays 1)
11 // 2: live -> dead (temporarily marked as 2)
12 // -1: dead -> live (temporarily marked as -1)
13 for (int row = 0; row < rows; ++row) {
14 for (int col = 0; col < cols; ++col) {
15 // Count live neighbors in all 8 directions
16 // Start with negative current cell value to exclude it from count
17 int liveNeighbors = -board[row][col];
18
19 // Check all 8 adjacent cells
20 for (int neighborRow = row - 1; neighborRow <= row + 1; ++neighborRow) {
21 for (int neighborCol = col - 1; neighborCol <= col + 1; ++neighborCol) {
22 // Check if neighbor is within bounds and currently alive
23 // Cells marked as 2 (live -> dead) are still counted as alive
24 if (neighborRow >= 0 && neighborRow < rows &&
25 neighborCol >= 0 && neighborCol < cols &&
26 board[neighborRow][neighborCol] > 0) {
27 ++liveNeighbors;
28 }
29 }
30 }
31
32 // Apply Conway's Game of Life rules
33 // Rule 1 & 3: Live cell with < 2 or > 3 live neighbors dies
34 if (board[row][col] == 1 && (liveNeighbors < 2 || liveNeighbors > 3)) {
35 board[row][col] = 2; // Mark as live -> dead
36 }
37
38 // Rule 4: Dead cell with exactly 3 live neighbors becomes alive
39 if (board[row][col] == 0 && liveNeighbors == 3) {
40 board[row][col] = -1; // Mark as dead -> live
41 }
42 // Rule 2: Live cell with 2 or 3 neighbors survives (no change needed)
43 }
44 }
45
46 // Second pass: Update board to final state
47 for (int row = 0; row < rows; ++row) {
48 for (int col = 0; col < cols; ++col) {
49 if (board[row][col] == 2) {
50 board[row][col] = 0; // Live -> dead transition complete
51 } else if (board[row][col] == -1) {
52 board[row][col] = 1; // Dead -> live transition complete
53 }
54 }
55 }
56 }
57};
58
1/**
2 * Conway's Game of Life - modifies board in-place
3 * Rules:
4 * 1. Any live cell with 2-3 live neighbors survives
5 * 2. Any dead cell with exactly 3 live neighbors becomes alive
6 * 3. All other live cells die, and all other dead cells stay dead
7 *
8 * @param board - 2D array representing the game board (1 = alive, 0 = dead)
9 */
10function gameOfLife(board: number[][]): void {
11 const rows = board.length;
12 const cols = board[0].length;
13
14 // First pass: mark cells that will change state
15 // Use encoding: 2 = alive -> dead, -1 = dead -> alive
16 for (let row = 0; row < rows; row++) {
17 for (let col = 0; col < cols; col++) {
18 // Count live neighbors (subtract current cell if alive)
19 let liveNeighbors = -board[row][col];
20
21 // Check all 8 surrounding cells
22 for (let neighborRow = row - 1; neighborRow <= row + 1; neighborRow++) {
23 for (let neighborCol = col - 1; neighborCol <= col + 1; neighborCol++) {
24 // Check if neighbor is within bounds and alive
25 if (neighborRow >= 0 && neighborRow < rows &&
26 neighborCol >= 0 && neighborCol < cols &&
27 board[neighborRow][neighborCol] > 0) {
28 liveNeighbors++;
29 }
30 }
31 }
32
33 // Apply Game of Life rules
34 // Rule: Live cell with < 2 or > 3 neighbors dies
35 if (board[row][col] === 1 && (liveNeighbors < 2 || liveNeighbors > 3)) {
36 board[row][col] = 2; // Mark as alive -> dead
37 }
38
39 // Rule: Dead cell with exactly 3 neighbors becomes alive
40 if (board[row][col] === 0 && liveNeighbors === 3) {
41 board[row][col] = -1; // Mark as dead -> alive
42 }
43 }
44 }
45
46 // Second pass: update board to final state
47 for (let row = 0; row < rows; row++) {
48 for (let col = 0; col < cols; col++) {
49 // Convert temporary states to final states
50 if (board[row][col] === 2) {
51 board[row][col] = 0; // Alive -> Dead
52 }
53 if (board[row][col] === -1) {
54 board[row][col] = 1; // Dead -> Alive
55 }
56 }
57 }
58}
59
Time and Space Complexity
The time complexity is O(m × n)
, where m
and n
are the number of rows and columns of the board, respectively. The algorithm consists of two main phases:
- First nested loop: iterates through all
m × n
cells, and for each cell, it checks at most 9 neighboring positions (including itself). Since the neighbor checking is bounded by a constant (9), this phase takesO(m × n)
time. - Second nested loop: iterates through all
m × n
cells again to update the encoded values back to their final states, takingO(m × n)
time.
Therefore, the overall time complexity is O(m × n) + O(m × n) = O(m × n)
.
The space complexity is O(1)
. The algorithm modifies the board in-place using an encoding scheme where:
2
represents a cell that was alive but will become dead-1
represents a cell that was dead but will become alive1
represents a currently alive cell0
represents a currently dead cell
No additional data structures are used that scale with the input size. Only a constant amount of extra space is used for variables like m
, n
, i
, j
, x
, y
, and live
.
Common Pitfalls
1. Incorrect Neighbor Counting Logic
Pitfall: A common mistake is incorrectly initializing the live neighbor count. The code uses live = -board[i][j]
to exclude the current cell from its own neighbor count, but developers often forget this adjustment and simply start with live = 0
, leading to off-by-one errors.
Why it happens: When iterating through the 3x3 grid centered on a cell, the iteration includes the cell itself. If the current cell is alive (value = 1), it would incorrectly count itself as a neighbor.
Incorrect approach:
live = 0 # Wrong: will count the current cell if it's alive
for x in range(i - 1, i + 2):
for y in range(j - 1, j + 2):
if 0 <= x < m and 0 <= y < n and board[x][y] > 0:
live += 1
Correct solution:
# Method 1: Start with negative current cell value
live = -board[i][j]
for x in range(i - 1, i + 2):
for y in range(j - 1, j + 2):
if 0 <= x < m and 0 <= y < n and board[x][y] > 0:
live += 1
# Method 2: Skip the current cell explicitly
live = 0
for x in range(i - 1, i + 2):
for y in range(j - 1, j + 2):
if x == i and y == j:
continue # Skip the current cell
if 0 <= x < m and 0 <= y < n and board[x][y] > 0:
live += 1
2. Misunderstanding State Encoding During Neighbor Counting
Pitfall: Using the wrong condition to check if a neighbor is currently alive. The code checks board[x][y] > 0
to determine if a cell is currently alive (states 1 or 2), but developers might mistakenly use board[x][y] == 1
or board[x][y] != 0
.
Why it matters:
- State
2
represents a cell that is currently alive but will die - State
-1
represents a cell that is currently dead but will become alive - During the first pass, we need to count based on the current state, not the future state
Incorrect approaches:
# Wrong: Misses cells marked as state 2 (currently alive, will die) if board[x][y] == 1: live += 1 # Wrong: Counts state -1 cells as alive (they're currently dead) if board[x][y] != 0: live += 1
Correct solution:
# Correct: Counts both state 1 and state 2 as currently alive if board[x][y] > 0: live += 1
3. Modifying the Wrong Cells in the First Pass
Pitfall: Forgetting to check the current state before applying transitions, leading to overwriting already-marked cells.
Example mistake:
# Wrong: Overwrites state 2 with -1 if it happens to have 3 live neighbors if live == 3: board[i][j] = -1 # Should only apply to dead cells!
Correct solution:
# Check current state before marking transitions if board[i][j] == 1 and (live < 2 or live > 3): board[i][j] = 2 # Only mark live cells for death if board[i][j] == 0 and live == 3: board[i][j] = -1 # Only mark dead cells for birth
4. Boundary Check Errors
Pitfall: Forgetting to check grid boundaries when examining neighbors, causing index out of bounds errors.
Incorrect approach:
# Wrong: No boundary checking
for x in range(i - 1, i + 2):
for y in range(j - 1, j + 2):
if board[x][y] > 0: # IndexError for edge cells!
live += 1
Correct solution:
# Always check boundaries before accessing array elements
for x in range(i - 1, i + 2):
for y in range(j - 1, j + 2):
if 0 <= x < m and 0 <= y < n and board[x][y] > 0:
live += 1
Which of these properties could exist for a graph but not a tree?
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!