MathLabs

数学基础

递归函数与图灵机

精确刻画哪些函数可由算法计算的形式化模型。

直观机器能计算什么?

计算器能做加法和乘法;编译器能做类型检查;AI(有时)能回答问题。但是否存在没有任何算法、无论多么巧妙,都永远无法计算的函数?阿兰·图灵的回答是——有——这来自对"算法"的一个精确数学模型:图灵机,由一条纸带、一个读写头和一张有限的规则表组成。两种看似不同的形式化,递归函数(由简单构件通过复合与递归构造而成)与图灵机(一个机械的逐步过程),结果证明恰好计算出完全相同的函数类——这是丘奇-图灵论题的有力证据,即这确实就是"一切可计算的东西"。

带标签转移边的图灵机状态有向图。
一台小型图灵机的状态转移图:顶点是状态,边是标有(读符号→写符号,移动方向)的转移。

大学原始递归函数与 μ\mu-递归函数

定义: 原始递归函数

原始递归函数是 Nk→N\mathbb N^k \to \mathbb N 函数中最小的一类,包含零函数、后继函数 S(n)=n+1S(n)=n+1、所有投影函数,并对复合与原始递归封闭:给定 g:Nk→Ng:\mathbb N^k\to\mathbb N 与 h:Nk+2→Nh:\mathbb N^{k+2}\to\mathbb N,递归式 f(x⃗,0)=g(x⃗)f(\vec x,0)=g(\vec x),f(x⃗,n+1)=h(x⃗,n,f(x⃗,n))f(\vec x,n+1)=h(\vec x,n,f(\vec x,n)) 定义出一个新的原始递归函数 ff。加法、乘法、指数运算,以及所有带固定上界的"for 循环"程序都是原始递归的——而且每个原始递归函数都是全函数(对所有输入都有定义),并且对每个输入都在有界步数内停机。

f(x⃗,0)=g(x⃗),f(x⃗,n+1)=h(x⃗,n,f(x⃗,n))f(\vec x,0)=g(\vec x), \qquad f(\vec x,n+1)=h(\vec x,n,f(\vec x,n))

要囊括所有可计算函数——包括可能不停机的那些——我们再加一个算子。**μ\mu-递归(一般递归)函数加入了无界极小化**:μy. [P(x⃗,y)=0]\mu y.\,[P(\vec x,y)=0] 返回使 P(x⃗,y)=0P(\vec x,y)=0 成立的最小 yy,依次搜索 y=0,1,2,…y=0,1,2,\dots——若不存在这样的 yy,则干脆永不返回。正是这一点使 μ\mu-递归函数具备不停机的能力,克莱尼定理表明它们恰好计算出与图灵机相同的(部分)函数类。

μy. [P(x⃗,y)=0]=min⁡{y:P(x⃗,y)=0}\mu y.\,[P(\vec x,y)=0] = \min\{y : P(\vec x,y)=0\}
原始递归、一般(μ\mu)递归与图灵可计算性的比较
类构造方式总会停机吗?例子
原始递归复合+有界递归是,总是全函数+,×,+,\times, 指数运算
一般(μ\mu)递归原始递归+无界 μ\mu否,可能永远循环阿克曼函数 AA
图灵可计算(部分)状态+纸带+转移规则否,恰好等于 μ\mu-递归任何算法

不存在这样的算法 H(e,x)H(e,x):对每个程序索引 ee 和输入 xx,它总是停机并正确输出 φe(x)\varphi_e(x)(即以输入 xx 运行程序 ee)是否会停机。

为什么成立?

这正是无论何种杀毒软件、编译器或集成开发环境,都无法在完全一般的意义下完美检测出死循环、死代码或"这个函数总是崩溃"的数学原因——这不是当今工程技术的局限,而是一堵坚硬的数学壁垒。

证明

反证:假设存在这样的判定器 HH:若 φe(x) ⁣↓\varphi_e(x)\!\downarrow(停机)则 H(e,x)=1H(e,x)=1,若 φe(x) ⁣↑\varphi_e(x)\!\uparrow(永远运行)则 H(e,x)=0H(e,x)=0,且 HH 本身总是停机并给出正确答案。

利用 HH,构造一个新程序 DD,对输入 ee:计算 H(e,e)H(e,e);若 H(e,e)=1H(e,e)=1,则 DD 进入无限循环;若 H(e,e)=0H(e,e)=0,则 DD 立即停机。DD 是由 HH 有效构造而成的(只是 HH 加上一条 if 语句和一个循环),所以它有某个程序索引 dd,即 D=φdD=\varphi_d。

现在提出自指的问题:φd(d)\varphi_d(d) 是否停机?

情形1:若 φd(d)\varphi_d(d) 停机,那么由 HH 的正确性,H(d,d)=1H(d,d)=1。但由 DD 的定义,H(d,d)=1H(d,d)=1 会使 DD 在输入 dd 上永远循环——即 φd(d)\varphi_d(d) 不停机。矛盾。

情形2:若 φd(d)\varphi_d(d) 不停机,那么由 HH 的正确性,H(d,d)=0H(d,d)=0。但由 DD 的定义,H(d,d)=0H(d,d)=0 会使 DD 在输入 dd 上停机——即 φd(d)\varphi_d(d) 确实停机。矛盾。

两种情形都导致矛盾,所以 HH 存在的假设为假。停机问题不可判定。■\blacksquare

进阶赖斯定理

停机问题只是一个更广泛现象的一个例子。称偏可计算函数的性质 PP 是语义的,如果它只依赖于程序 ee 所计算的函数 φe\varphi_e 本身,而不依赖于源代码;称其为非平凡的,如果某个可计算函数具有该性质而另一个不具有。

定理: 赖斯定理

对偏可计算函数的每个非平凡语义性质 PP,集合 {e:φe has property P}\{e : \varphi_e \text{ has property } P\} 都是不可判定的。

为什么成立?

这一个定理立刻排除了诸如"这个程序是否计算零函数""这个程序是否计算全函数""这两个程序是否等价"等无数关于程序行为的自然问题的算法——无需为每个问题单独构造对角线论证,一击即中。

证明

不失一般性,假设处处未定义的函数 ∅\emptyset(由一个对任何输入都永不停机的程序所计算)不具有性质 PP——否则改用补性质 ¬P\lnot P 论证,它可判定当且仅当 PP 可判定。由于 PP 非平凡,固定某个程序 e0e_0,其函数 φe0\varphi_{e_0} 具有性质 PP。

反证:假设 PP 可由某算法 DD 判定(给定一个程序索引,DD 停机并正确报告该程序的函数是否具有性质 PP)。我们把停机问题归约到 PP,从而与定理1矛盾。

对任意一对 (e,x)(e,x),有效地构造(通过简单的文本操作——克莱尼的 s-m-n 定理)一个新程序 e′e',它对任意输入 yy:先模拟程序 ee 在输入 xx 上运行;一旦该模拟停机,e′e' 就接着模拟程序 e0e_0 在输入 yy 上运行并输出其结果。

考察两种情形。若 ee 在 xx 上停机:对 ee 在 xx 上的模拟会结束,于是 e′e' 在所有输入上的行为都与 e0e_0 完全一致,即 φe′=φe0\varphi_{e'}=\varphi_{e_0}——它具有性质 PP(因为 PP 是语义的,只依赖于所计算的函数,而 φe0\varphi_{e_0} 具有 PP)。若 ee 在 xx 上不停机:对 ee 在 xx 上的模拟永不结束,于是 e′e' 在任何输入 yy 上都永远到不了模拟 e0e_0 的那一步;因此 φe′\varphi_{e'} 就是处处未定义的函数 ∅\emptyset,按假设它不具有性质 PP。

所以:ee 在 xx 上停机   ⟺  \iff φe′\varphi_{e'} 具有性质 PP   ⟺  \iff D(e′)D(e') 回答"是"。由于 (e,x)↦e′(e,x)\mapsto e' 可计算,算法"由 (e,x)(e,x) 计算出 e′e',再运行 D(e′)D(e')"就能判定停机问题——与定理1矛盾。故不存在这样的 DD:PP 不可判定。■\blacksquare

大学实际应用与典型例题

编译器优化器必须判断诸如"这段代码是否可达"或"这个变量的值是否重要"之类的问题——根据赖斯定理,这些在完全一般的意义下是不可判定的,这正是真实编译器使用保守近似的原因(它们宁可保留一些确实死掉的代码,也不愿冒删除存活代码的风险)。忙碌海狸函数 BB(n)BB(n)——一台 nn 状态、会停机的图灵机在停机前所能执行的最大步数——是一个具体的不可计算函数:已知值有 BB(1)=1BB(1)=1,BB(2)=6BB(2)=6,BB(3)=21BB(3)=21,BB(4)=107BB(4)=107,而在2024年,协作项目"忙碌海狸挑战"(由 Tristan Stérin 及合作者主导,由化名"mxdys"的贡献者给出经 Coq 验证的证明)确立了 BB(5)=47,176,870BB(5)=47{,}176{,}870——这表明即便是这个看似"简单"的组合问题,也只能逐例计算,永远无法用一个通用算法解决。

例题: 展开阿克曼函数 A(2,2)A(2,2)

利用规则 A(0,n)=n+1A(0,n)=n+1、当 m>0m>0 时 A(m,0)=A(m−1,1)A(m,0)=A(m-1,1)、当 m,n>0m,n>0 时 A(m,n)=A(m−1,A(m,n−1))A(m,n)=A(m-1,A(m,n-1)),逐步计算 A(2,2)A(2,2),并解释为何阿克曼函数是全函数却不是原始递归的。

解答

由第三条规则,A(2,2)=A(1,A(2,1))A(2,2)=A(1,A(2,1))。首先需要 A(2,1)=A(1,A(2,0))A(2,1)=A(1,A(2,0)),而由第二条规则 A(2,0)=A(1,1)A(2,0)=A(1,1)。

A(1,1)=A(0,A(1,0))=A(0,A(0,1))=A(0,2)=3A(1,1)=A(0,A(1,0))=A(0,A(0,1))=A(0,2)=3(用第二、第一条规则展开两次)。故 A(2,0)=A(1,1)=3A(2,0)=A(1,1)=3,从而 A(2,1)=A(1,3)A(2,1)=A(1,3)。

A(1,3)=A(0,A(1,2))A(1,3)=A(0,A(1,2)),而 A(1,2)=A(0,A(1,1))=A(0,3)=4A(1,2)=A(0,A(1,1))=A(0,3)=4。故 A(1,3)=A(0,4)=5A(1,3)=A(0,4)=5。因此 A(2,1)=5A(2,1)=5。

回到最上层:A(2,2)=A(1,A(2,1))=A(1,5)=A(0,A(1,4))A(2,2)=A(1,A(2,1))=A(1,5)=A(0,A(1,4));展开 A(1,4)=A(0,A(1,3))=A(0,5)=6A(1,4)=A(0,A(1,3))=A(0,5)=6;故 A(1,5)=A(0,6)=7A(1,5)=A(0,6)=7。所以 A(2,2)=7A(2,2)=7。

阿克曼函数已被证明是全函数(它总会最终归约到 A(0,n)A(0,n) 的情形),所以它属于一般递归函数类——但它比每一个原始递归函数增长得都快(例如 A(3,n)=2n+3−3A(3,n)=2^{n+3}-3,A(4,n)A(4,n) 已经是指数塔)。由于可以证明每个原始递归函数最终都被某个固定的 A(k,⋅)A(k,\cdot) 所支配,没有任何原始递归函数能等于 AA 本身——正是这种对角线式的支配论证,而非 μ\mu 算子,把阿克曼函数排除在原始递归之外,尽管它是全函数。

例题: 把停机问题归约到"这个程序会打印hello吗?"

证明问题"给定程序 ee,以(无输入)方式运行 ee,是否曾打印出字符串 `hello`?"是不可判定的,方法是直接从停机问题归约——不使用赖斯定理。

解答

反证:假设存在算法 Q(e)Q(e),能判定运行 ee 是否曾打印出 `hello`。我们用 QQ 来判定停机问题,从而与定理1矛盾。

对任意程序 ee 和输入 xx,有效地构造一个新程序 e′e'(无需输入):模拟 ee 在 xx 上运行;一旦该模拟停机,e′e' 就打印 `hello` 并停止。

若 ee 在 xx 上停机:模拟会结束,于是 e′e' 到达打印步骤并打印 `hello`。若 ee 在 xx 上不停机:模拟永不结束,于是 e′e' 永远到不了打印步骤,也永远不打印 `hello`。

所以 ee 在 xx 上停机   ⟺  \iff Q(e′)Q(e') 回答"是"。由于 (e,x)↦e′(e,x)\mapsto e' 可计算,"构造 e′e' 再运行 Q(e′)Q(e')"就能判定停机问题——与定理1矛盾。故 QQ 不可能存在:"打印hello"问题不可判定。(这正是编译器分析的典型模式:"这行代码是否可达"具有相同的结构。)

利用 A(1,n)=n+2A(1,n)=n+2,A(1,3)A(1,3) 等于多少?

以下哪项是停机问题不可判定性对编译器设计的影响?

赖斯定理不适用于哪个性质?

在停机问题不可判定性的对角线证明中,是什么导致了矛盾?

参考文献

  1. Wikipedia contributors (2024). Halting problem
  2. Wikipedia contributors (2024). Rice's theorem
  3. Wikipedia contributors (2024). Ackermann function