LeetCode-289. 生命游戏

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

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

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

根据当前状态,写一个函数来计算面板上所有细胞的下一个(一次更新后的)状态。下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是同时发生的。

示例:

输入: 
[
  [0,1,0],
  [0,0,1],
  [1,1,1],
  [0,0,0]
]
输出:
[
  [0,0,0],
  [1,0,1],
  [0,1,1],
  [0,1,0]
]

进阶:

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

题目链接:https://leetcode-cn.com/problems/game-of-life

方法一:复制原数组进行模拟
思路

这个问题看起来很简单,但有一个陷阱,如果你直接根据规则更新原始数组,那么就做不到题目中说的 同步 更新。假设你直接将更新后的细胞状态填入原始数组,那么当前轮次其他细胞状态的更新就会引用到当前轮已更新细胞的状态,但实际上每一轮更新需要依赖上一轮细胞的状态,是不能用这一轮的细胞状态来更新的。
在这里插入图片描述
如上图所示,已更新细胞的状态会影响到周围其他还未更新细胞状态的计算。一个最简单的解决方法就是复制一份原始数组,复制的那一份永远不修改,只作为更新规则的引用。这样原始数组的细胞值就不会被污染了。
在这里插入图片描述
算法

  1. 复制一份原始数组;
  2. 根据复制数组中邻居细胞的状态来更新 board 中的细胞状态。
class Solution {
public:
    vector<vector<int>> gameOfLife(vector<vector<int>>& board) {
        int rows = board.size(); 
        int cols = board[0].size();
        //复制数组
        vector<vector<int>> board_copy(rows, vector<int>(cols, 0));
        for(int i=0; i<rows; i++){
            for(int j=0; j<cols; j++){
                board_copy[i][j] = board[i][j];
            }
        }
        //声明8个方向数组
        int neighbors[8][2] = {{-1, -1}, {-1, 0}, {-1, 1}, 
                                {0, -1}, {0, 1}, 
                                {1, -1}, {1, 0}, {1, 1}};
        for(int i=0; i<rows; i++){
            for(int j=0; j<cols; j++){
                //8个方向扫描
                int liveNeighbors = 0; //统计8个方向的活细胞数目
                for(int k=0; k<8; k++){
                    int row = i + neighbors[k][0];
                    int col = j + neighbors[k][1];
                    if((row>=0 && row<rows) && (col>=0 && col<cols) 
                        && board_copy[row][col]==1){
                            liveNeighbors += 1;
                        }
                }
                //1, 3条件, 细胞失活
                if(board_copy[i][j]==1 && (liveNeighbors<2 or liveNeighbors>3)){
                    board[i][j] = 0;
                }
                //条件4复活
                if(board_copy[i][j]==0 && liveNeighbors==3){
                    board[i][j] = 1;
                }
            }
        }

        return board;
    }
};

复杂度分析

  1. 时间复杂度:O(mn)O(mn),其中 mm 和 nn 分别为 board 的行数和列数。
  2. 空间复杂度:O(mn)O(mn),为复制数组占用的空间。

方法二:使用额外的状态
思路

方法一中 O(mn)O(mn) 的空间复杂度在数组很大的时候内存消耗是非常昂贵的。题目中每个细胞只有两种状态 live(1) 或 dead(0),但我们可以拓展一些复合状态使其包含之前的状态。举个例子,如果细胞之前的状态是 0,但是在更新之后变成了 1,我们就可以给它定义一个复合状态 2。这样我们看到 2,既能知道目前这个细胞是活的,还能知道它之前是死的
在这里插入图片描述
算法

  1. 遍历 board 中的细胞。

  2. 根据数组的细胞状态计算新一轮的细胞状态,这里会用到能同时代表过去状态和现在状态的复合状态。

  3. 具体的计算规则如下所示:
    规则 1:如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡。这时候,将细胞值改为 -1,代表这个细胞过去是活的现在死了;
    规则 2:如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活。这时候不改变细胞的值,仍为 1;
    规则 3:如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡。这时候,将细胞的值改为 -1,代表这个细胞过去是活的现在死了。可以看到,因为规则 1 和规则 3 下细胞的起始终止状态是一致的,因此它们的复合状态也一致;
    规则 4:如果死细胞周围正好有三个活细胞,则该位置死细胞复活。这时候,将细胞的值改为 2,代表这个细胞过去是死的现在活了。

  4. 根据新的规则更新数组;

  5. 现在复合状态隐含了过去细胞的状态,所以我们可以在不复制数组的情况下完成原地更新;

  6. 对于最终的输出,需要将 board 转成 0,1 的形式。因此这时候需要再遍历一次数组,将复合状态为 2 的细胞的值改为 1,复合状态为 -1 的细胞的值改为 0。

class Solution {
public:
    vector<vector<int>> gameOfLife(vector<vector<int>>& board) {
        int rows = board.size(); 
        int cols = board[0].size();
        //声明8个方向数组
        int neighbors[8][2] = {{-1, -1}, {-1, 0}, {-1, 1}, 
                                {0, -1}, {0, 1}, 
                                {1, -1}, {1, 0}, {1, 1}};
        for(int i=0; i<rows; i++){
            for(int j=0; j<cols; j++){
                //8个方向扫描
                int liveNeighbors = 0; //统计8个方向的活细胞数目
                for(int k=0; k<8; k++){
                    int row = i + neighbors[k][0];
                    int col = j + neighbors[k][1];
                    if((row>=0 && row<rows) && (col>=0 && col<cols) 
                        && abs(board[row][col])==1){
                            //此处使用abs这个技巧,原来失活设置为-1,取绝对值之后就能恢复原样。
                            liveNeighbors += 1;
                        }
                }
                //1, 3条件, 细胞失活
                if(board[i][j]==1 && (liveNeighbors<2 or liveNeighbors>3)){
                    //-1代表细胞失活,本来是1
                    board[i][j] = -1;
                }
                //条件4复活
                if(board[i][j]==0 && liveNeighbors==3){
                    //细胞复活,本来是0
                    board[i][j] = 2;
                }
            }
        }

        //标志回位
        for(int i=0; i<rows; i++){
            for(int j=0; j<cols; j++){
                if(board[i][j]>0){
                    board[i][j] = 1;
                }
                else{
                    board[i][j] = 0;
                }
            }
        }

        return board;
    }
};

方法三:
使用卷积的思想

class Solution:
    def gameOfLife(self, board: List[List[int]]) -> None:
        """
        Do not return anything, modify board in-place instead.
        """
        import numpy as np
        r,c=len(board),len(board[0])
        #下面两行做zero padding
        board_exp=np.array([[0 for _ in range(c+2)] for _ in range(r+2)])
        board_exp[1:1+r,1:1+c]=np.array(board)
        #设置卷积核
        kernel=np.array([[1,1,1],[1,0,1],[1,1,1]])
        #开始卷积
        for i in range(1,r+1):
            for j in range(1,c+1):
                #统计细胞周围8个位置的状态
                temp_sum=np.sum(kernel*board_exp[i-1:i+2,j-1:j+2])
                #按照题目规则进行判断
                if board_exp[i,j]==1:
                    if temp_sum<2 or temp_sum>3:
                        board[i-1][j-1]=0
                else:
                    if temp_sum==3:
                        board[i-1][j-1]=1          

        return board
     
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值