题目描述

✅ 289. 生命游戏

image-20260929092146443

image-20260929092146557

image-20260929092146705

题意分析

根据当前面板计算下一代,并直接修改原数组。每格查看水平、垂直和对角线方向的八个邻居:原来存活的细胞只有在活邻居数为 $2$ 或 $3$ 时继续存活;原来死亡的细胞只有在活邻居数恰为 $3$ 时复活。

所有格子必须依据同一代状态同时变化。逐格计算时,前面已经写出的新状态不能影响后面格子的判断,这正是原地更新需要解决的问题。

解法:原地状态编码

核心思路

[!blue]

一个格子需要同时表达旧状态和下一代状态,但不必复制整张面板。保留未发生变化的 $0$、$1$,再引入两个临时值:$2$ 表示“原来活、下一代死”,$3$ 表示“原来死、下一代活”。于是四种旧新组合都能在原格子中区分。

第一遍遍历只标记状态变化。统计某个邻居的旧状态时,值为 $1$ 或 $2$ 都说明它原来存活;$0$ 或 $3$ 都说明它原来死亡。因此不论邻居是否已经处理过,计数使用的始终是同一代信息。

当前格子只会在轮到自己时被写入一次,所以判断它自身时仍是原始的 $0$ 或 $1$:若原来为 $1$ 且活邻居少于 $2$ 或多于 $3$,写成 $2$;若原来为 $0$ 且活邻居恰有 $3$ 个,写成 $3$;其余情况保持原值。方向偏移由 -1、0、1 两两组合,排除 (0, 0) 后恰好是八个方向,越界位置直接略过。

等整张面板完成判断,再开始第二遍:把 $2$ 解码为 $0$,把 $3$ 解码为 $1$,未变化的值保持不动。此时已经没有格子需要读取旧状态,可以安全丢弃旧信息,得到同时更新后的面板。

无限面板的进阶:若活细胞数量有限且分布稀疏,可以用坐标集合只保存活细胞,坐标不受原数组边界限制。遍历每个活细胞,为它的八个邻居累计活邻居数;只有这些邻居位置可能存活或复活。用旧集合判断原状态,再把“邻居数为 $3$”或“邻居数为 $2$ 且原来存活”的位置放入新集合,最后整体替换。没有活邻居的旧活细胞自然不进入新集合,边界外可能诞生的新细胞也不会被截掉。若当前有 p 个活细胞,候选位置至多为 8p,一次更新的期望时间和额外空间都是 $O(p)$。

解题步骤

  1. 遍历每个格子,检查八个方向,跳过自身与越界位置。
  2. 按邻居值为 1 或 2 统计旧活细胞数。
  3. 活转死标为 2,死转活标为 3,其他保持。
  4. 第二次遍历统一解码。

代码实现

class Solution {
    public void gameOfLife(int[][] board) {
        int m = board.length;
        int n = board[0].length;
        int[] dirs = {
            -1,
            0,
            1
        };

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                int live = 0;

                for (int dx : dirs) {
                    for (int dy : dirs) {
                        if (dx == 0 && dy == 0) {
                            continue;
                        }

                        int x = i + dx;
                        int y = j + dy;

                        if (x < 0 || x >= m || y < 0 || y >= n) {
                            continue;
                        }

                        // 值一和值二都表示上一代活细胞,不能读取新一代结果。
                        if (board[x][y] == 1 || board[x][y] == 2) {
                            live++;
                        }
                    }
                }

                if (board[i][j] == 1 && (live < 2 || live > 3)) {
                    // 活转死先记为二,保留旧活状态供邻居读取。
                    board[i][j] = 2;
                } else if (board[i][j] == 0 && live == 3) {
                    board[i][j] = 3;
                }
            }
        }

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                // 整轮判断完成后才统一解码,释放旧状态。
                if (board[i][j] == 2) {
                    board[i][j] = 0;
                } else if (board[i][j] == 3) {
                    board[i][j] = 1;
                }
            }
        }
    }
}
func gameOfLife(board [][]int) {
    m := len(board)
    n := len(board[0])
    dirs := []int{
        -1,
        0,
        1,
    }

    for i := 0; i < m; i++ {
        for j := 0; j < n; j++ {
            live := 0
            for _, dx := range dirs {
                for _, dy := range dirs {
                    if dx == 0 && dy == 0 {
                        continue
                    }
                    x := i + dx
                    y := j + dy
                    if x < 0 || x >= m || y < 0 || y >= n {
                        continue
                    }
                    // 值一和值二都表示上一代活细胞,不能读取新一代结果。
                    if board[x][y] == 1 || board[x][y] == 2 {
                        live++
                    }
                }
            }

            if board[i][j] == 1 && (live < 2 || live > 3) {
                // 活转死先记为二,保留旧活状态供邻居读取。
                board[i][j] = 2
            } else if board[i][j] == 0 && live == 3 {
                board[i][j] = 3
            }
        }
    }

    for i := 0; i < m; i++ {
        for j := 0; j < n; j++ {
            // 整轮判断完成后才统一解码,释放旧状态。
            if board[i][j] == 2 {
                board[i][j] = 0
            } else if board[i][j] == 3 {
                board[i][j] = 1
            }
        }
    }
}

复杂度分析

  • 时间复杂度:$O(mn)$,每格检查固定八个邻居,再统一解码。
  • 空间复杂度:$O(1)$,新旧信息保存在原面板中。

关键点总结

[!green]

  • 所有判断读取同一代旧状态。
  • 只有发生变化的格子需要额外编码。
  • 编码与解码分开,直到整轮结束才丢弃旧信息。

易错点总结

[!yellow]

  • 邻居只认值一:漏掉已经标为活转死的旧活细胞。
  • 把值三也算作旧活:提前计入本轮刚复活的细胞。
  • 统计时包含自身:邻居数多一,规则被改变。
  • 第一遍就解码:后续格子无法恢复正确旧状态。

相似题目

题目 难度 关联与区别
73. 矩阵置零 中等 同样需要避免新写入的状态影响后续读取,原地编码或分两阶段处理可保存旧信息。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2022/36562291
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!