题目
标题和出处
标题:生命游戏
出处:289. 生命游戏
难度
4 级
题目描述
要求
生命游戏,简称为生命,是英国数学家约翰·何顿·康威在 1970 \texttt{1970} 1970 年发明的细胞自动机。
给定一个包含 m × n \texttt{m} \times \texttt{n} m×n 个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态: 1 \texttt{1} 1 即为活细胞(live),或 0 \texttt{0} 0 即为死细胞(dead)。每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律:
-
如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡;
-
如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活;
-
如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡;
-
如果死细胞周围正好有三个活细胞,则该位置死细胞复活。
下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是同时发生的。给你 m × n \texttt{m} \times \texttt{n} m×n 网格面板 board \texttt{board} board 的当前状态,返回下一个状态。
示例
示例 1:
输入:
board
=
[[0,1,0],[0,0,1],[1,1,1],[0,0,0]]
\texttt{board = [[0,1,0],[0,0,1],[1,1,1],[0,0,0]]}
board = [[0,1,0],[0,0,1],[1,1,1],[0,0,0]]
输出:
[[0,0,0],[1,0,1],[0,1,1],[0,1,0]]
\texttt{[[0,0,0],[1,0,1],[0,1,1],[0,1,0]]}
[[0,0,0],[1,0,1],[0,1,1],[0,1,0]]
示例 2:
输入:
board
=
[[1,1],[1,0]]
\texttt{board = [[1,1],[1,0]]}
board = [[1,1],[1,0]]
输出:
[[1,1],[1,1]]
\texttt{[[1,1],[1,1]]}
[[1,1],[1,1]]
数据范围
- m = board.length \texttt{m}=\texttt{board.length} m=board.length
- n = board[i].length \texttt{n}=\texttt{board[i].length} n=board[i].length
- 1 ≤ m, n ≤ 25 \texttt{1} \le \texttt{m, n} \le \texttt{25} 1≤m, n≤25
- board[i][j] \texttt{board[i][j]} board[i][j] 为 0 \texttt{0} 0 或 1 \texttt{1} 1
进阶
-
你可以使用原地算法解决本题吗?请注意,面板上所有格子需要同时被更新:你不能先更新某些格子,然后使用它们的更新后的值再更新其他格子。
-
本题中,我们使用二维数组来表示面板。原则上,面板是无限的,但当活细胞侵占了面板边界时会造成问题。你将如何解决这些问题?
解法一
思路和算法
显然,对面板中的格子进行更新时,必须每次更新一个格子。但是题目要求所有格子需要同时被更新,因此需要根据面板的原始状态决定面板的新状态。
如果直接更新格子的值,在判断后面的格子是否需要更新时就无法知道前面的格子的原始值。为了避免前面格子的新值影响后面格子的更新,一个办法是将面板值复制出一份作为原始值,然后就能根据原始值得到面板上每个格子的新值。
具体而言,已知给定的面板 board \textit{board} board 是 m m m 行 n n n 列的,创建一个 m m m 行 n n n 列的二维数组 original \textit{original} original,将 board \textit{board} board 的值复制到 original \textit{original} original 中。然后遍历面板的每个位置,对于每个位置,计算 original \textit{original} original 中的该位置的格子的相邻格子的活细胞数,并更新 board \textit{board} board 中的该位置的值。
位于角上的格子有 3 3 3 个相邻的格子,位于边上的格子有 5 5 5 个相邻的格子,位于中间的格子有 8 8 8 个相邻的格子。遍历全部相邻的格子即可知道相邻格子的活细胞数,然后进行如下更新:
-
如果 original \textit{original} original 中的元素是 1 1 1,则当周围的活细胞数少于 2 2 2 个或多于 3 3 3 个时, board \textit{board} board 中的对应元素变成 0 0 0,否则仍然是 1 1 1;
-
如果 original \textit{original} original 中的元素是 0 0 0,则当周围的活细胞数等于 3 3 3 个时, board \textit{board} board 中的对应元素变成 1 1 1,否则仍然是 0 0 0。
遍历结束之后, board \textit{board} board 即为面板的下一个状态的值。
代码
class Solution {
public void gameOfLife(int[][] board) {
int[][] directions = {{-1, -1}, {-1, 0}, {-1, 1}, {0, 1}, {1, 1}, {1, 0}, {1, -1}, {0, -1}};
int m = board.length, n = board[0].length;
int[][] original = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
original[i][j] = board[i][j];
}
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
int count = 0;
for (int[] direction : directions) {
int newRow = i + direction[0], newColumn = j + direction[1];
if (newRow >= 0 && newRow < m && newColumn >= 0 && newColumn < n) {
int value = original[newRow][newColumn];
if (value == 1) {
count++;
}
}
}
if (original[i][j] == 1) {
if (count < 2 || count > 3) {
board[i][j] = 0;
}
} else {
if (count == 3) {
board[i][j] = 1;
}
}
}
}
}
}
复杂度分析
-
时间复杂度: O ( m n ) O(mn) O(mn),其中 m m m 和 n n n 分别是面板的行数和列数。需要遍历面板中的每个元素,并对每个元素遍历其相邻元素计算活细胞的数量和计算新状态的值,每个元素的操作时间都是常数。
-
空间复杂度: O ( m n ) O(mn) O(mn),其中 m m m 和 n n n 分别是面板的行数和列数。需要创建与面板 board \textit{board} board 相同大小的二维数组 original \textit{original} original。
解法二
思路和算法
上述解法为了避免前面格子的新值影响后面格子的更新,将面板值复制出一份作为原始值。其实还有一种做法,如果每个格子的值可以包含多种信息,则可以独立地得到原状态和新状态,然后原地更新面板。
为了包含多种信息,需要引入两种新的状态,因此共有四种状态:
-
0 0 0 表示原状态和新状态都是死细胞;
-
1 1 1 表示原状态和新状态都是活细胞;
-
2 2 2 表示原状态是活细胞,新状态是死细胞;
-
3 3 3 表示原状态是死细胞,新状态是活细胞。
四种状态中, 1 1 1 和 2 2 2 表示原状态是活细胞, 0 0 0 和 3 3 3 表示原状态是死细胞,在计算每个位置的格子的相邻格子的活细胞数时,需要考虑 1 1 1 和 2 2 2 这两种状态。
更新面板中的格子的规则也需要调整:
-
如果 board \textit{board} board 中的元素是 1 1 1,则当周围的活细胞数少于 2 2 2 个或多于 3 3 3 个时, board \textit{board} board 中的对应元素变成 2 2 2,否则仍然是 1 1 1;
-
如果 board \textit{board} board 中的元素是 0 0 0,则当周围的活细胞数等于 3 3 3 个时, board \textit{board} board 中的对应元素变成 3 3 3,否则仍然是 0 0 0。
遍历结束之后,需要再次遍历 board \textit{board} board 并更新每个格子的值。注意到 0 0 0 和 2 2 2 表示新状态是死细胞, 1 1 1 和 3 3 3 表示新状态是活细胞,因此将 board \textit{board} board 中的每个元素的值更新成该元素除以 2 2 2 的余数即可。利用位运算的性质,一个数除以 2 2 2 的余数等价于这个数和 1 1 1 按位与运算的结果。
代码
class Solution {
public void gameOfLife(int[][] board) {
int[][] directions = {{-1, -1}, {-1, 0}, {-1, 1}, {0, 1}, {1, 1}, {1, 0}, {1, -1}, {0, -1}};
int m = board.length, n = board[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
int count = 0;
for (int[] direction : directions) {
int newRow = i + direction[0], newColumn = j + direction[1];
if (newRow >= 0 && newRow < m && newColumn >= 0 && newColumn < n) {
int value = board[newRow][newColumn];
if (value == 1 || value == 2) {
count++;
}
}
}
if (board[i][j] == 1) {
if (count < 2 || count > 3) {
board[i][j] = 2;
}
} else {
if (count == 3) {
board[i][j] = 3;
}
}
}
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
board[i][j] &= 1;
}
}
}
}
复杂度分析
-
时间复杂度: O ( m n ) O(mn) O(mn),其中 m m m 和 n n n 分别是面板的行数和列数。需要遍历面板两次,第一次遍历时需要对每个元素遍历其相邻元素计算活细胞的数量和计算新状态的值,每个元素的操作时间都是常数,第二次遍历时需要对每个元素得到最终状态的值,每个元素的操作时间都是常数。
-
空间复杂度: O ( 1 ) O(1) O(1)。
进阶问题答案
第一个进阶问题,使用原地算法解决。解法二使用额外的信息原地修改 board \textit{board} board 的值,空间复杂度是 O ( 1 ) O(1) O(1),就是原地算法的实现。
第二个进阶问题,面板是无限的。对于无限的情况,无法使用数组或者其他数据结构存储整个面板,因此需要换一种思路。其实,在生命游戏中,有生命的格子是少数,没有生命的格子才是大多数,而需要关心的只有活细胞,只要记录活细胞所在格子的位置即可。