全栈开发知识库

38.生命游戏

题目

生命游戏,简称为 生命 ,是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。

给定一个包含 m × n 个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态: 1 即为 活细胞 (live),或 0 即为 死细胞 (dead)。每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律:

  • 如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡;
  • 如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活;
  • 如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡;
  • 如果死细胞周围正好有三个活细胞,则该位置死细胞复活;

下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是 同时 发生的。给你 m x n 网格面板 board 的当前状态,返回下一个状态。

给定当前 board 的状态,更新 board 到下一个状态。

注意 你不需要返回任何东西。

示例1:

image

  • 输入: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]]

示例2:

image

  • 输入:board = [[1,1],[1,0]]
  • 输出:[[1,1],[1,1]]

提示:

  • m == board.length
  • n == board[i].length
  • 1 <= m, n <= 25
  • board[i][j] 为 0 或 1

进阶:

你可以使用原地算法解决本题吗?请注意,面板上所有格子需要同时被更新:你不能先更新某些格子,然后使用它们的更新后的值再更新其他格子。 本题中,我们使用二维数组来表示面板。原则上,面板是无限的,但当活细胞侵占了面板边界时会造成问题。你将如何解决这些问题?

思路

简化规则:
1. 原来是活的,周围有2-3个活的,成为活的 2. 原来是死的,周围有3个活的,成为活的 3. 其他都是死了

这道题主要就是模拟,遍历每一个格子,然后统计其周围八个格子的活细胞个数,来看这个格子的状态是否改变。

但难点在于:如果这个格子的状态改变,不能直接改变。这样会影响后面格子的统计。即题目中说的:你不能先更新某些格子,然后使用它们的更新后的值再更新其他格子。

因此我们需要使用特殊值去标记发生改变的格子,从而根据特殊值可以知道这个格子原状态是什么,要更新的状态是什么。

有两种方法:

  • 最朴素的方法: 复制一份原矩阵,读旧的、写新的,但是需要O(m*n)的空间。

  • 进阶方法: 用巧妙的状态编码在原地同时保存新旧状态,需要O(1)空间。


  • 可以使用 2表示活细胞变成死细胞,3表示死细胞变成活细胞。【这样的好处是最终是死细胞的都是偶数,活细胞的都是奇数,模2即结果;

也可以用下面的方式:

  • 由于每个位置的细胞的状态是取决于当前四周其他状态的,而且每个细胞的状态是同时变化的,所以不能一个一个地更新,只能在一个新的数组里创建新的状态。

  • 当然上面所说的也不是绝对的,因为这道题目的输入是int[][],矩阵是 int 类型的,有 32 位,而状态只有 0,1 两种,只需一位且只有最低位用上了,我们用其他位存储下一个状态即可,相当于用原矩阵当一个复制的矩阵。

  • 所以可以,原有的最低位存储的是当前状态,那倒数第二低位存储下一个状态就行了。

class Solution {
    public void gameOfLife(int[][] board) {
        int m = board.length;       // 行数
        int n = board[0].length;    // 列数
        int count = 0;              // 统计每个格子周围八个位置的活细胞数
        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                count = 0;      // 每个格子计数重置为0
                for(int x = -1; x <= 1; x++){ // -1 0 1 分别代表当前位置左边的格子、当前格子、当前位置右边的格子
                    for(int y = -1; y <= 1; y++){ // -1 0 1 分别代表当前位置下边的格子、当前格子、当前位置上边的格子
                        // 枚举周围八个位置,其中去掉本身(x = y = 0)和越界(靠近边缘)的情况
                        if((x == 0 && y == 0) || i + x < 0 || i + x >= m || j + y < 0 || j + y >= n)continue;
                        // 如果周围格子是活细胞(1)或者是活细胞变死细胞(2)的,都算一个活细胞
                        if(board[i + x][j + y] == 1 || board[i + x][j + y] == 2) {
                            count++; 
                        }
                    }
                }

                if(board[i][j] == 1 && (count < 2 || count > 3)) {
                    board[i][j] = 2;    // 格子本身是活细胞,周围满足变成死细胞的条件,标记为2
                }

                if(board[i][j] == 0 && count == 3) {
                    board[i][j] = 3;      // 格子本身是死细胞,周围满足复活条件,标记为3
                }
            }
        }

        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                // 死细胞为0,活细胞变成死细胞为2,都为偶数,模2为0,刚好是死细胞
                // 活细胞为1,死细胞变成活细胞为3,都为奇数,模2为1,刚好是活细胞
                board[i][j] %= 2;
            }
        }
    }
}

Kotlin版的实现,先用拷贝矩阵的方法:

拷贝一份数据用于读,然后读取完后再更新到原来的数据中

class Solution {
    fun gameOfLife(board: Array<IntArray>) {
        val m = board.size
        val n = board[0].size
        // 拷贝一份旧状态,专门用来「读」
        val copy = Array(m) { i -> board[i].clone() }

        for (i in 0 until m) {
            for (j in 0 until n) {

                // 用 -1..1 双重循环数活邻居(从 copy 里读旧状态)
                var liveNeighbors = 0
                for (dr in -1..1) {
                    for (dc in -1..1) {
                        if (dr == 0 && dc == 0) {
                            continue
                        }
                        val ni = i + dr
                        val nj = j + dc
                        if (ni in 0 until m && nj in 0 until n && copy[ni][nj] == 1) {
                            liveNeighbors++
                        }
                    }
                }

                // 应用规则,直接写最终 0/1 到 board
                if (copy[i][j] == 1) {
                    // 活细胞:邻居 <2 或 >3 才死,否则保持活
                    if (liveNeighbors !in 2..3) {
                        board[i][j] = 0
                    }
                } else {
                    // 死细胞:正好 3 个活邻居才复活
                    if (liveNeighbors == 3) {
                        board[i][j] = 1
                    }
                }
            }
        }
    }
}

优化方法:

旧活 -> 新将死 用2
旧死 -> 新将活 用3

class Solution {
    fun gameOfLife(board: Array<IntArray>) {
        val m = board.size
        val n = board[0].size

        // ===== 第一遍:用 2、3 编码即将变化的细胞 =====
        for (i in 0 until m) {
            for (j in 0 until n) {

                // ---- 用 -1..1 双重循环数 8 个邻居里的活细胞 ----
                var liveNeighbors = 0
                for (dr in -1..1) {
                    for (dc in -1..1) {
                        if (dr == 0 && dc == 0) continue   // 跳过自己
                        val ni = i + dr
                        val nj = j + dc
                        // 没越界,且邻居「旧状态」是活的(1=活,2=旧活将死)
                        if (ni in 0 until m && nj in 0 until n &&
                            (board[ni][nj] == 1 || board[ni][nj] == 2)) {
                            liveNeighbors++
                        }
                    }
                }

                // ---- 应用规则,把变化的细胞编码成 2 或 3 ----
                if (board[i][j] == 1 && (liveNeighbors < 2 || liveNeighbors > 3)) {
                    board[i][j] = 2   // 旧活 → 将死
                }
                if (board[i][j] == 0 && liveNeighbors == 3) {
                    board[i][j] = 3   // 旧死 → 将活
                }
            }
        }

        // ===== 第二遍:用 %2 把编码翻译回最终的 0/1 =====
        // 0%2=0, 1%2=1, 2%2=0, 3%2=1  正好是各自的新状态
        for (i in 0 until m) {
            for (j in 0 until n) {
                board[i][j] = board[i][j] % 2
            }
        }
    }
}


  • 邮箱 :charon.chui@gmail.com
  • Good Luck!