算法:生命游戏

778 阅读3分钟

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

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

  • 如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡;
  • 如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活;
  • 如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡;
  • 如果死细胞周围正好有三个活细胞,则该位置死细胞复活; 下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是同时发生的。给你 m x n 网格面板 board 的当前状态,返回下一个状态。

示例 1:
image.png

输入: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.png

输入: 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
    进阶:
  • 你可以使用原地算法解决本题吗?请注意,面板上所有格子需要同时被更新:你不能先更新某些格子,然后使用它们的更新后的值再更新其他格子。
  • 本题中,我们使用二维数组来表示面板。原则上,面板是无限的,但当活细胞侵占了面板边界时会造成问题。你将如何解决这些问题? 计算细胞的下一个状态,需要获取每个细胞周围的状态,并结合策略条件计算出细胞的下一个状态。整个计算过程可拆解为三个步骤:
  • 获取当前细胞的相关(周围)细胞,一共包含8个,但得考虑边缘细胞,不存在可以用专门的标示来表示
  • 根据当前细胞状态和获取的周围细胞状态,结合更新策略计算下一个状态
  • 遍历所有细胞并重复执行前两个步骤 定义getNearCells函数来获取curRow、curCol周围的细胞,不存在的用空数组[]表示,考虑扩展性,计算的每个周边细胞都存储了索引以及当前状态[rowIndex, colIndex, status]。
/**
 * 获取当前索引周围的细胞集合
 * @param {*} rowIndex 
 * @param {*} colIndex 
 * @param {*} board 
 * @returns 
 */
    function getNearCells(curRow, curCol, board) {
    //从左上沿顺时针方向计算8个位置,不存在的设置为空数组[]
    const nears = []
    for (let startRow = curRow - 1; startRow <= curRow + 1; startRow++) {
        for (let startCol = curCol - 1; startCol <= curCol + 1; startCol++) {
            if (curRow === startRow && curCol === startCol) {
                continue
            }
            if (board[startRow] === undefined) {
                nears.push([])
            } else if (board[startRow][startCol] === undefined) {
                nears.push([])
            } else {
                // 存储状态和索引,便于扩展
                nears.push([startRow, startCol, board[startRow][startCol]])
            }
        }
    }

    return nears
}

在遍历周边细胞时要注意判断条件,需要用undefined来判断是否超出数组范围,而不是取非!,因为但状态为0时取非也为true。

定义calcNextStatus函数获取细胞的下一个状态,参数包含通过函数getNearCells计算出来周边细胞列表,策略主要判断活细胞的个数,所以先把周边的活细胞数量求出。策略按细胞状态分为死细胞判断、活细胞判断两类。

/**
 * 计算当前细胞的下一个状态
 * @param {*} curCell 
 * @param {周围细胞} nearCells 
 * @returns 
 */
function calcNextStatus(curCell, nearCells) {
    const activeCellCount = nearCells.reduce((preVal, curVal) => {
        if (curVal.length === 3) {
            return preVal + curVal[2]
        }
        return preVal
    }, 0)

    const next = {
        // 当前细胞状态为0,命中死亡细胞判断策略
        0: () => {
            const isEqualThree = activeCellCount === 3

            return isEqualThree ? 1 : 0
        },
        // 当前细胞状态为1, 命中活细胞判断策略
        1: () => {
            if (activeCellCount < 2) {
                return 0
            } else if (activeCellCount === 2 || activeCellCount === 3) {
                return 1
            } else if (3 < activeCellCount) {
                return 0
            }
        }
    }

    return next[curCell[2]]()
}

代码定义了next函数,next下包含0、1两个处理函数,分别表示死细胞计算策略、活细胞计算策略。判断条件比较简单,直接用了if/else实现,如果判断比较复杂,可以考虑用决策树的方式判断。

最后定义gameOfLife函数遍历每个细胞,并结合getNearCells、calcNextStatus获取下一状态,需要注意的是,题目要求不返回任何结果,直接在原数组上修改,可以通过拷贝的方式备份上一状态集合。

/**
 * @param {number[][]} board
 * @return {void} Do not return anything, modify board in-place instead.
 */
 var gameOfLife = function(board) {
    if (!board.length || !board[0].length) {
        throw new Error('网格格式错误.')
    }
    // 拷贝
    const preBoard = JSON.parse(JSON.stringify(board))
    const rowLen = board.length, colLen = board[0].length

    for (let rowIndex = 0; rowIndex < rowLen; rowIndex++) {
        preBoard[rowIndex] = preBoard[rowIndex] || []
        for (let colIndex = 0; colIndex < colLen; colIndex++) {
            board[rowIndex][colIndex] = calcNextStatus([rowIndex, colIndex, preBoard[rowIndex][colIndex]], getNearCells(rowIndex, colIndex, preBoard))
        }
    }
}

通过拷贝加遍历的方式,时间复杂度为O(m * n)、空间复杂度为O(m * n),当细胞数量比较大的时候拷贝方式会占用大量内存,可以考虑用一个特殊状态同时标示出下一状态、上一状态,例如状态从死细胞变为活细胞,可用2来表示该细胞由死变活,这样在遍历其他细胞时也能推算出细胞的原始状态。但也要考虑两个点:

  • 最后还需要在遍历一次细胞,把特殊标示2设置为1
  • 空间复杂度变为O(1),但时间复杂度也会增加 另外一种思路,由原来周围细胞影响当前细胞的方式,变为由当前细胞主动影响周围细胞的方式,例如如果当前细胞为活细胞,可给周围的细胞加10, 例如41表示周围有4个活细胞影响。但如果细胞状态比较多的话,这种思路写起来就比较饶了。