R.I.P John Conway.

由于近期席卷全球的新冠肺炎,John Conway 不幸染病离世。生命游戏 (Game of Life) 是其最为著名的成果。几天前我在 Codewars 上刷到了相关的题目,花了一段时间做了出来,在此写一篇文章,就当是为了纪念 Conway 吧。

题目

Given a 2D array and a number of generations, compute n timesteps of Conway’s Game of Life.

The rules of the game are:

  1. Any live cell with fewer than two live neighbours dies, as if caused by underpopulation.
  2. Any live cell with more than three live neighbours dies, as if by overcrowding.
  3. Any live cell with two or three live neighbours lives on to the next generation.
  4. Any dead cell with exactly three live neighbours becomes a live cell.

Each cell’s neighborhood is the 8 cells immediately around it (i.e. Moore Neighborhood). The universe is infinite in both the x and y dimensions and all cells are initially dead - except for those specified in the arguments. The return value should be a 2d array cropped around all of the living cells. (If there are no living cells, then return [[]].)

题目其实就是生命游戏的定义:

在一个无限大的棋盘上,我们以 1 代表存活的生命,以 0 代表死去的生命或

其上的生命遵循上面的四条规则存活、死去、或是产生下一代。

思路

public static int[,] GetGeneration(int[,] cells, int generation)

可以看到函数接收了二维数组与一个整数,分别代表了其上的生命与要演化的代数。

首先我们要认识清楚,棋盘的大小是无限的,而不仅仅是接收的二维数组的大小。当然,题中也给出了计算邻居的规则,我们在处理时,要将接收到的数组扩大一圈。

新建一个长宽各自 +2 的矩形,再把目前的矩形放在其正中间,最后返回这个矩形。

public static int[,] NewBoard(int[,] cells)
{
    var hang = cells.GetLength(0);
    var lie = cells.GetLength(1);

    var newBoard = new int[hang + 2, lie + 2];

    for (int i = 0; i < hang + 2; i++)
    {
        for (int j = 0; j < lie + 2; j++)
        {
            newBoard[i, j] = 0;
        }
    }

    for (int i = 1; i < hang + 1; i++)
    {
        for (int j = 1; j < lie + 1; j++)
        {
            newBoard[i, j] = cells[i - 1, j - 1];
        }
    }

    return newBoard;
}

相应的,在返回最终的结果时,棋盘除了有生命存在的一个矩形之外,也要裁剪掉多出的全为 0 的行与列。

这里是分别从上下、左右遍历了数组,获取了其存在 1 的最外层的坐标,然后把它裁剪了下来,返回了最精简的数组。

public static int[,] cropped(int[,] cells)
{
    var hang = cells.GetLength(0);
    var lie = cells.GetLength(1);
    var (hStart, lStart, hEnd, lEnd, hLen, lLen) = (0, 0, 0, 0, 0, 0);

    for (int i = 0; i < hang; i++)
    {
        for (int j = 0; j < lie; j++)
        {
            if (cells[i, j] != 0)
            {
                hStart = i;
                i = hang;
                break;
            }
        }
    }

    for (int i = hang - 1; i >= 0; i--)
    {
        for (int j = lie - 1; j >= 0; j--)
        {
            if (cells[i, j] != 0)
            {
                hEnd = i;
                i = 0;
                break;
            }
        }
    }

    for (int i = 0; i < lie; i++)
    {
        for (int j = 0; j < hang; j++)
        {
            if (cells[j, i] != 0)
            {
                lStart = i;
                i = lie;
                break;
            }
        }
    }


    for (int i = lie - 1; i >= 0; i--)
    {
        for (int j = hang - 1; j >= 0; j--)
        {
            if (cells[j, i] != 0)
            {
                lEnd = i;
                i = 0;
                break;
            }
        }
    }

    hLen = hEnd - hStart + 1;
    lLen = lEnd - lStart + 1;

    var newCell = new int[hLen, lLen];

    for (int i = 0; i < hLen; i++)
    {
        for (int j = 0; j < lLen; j++)
        {
            newCell[i, j] = cells[hStart + i, lStart + j];
        }
    }

    return newCell;
}

确定了最先与最后的步骤,就要处理中间的过程了。

为了确定某一个坐标下的生物在下一轮中是死是活,只需要获取它有多少存活的邻居,再用给出的四条规则来判定就能得出了。

首先是获取某一特定坐标的生物有多少活着的邻居,仅需遍历它八个邻居的位置并确定是否存活即可。值得注意的是,对于最外层、邻居不足八个的生物要做特殊的处理,以防数组越界。由于我懒得确定是否越界,就用了一个邪道玩法,如果出现了数组越界的 Exception 就忽略掉它继续访问下一个地址。

public static int GetLiveNeighbors(int x, int y, int[,] ceil)
{
    var val = 0;
    int[] neighbors = { 0, 1, -1 };

    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 3; j++)
        {
            if (!(neighbors[i] == 0 && neighbors[j] == 0))
            {
                int r = (x + neighbors[i]);
                int c = (y + neighbors[j]);

                try
                {
                    if (ceil[r, c] == 1)
                    {
                        val += 1;
                    }
                }
                catch (System.Exception)
                {
                    val += 0;
                    continue;
                }
            }
        }
    }

    return val;
}

下面则是确定某一坐标下的生物在下一轮中是死是活。

var hang = cells.GetLength(0);
var lie = cells.GetLength(1);
var newGen = new int[hang + 2, lie + 2];
var biggerCells = NewBoard(cells);

for (int x = 0; x < hang + 2; x++)
{
    for (int y = 0; y < lie + 2; y++)
    {
        var n = GetLiveNeighbors(x, y, biggerCells);
        var c = biggerCells[x, y];

        newGen[x, y] = ((c == 1) && (n == 2 || n == 3) || (c == 0) && n == 3) ? 1 : 0;
    }
}

var result = cropped(newGen);

至于代数,一个递归就能解决问题了。在此不作赘述。

完整代码

using System;
using System.Linq;

public class ConwayLife
{
    public static int[,] GetGeneration(int[,] cells, int generation)
    {
        if (generation <= 0)
        {
            return cells;
        }

        var hang = cells.GetLength(0);
        var lie = cells.GetLength(1);

        var newGen = new int[hang + 2, lie + 2];

        var biggerCells = NewBoard(cells);


        for (int x = 0; x < hang + 2; x++)
        {
            for (int y = 0; y < lie + 2; y++)
            {
                var n = GetLiveNeighbors(x, y, biggerCells);
                var c = biggerCells[x, y];

                newGen[x, y] = ((c == 1) && (n == 2 || n == 3) || (c == 0) && n == 3) ? 1 : 0;
            }
        }

        var result = cropped(newGen);

        return GetGeneration(result, generation - 1);
    }

    public static int GetLiveNeighbors(int x, int y, int[,] ceil)
    {
        var val = 0;
        int[] neighbors = { 0, 1, -1 };

        for (int i = 0; i < 3; i++)
        {
            for (int j = 0; j < 3; j++)
            {
                if (!(neighbors[i] == 0 && neighbors[j] == 0))
                {
                    int r = (x + neighbors[i]);
                    int c = (y + neighbors[j]);

                    try
                    {
                        if (ceil[r, c] == 1)
                        {
                            val += 1;
                        }
                    }
                    catch (System.Exception)
                    {
                        val += 0;
                        continue;
                    }
                }
            }
        }

        return val;
    }

    public static int[,] cropped(int[,] cells)
    {
        var hang = cells.GetLength(0);
        var lie = cells.GetLength(1);
        var (hStart, lStart, hEnd, lEnd, hLen, lLen) = (0, 0, 0, 0, 0, 0);

        for (int i = 0; i < hang; i++)
        {
            for (int j = 0; j < lie; j++)
            {
                if (cells[i, j] != 0)
                {
                    hStart = i;
                    i = hang;
                    break;
                }
            }
        }

        for (int i = hang - 1; i >= 0; i--)
        {
            for (int j = lie - 1; j >= 0; j--)
            {
                if (cells[i, j] != 0)
                {
                    hEnd = i;
                    i = 0;
                    break;
                }
            }
        }

        for (int i = 0; i < lie; i++)
        {
            for (int j = 0; j < hang; j++)
            {
                if (cells[j, i] != 0)
                {
                    lStart = i;
                    i = lie;
                    break;
                }
            }
        }


        for (int i = lie - 1; i >= 0; i--)
        {
            for (int j = hang - 1; j >= 0; j--)
            {
                if (cells[j, i] != 0)
                {
                    lEnd = i;
                    i = 0;
                    break;
                }
            }
        }

        hLen = hEnd - hStart + 1;
        lLen = lEnd - lStart + 1;

        var newCell = new int[hLen, lLen];

        Console.WriteLine($"h {hLen} l {lLen}");

        for (int i = 0; i < hLen; i++)
        {
            for (int j = 0; j < lLen; j++)
            {
                newCell[i, j] = cells[hStart + i, lStart + j];
            }
        }

        return newCell;
    }

    public static int[,] NewBoard(int[,] cells)
    {
        var hang = cells.GetLength(0);
        var lie = cells.GetLength(1);

        var newBoard = new int[hang + 2, lie + 2];

        for (int i = 0; i < hang + 2; i++)
        {
            for (int j = 0; j < lie + 2; j++)
            {
                newBoard[i, j] = 0;
            }
        }

        for (int i = 1; i < hang + 1; i++)
        {
            for (int j = 1; j < lie + 1; j++)
            {
                newBoard[i, j] = cells[i - 1, j - 1];
            }
        }

        return newBoard;
    }
}