Wolfram|Alpha 计算时显示的元胞自动机(一)

果壳网已完,不知道我发过的帖子什么时候会消失,于是我要把它们搬到简书。

但这篇不是搬运,只是想说一下这个帖子背后的故事……以及代码。


Wolfram|Alpha 在显示计算或查询的结果之前,往往需要思考一番。几年前,它在思考过程的中会显示一个漂亮的图案。现在那个图案已经变成了一个无聊的 COMPUTING...,不过网上还是留下了一点痕迹,比如说知乎的这个问题:Wolfram|Alpha在搜索时出现的图案有什么含义?和 StackOverflow 的这个问题:What is the cellular automaton shown as loading screen on Wolfram Alpha?

我也一直想知道这是什么图案。它显然是一个元胞自动机,大小不同的点表示不同的状态;而且状态不止一种,因此肯定不是生命游戏,也不是生命游戏家族(Life-like)的规则。但它思考的速度太快,我总是来不及看清它到底是什么规则。

直到有一天,我在人人网上——当时人人网还不是直播平台——看到这么一个截图:

Wolfram|Alpha 的元胞自动机。忘了是哪位同学的截图。

不知那位同学用的是什么截图工具,能够截得这么清晰,而且没有掉帧。总之,有了这个截图,我就能把它导入到 Mathematica,一帧一帧地慢慢看……并不能看出它是什么规则,甚至不能看清它有几种状态——最小的两种点个头差不多,只是颜色的深浅稍稍有点区别,肉眼不容易看清。不过我还是看出了一点规律:稍大一点的点会变得越来越大,直到爆掉,消失。而且,我还看到了这个一个振荡子:

一个周期8的小振荡子。我想给它起名叫 Loading。

除了 Life-like 的规则,还有什么规则?我在 Golly (最好的元胞自动机模拟器,没有之一)的帮助里看到了一类叫 Generations 的规则,觉得和这个有点像。不过那里给的几个 Generations 规则的例子和 Wolfram|Alpha 的这个元胞自动机都对不上。

Generations 的规则和 Life-like 的规则类似,每个细胞也有死活两种状态,下一回合的状态由这一回合的死活、以及与它相邻的活细胞的个数来决定。与 Life-like 不同的是,活细胞在临死之前,还能苟延残喘几个回合,但死亡的过程并不受周围的细胞影响,最终也无法摆脱死亡的命运。

比如说,那个叫 Frogs 的元胞自动机,规则按 Golly 的写法是 12/34/3。最后这个数字 3 表示它有三种状态:一个是死,一个是活,一个是正在死亡。也就是说,一个活细胞在临死前只能多活一个回合。前面的 12 说的是:一个活细胞要想继续好好活着,八个邻居里活细胞的个数必须是1或者2; 34 的意思则是:一个已经死掉的细胞要想活过来,周围的活细胞个数必须是3或者4。

Frogs 规则因图中的这个像青蛙一样跳跃前进的飞船而得名。小点、大点和没有点分别代表活细胞、半死的细胞和死透了的细胞。

Wolfram|Alpha 的元胞自动机应该也是 Generations 一类,但肉眼不容易看出具体是什么规则。不过不要紧,我们还有 Mathematica。直接看不出,我们就把图片二值化了看,拆成一个个连通分支来看,数清了每个点周围的活细胞个数再看。

当年的代码我没有留着。下面的代码都是新写的,用了不少这几年才引进的新函数。


首先当然是导入图片并去掉重复帧。我一开始没发现截图中有重复帧,导致结果很奇怪,还以为它不是 Generations 的规则。

CA = First /@ Split@Import["WolframAlphaCA.gif"];

先试着把第一帧二值化一下,发现它还有一个框框:

Binarize[CA[[1]]]
不二值化的话根本看不出这截图还戴黑框。

摘掉黑框再二值化。二值化的阈值不能太低,以免最小的点消失;也不能太高,以免相邻的点连起来。试了好几个数,0.85 是最合适的。

Binarize[ImagePad[#, -BorderDimensions@#], 0.85] &@CA[[1]]
这只是第一帧二值化的结果。

看起来不错。于是我就把每一帧都二值化了。顺手还给它们反个色,以方便下一步处理。

CABinarized = 1 - Binarize[ImagePad[#, -BorderDimensions@#], 0.85] & /@ CA;
这样好看多了。

现在每个细胞——死细胞不算——是这个图片的一个连通分支。细胞的状态对应于连通分支的大小。于是可以用上 ComponentMeasurements 函数。

先来看看这些细胞的位置。还是先看第一帧,算出所有连通分支的重心,把所有的横坐标和所有的纵坐标分别从小到大排列:

Union /@ Transpose[Last /@ ComponentMeasurements[CABinarized[[1]], "Centroid"]]

结果是:

{{8.5, 18.5, 28.5, 38.5, 48.5, 58.5, 68.5, 78.5, 88.5, 98.5, 108.5, 
  118.5, 128.5, 138.5, 148.5, 158.5, 168.5, 178.5, 188.5, 198.5, 
  208.5, 218.5, 228.5, 238.5, 248.5, 258.5, 268.5, 278.5, 288.5, 
  358.5, 368.5, 378.5, 388.5, 398.5, 408.5, 418.5, 428.5, 438.5, 
  448.5, 458.5, 468.5, 478.5, 488.5, 498.5, 508.5, 518.5, 528.5, 
  538.5, 548.5, 558.5, 568.5, 578.5, 588.5, 598.5, 608.5, 618.5, 
  628.5, 638.5, 648.5},
 {12., 12.2193, 21.5, 22., 31.5, 32., 41.5, 42., 51.5, 52., 61.5, 62., 
  71.5, 72., 81.5, 82., 91.5, 92., 101.5, 102., 111.5, 112., 121.5, 
  122., 131.5, 132., 141.5, 142., 151.5, 152., 161.5, 162., 171.5, 
  172., 181.5, 182., 191.5, 192., 201.5, 202., 211.5, 212., 221.5, 
  222., 231.5, 232., 241.5, 242., 251.5, 252., 261.5, 262., 271.5, 
  272., 281.5, 282., 291.5, 292., 301.5, 302., 311.5, 312., 321.5, 
  322., 331.5, 332., 341.5, 342., 351.5, 352., 361.5, 362., 372.}}

可以看出相邻的两个点之间大概差10个像素,整个图片中的细胞个数是 65 × 37。这里出现了一个很奇怪的 12.2193,是因为最下面一排有一个点只露出了大半。为了避免这种问题,我去掉了最外面的一圈细胞,只取重心的横坐标在 15 和 645 之间,纵坐标在 15 和365 之间的那些连通分支。

然后看连通分支的大小。取了一下 Union,发现图片的每一帧,在去掉了最外面的一圈细胞之后,连通分支的大小都只有 {6, 9, 37, 69} 这么四种:

Union[Last /@ 
    ComponentMeasurements[#, "Count", 
     15 < #Centroid[[1]] < 645 && 
       15 < #Centroid[[2]] < 365 &]] & /@ CABinarized

于是这四种不同大小的活细胞和濒死的细胞,再加上死细胞,一共有五种状态。最初的截图里分不太清的两种状态,在这里分别对应 69

然后我们可以把这些图片化成代表细胞状态的数组,把 {6, 9, 37, 69} 分别换成 {1, 2, 3, 4},死细胞还是用 0 表示。

CAArray = 
  SparseArray[
     Round[(#[[2, 1]] - {8.5, 12})/10] -> (#[[2, 2]] /. 
          Thread[{6, 9, 37, 69} -> Range@4]) & /@ 
      ComponentMeasurements[#, {"Centroid", "Count"}, 
       15 < #Centroid[[1]] < 645 && 15 < #Centroid[[2]] < 365 &],
     {63, 35}] & /@ CABinarized;

如果它真的是 Generations 的规则,每个细胞在下一回合的状态就完全由它在这一回合的状态和周围的活细胞的个数来决定。于是来算一算,看看是否如此:

Union@Flatten@
  BlockMap[{#[[1, 2, 2]], Count[#[[1]], 1, {2}]} -> #[[2, 2, 2]] &, 
   CAArray, {2, 3, 3}, 1]

结果是:

{{0, 0} -> 0, {0, 1} -> 0, {0, 2} -> 0, {0, 3} -> 1, {0, 4} -> 0,
 {0, 5} -> 1, {0, 6} -> 0, {0, 7} -> 1, {0, 8} -> 0, {1, 1} -> 2,
 {1, 2} -> 2, {1, 3} -> 2, {1, 4} -> 1, {1, 5} -> 1, {1, 6} -> 1,
 {1, 7} -> 2, {1, 8} -> 1, {1, 9} -> 2, {2, 0} -> 3, {2, 1} -> 3,
 {2, 2} -> 3, {2, 3} -> 3, {2, 4} -> 3, {2, 5} -> 3, {2, 6} -> 3,
 {2, 7} -> 3, {2, 8} -> 3, {3, 0} -> 4, {3, 1} -> 4, {3, 2} -> 4,
 {3, 3} -> 4, {3, 4} -> 4, {3, 5} -> 4, {3, 6} -> 4, {3, 7} -> 4,
 {3, 8} -> 4, {4, 0} -> 0, {4, 1} -> 0, {4, 2} -> 0, {4, 3} -> 0,
 {4, 4} -> 0, {4, 5} -> 0, {4, 6} -> 0, {4, 7} -> 0}

结果中的这些数据是 {本回合的状态, 周围的活细胞个数} -> 下一回合的状态。比如说, {1, 3} -> 2 表示某个细胞的状态是 1,也就是一个活细胞;它周围的活细胞个数,包括它本身,是 3;它在下一回合的状态是 2,也就是说刚刚开始死亡。这里同样的键总是对应同样的值,因此每个细胞在下一回合的状态确实由这两个数来决定。

我们还能看出,0 只会变成 011 只会变成 122 只会变成 33 只会变成 44 只会死亡。

果然是 Generations。

再来算算这个规则在 Golly 里该怎么写:

{Cases[%, ({1, x_} -> 1) :> x - 1], Cases[%, ({0, x_} -> 1) :> x], 
 Max@%[[;; , 1, 1]] + 1}

结果是 {{3, 4, 5, 7}, {3, 5, 7}, 5},也就是说它在 Golly 里叫做 3457/357/5

这个规则的含义是:它有五种状态。0 表示死,1 表示活,234 都是死亡的过程。每个回合每个细胞的变化如下:

  • 0 -> 如果相邻的活细胞个数是3、5、7,则变成 1,否则还是 0
  • 1 -> 如果相邻的活细胞个数是3、4、5、7,则保持是 1,否则变成 2
  • 2 -> 3
  • 3 -> 4
  • 4 -> 0

接下来我们把时间交给 Golly。Mathematica 只负责画图。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容