《论可计算数及其在判定上的应用》简单理解

刚刚拜读了一本书, 《图灵的秘密》. 该书介绍了图灵的论文《论可计算数及其在判定上的应用》, 其指出: 一个拥有铅笔, 纸和一串明确指令的人类计算者, 可以被看做是一种图灵机. 那么图灵机是什么呢? 是图灵为了描述可计算数而引出的一个虚构的可以做一些简单操作的计算机器. 尽管这个机器很简单, 但图灵断言它再功能上等价与一个进行数学运算的人.

先提个小醒, 文章有些长, 而且还比较枯燥.

当然了, 那些数学证明并不是我关心的, 我关心的是这个图灵机. 图灵为了说明他的数学理论, 描述了一台机器, 而这台机器, 看过之后发现其实已经算是现代计算机的雏形了, 虽然他的出发点和发明计算机并不沾边吧. 先简单介绍一下这台机器.

这是一个配有一条纸带的机器, 纸带由一个个方格隔开, 图灵只使用其中相见的格子打印数据, 暂且称之为数字格, 数字格之间的用来辅助计算. 大概就这么一个纸带:

《论可计算数及其在判定上的应用》简单理解

而这台机器对操作的定义由一张状态表来决定:

状态 符号 操作 状态切换

其中每个状态(原文为: 格局)对应的符号不同, 会执行不同 的操作并切换的对应的下一个状态. 可以将状态类比成不同的函数, 再不同的选择分支下执行不同的逻辑并调用不同的函数. 下面给出一些符合和操作的定义:

符号

  • 0/1 : 指定字符
  • Any: 非空的任意符号
  • None: 空
  • 留空: Any 和 None
  • else: 状态的其他符号都没有匹配

操作

  • R: 向右移动一格
  • L: 想左移动一格
  • P: 打印字符(P0, 打印0)
  • E: 擦除当前格子内容

OK, 对图灵这个简单的机器介绍完毕, 是不是特别简单. 一起跟着图灵来看看, 他在这台机器上都能够做些什么操作吧.

打印序列010101...

先给出一格简单的例子, 来看看这台机器是如何运行的. 打印序列01010101..., 在前面加一个小数点, 这就是二进制的1/3了, 而将0和1的位置互换之后, 就是2/3了. 来看看图灵是如何实现这一功能的.

状态 符号 操作 状态切换
b None P0,R c
c None R e
e None P1,R f
f None R b

对了, 图灵的机器运行都是从状态b开始的. 让我们一起跟着状态表走起来. (图灵只使用相间的各自来打印数字, 数字格中间的用来辅助计算, 数字格不可更改)

1.展示纸带的初始状态

其中红色方块标记机器当前的扫描方格.

《论可计算数及其在判定上的应用》简单理解

2.当前状态: b, 打印0并向右移动一格, 切换状态: c

《论可计算数及其在判定上的应用》简单理解

3.当前状态c, 向右移动一格, 切换状态: e

《论可计算数及其在判定上的应用》简单理解

4.当前状态e, 打印0并向右移动一格, 切换状态: f

《论可计算数及其在判定上的应用》简单理解

5.当前状态 f, 向右移动一格, 切换回状态: b

《论可计算数及其在判定上的应用》简单理解

此时, 切换回了初始的状态, 然后周而复始的打印下去. 当然, 上述状态是可以进行简化的, 通过当前方格的不同符号, 可以进行不同的操作, 简化后的状态表:

状态 符号 操作 状态切换
b None P0 b
b 0 R,R,P1 b
b 1 R,R,P0 b

简单试一下, 简化后的状态实现的功能完全一样.

当然, 这个例子实在太简单了, 不过为了理解图灵这台机器, 还是有必要介绍一下的.

打印序列 001011011101111...

在这个序列中, 1的数量会依次加一, 也就是说要让这台机器再这个一维的纸带上记住前面打印了多少个1. 那么图灵是如何做到这一点的呢?

终于, 前面没有用到的非数字格出场了, 它用来辅助打印.

状态 符号 操作 状态切换
b Pa,R,Pa,R,P0,R,R,P0,L,L e
e 1 R,Px,L,L,L e
e 0 q
q Any R,R q
q None P1, L p
p x E,R q
p a R f
p None L,L p
f Any R,R f
f None P0,L,L e

老规矩, 直接来走一遍状态表, 边走边分析.

1. 当前状态 b, 操作Pa,R,Pa,R,P0,R,R,P0,L,L, 切换状态e

《论可计算数及其在判定上的应用》简单理解

可以发现, 状态b在初次执行之后, 就再也没有出现过了, 所以可以将其形容为初始化的操作.

2.当前状态e, 符号0,直接切换状态: q

3.当前状态q, 符号0, 操作: R,R, 切换状态q

《论可计算数及其在判定上的应用》简单理解

4.当前状态q, 符号0, 操作: R,R, 切换状态: q

《论可计算数及其在判定上的应用》简单理解

5.当前状态 q, 符号None, 操作: P1, L, 切换状态p

《论可计算数及其在判定上的应用》简单理解

可以看到, q状态的操作就是向当前序列的尾部添加一个1.

6.当前状态 p, 符号None, 操作: L,L, 切换状态p

这不的操作就是向左移动到第一个不为空的非数字格. 省略一步, 结果:

《论可计算数及其在判定上的应用》简单理解

7.当前状态p, 符号a, 操作: R, 切换状态: f

从这里可以看出来, p状态其实是充当各个状态之间的调度员角色的.

《论可计算数及其在判定上的应用》简单理解

8.当前状态f, 符号0, 操作: R,R, 切换到状态f

这里, 可以观察到, 状态f的作用是向数字的结尾打印0, 然后左移两格, 切换到e状态. 跳过中间步骤:

《论可计算数及其在判定上的应用》简单理解

**9.当前状态e, 符号1, 操作: R,Px,L,L,L, 切换状态e. **

简单分析, e状态的操作是在连续1的每一个右边打印x, 之道遇到0, 切换到q状态

《论可计算数及其在判定上的应用》简单理解

10.当前状态q, 符号0, 则向尾部添加一个1, 切换状态p

《论可计算数及其在判定上的应用》简单理解

**11.当前状态p, 符号None **

通过前面分析, p状态向左找到第一个不为空的非数字格, 在这里则是x. 然后会擦除x并切换到q状态, 既向右打印1. q状态执行完毕后的纸带状态如下:

《论可计算数及其在判定上的应用》简单理解

此时会再次切换回p状态, 向左找到a, 然后切换到f状态, 向右打印一个0.

完美, 此时其实已经发现了, 图灵的方法是在连续1的后面添加x标记, 每个x标记都对应一格末尾的1. 以此来获得上一次打印1的数量.

至此, 这台简单的机器已经能够记忆一些内容了.

数字递增

至此, 图灵这台机器虽然已经能够打印一些复杂的内容了, 不过都是一些简单的重复工作, 还没有能够称之为计算的东西. 为了引出后面的计算, 先来实现一个简单的数字递增.

这是一个不符合图灵约定的机器, 加在这里只是为了引出计算. 而且他的最高位数字向左打印. 来看一下这个状态表:

状态 符号 操作 切换状态
b None P0 i
i 0 P1 r
i 1 P0,L i
i None P1 r
r None L i
r Any R r

这个简单的例子就不再展开步骤了, 可以动手试一下, 它确实实现了数字的递增. 从这里能够看出使用二进制的好处, 如果使用十进制相同的功能, 则i状态需要列出0-9始终状态十种状态, 而二进制则只需要两种.

计算\(\sqrt{2}\)

好了, 论文中首个可以称为计算的例子来了. \(\sqrt{2}\)是一个无限不循环小数.

先来介绍一下在计算\(\sqrt{2}\)时涉及的数学知识. 首先, \(\sqrt{2}\)一定是介于1-2之间的一个小数. 二进制的\(\sqrt{2}\)前十位是: 1.011. 如何确定下一位是0还是1呢? 方法听上去很简单, 先假设下一位是1, 然后让这个 n 位数与自身相乘, 若结果是2n-1位, 则说明结果小于2, 下一位是1. 若结果是2n位, 则大于2, 下一位是0. 这个很好证明, 可以自己动手算一下.

而一个 n 位数与自身相乘, 需要将其中的每一位与每一位相乘, 共需要计算n*n次. 产生n个局部乘积, 然后将局部乘积进行相加得到结果.

而图灵在计算时, 使用了稍有不同的方法进行乘法计算, 在运算中维护一个过程和, 每一位的相乘结果加到这个过程和中. 当然, 每一个位与位的乘积, 并不是加到过程和的最低位, 而是加到中间的某个位置上.

二进制的乘法很简单, 1*1=1, 其他情况都是0. 而乘积加到过程和的哪一位, 如果右起第2位(从0开始)乘以第3位, 则加到结果和的第2+3=5位上. 先说明, 在下方的过程和计算中, 过程和的最低位在左侧, 与数字格的顺序相反, 应该是为了简化计算过程吧.

来一起看看图灵是如何实现这个功能的呢? 这次就不直接上状态表了, 这个表有点长, 就拆成多个放到分析中了. 如果有感兴趣的, 将下方所有状态合起来就是完整的状态表了.

状态 符号 操作 切换状态
begin None Pa,R,P1 new

为了方便介绍, 我们就不从头跑一遍了, 太费篇章. 我们就从中间位置开始来看一下他的计算过程, 具体过程可以自己跟着状态走一遍. 假设现在已经计算出了三位1.01.

其中?标识当前需要计算的下一位, 并没有实际出现在纸带上. 每次计算新的一位, 都会调用new状态将扫描格重置到最左边的数字上:

状态 符号 操作 切换状态
new a R mark_digits
new else L new

假设此时, 纸带的状态:

《论可计算数及其在判定上的应用》简单理解

现在对各个数字位进行标记.

状态 符号 操作 切换状态
mark_digits 0 R,Px,R mark_digits
mark_digits 1 R,Px,R mark_digits
mark_digits None R,Pz,R,R,Pr find_x

很简单, 在所有已知位后面都标记x. 未知位标记z, 然后在过程和的最低位打印r.

《论可计算数及其在判定上的应用》简单理解

当前状态: find_x

状态 符号 操作 切换状态
find_x x E first_r
find_x a find_digits
find_x else L,L find_x
first_r r R,R last_r
first_r else R,R first_r
last_r r R,R last_r
last_r None Pr,R,R,Pr find_x

r是过程和的最低位, 可以将其看做0. 接下来的步骤, 会将每一个x都对应的打印两个r. 也就是说, 现在数字一共4位(包括?, 其为1). 然后打印了7个 r (2n-1). 根据之前的推测, 若结果需要新的一位, 则值为0, 否则为1.

《论可计算数及其在判定上的应用》简单理解

当前状态: find_digits

状态 符号 操作 切换状态
find_digits a R,R find_first_digit
find_digits else L,L find_digits
find_first_digit x L found_first_digit
find_first_digit y L found_first_digit
find_first_digit z L found_second_digit
find_first_digit None R,R find_first_digit

如果未知位是1, 那么过程和7位就够用了, 否则未知位就是0. 现在, 已经有了都是0的7位过程和, 可以开始做乘法运算了.

现在, 开始准备首次的位与位相乘了. 为了记录当前是那两位在做乘法运算, 使用x, y, z进行标记. 其中x标记与y标记做乘法运算, 若自身相乘, 则标记为z. 先通过find_digits 回到第一个非数字格. 然后通过find_first_digit 跳到第一个做乘法运算的数字格. 并根据标记的不同调用不同的方法.

《论可计算数及其在判定上的应用》简单理解

当前状态: found_second_digit

状态 符号 操作 切换状态
find_second_digit x L found_second_digit
find_second_digit y L found_second_digit
find_second_digit None R,R find_second_digit
found_first_digit 0 R add_zero
found_first_digit 1 R,R,R find_second_digit
found_second_digit 0 R add_zero
found_second_digit 1 R add_one
found_second_digit None R add_one

这里可以看到, 若找到的数字是0, 则直接加0, 因为相乘后的结果必是0. 若相乘之后的结果是1, 则向过程和加1.

若找到的第一个数字是1, 则转换去寻找第二个数字.

《论可计算数及其在判定上的应用》简单理解

当前状态: add_one

状态 符号 操作 切换状态
add_zero r Ps add_finished
add_zero u Pv add_finished
add_zero else R,R add_zero
add_one r Pv add_finished
add_one u Ps, R,R carry
add_one else R,R add_one

虽然给过程和中加0并不会对其值造成改变, 但是不管想其中加入了什么, 机器都需要对其进行一些维护. 之前说过, 过程和的r表示0, 其实s, t也表示0, 对应的, u, v, w 则表示1. 为什么需要多个字母来表示同一个数字呢? 是为了下一次加法运算时, 用来标识当前数字已经参与过运算, 应该将结果加到下一位上.

add_zero会将它找到的第一个r标记为s, 或者找到的第一个u标记为v. 然后结束加法操作.

而向过程和中加一, 则需要更多的操作, 毕竟数字已经变了嘛, 而且还需要处理进位. 将找到的第一个r变成v(0变成1), 或者找到的第一个u变成s(1变成0)并同时处理进位.

《论可计算数及其在判定上的应用》简单理解

当前状态: add_finished

状态 符号 操作 切换状态
add_finished a R,R erase_old_x
add_finished else L, L add_finished
erase_old_x x E,L,L print_new_x
erase_old_x z Py,L,L print_new_x
erase_old_x else R,R erase_old_x
print_new_x a R,R erase_old_y
print_new_x y Pz find_digits
print_new_x None Px find_digits
erase_old_y y E,L,L print_new_y
erase_old_y else R,R erase_old_y
print_new_y a R new_digit_is_one
print_new_y else Py,R reset_new_x

此时, 加法结束了, 需要移动x, y, z标记, 来标识下一对需要相乘的数字. 简单设想一下, 若只有一个z则将其标记为y并将左侧标记为x即可. 若一个x一个y, 则将x左移一位, 但是当x到达最高位时, 需要将x重置到最低位, 同时左移y. 当y到达最左侧的时候, 计算结束.

《论可计算数及其在判定上的应用》简单理解

当前状态: find_digits

现在, 计算又回到了最初的状态, 可以开始进行新一轮的计算了. 这次相乘的结果1*1=1, 再次向过程和中加一. 结果:

《论可计算数及其在判定上的应用》简单理解

继续执行后, 需要下面几个状态(下面为每次回到find_digits状态的纸带情况)

状态 符号 操作 切换状态
reset_new_x None R,Px flag_result_digits
reset_new_x else R,R reset_new_x
flag_result_digits s Pt,R,R unflag_result_digits
flag_result_digits v Pw,R,R unflag_result_digits
flag_result_digits else R,R flag_result_digits
unflag_result_digits s Pr,R,R unflag_result_digits
unflag_result_digits v Pu,R,R unflag_result_digits
unflag_result_digits else find_digits

《论可计算数及其在判定上的应用》简单理解

《论可计算数及其在判定上的应用》简单理解

可以看到, 当x重置的时候, 下一次位与位相乘的结果需要加到过程和的第二位上, 因此, 需要对过程和的内容做少许修改: 第一个sv变成tw, 剩下的sv变成ru. 为了下一次计算的时候, 能够将结果加到对应的位置上, 就是下一次相乘结果的相加位要向后一格, 在做加一操作的时候, 只识别r, u, 所以之后的标识符还需要重置.

操作已经简单的走了, 是时候将所有状态放出来了.

状态 符号 操作 切换状态
carry r Pu add_finished
carry u Pr,R,R carry
carry None Pu new_digit_is_zero
new_digit_is_zero a R print_zero_digit
new_digit_is_zero else L new_digit_is_zero
print_zero_digit 0 R,E,R print_zero_digit
print_zero_digit 1 R,E,R print_zero_digit
print_zero_digit None P0,R,R,R cleanup
new_digit_is_one a R print_one_digit
new_digit_is_one else L new_digit_is_one
print_one_digit 0 R,E,R print_one_digit
print_one_digit 1 R,E,R print_one_digit
print_one_digit None P1, R,R,R cleanup
cleanup None new
cleanup else E,R,R cleanup

其中carry的操作就是进位操作, 当遇到符号1时, 将其改为0继续进位, 当遇到符号0的时候, 则改为1, 结束. 若遇到空内容, 说明计算产生第8位了, 则未知位必为0, 直接结束计算.

从上面看到, 还有一种结束情况就是y走到头了, 这时没有产生进位, 说明未知位为1.

剩余的几个状态就一带而过了, 未知位是1, 未知位是0, 以及cleanup清理所有过程和的内容.

整个乘法会以两种情况结束:

  1. 当进位产生新的位数时, 结束. 未知位是0
  2. y标记下一步走到头了, 结束. 未知位是1

至此, 已经完成了计算\(\sqrt{2}\)的操作. 这个状态可以周而复始的一直计算下去. 不再继续延时了, 感兴趣的可以自己按照上面的状态表算一遍. 看了上面命名的状态, 有没有觉得和函数很像呀.

其实其原理并不复杂, 就是进行0和1的简单尝试, 然后根据结果的大小来决定后一位是什么内容. 但我还是被图灵能够在一维的纸带上实现的操作折服了.

方法集

看了上面的内容, 有没有觉得少了点什么? 那一个个的不就是函数嘛.而图灵下一步要做的, 就是要组建一格常用表集, 作为基础来搭建一些更为复杂的表. 更形象的说法就是封装函数. 同时, 函数的封装也方便他后面构建更大的程序. 关于函数的概念就不在赘述了, 天天用的也不少了. 就给出图灵的表达形式, 大家自然能够看懂.

回看一下上面的new_digit_is_zeronew_digit_is_one两个函数, 函数化之后的标识:

状态 符号 操作 下一个状态
print_digit(A) 0 R,E,R print_digit(A)
print_digit(A) 1 R,E,R print_digit(A)
print_digit(A) None P(A),R,R,R cleanup

很好理解哈, 就不解释了. 同时, 其参数也可以传递状态. 将这个基础表称之为骨架表. 可以看的出来, 所有的骨架表都可以转换成不带参数的形式. 到这里, 其实和现在的函数式编程思想已经很接近了有木有.

举个栗子:

状态 符号 操作 下一个状态
f(S, B, a) α L f1(S, B, a)
else L f(S, B, a)
f1(S, B, a) a S
None R f2(S, B, a)
else R f1(S, B, a)
f2(S, B, a) a S
None R B
else R f1(S, B, a)

其中 α 用来标识开始.

来看一下这个骨架表是做什么用的? 简单分析一下:

  • f 函数: 向左找到标识符α, 然后转到 f1函数

    • 将扫描格进行定位
  • f1函数: 向右找, 若找到 a, 执行 S 函数, 空格向右转到 f2函数, 否则继续向右寻找

    • 找到向右的第一个 a, 执行 S 函数
  • f2函数: 向右找, 若找到 a, 执行 S 俺叔, 空格向右执行 B 函数, 否则向右转到 f1函数

    • 找到向右的第一个 a, 执行 S 函数
    • 若找到连续两个空格, 执行 B 函数(与 f1函数配合, 识别连续的两个空格)

可以看出, f 就是 find, 他会寻找 a(也是参数), 若找到, 执行 S, 没找到则执行 B.

再来看一个栗子:

状态 符号 操作 下一个状态
e1(S) E S
e(S, B, a) f(e1(S), B, a)

看这个骨架表. 函数 e1 的作用是将符号擦除, 然后转到 S 状态.

那么相对的, e 函数的作用是, 向右找到第一个 a, 若找到了则擦除并转到 S, 若没有找到, 转到 B. 同时, 图灵还允许一个函数同时接收不同个数的参数(想到了什么? 函数重载)

状态 符号 操作 下一个状态
e(B, a) e(e(B, a), B, a)

这个两个参数的e函数是不是有点皮了. 来分析, 三参数的e, 作用是找到 a 符号并擦除, 然后转到 S. 再看两参数的e函数, S 是什么? 还是他自己, 继续找一个 a 符号并擦除, 直到擦除调所有的 a.

也就是说, 这个函数实现的功能是, 擦除所有的 a, 然后转到 B. 从这可以看出, 图灵的思想对现在的编程提供了一定思路, 已经有函数的嵌套调用了, 膜拜了.

再来看一些定义的基础库, 来帮助理解图灵的这个概念.

找到出现的最后一格 a

函数 f 从左向右查找, 函数 g 从右向左找.

状态 符号 操作 下一个状态
g(S) Any R g(S)
None R g1(S)
g1(s) Any R g(S)
None S
g1(S, a) a s
else L g1(S, a)
g(S, a) g(g1(S, a))

其中单参数的函数 g 和单参数的函数 g1配合, 将扫描格移到最右侧.

在结尾打印

状态 符号 操作 下一个状态
pe(S, b) f(pe1(S, b), S, α)
pe1(S, b) Any R,R pe(S, b)
None Pb S

其中, 这个函数有一个假设, 就是最左侧有两个连续α, f 函数先将扫描格移动到最左侧α, 然后右移一格开始寻找, 这时当前格就是α.

在结尾打印两个字符

状态 下一个状态
pe2(S, a, b) pe(pe(S, b), a)

直接先在结尾打印 a, 然后在在结尾打印 b

增强 find 函数

f 函数在找到所需字符后, 将扫描格向左或向右移动.

状态 操作 下一个状态
l(S) L S
r(S) R S
fl(S, B, a) f(l(S), B, a)
fr(S, B, a) f(r(S), B, a)

复制字符

找到用 a 标记的字符, 复制到结尾. 然后调用 S 函数.

状态 符号 下一个状态
c(S, B, a) fl(c1(S), B, a)
c1(S) β pe(S, β)

这里有一个特殊的地方, c1函数中, 符号β表示扫描到的字符. 当然也可以将不同的字符进行排列(可能只有0或1).

复制并擦除

状态 下一个状态
ce(S, B, a) c(e(S, B, a), B, a)
ce(B, a) ce(ce(B, a), B, a)

其中三参数的ce, 会找到 a 标记的符号, 并复制到结尾. 然后调用e擦除 a 标记. 擦除后执行第一个参数的状态. 而在两参数的ce中, 传递过期的第一个参数是它自己. 就是说, 两参数的ce会将所有 a 标记的符号复制, 同时擦除 a 标记, 最终转到 B.

(在之前打印'001011011101111...'的例子中, 就可以使用这个函数对1进行复制)

看到这里已经发现了, 图灵机令人咋舌的效率问题. 为了执行以此复制并擦除, 函数 c 会先调用 f 遍历一遍(f 函数会先回到开头的位置)进行复制操作, 然后再调用 f 进行遍历并将其擦除. 而复制擦除下一个字符, 又要重复这些操作. 如果在第一次找到的时候, 就顺手擦除, 然后再进行复制, 岂不更好.

不过, 想必图灵在当时并没有考虑效率的问题, 或者说他并不关心效率的好坏, 毕竟连这台机器都是想象出来的. 现在, 图灵已经可以构建一格函数库了, 类似与现在的系统库, 想必能够构造更为强大的机器了.

数字化

接下来, 图灵对他的表格进行了重新定义. 他先是证明了所有状态都可以拆解成如下所示的三个状态:

状态 符号 操作 符号 标识
qi Sj PSk, L qm N1
qi Sj PSk, R qm N2
qi Sj PSk qm N3

其中 Sk 用来表示符号. 规定:

  • S0 : 表示空格
  • S1 : 表示0
  • S2 : 表示1
  • 其他: 一些自定义符号

其中的操作是:

  • N1 : 打印并左移
  • N2 : 打印并右移
  • N3 : 打印

疑问, 改成这样真的能够表示之前的所有操作么? 举例一下 :

  • 擦除操作: 既打印空格. PS0
  • 左移操作: 既打印格子原本的符号.

而之前的一些较长的操作, 通过拆解也可以拆成这样的基本形式. 然后, 图灵定义了这么一格五元组:

qiSjSkLqm 用来直接表示上方的 N1 . 无用说也能和上面的表格对上吧. 有没有想到图灵要做什么? 这里每一个五元组, 都对应一个操作, 如果将多个五元组连起来, 并用分号隔开, 是不是就能完整的描述一个程序了.

至此, 他的状态表已经经过了一次转换, 变成了下标的形式. 接下来, 图灵要把下标去掉. 替换:

  • qi -> D 后面 i 个 A
  • Sj -> D 后面 j 个 C

如此一来, 他的机器就只剩下以下几个字符: D, A, C, L, R, N, ;. 其中N表示不移动. 图灵将这样的描述称为标准描述.

再然后, 图灵将仅存的这7个字符, 用1-7的数字来表示. 既: 1(A), 2(C), 3(D), 4(L), 5(R), 6(N), 7(

上一篇:Java虚拟机 运行时数据区


下一篇:Python基础案例练习:制作学生信息管理系统