news 2026/10/12 12:35:20

从一维到二维:用Python实现细胞自动机与生命游戏

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从一维到二维:用Python实现细胞自动机与生命游戏

1. 从一行规则到一片森林:细胞自动机到底在算什么

第一次接触“简单细胞自动机”这个概念,很多人会以为它不过是屏幕上跑马灯一样的像素动画。但真正动手写一遍就会发现,这东西的野心远不止于此——它试图用一条极其简单的局部规则,推演出整个系统的全局行为。你给它一个初始状态,它按照固定规则一格一格地演化,几轮之后,有的图案会自我复制,有的会彻底消亡,有的会进入永不停歇的循环。这种“简单规则生成复杂行为”的特性,正是它最迷人的地方。

所谓细胞自动机,核心就三件事:格子、状态、规则。把一条线或一个平面切成若干格子,每个格子只有有限几种状态(最常见的就是“活”和“死”两种),然后规定每个格子下一时刻的状态由它自己和邻居当前的状态决定。听起来简单得过分,但就是这套机制,能模拟出湍流、晶体生长、甚至生命游戏里那种会“走动”的结构。

我最初写这个东西,是想找一个周末能一口气写完、又能反复玩味的小项目。它不需要任何外部依赖,不需要网络,不需要数据库,一个文件就能跑起来。对于想练手编程、理解“涌现”概念、或者单纯想找个能盯着看半天的可视化小玩意的人来说,简单细胞自动机是极好的选择。这篇文章我会从最基础的一维自动机讲起,一路做到二维的生命游戏,把规则设计、边界处理、性能优化、可视化这些坑一个个填过去。你不需要有很深的数学背景,只要会写循环和数组,就能跟着走完。

2. 一维细胞自动机:用一条线看懂规则编号的玄机

2.1 为什么从一维开始而不是直接上二维

很多人一上来就想写二维的生命游戏,觉得那个看起来更酷。但我建议先在一维上把概念跑通,原因很实际:一维自动机的状态可以用一行字符直接打印在终端里,不需要任何图形库,调试成本几乎为零。你能在几秒钟内看到规则对不对、边界处理有没有问题。等一维的逻辑烂熟于心,再扩展到二维只是多一层循环的事。

一维细胞自动机里,每个格子只看自己和左右两个邻居,也就是三个格子的状态决定下一时刻中间那个格子的状态。三个格子每个有两种状态,总共就是 2³ = 8 种输入组合。对每一种组合,你规定输出是 0 还是 1,于是就有 2⁸ = 256 种可能的规则。这就是著名的“规则编号”来源——把 8 种输出的二进制串转成十进制,就得到规则号。

2.2 规则 30 和规则 90:两个必须亲手跑一遍的例子

规则 30 是我强烈建议第一个实现的。它的输出模式看起来像随机噪声,但实际上是确定性的。具体规则是:只有当前格子是 1 且左右邻居不同时,下一时刻才是 1,其余情况都是 0。用表格表示就是:

左中右下一时刻
1110
1100
1011
1001
0111
0101
0010
0000

把输出列从上到下读成二进制 00011110,转十进制就是 30。我第一次跑出规则 30 的图案时盯着屏幕看了很久——它从中间一个 1 开始,逐行向下展开,形成一种既不对称又不像完全随机的纹理。这种“确定性中的伪随机”是细胞自动机最反直觉的地方。

规则 90 则更规整,它生成的是谢尔宾斯基三角形那样的分形结构。规则很简单:下一时刻的状态等于左右邻居的异或。如果你把初始状态设成中间一个 1,跑几十行,就能看到一个漂亮的自相似图案。这两个规则跑通,你对“局部规则决定全局形态”就会有肌肉记忆了。

2.3 边界处理:最容易让新手翻车的地方

一维自动机在两端会遇到“左边没有左邻居”的问题。常见处理方式有三种:固定边界(两端永远为 0)、循环边界(最右的右邻居是最左)、反射边界(把边界当作镜像)。我踩过的坑是:一开始用固定边界跑规则 90,结果图案在边缘被截断,看起来像被削掉一块,还以为是规则写错了。后来换成循环边界,图案才完整地卷绕起来。

提示:如果你想让图案在终端里看起来连续,循环边界通常是最省心的选择。实现时只需要对索引取模,一行代码就能搞定。

具体做法是:计算下一行时,对每个位置 i,取left = state[(i-1) % width],right = state[(i+1) % width]。注意在 Python 里负数取模会得到正数,所以(i-1) % width在 i=0 时正好是 width-1,天然支持循环。这个细节如果不注意,用其他语言时可能会数组越界。

3. 二维生命游戏:从“活死”规则到会移动的结构

3.1 康威规则的四个数字为什么是 2、3、3、3

二维细胞自动机最出名的就是生命游戏。每个格子有活和死两种状态,下一时刻的状态由周围 8 个邻居中活着的数量决定。康威定的规则是:活格子如果邻居少于 2 个,孤独而死;邻居是 2 或 3 个,继续活;邻居多于 3 个,拥挤而死。死格子如果恰好有 3 个活邻居,就复活。

这四个数字不是随便拍的。少于 2 个会死,是为了防止孤立结构无限存活;多于 3 个会死,是为了防止区域无限膨胀;死格子需要恰好 3 个才能复活,是为了让结构有“繁殖”的可能但又不至于太容易。我试过把复活条件改成 2 个,结果整个网格几轮之内就全亮了,毫无结构可言。改成 4 个又几乎什么都长不出来。3 这个数字是经过大量手工推演才定下来的,背后是对“稳定与变化平衡”的直觉。

3.2 邻居计数:八邻域和四邻域的区别

生命游戏用的是八邻域,也就是上下左右加四个对角。有些变体用四邻域,只算上下左右。两者生成的图案差别很大。八邻域更容易产生斜向移动的结构,四邻域则偏向正交方向。实现时,八邻域的偏移量是(-1,-1)到(1,1)去掉(0,0),一共 8 个。写循环时可以用双重 for 加条件跳过中心,也可以直接列一个偏移列表。

neighbors = [(-1,-1),(-1,0),(-1,1), (0,-1), (0,1), (1,-1), (1,0), (1,1)]

用列表的好处是代码清晰,而且方便以后换成其他邻域定义。我见过有人用两重循环加if not (dx == 0 and dy == 0)来跳过中心,也能跑,但可读性差一些。这种小地方多花几秒钟写清楚,后面调试会省很多事。

3.3 滑翔机为什么是检验实现的试金石

滑翔机是生命游戏里最经典的结构:五个格子组成一个小斜线,每四代向右下角移动一格。它之所以重要,是因为它同时检验了邻居计数、边界处理和状态更新三个环节。如果你的滑翔机走着走着散架了,或者卡在边界上不动,那说明某个环节有问题。

我第一次实现时,滑翔机走到右边界就消失了,因为我用了固定边界,超出范围的邻居直接当死处理。后来改成循环边界,滑翔机就从另一边钻出来,继续走。这个现象让我意识到,边界条件不是无关紧要的细节,它直接决定了系统的拓扑结构。循环边界相当于把平面卷成一个环面,固定边界则是一个有墙的盒子。选哪种取决于你想模拟什么。

注意:更新状态时一定要用“同步更新”,也就是所有格子下一时刻的状态都基于当前时刻的邻居计算,不能一边算一边改。我最初图省事直接在原数组上改,结果滑翔机变成了一个奇怪的团块。正确做法是新建一个同样大小的数组存放下一时刻状态,全部算完后再整体替换。

4. 规则设计的底层逻辑:为什么简单规则能长出复杂结构

4.1 局部信息与全局行为的鸿沟

细胞自动机最让人困惑的一点是:每个格子只能看到自己周围几个邻居,它根本不知道整个网格长什么样,但所有格子同时按规则行动,却能产生有组织的大尺度结构。这种“局部简单、全局复杂”的现象叫涌现。它不是细胞自动机独有的,蚁群、鸟群、神经网络里都有类似现象,但细胞自动机把它压缩到了最纯粹的形式。

理解这一点的关键在于:规则本身不包含任何关于全局结构的信息,全局结构是规则在时间维度上迭代出来的。就像下棋,规则只有几步,但棋局可以千变万化。你不需要在规则里写“这里要长出一个滑翔机”,滑翔机是规则运行的自然产物。这种“不设计结果,只设计过程”的思路,对做系统设计的人很有启发。

4.2 规则空间有多大:256 种一维规则里有多少能产生有趣行为

一维两状态三邻居的规则有 256 种。我花了一个下午把 256 种全部跑了一遍,按行为分了类。大致可以分成四类:一类很快全部变成 0 或全部变成 1,叫“死寂型”;一类进入短周期循环,叫“周期型”;一类产生看似随机的纹理,叫“混沌型”;还有极少数能产生分形或复杂结构,叫“复杂型”。规则 30 属于混沌型,规则 90 属于分形型,规则 110 则被证明具有图灵完备性,也就是说它能做通用计算。

这个分类实验让我明白一件事:有趣的行为在规则空间里是稀有的。大部分规则要么太死,要么太乱。真正能产生“有结构但不重复”的规则,比例很低。这也解释了为什么生命游戏的规则那么难调——康威是在没有计算机辅助的情况下手工推演的,能找到 2、3、3、3 这组数字,运气和直觉都占了很大成分。

4.3 从规则到图灵完备:规则 110 意味着什么

规则 110 的图灵完备性是一个里程碑式的结论。它意味着,只要给一个足够长的初始状态,规则 110 可以模拟任何计算机能做的计算。换句话说,一条只看三个格子的简单规则,其计算能力不亚于一台通用计算机。这个结论最初是猜想的,后来被严格证明。

对写代码的人来说,这个结论的实际意义是:你不需要复杂的逻辑就能实现复杂的功能。如果你在设计一个系统,发现规则越来越臃肿,也许应该退一步想想,是不是可以用更简单的局部规则加上迭代来替代。当然,这不是说所有问题都能这样解决,但它提供了一个思考方向。

5. 性能与可视化:让自动机跑得又快又好看

5.1 用 NumPy 做向量化:从双重循环到一次切片

纯 Python 的双重循环在网格稍大时就慢得让人抓狂。一个 200×200 的网格,每代要算 40000 个格子,每个格子再看 8 个邻居,就是 32 万次操作。跑几百代就是几千万次,纯 Python 要好几秒。用 NumPy 的数组切片可以把邻居计数变成几次矩阵加法,速度提升几十倍。

具体做法是:把网格存成 NumPy 数组,然后用np.roll沿不同方向滚动,得到 8 个邻居数组,全部加起来就是邻居数。np.roll默认就是循环边界,正好符合我们的需求。核心代码大概是这样:

import numpy as np def step(grid): neighbors = sum(np.roll(np.roll(grid, i, 0), j, 1) for i in (-1, 0, 1) for j in (-1, 0, 1) if not (i == 0 and j == 0)) new_grid = ((neighbors == 3) | ((grid == 1) & (neighbors == 2))).astype(int) return new_grid

这段代码第一次跑通时我有点不敢相信——原本几十行的逻辑,用 NumPy 三行就搞定了,而且快得多。np.roll的循环特性还顺便解决了边界问题,不用再手动取模。

5.2 终端可视化:用字符画快速验证

在写图形界面之前,我强烈建议先用终端字符画验证逻辑。活格子打印#,死格子打印空格或.,每代清屏再打印。这样你能立刻看到图案的演化,而且不需要任何额外依赖。Python 里用os.system('clear')或print('\033[H\033[J', end='')就能清屏。

字符画的分辨率有限,但足够看清滑翔机、振荡器、静物这些基本结构。我调试生命游戏时,前两个小时全在终端里跑,确认规则无误后才转到图形界面。这个顺序很重要——图形界面会引入额外变量,如果逻辑本身有问题,你会分不清是规则错了还是渲染错了。

5.3 图形界面选型:pygame 还是 matplotlib

如果要做图形界面,pygame 和 matplotlib 是两个常见选择。pygame 适合实时动画,帧率高,交互方便,但需要自己处理事件循环和绘制。matplotlib 的imshow加FuncAnimation更简单,适合做演示和保存动画,但帧率低一些,网格大了会卡。

我的经验是:如果只是自己看,matplotlib 足够;如果想做交互(比如鼠标点击切换格子状态),pygame 更合适。还有一个轻量选择是curses,直接在终端里做彩色字符界面,不需要图形环境,适合在服务器上跑。选哪个取决于你的使用场景,没有绝对优劣。

方案依赖帧率交互适用场景
终端字符无低无快速验证逻辑
matplotlib较重中有限演示、保存动画
pygame中等高丰富实时交互
curses无中键盘无图形环境

6. 那些文档里不会写的踩坑记录

6.1 同步更新写成异步更新:一个字母引发的血案

前面提过同步更新的问题,但这里值得再展开说。我最初写生命游戏时,直接在原数组上更新,代码看起来没问题,跑起来却完全不对。原因是:当你更新了第 i 行第 j 列后,再算第 i 行第 j+1 列时,它的左邻居已经是新状态了,而不是旧状态。这相当于每个格子的更新时机不同,整个系统的语义就变了。

修复方法很简单:新建一个new_grid,所有计算都读grid,写new_grid,最后grid = new_grid。但这个小错误很隐蔽,因为图案不会立刻崩掉,而是慢慢变得奇怪。我花了半小时才定位到。后来养成了习惯:只要涉及“下一时刻状态”,一律用双缓冲。

6.2 边界条件选错:滑翔机撞墙还是穿墙

固定边界和循环边界没有对错,只有适不适合。如果你模拟的是封闭空间里的种群,固定边界合理;如果你想看结构在无限平面上怎么走,循环边界更接近。我一开始用固定边界,滑翔机走到边缘就死了,我还以为是规则写错了。换成循环边界后,滑翔机从另一边出来,我才确认规则没问题。

提示:如果你不确定选哪种,可以先在终端里用循环边界跑,因为循环边界不会产生边缘伪影,更容易看出规则本身的行为。

6.3 初始状态太满:为什么随机填充往往不好看

很多人第一次跑生命游戏,喜欢把网格随机填满 50% 的活格子。结果通常是:前几代很热闹,然后迅速变成一片混乱,最后要么全死要么进入一个无聊的周期。这是因为高密度随机状态缺乏结构,规则会快速把它抹平。

好的初始状态通常是稀疏的、有结构的。比如放一个滑翔机、一个振荡器、几个静物,看它们之间怎么互动。或者用低密度随机(比如 10%)加一些手工放置的结构。我后来写了一个小函数,专门在网格上随机撒几个已知结构,这样每次跑都有看点。

6.4 性能陷阱:每代都重新分配数组

用 NumPy 时,如果每代都new_grid = np.zeros(...),会产生大量临时对象,网格大时内存分配开销很明显。更好的做法是预分配两个数组,交替使用,也就是双缓冲。这样每代只需要写入已有数组,不需要重新分配。对于 1000×1000 的网格,这个优化能省下可观的时间。

grid = np.random.randint(2, size=(1000, 1000)) buffer = np.empty_like(grid) for _ in range(1000): # 计算写入 buffer # ... grid, buffer = buffer, grid

这种交替引用的写法在图像处理、物理模拟里都很常见,值得养成习惯。

7. 从玩具到工具:细胞自动机还能怎么玩

7.1 用自动机生成地形和纹理

细胞自动机不只能做生命游戏。一维规则 30 可以用来生成伪随机数,二维自动机可以用来生成游戏地图。比如经典的“洞穴生成”算法:先随机填充,然后反复应用一个平滑规则(邻居多的保持,邻居少的变空),几轮之后就能得到连通的洞穴结构。这个算法在 roguelike 游戏里很常见,实现简单,效果也不错。

我试过用类似方法生成像素风格的云朵纹理:从随机噪声开始,用元胞自动机做几轮平滑,再取阈值,就能得到边缘自然的云。相比纯随机噪声,自动机生成的纹理有空间相关性,看起来更自然。

7.2 自动机与神经网络:可微分自动机的思路

近年来有一个有趣的方向叫“神经元胞自动机”,把每个格子的更新规则换成一个小的神经网络,然后用梯度下降训练它完成特定任务,比如图像分类或纹理生成。这个思路把细胞自动机的“局部规则”和神经网络的“可学习参数”结合起来了。虽然这超出了简单自动机的范围,但理解基础自动机是理解这些进阶方法的前提。

如果你已经跑通了一维和二维的基本版本,想再往前走一步,可以试试把规则表换成可学习的参数,用一个小型优化器去调。你会发现,即使只有几个参数,也能训练出有趣的行为。

7.3 用自动机做艺术:生成图案和动画

细胞自动机生成的图案有一种独特的机械美感。规则 90 的分形、规则 30 的噪声、生命游戏里的滑翔机编队,都可以直接作为艺术素材。我见过有人把自动机的演化过程录成视频,配上音乐,做成生成艺术。也有人把每一代的状态叠加起来,形成长时间曝光的轨迹图。

如果你想做类似的东西,建议从一维自动机开始,因为它的输出天然就是一张二维图像(横轴是空间,纵轴是时间)。规则 30 跑 500 代,把每代存成一行,最后拼成一张 500×N 的图,看起来就像一幅抽象画。这种“时间换空间”的表示方式,是细胞自动机可视化里很实用的一招。

8. 我个人的几个使用习惯

写这类小项目,我最大的体会是:先把逻辑跑通,再考虑好看。终端字符画虽然丑,但它能让你在几秒钟内看到结果,调试效率极高。等逻辑稳定了,再花时间做图形界面。反过来做,往往会在渲染问题上浪费大量时间,而核心逻辑其实还没对。

另一个习惯是:每实现一个新规则,先用已知的简单初始状态验证。比如生命游戏先用一个滑翔机,一维自动机先用单个 1。已知输入产生已知输出,才能确认实现无误。如果一上来就用复杂初始状态,出了问题根本不知道是规则错了还是初始状态本身就会那样。

最后,不要怕把参数调来调去。细胞自动机的乐趣就在于试错。改一个规则位,整个行为可能完全不同。我经常花一整个下午就为了看不同规则号下图案怎么变。这种“玩”的过程,比读任何教程都更能建立直觉。你如果刚开始写,不妨也给自己留出这种随意折腾的时间,收获会比按部就班大得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 12:33:35

Python与NumPy从零实现BP神经网络手写字体识别源码详解

简介:基于Python实现BP神经网络识别手写字体的完整项目源码,源自大三期末大作业,经导师指导并获98分评审核定。资源定位于计算机相关专业的学生与实战学习者,可直接用于课程设计、期末大作业或深度学习入门练习。压缩包共10个文件…

作者头像 李华
网站建设 2026/10/12 12:33:07

风电功率超短期预测:BiTCN-BiGRU-Attention+EVO工程实践

简介:本资源是一套面向新能源电力系统研究者与高校科研人员的风电功率短期预测完整Matlab实现方案,聚焦于前沿智能优化与深度学习融合建模。方案创新性地将能量谷优化算法(EVO)与双向时间卷积网络(BiTCN)、…

作者头像 李华
网站建设 2026/10/12 12:30:57

Matlab小波变换图像处理实战:分解、去噪、压缩与边缘检测

1. 从傅里叶的局限说起:为什么图像处理需要小波变换做图像处理这行的人,迟早会撞上傅里叶变换这堵墙。傅里叶变换把图像从空间域搬到频率域,告诉你这张图里有哪些频率成分,但它有个致命短板——丢掉了时间信息,或者说在…

作者头像 李华
网站建设 2026/10/12 12:30:12

【Cursor实战】浏览器Tab自动分组插件:把Base URL改到TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/12 12:29:32

Windows Server 2012多用户远程桌面配置:会话原理与故障排查

简介:Windows Server 2012 默认每个用户最多只允许两个远程会话,多人同时登录常需通过组策略手动调整默认限制。配置文档面向系统管理员与 IT 运维人员,专门解决多用户远程桌面接入的问题,覆盖从启用远程桌面到设置最大连接数的关…

作者头像 李华
网站建设 2026/10/12 12:28:17

肺部CT多病种检测:多尺度特征融合与解剖先验建模

简介:本资源是2019年天池全球数据智能大赛“数字人体”赛道——肺部CT多病种智能诊断赛题的完整Baseline实现方案,面向AI初学者与医疗影像入门实践者,聚焦CT图像中病灶检测与假阳性抑制这一典型医学AI任务。压缩包共27个文件,含14…

作者头像 李华