生命游戏(头条、Google 面试题)

今天是小浩算法“365刷题计划”第102天。每个人的起点和终点都是一样的,但过程却各不相同。我们无法主宰生死却可以选择如何让生命有意义。我们如何用算法来进行一场生命的游戏呢!

01

PART

生命游戏

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

给定一个包含 m × n 个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态:1 即为活细胞(live),或 0 即为死细胞(dead)。

每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律:

  • 如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡;

  • 如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活;

  • 如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡;

  • 如果死细胞周围正好有三个活细胞,则该位置死细胞复活;

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

题目有点复杂,举例说明:

注意:面板上所有格子需要同时被更新:你不能先更新某些格子,然后使用它们的更新后的值再更新其他格子。

02

PART

题解分析

这道题目的关键点是:面板上所有格子需要同时被更新

本题的复杂主要复杂在 4 个更新规则,所以我们需要先对 4 个规则进行掌握(我们仅对下面绿色标出的元素进行说明)

  • R1:细胞数少于两个,则该位置活细胞死亡

  • R2:有两个或三个活细胞,则该位置活细胞仍然存活

  • R3:有超过三个活细胞,则该位置活细胞死亡

  • R4:有三个活细胞,则该位置死细胞复活

四个规则理解起来并不复杂,现在考虑如何解决问题。最自然的想法是:一个个的更新细胞状态。

但是这里我们会遇到一个问题:假设你每次更新完毕后,都把更新后的结果填入数组。那么当前轮其他格子的更新就会引用到你已经更新的结果。啥意思呢:

比如上面这个就是错误的:我们先依据规则4更新了绿色框处的状态,此时蓝色框色周围同样满足规则4。已更新细胞的状态会影响到周围其他还未更新细胞状态的计算。这明显不是我们想要的!

那我们最简单的思路:是不是只要我们能一直获取原始数组的数据,不就可以保证更新一直正确了吗!至于在哪里,其实不管是copy一个数组,还是说用hashmap存一下数值其实都ok。

因为这种思路相对比较简单,我就直接上 leetcode 官方题解的代码了:

//JAVAclass Solution {    public void gameOfLife(int[][] board) {        int[] neighbors = {0, 1, -1};        int rows = board.length;        int cols = board[0].length;        // 创建复制数组 copyBoard        int[][] copyBoard = new int[rows][cols];        // 从原数组复制一份到 copyBoard 中        for (int row = 0; row < rows; row++) {            for (int col = 0; col < cols; col++) {                copyBoard[row][col] = board[row][col];            }        }        // 遍历面板每一个格子里的细胞        for (int row = 0; row < rows; row++) {            for (int col = 0; col < cols; col++) {                // 对于每一个细胞统计其八个相邻位置里的活细胞数量                int liveNeighbors = 0;                for (int i = 0; i < 3; i++) {                    for (int j = 0; j < 3; j++) {                        if (!(neighbors[i] == 0 && neighbors[j] == 0)) {                            int r = (row + neighbors[i]);                            int c = (col + neighbors[j]);                            // 查看相邻的细胞是否是活细胞                            if ((r < rows && r >= 0) && (c < cols && c >= 0) && (copyBoard[r][c] == 1)) {                                liveNeighbors += 1;                            }                        }                    }                }                // 规则 1 或规则 3                      if ((copyBoard[row][col] == 1) && (liveNeighbors < 2 || liveNeighbors > 3)) {                    board[row][col] = 0;                }                // 规则 4                if (copyBoard[row][col] == 0 && liveNeighbors == 3) {                    board[row][col] = 1;                }            }        }    }}

然后有意思的事情发生了,大神之所以是大神,就是大神的思路和普通人不一样。大神说,你这种方法可以是可以,但是占用了新的空间。

你不就想既可以保存原数组的状态,还可以更新新的状态吗?这些统统都可以在原有数组上搞。具体怎么搞呢?

  • 原来的 0 和 1 不就是代表死和生吗?但是你要更新新的状态,无非就是从生->死,从死->生。那我们加个状态 2,代表 生->死,加个状态 3 表示从 死>生。

  • 对于一个节点来说,如果它周边的点是 1 或者 2,就说明该点上一轮是活的。

  • 整体策略是完成 原始状态->过渡状态->真实状态 的过程。

  • 过渡状态 到 真实状态,代码就是把 0 和 2 变回 0,1 和 3 变回1的过程。用模只是代码技巧。

  • 策略实现的第一步是先找到当前节点周围的存活节点数。

代码大概就是这样:

//JAVApublic class Solution {    public void gameOfLife(int[][] board) {        int m = board.length, n = board[0].length;        // 原始状态 -> 过渡状态        for(int i = 0; i < m; i++){            for(int j = 0; j < n; j++){                int liveNeighbors  = 0;                // 判断上边                if(i > 0){                    liveNeighbors  += board[i - 1][j] == 1 || board[i - 1][j] == 2 ? 1 : 0;                }                // 判断左边                if(j > 0){                    liveNeighbors  += board[i][j - 1] == 1 || board[i][j - 1] == 2 ? 1 : 0;                }                // 判断下边                if(i < m - 1){                    liveNeighbors  += board[i + 1][j] == 1 || board[i + 1][j] == 2 ? 1 : 0;                }                // 判断右边                if(j < n - 1){                    liveNeighbors  += board[i][j + 1] == 1 || board[i][j + 1] == 2 ? 1 : 0;                }                // 判断左上角                if(i > 0 && j > 0){                    liveNeighbors  += board[i - 1][j - 1] == 1 || board[i - 1][j - 1] == 2 ? 1 : 0;                }                //判断右下角                if(i < m - 1 && j < n - 1){                    liveNeighbors  += board[i + 1][j + 1] == 1 || board[i + 1][j + 1] == 2 ? 1 : 0;                }                // 判断右上角                if(i > 0 && j < n - 1){                    liveNeighbors  += board[i - 1][j + 1] == 1 || board[i - 1][j + 1] == 2 ? 1 : 0;                }                // 判断左下角                if(i < m - 1 && j > 0){                    liveNeighbors  += board[i + 1][j - 1] == 1 || board[i + 1][j - 1] == 2 ? 1 : 0;                }                // 根据周边存活数量更新当前点,结果是 0 和 1 的情况不用更新                if(board[i][j] == 0 && liveNeighbors  == 3){                    board[i][j] = 3;                } else if(board[i][j] == 1){                    if(liveNeighbors  < 2 || liveNeighbors  > 3) board[i][j] = 2;                }            }        }        // 过渡状态 -> 真实状态        for(int i = 0; i < m; i++){            for(int j = 0; j < n; j++){                board[i][j] = board[i][j] % 2;            }        }    }}

细心的读者也许会想到,这不就是卡诺图吗?是的。在大多数的矩阵状态变化类题目中,卡诺图、状态机 等都是一些常用的技巧。

一般的通用解法为:

1、大部分都是遍历两次矩阵,第一遍引入中间值(中间状态),存储一些原矩阵不包含的额外信息。

2、通过 原始矩阵->过渡矩阵->真实矩阵 的策略,在结尾处将中间状态置成真实状态。

3、当遍历到某个位置时,需要查看它周边的位置,此时如果每一个周围的位置都手写,然后再判断是否越界,就很麻烦。一般用一个数组,保存向周边位置变化的坐标偏移值。

4、如果是 0 和 1 的状态置换,可以使用位运算来秀操作。如果涉及状态过多,考虑是否可以简化状态。

总之:这是一道相对比较经典的题目,如果大家有兴趣,大家可以练习一下 第73题 矩阵置零,也是类似的解法。

03

PART

每日算法

宽度优先搜索算法(又称广度优先搜索)是最简便的图的搜索算法之一,这一算法也是很多重要的图的算法的原型。Dijkstra单源最短路径算法和Prim最小生成树算法都采用了和宽度优先搜索类似的思想。其别名又叫BFS,属于一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。

(0)

相关推荐

  • ​LeetCode刷题实战37: 解数独

    算法的重要性,我就不多说了吧,想去大厂,就必须要经过基础知识和业务逻辑面试+算法面试.所以,为了提高大家的算法能力,这个公众号后续每天带大家做一道算法题,题目就从LeetCode上面选 ! 今天和大家 ...

  • 动态内存分配连续内存空间的二维数组

    可以直接使用一维数组来模拟二维数组,下面的代码就是在此基础上,用一个二级指针指向一维数组的相应地方,详见代码 #include <stdio.h> #include <malloc. ...

  • C |深入理解数组的维度及其指针的级数和移动

    C |深入理解数组的维度及其指针的级数和移动 小智雅汇2021-02-15 14:35:14 数组作为函数参数时会转变为指针.怎样转变呢?将数组名转变为指向数组首元素的指针变量.如有三维数组: int ...

  • 每日一题 剑指offer(机器人的运动范围)

    编程是很多偏计算机.人工智能领域必须掌握的一项技能,此编程能力在学习和工作中起着重要的作用.因此小白决定开辟一个新的板块"每日一题",通过每天一道编程题目来强化和锻炼自己的编程能力 ...

  • 传奇数学家康威,生命游戏的创造者,最伟大的数学玩家

    2020年4月11日,约翰·霍顿·康威因新冠病逝,享年82岁.这位杰出的数学家的研究领域包括群论.扭结理论.几何.分析.组合博弈论.代数.算法,甚至理论物理学. 康威发明了一种游戏,叫做"生 ...

  • 【面试分享】今日头条Java面试题,复习资料完整版PDF

    2021年,字节的技术岗依旧是最香的,而且随着字节的规模不断扩大,机会也越来越多.马上迎来金三银四,很多小伙伴都在撸题备战中. 下面是1月份最新的头条面经,我们来看看今年大厂的Java面试主要考察哪些 ...

  • 康威生命游戏(细胞自动机)C# 控制台

    效果 规则(来自百度百科,康威生命游戏词条) 游戏开始时,每个细胞随机地设定为"生"或"死"之一的某个状态.然后,根据某种规则,计算出下一代每个细胞的状态,画出 ...

  • 好学校的五大生命气象 | 头条

    什么样的学校是好学校?好学校究竟是什么样子?好学校有哪些生命气象?对这些问题的思考和追问,都非常必要和重要. 有人说,"好学校,学生不是惴惴不安而来,满腹惆怅而去,而是每天带着向往和期待走进 ...

  • 视频 | 数学家约翰·康威与他发明的生命游戏

    非常欢迎更多朋友加入到[遇见数学]的翻译小组, 一道为传播数学文化.展现更精彩的数学魅力而前行.  » 加入链接康威因发明了康威生命游戏(Game of Life)而特别出名,它是元胞自动机的先声.他 ...

  • 《生命游戏》FPGA实例评测:​有限状态机用哪种编码更好?(附:源码下载)

    有限状态机(FSM)是几乎每个数字系统中非常常见的部分.综合工具通常会检查你的代码以检测FSM,并执行可能会修改状态编码的优化. FSM的三种编码 用于FSM的三种最受欢迎的编码是二进制(Binary ...

  • 一路打怪回家!帝王鲑的生命游戏是very hard模式的

    "海外鱼来亿万浮,逆流方口是鳑[pánɡ]头.至今腹上留红印,曾说孤东入御舟."这首诗载于民国8年黑龙江报馆出版的书籍<龙城旧闻>,是近代医学专家朱履中根据清朝人西清& ...

  • 生命游戏

    一 著名数学家,普林斯顿大学和剑桥大学教授约翰·康威因感染新冠病毒于2020年4月11日去世,享年82岁. 康威活跃于有限群的研究.趣味数学.纽结理论.数论.组合博弈论和编码学等范畴. 他年少时就对数 ...

  • 当生命游戏遇上新冠病毒

    [正文在后面] 公众号"数学风景"创建于2016年4月,致力于和大家分享好文章,内容涉及到高中数学的知识体系,趣味史话,解题技巧,规律总结,高考研究等等,相信会给高中学生以及高中数 ...