Skip to content

Commit 34cac4d

Browse files
committed
feat(algorithms, matrices): game of life
1 parent 73f7378 commit 34cac4d

6 files changed

Lines changed: 169 additions & 0 deletions

File tree

puzzles/arrays/maxlen_contiguous_binary_subarray/README.md renamed to algorithms/hash_table/maxlen_contiguous_binary_subarray/README.md

File renamed without changes.

puzzles/arrays/maxlen_contiguous_binary_subarray/__init__.py renamed to algorithms/hash_table/maxlen_contiguous_binary_subarray/__init__.py

File renamed without changes.

puzzles/arrays/maxlen_contiguous_binary_subarray/test_maxlen_contiguous_binary_subarray.py renamed to algorithms/hash_table/maxlen_contiguous_binary_subarray/test_maxlen_contiguous_binary_subarray.py

File renamed without changes.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Game of Life
2+
3+
According to [Wikipedia's article](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life): "The Game of Life, also known
4+
simply as Life, is a cellular automaton devised by the British mathematician John Horton Conway in 1970."
5+
6+
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
7+
(represented by a 0). Each cell interacts with its [eight neighbors](https://en.wikipedia.org/wiki/Moore_neighborhood)
8+
(horizontal, vertical, diagonal) using the following four rules (taken from the above Wikipedia article):
9+
10+
- Any live cell with fewer than two live neighbors dies as if caused by under-population.
11+
- Any live cell with two or three live neighbors lives on to the next generation.
12+
- Any live cell with more than three live neighbors dies, as if by over-population.
13+
- Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.
14+
15+
The next state of the board is determined by applying the above rules simultaneously to every cell in the current state of the m x n grid board. In this process, births and deaths occur simultaneously.
16+
17+
Given the current state of the board, update the board to reflect its next state.
18+
19+
Note that you do not need to return anything.
20+
21+
## Examples
22+
23+
Example 1:
24+
25+
```text
26+
Input: board = [[0,1,0],[0,0,1],[1,1,1],[0,0,0]]
27+
Output: [[0,0,0],[1,0,1],[0,1,1],[0,1,0]]
28+
```
29+
30+
Example 2:
31+
32+
```text
33+
Input: board = [[1,1],[1,0]]
34+
Output: [[1,1],[1,1]]
35+
```
36+
37+
## Constraints
38+
39+
- m == board.length
40+
- n == board[i].length
41+
- 1 <= m, n <= 25
42+
- `board[i][j]` is 0 or 1.
43+
44+
Follow up:
45+
46+
- Could you solve it in-place? Remember that the board needs to be updated simultaneously: You cannot update some cells
47+
first and then use their updated values to update other cells.
48+
- In this question, we represent the board using a 2D array. In principle, the board is infinite, which would cause
49+
problems when the active area encroaches upon the border of the array (i.e., live cells reach the border). How would
50+
you address these problems?
51+
52+
## Topics
53+
54+
- Array
55+
- Matrix
56+
- Simulation
57+
58+
## Solution
59+
60+
The core intuition is to update all cells simultaneously without corrupting neighbor counts. We encode transitions in
61+
place: 1 means alive (and may stay alive), -1 means was alive and will die (underpopulation/overpopulation), 0 means
62+
dead (and may stay dead), and 2 means was dead and will become alive (reproduction). While counting neighbors, we only
63+
care about the original state, so we test abs(`board[r][c]) == 1`. This is true for 1 and -1 (originally alive) and false
64+
for 0 and 2 (originally dead). With that, we finish a full pass marking transitions, then a quick second pass to normalize
65+
to 0 or 1.
66+
67+
Using the intuition above, we implement the algorithm as follows:
68+
69+
1. Store the board’s dimensions in rows and cols.
70+
2. Define an array neighbors = {-1, 0, 1}. The Cartesian product {−1,0,1} × {−1,0,1} gives the relative offsets to all
71+
eight neighboring cells around a given cell. We skip the (0, 0) pair as it represents the cell itself. This allows us
72+
to systematically check every neighbor’s state when counting alive cells.
73+
3. Iterate over every cell (row, col) in the grid. For each cell:
74+
- Initialize a counter liveNeighbors = 0.
75+
- To count alive neighbors, loop i = 0..2 and j = 0..2 to form a relative offset (neighbors[i], neighbors[j]):
76+
- Compute the neighbor’s coordinates: r = row + neighbors[i] and c = col + neighbors[j].
77+
- If (r, c) is in bounds and abs(``board[r][c]) == 1`` (originally alive), increment liveNeighbors.
78+
- After counting liveNeighbors, apply the Game of Life rules using the cell’s original state (the current value before
79+
any normalization):
80+
- If `board[row][col] == 1` and liveNeighbors is less than 2 (underpopulation) or greater than 3 (overpopulation),
81+
mark it as -1 to encode alive → dead.
82+
- Otherwise, if `board[row][col] == 0` and liveNeighbors is 3, mark it as 2 to encode dead → alive (reproduction).
83+
- Otherwise, leave the cell unchanged (1 stays 1, 0 stays 0).
84+
4. Iterate through the board again to finalize the next generation by normalizing each cell back to 0 or 1:
85+
- If `board[row][col]` > 0 (values 1 or 2), set it to 1.
86+
- Otherwise (for values 0 or -1), set it to 0.
87+
5. The normalized board now represents the next state after applying the rules simultaneously.
88+
89+
90+
### Time complexity
91+
92+
The time complexity is O(m×n), where m and n are the dimensions of the board. Each cell inspects up to 8 neighbors
93+
(constant work) and is processed twice (marking, then normalization).
94+
95+
### Space complexity
96+
97+
The algorithm’s space complexity is constant, O(1).
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from typing import List
2+
3+
4+
def game_of_life(board: List[List[int]]) -> None:
5+
# Store the board’s dimensions
6+
rows = len(board)
7+
cols = len(board[0])
8+
9+
# Offsets to generate the 8 neighbors (order doesn't matter)
10+
neighbors = [-1, 0, 1]
11+
12+
# Pass 1: mark transitions in place
13+
# Encoding:
14+
# -1 : was 1, becomes 0 (live → dead)
15+
# 2 : was 0, becomes 1 (dead → live)
16+
for row in range(rows):
17+
for col in range(cols):
18+
liveNeighbors = 0
19+
20+
# Count originally-live neighbors around (row, col)
21+
for i in range(3):
22+
for j in range(3):
23+
# Skip (0,0) so we don't count the cell itself
24+
if not (neighbors[i] == 0 and neighbors[j] == 0):
25+
r = row + neighbors[i]
26+
c = col + neighbors[j]
27+
28+
# In bounds and originally alive?
29+
# abs(...) == 1 is true for 1 and -1 (originally live),
30+
# and false for 0 and 2 (originally dead)
31+
if 0 <= r < rows and 0 <= c < cols and abs(board[r][c]) == 1:
32+
liveNeighbors += 1
33+
34+
# Apply Conway's rules using the cell's original state
35+
if board[row][col] == 1 and (liveNeighbors < 2 or liveNeighbors > 3):
36+
board[row][col] = -1 # live → dead (under/overpopulation)
37+
38+
if board[row][col] == 0 and liveNeighbors == 3:
39+
board[row][col] = 2 # dead → live (reproduction)
40+
41+
# Pass 2: normalize markers to final 0/1 states
42+
for row in range(rows):
43+
for col in range(cols):
44+
board[row][col] = 1 if board[row][col] > 0 else 0
45+
46+
return board
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import unittest
2+
from typing import List
3+
from parameterized import parameterized
4+
from utils.test_utils import custom_test_name_func
5+
from algorithms.matrix.game_of_life import game_of_life
6+
7+
8+
GAME_OF_LIFE_TEST_CASES = [
9+
([[1]], [[0]]),
10+
([[1, 1], [1, 1]], [[1, 1], [1, 1]]),
11+
([[0, 1, 0], [0, 1, 0], [0, 1, 0]], [[0, 0, 0], [1, 1, 1], [0, 0, 0]]),
12+
([[1, 0, 1], [0, 0, 0], [0, 1, 0]], [[0, 0, 0], [0, 1, 0], [0, 0, 0]]),
13+
([[1, 1, 1], [1, 1, 0], [0, 0, 0]], [[1, 0, 1], [1, 0, 1], [0, 0, 0]]),
14+
]
15+
16+
17+
class GameOfLifeTestCase(unittest.TestCase):
18+
@parameterized.expand(GAME_OF_LIFE_TEST_CASES, name_func=custom_test_name_func)
19+
def test_game_of_life(self, board: List[List[int]], expected: List[List[int]]):
20+
board_copy = board[:]
21+
game_of_life(board_copy)
22+
self.assertEqual(expected, board_copy)
23+
24+
25+
if __name__ == "__main__":
26+
unittest.main()

0 commit comments

Comments
 (0)