什么是图灵完备?手把手教你证明brainfuck的图灵完备性
Intro
上篇文章中对图灵机的讨论是错误的,因为那篇文章中试图去使用一个具体的机器去指代图灵机,这会造成极大的误解。
本文将会解决这些问题。
Tips:发现错漏请指出,我尽力修改( ;´д`)
图灵机
图灵机的形式化定义如下
图灵机是一个七元组(
)其中 都是有穷集合。
是状态集 是输入字母表,不包括空白符号 是带字母表,其中 是转移函数 是起始状态 是接收状态 是拒接状态,并且
图灵机
接收方格带上的输入 ,方格带上除去 其余保持空白(填入空白符号)。 开始运行,按转移函数所描述的规则运行。如果读写头在最左端试图左移,则读写头不动,其他照常运行。- 不断运行直到
进入接收状态或者拒绝状态。 - 如果最终
无法进入这两种状态,则M将永远运行下去,不会停机。
图灵机可以形式化描述,图灵机的计算过程自然也能形式化描述。这需要引入一个新的概念——格局。
图灵机计算过程中,将当前状态、当前带内容与读写头位置组合在一起,称为图灵机的格局。
格局常以特殊的方法表示,对于状态与当前带上被读写头分割的两个字符串 和 ,以 表示如下格局:
当前状态是,当前带内容是 ,读写头当前的位置是 的第一个附后,带上 的最后一个符号之后的符号都是空白符。
如果图灵机能从格局
于是,对图灵机计算进行形式化描述即为:
设
均为 中的符号, 和 是 中的字符串, 和 是状态,则 和 是两个格局,如果转移函数满足 则说:
产生
上面是左移的情形,下面是右移的情形:
如果,则说
产生
当读写头处于左端点或者右端点时,这个情形会发生变化,
左端点下,左移是产生 ,右移是 产生
右端点下,等价于 ,这是由于假设了带子上没有描述的位置都是空白符,这样就只需要正常处理就行了。
M在输入
上的起始格局是 ,表示机器处在起始状态,且读写头在最左端。
处在接受状态的格局是接受格局;处在拒绝状态的格局是拒绝格局。
接受与拒绝状态都是停机状态,它们不产生新的格局。
于是我们形式化了图灵机的工作方式。
此时一个确定的输入
如果格局序列满足:
是 在输入 上的起始格局。- 每一个
产生 。 是接受格局。
我们就认为M接受输入
如果一个语言能被某一图灵机识别,则称该语言是图灵可识别的
我们可以很自然的想到,当图灵机接收一个输入,最终只会处在三种状态:接受、拒绝、不停机。
于是,现在我们有了一个较为准确的对图灵机的描述。
图灵完备
众所周知,想要证明一个系统是图灵完备的,就需要使得它能模拟图灵机的所有行为。
本部分思路来自Frans Faase;发现未知宏可参考Alva L. Couch的文章
brainfuck
依据前面的内容我们不难发现,一台确定的图灵机
也就是说,图灵机的运行,可以写成一长串的格局序列。这意味着,只要能找到一个方法,让brainfuck正确的表示出这一串格局序列,就能用brainfuck模拟图灵机。
所以,我们需要找到一个办法,来让brainfuck可以描述出每一个格局。并实现格局到下一个格局的产生。
由前面的内容我们可以知道,一个格局记录3种信息:当前状态、读写头位置、当前带内容。
想用brainfuck来表达这些信息,先要做一些简单的准备工作。
我们知道一台图灵机
- 先映射
到自然数 ~ , 为符号数。特别的,先将空白符 映射到 ,记作 。 - 再映射
到自然数 ~ , 为状态数。特别的, 。
这样,我们就能用数字来表达图灵机的全部符号了。
于是乎,我们可以开始用brainfuck来表示格局了。
当前带内容可以是是无限的,但是没关系,brainfuck能操作的数组也是无限长的。但很快我们就发现了,brainfuck的指针并不能储存状态。我们将不得不将一些额外的信息储存在数组上,并且,brainfuck想要对数据进行操作,几乎不可避免地需要一些额外的空间,但brainfuck只有一个数组。好在它是无限长的,就如希尔伯特所提出的无限旅馆问题那样,我们的这个数组完全足够。
我们规定数组的一第个位置用于储存状态,第二个位置用于储存读写头位置,第三个位置用来储存读写头当前指向的字符,第四个位置用来作为新状态的临时储存,第五个位置用来做新的读写头位置的临时储存,第六个位置用来做新字符的临时储存。
并且,由于brainfuck对数据的操作需要一些额外的空间,所以我们额外分配五个位置做临时存储位置。
后续的数组便用来存储当前带上的字符串。
所以,我们的数组是这样的
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ... |
---|---|---|---|---|---|---|---|---|---|---|---|---|
state | head | cursymbol | newstate | newhead | newsymbol | t1 | t2 | t3 | t4 | t5 | tape0 | ... |
我们规定数组前面的部分(0~10)为操作区,后续部分为纸带区。
brainfuck宏
brainfuck的可读性并不好,所以我们需要一些宏,用来提高可读性。
# 首先我们需要一个用来寻址的宏to(),它将展开成多个`<>`,能精确地移动到正确的位置。
# 并且我们规定这个宏只能用在操作区与纸带的第一个位置tape0之间的移动。
# 用to()给循环指定判断位置[]
for(x) = to(X)[;
next(x) = to(X)-];
# 用to()精确操作数
dec(x) = to(x)-;
inc(x) = to(x)+;
# 将s上的数字移动到d。
move(s,d) = to(s) [ to(d) + to(s) - ];
# 如何拷贝呢?移动不只可以执行一次。
move2(s,d1,d2)= {
for(s)
to(d1) +
to(d2) +
next(s)
}
copy(s,d,t) = move2(s,d,t) move(t,s);
# [条件判断是brainfuck唯一的条件判断,我们需要让它变得更加好用一点。
# 首先我们需要一个可以在任意位置写入0的宏。
zero(a) = to(a) [-];
# 于是现在有条件判断了。
if(a) = to(a) [;
endif(a) = zero(a) ];
ifelse(a,t) = inc(t) if(a) dec(t);
else(a,t) = endif(a) if(t);
endelse(t) = endif(t);
# 由于[是一个零跳转,所以我们很自然的认为0是False,而其余的数则是True,于是可以有逻辑运算
# 值得注意的是,三种逻辑操作都会使得原操作数被清零。s是被操作的数,d是结果输出的位置。
or(s1,s2,d) = move(s1,d) move(s2,d);
and(s1,s2,d) = if(s1) move(s2,d) endif(s1) zero(s2);
not(s,d) = inc(d) if(s) dec(d) endif(s);
# 与常数比较
# 与零比较,其实就是进行not()操作,至于其他的数?1-1=0,2-1=1...
isZero(s,d) = not(s,d);
isOne(s,d) = if(s) dec(s) isZero(s,d) endif(s);
isTwo(s,d) = if(s) dec(s) isOne(s,d) endif(s);
isThree(s,d) = if(s) dec(s) isTwo(s,d) endif(s);
....
# 图灵机运行就是不断产生新格局的过程,所以我们还需要另一个循环——while
while(X) = to(X)[;
wend(X) = to(X)];
# 接下来要处理读写头的读写操作,我们需要新的移动宏,用来移动到纸带上。
# 这是两个特殊的宏,它只有在输入被确定后才替换。
# {(*head)*>}意思是写入head位置储存的值相等数量的'>',后面的同理
totape() = to(tape0){(*head)*>};
back() = {(*head)*<}
# 获取/写入字符
# 其实这是一个改写过的copy(),将纸带上对应的符号复制到操作区里。
getsymbol(x,t1) ={
totape()[
back()
to(d1) +
back()
to(d2) +
totape()-]
back()
to(t1) [ totape() + back()to(t1) - ]
}
setsymbol(x) ={
totape()
[-]
back()
to(x) [ totape() + back()to(x) - ]
}
;
brainfuck is Turing complete
将指针位置存放在操作区,仅在读写纸带时前往纸带区。
依据上面所给的规定,我们来处理程序的主体部分。
它做了5件事:
- 判断是否停机
- 读取字符
- 按照转移函数产生一个新格局
- 写入更改
- 循环回到开头
IsOne(state,t1)
IsTwo(state,t2)
and(t1,t2,t3)
not(t3,t1)
while(t1)
getsymbol(cursymbol,t1)
/***map***/
setsymbol(newsymbol)
zero(cursymbol)
zero(head) move(newhead,head,t1)
zero(state) move(newstate,state,t1)
IsOne(state,t1)
IsTwo(state,t2)
and(t1,t2,t3)
not(t3,t1)
wend(t1)
接下来我们需要给每一个
我们定义一个映射模板map
。它在接受输入后,与自身规则做比较,如果不匹配,则什么都不做。
map
用到了十一个参数,其中的astate,asymbol,anewstate,anewsymbol,movement,
是根据相应的map
,之后,我们只要将所有的map
插进循环里,就能完成程序的主体部分了。
map(state,head,cursymbol,
astate,asymbol,anewstate,anewsymbol,movement,
newstate,newhead,newsymbol)
=
{
# 判断是否匹配了正确的转移函数,如果不是则直接退出。
copy(state,t1,t1)
write(t2,astate)
Equal(t1,t2,t3,t4,t5)
if(t3)
copy(cursymbol,t1,t2)
write(t2,asymbol)
Equal(t1,t2,t3,t4,t5)
if(t3)
# 按规则写入新的符号、状态并进行移动。
write(newstate,anewstate)
write(newsymbol,anewymbol)
copy(head,newhead,t1)
ifelse(movement,t1)
inc(newhead)
else(movement,t1)
dec(newhead)
endelse(t1)
endif(t3)
endif(t3)
}
这样,只要确定图灵机的种类与输入,我们就能将它翻译为一个brainfuck程序。
在完成运行后,只要打印出数组,就能得到这台机器运行的结果。
我们相信这个程序能翻译任何一台图灵机,并且模拟它的运行,因此可以说brainfuck是图灵完备的。
Reference
- BF is Turing-complete-Frans Faase:https://www.iwriteiam.nl/Ha_bf_Turing.html
- An introduction to programming in BF-Frans Faase:https://www.iwriteiam.nl/Ha_bf_intro.html
- Bfmacro: a bf macro-interpreter-Alva L. Couch:http://www.cs.tufts.edu/~couch/bfmacro/bfmacro/
- 《计算理论引导》-Michael Sipser -机械工业出版社
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通