跳转到内容

Turing 1936 可计算性

待复核

Turing 1936 这篇论文做了一件事:给”机器能算什么”画了一条数学边界

日常类比:想象一个只会按表格走格子的机器人——它面前有一条长长的纸带(一格一格写着符号),它能往左走一步、往右走一步、读当前格子的符号、改写当前格子的符号。它脑子里只有有限张”卡片”(状态),每张卡片上写着”看到什么符号就做什么动作 + 切到哪张卡片”。

这个看起来弱到不行的机器人,就是 Turing 定义的”图灵机”。它能算的东西,正好就是”机械过程能算的所有东西”。

更狠的是:Turing 还构造了一个”超级机器人”——它能读另一台机器人的指令表当作自己的输入,然后模仿那台机器人。这就是通用图灵机(CPU 的数学祖宗)。

最后他证明:有一个具体问题,所有这种机器人都解不了——叫停机问题

不理解 Turing 1936,下面这些事都没法解释:

  • 为什么 CPU 是 CPU——它的数学定义就是”通用图灵机”,所有现代电脑都是它的物理实现
  • 为什么 IDE / linter / 死循环检测器永远做不到 100% 准确——这不是工程不够好,是数学上的不可能
  • 为什么”程序”和”数据”在内存里长得一模一样(冯诺依曼架构的灵感来源)
  • 为什么”图灵完备”是个需要斟酌的标签——它意味着”什么都能算”也意味着”什么都分析不准”
  • 为什么 lambda-calculus 和图灵机被并列——它们是同一件事的两种说法

理解这篇论文,抓住 三块

  1. 图灵机长什么样:一个有限状态控制器 + 一条无限长的纸带 + 一张5 元组规则表。每条规则:「状态 q + 读符号 a → 写 a’ + 左/右移 + 新状态 q’」。下文用现代教材的「接受就停」讲法(Sipser 风格);原文更侧重打印可计算实数的数字,骨架同一台机器。

  2. 通用图灵机(UTM):一台机器 U,输入是「另一台机器 M 的描述」+「M 的输入 w」,输出和 M(w) 一模一样。关键洞察:M 的描述本身是有限符号——可以塞进纸带当数据。这就是”程序即数据”的最早形态。

  3. 停机问题不可判定:不存在一台图灵机 H,对任何「机器 M + 输入 w」都能正确回答”M 在 w 上会不会停”。证明用对角线法:假设 H 存在,构造 D = “如果 H 说我会停,我就死循环;否则就停”,然后让 D 读自己——两种情况都矛盾,所以 H 不存在。

这三块加起来,顺手摧毁了 Hilbert 的”机械数学判定机”梦想(godel-1931 已经摧毁了”完备性”,Turing 摧毁了最后一根柱子”可判定性”)。

案例 1:写一个最小图灵机识别 “abab”

Section titled “案例 1:写一个最小图灵机识别 “abab””

任务:纸带是 “abab” 就进接受态 q4;读到不符符号进拒绝态 qrej。

当前状态读到方向切到
q0aaRq1
q1bbRq2
q2aaRq3
q3bbRq4
任一 qi不符/空白原样Rqrej

逐步走格子(输入 abab)

  1. q0 读 a → 右移,切 q1
  2. q1 读 b → 右移,切 q2
  3. q2 读 a → 右移,切 q3
  4. q3 读 b → 右移,切 q4(接受,停)

若第 2 步读到 a(输入是 aab...),直接切 qrej。整台机器就是一张表。

案例 2:通用图灵机 = 现代 CPU 的祖先

Section titled “案例 2:通用图灵机 = 现代 CPU 的祖先”

三步跟做:

  1. 编码:把 M 的每条 5 元组写成固定串,例如 q0,a→a,R,q1 拼成 ⟨M⟩
  2. U 读描述:纸带上放 ⟨M⟩#w,U 先解析出「当前状态 + 当前符号」对应哪条规则
  3. 模拟一步:按那条规则改写 w 区、移动虚拟磁头、更新当前状态;循环直到 M 停
U(desc, w):
state, head = start(desc), 0
while state not in {accept, reject}:
rule = lookup(desc, state, w[head])
w[head], head, state = rule.write, head+rule.dir, rule.next

对照:CPU≈U,二进制程序≈⟨M⟩,输入≈w。1945 年 EDVAC 的程序存储思想,与通用图灵机一脉相承。

案例 3:停机问题的对角线证明(简化版)

Section titled “案例 3:停机问题的对角线证明(简化版)”

假设有万能死循环检测器 H(M, w):停返回 1,不停返回 0。写变态程序 D:

D(M):
if H(M, M) == 1: 死循环
else: 立即停

D(D) 会不会停?

  • 若会停 → 须走「立即停」→ 须 H(D,D)=0 → 即不停 → 矛盾
  • 若不停 → 须走「死循环」→ 须 H(D,D)=1 → 即会停 → 矛盾

所以 H 不存在。

  1. 图灵机是慢的?错——图灵机不是真实硬件,没”快慢”概念。它是可计算性的定义工具,不是性能模型。算法书里的 “O(n)” 用的是 RAM 模型,不是 TM 步数,两者别混。

  2. 图灵完备一定好?错——图灵完备意味着”能算一切可计算函数”,但也继承了停机问题不可判定。Datalog / 标准 SQL 故意图灵完备,换来了”查询必停”的可分析性。设计 DSL 时这是关键 trade-off。

  3. 量子计算能突破?错——量子图灵机在可计算性上和经典图灵机等价。能算的东西完全相同,只是某些问题更快(指数级加速)。停机问题在量子计算机上仍然不可判定。

  4. 现代 CPU 是图灵机?严格说错——你的电脑只有有限内存,本质上是有限状态自动机或线性有界自动机。但抽象上等价——理论分析时假装内存无限是有用的简化。

用图灵机思维分析

  • 问”原则上能不能算完”——可判定性(停机、等价性等)
  • 设计 DSL 时权衡表达力 vs 可分析性——要不要图灵完备
  • 理解”为什么 IDE 不能 100% 检测死循环”
  • 入门复杂性时认清底座——P/NP 建立在 TM 上,但那是多快,不是能不能

不要用图灵机思维分析

  • 真实硬件性能——用 RAM / cache profiler(别拿 TM 步数当 O 记号)
  • Spectre 类微架构漏洞——TM 没有推测执行
  • 并发 / 分布式——用 actor / CSP / 共享内存模型
  • Web 前端状态——有限状态机就够,不必上 TM
  • 1900 年:Hilbert 在巴黎数学家大会上提了 23 个问题,第 2 个是”算术系统的相容性”。后来发展成 Hilbert 计划:完备性 + 相容性 + 可判定性。
  • 1931 年godel-1931 干掉了完备性——任何包含算术的形式系统都有”既证不出也反不出”的命题。
  • 1936 年初:Hilbert 计划只剩”可判定性”一根柱子。
  • 1936 年 5 月23 岁的 Turing(剑桥研究生)递交这篇论文,把最后一根柱子也推倒了。同年 4 月,普林斯顿的 Alonzo Church 用 lambda-calculus 独立证明了等价结果——但 Turing 的形式化更直观、更接近物理机器,最终成为标准。
  • 1936 年秋:Turing 去普林斯顿做 Church 的博士生,两人一起把”图灵机 = λ-演算 = 递归函数”三种定义证明了完全等价。这就是 Church-Turing 论题
  • 1945 年:EDVAC 报告把”程序当数据存进内存”写成工程方案,与通用图灵机同一脉络。
  1. 形式化是革命的前提——把”机械过程”这个直觉概念翻译成数学对象,才能证明”某些东西原则上不可机械化”。这是 20 世纪计算机科学的奠基操作。

  2. 自指 + 对角线 = 强力武器——Cantor 的实数不可数性、Russell 悖论、godel-1931 不完备、Halting Problem 不可判定,都是同一招。学会这一招你能看穿很多”原则上不可能”的证明。

  3. 理论上的不可能 ≠ 工程上的无用——停机问题不可判定,不代表 IDE 不能检测大部分死循环。理论给 worst-case 保证;工程做 average-case 启发式。两者不矛盾。

  4. 抽象的层次很重要——可计算性用 TM,复杂性用 RAM 模型,性能优化用 cache profiler。同一段代码在不同层次有不同模型。学习时分清”现在问的是哪一层”。

  • Sipser, Introduction to the Theory of Computation —— 标准教材,TM 章节极清晰,建议从这里入门
  • Petzold, The Annotated Turing —— 逐行解读 1936 原论文,配着原文读非常顺
  • Davis, The Universal Computer —— 历史脉络(Hilbert → Gödel → Turing → 冯诺依曼),不需要数学背景
  • Hofstadter, Gödel, Escher, Bach —— 把不完备 / 自指 / 计算用文学方式串起来
  • 自己写:用 Python 30 行内实现一个 TM 模拟器,跑一个识别 “0ⁿ1ⁿ” 的小机器,比读 10 篇论文更快理解
  • lambda-calculus —— 同年 Church 给出的等价定义;图灵机和 λ-演算是”可计算性”的两条定义路径
  • godel-1931 —— 不完备定理;和图灵 1936 一起摧毁了 Hilbert 形式主义计划
  • mccarthy-lisp —— 第一个把 λ-演算工业化的编程语言
  • hindley-milner —— 给 λ-演算加类型系统的方法,TypeScript / OCaml / Haskell 的根基
  • cook-1971 —— 把可计算性细化为”多快能算”,开启 NP-completeness 时代
  • chomsky-1956 —— 形式语言层级(FSA ⊊ PDA ⊊ LBA ⊊ TM)
  • von-neumann-edvac —— 通用图灵机的工程实现,现代计算机的架构起点
  • algol-60 —— ALGOL 60 — BNF 与块结构
  • belady-1966 —— Belady 1966 — 缓存替换的理论最优与 FIFO 异常
  • cook-levin —— Cook-Levin 定理 — NP-完全性的诞生
  • diffie-hellman —— Diffie-Hellman 密钥交换
  • diffie-hellman-1976 —— New Directions 1976 — 给协议世界写下公钥宪法
  • dijkstra-1965 —— Dijkstra 1965 — N 个进程怎么轮流上厕所而且谁也别卡死
  • dijkstra-goto —— Dijkstra 1968 — Go To Statement Considered Harmful
  • dijkstra-shortest-path —— Dijkstra 最短路径 — 一杯咖啡时间想出来的贪心算法
  • donar-2010 —— DONAR 2010 — 把 DNS 全球调度写成一道可解的优化题
  • feynman-simulating-physics-1982 —— Feynman 1982 — 量子计算从模拟物理开始
  • fielding-rest-2000 —— Fielding 2000 — 用约束推导法把 Web 的成功讲成了一门方法
  • godel-1931 —— Gödel 1931 — 不完备性定理
  • hamming-1950 —— Hamming 纠错码
  • huffman-1952 —— Huffman 编码
  • kajiya-1986-rendering-equation —— Kajiya 渲染方程 — 把所有渲染算法统一成一个积分方程
  • karp-21 —— Karp 21 — 21 个 NP-完全问题
  • knuth-taocp —— Knuth TAOCP — 计算机程序设计艺术
  • lambda-calculus —— λ-演算 — 用三条规则表达所有可计算函数
  • minhash-broder-1997 —— MinHash — 用最小哈希值估算两个集合的重叠度
  • neumann-2015-large-joins —— Adaptive Optimization of Very Large Join Queries — 100 张表也敢精确求解
  • polar-codes-2009 —— Polar 极化码 — 把好坏不一的信道整成”完美/全错”两组
  • prolog-colmerauer —— Prolog 的诞生 — 让逻辑式子直接当程序跑
  • rest-fielding-2000 —— REST — Fielding 2000 给 Web API 写下的设计宪法
  • rsa —— RSA 公钥密码
  • scott-strachey-denotational —— Scott-Strachey 指称语义 — 给程序找一个独立于实现的数学含义
  • sel4-2009 —— seL4 — 第一个被数学证明”代码和规范完全一致”的操作系统内核
  • shannon-1948 —— Shannon 1948 — 信息论的诞生
  • shor-1994 —— Shor 1994 — 量子傅里叶变换把分解整数变成找周期
  • simhash-charikar-2002 —— SimHash — 用随机超平面把余弦相似度变成汉明距离
  • smalltalk-80 —— Smalltalk-80
  • wadler-prettier —— Wadler Prettier — 函数式优雅打印器
  • zk-snark —— zk-SNARK 零知识证明