08.3 自動機與形式語言:從圖靈機 1936 到喬姆斯基層級 1956#
0. 導言:兩個問題,一套理論#
1936 年,24 歲的 Alan Turing 為了回答 Hilbert 的 Entscheidungsproblem(可判定性問題),發明了「圖靈機」——不是為了造電腦,而是為了給「可計算」下一個數學定義。1956 年,Noam Chomsky 為了刻畫自然語言的語法,提出了形式文法層級。兩人素不相識、目的迥異,理論卻在十年後完美咬合:文法的四個層級,恰好對應四種自動機。
flowchart LR
T["Turing 1936<br/>圖靈機"] -->|"通用計算模型"| C["Chomsky 1956<br/>文法層級"]
K["Kleene 1943/1956<br/>正則表達式"] -->|等價| FA["有限自動機"]
C -->|Type 3| FA
C -->|Type 2| PDA["下推自動機"]
C -->|Type 1| LBA["線性有界自動機"]
C -->|Type 0| T
PDA --> PL["程式語言編譯器"]
FA --> RE["grep / 詞法分析"]驅動關係:Hilbert 問「數學能被完全機械化嗎」→ Gödel 1931 不完備定理震撼數學界 → Turing 1936 用機器模型證明停機問題不可判定 → 二戰密碼破譯(與 07.3 遙相呼應)→ 電腦誕生 → 編譯器需要語法理論 → Chomsky 層級 → 今天每個編程語言、每個 grep、每個 JSON parser 都是這套理論的實作。邏輯基礎危機 → 計算機革命 → 軟體工程,前因後果綿延一世紀。
1. 圖靈機:可計算性的定義#
定義 1.1(圖靈機,1936)#
圖靈機是一個七元組 \(M = (Q, \Sigma, \Gamma, \delta, q_0, q_{acc}, q_{rej})\) :
- \(Q\) :有限狀態集; \(\Sigma\) :輸入字母表; \(\Gamma \supseteq \Sigma \cup \{\blank\}\) :帶字母表;
- \(\delta: (Q \setminus \{q_{acc}, q_{rej}\}) \times \Gamma \to Q \times \Gamma \times \{L, R\}\) :轉移函數(寫、移動);
- 無限紙帶 + 讀寫頭。
「可計算」= 「某台圖靈機會停機並輸出」。這個定義的驚人之處在 Church–Turing 論題:所有合理的「有效計算」模型(λ-演算、遞迴函數、你的筆電、量子電腦)都被圖靈機涵蓋——計算能力有天花板,而且天花板 1936 年就被找到了。
定理 1.2(通用圖靈機)#
存在一台通用圖靈機 \(U\) ,輸入 \(\langle M, w \rangle\) ( \(M\) 的編碼與輸入 \(w\) ),模擬 \(M\) 在 \(w\) 上的行為。
意義:這是「程式 = 資料」概念的誕生。 \(U\) 就是作業系統與直譯器的數學原型——現代軟體的一切都建在這條定理上。
定理 1.3(停機問題不可判定)#
語言 \(\mathrm{HALT} = \{\langle M, w\rangle \mid M \text{ 在 } w \text{ 上停機}\}\) 不可判定。
證明(對角化,繼承 Gödel/Cantor 的手法)。反設判定器 \(H\) 存在,構造
問: \(D(\langle D \rangle)\) 停嗎?若停,依定義 \(H\) 說它不停——矛盾;若不停, \(H\) 說它停——矛盾。 \(\blacksquare\)
與前章的對照:08.2 的對稱性是「可數範圍內的等價」;本章出現的是不可計算的牆。組合數學告訴我們「怎麼數」,計算理論告訴我們「哪些數不了」。
# 停機問題的 Python 隱喻:無法寫出對所有程式都正確的 will_halt
import inspect
def will_halt(source_code, inp):
raise NotImplementedError("定理 1.3 說:這個函數不可能存在且總是正確")
def loop_forever(x):
while True: pass
# 但對特定簡單子類,我們可以可靠地分析(這就是為什麼有靜態分析工具)
def halts_on_simple(code: str, inp: int) -> bool | None:
"""僅對極小子集可判定;返回 None 表示『不知道』——保守近似"""
ast_ok = code.count("while") == 0 and code.count("for") == 0
return ast_ok if ast_ok else None # 有迴圈就拒絕判斷:Rice 定理的日常體現
2. 有限自動機與正則語言#
定義 2.1(DFA)#
確定有限自動機(DFA) \(A = (Q, \Sigma, \delta, q_0, F)\) ,其中 \(\delta: Q \times \Sigma \to Q\) (每個狀態、每個符號恰有一條出邊)。 \(A\) 接受 \(w = a_1 \cdots a_n\) 若從 \(q_0\) 逐字讀入後停在 \(F\) 中狀態。 \(A\) 接受的語言 \(L(A) = \{w \mid A \text{ 接受 } w\}\) 稱正則語言。
範例 2.2(二進位數模 3 餘 0 的辨識器)#
stateDiagram-v2
[*] --> s0
s0 --> s0 : 0
s0 --> s1 : 1
s1 --> s2 : 0
s1 --> s0 : 1
s2 --> s1 : 0
s2 --> s2 : 1
s0 : 餘 0(接受)
s1 : 餘 1
s2 : 餘 2觀察:狀態 \(s_r\) = 「目前讀到的數 mod 3」。DFA 的狀態數有限 ⟺ 語言只需記住有限種歷史摘要——這正是 07 章模運算的精神:同餘就是天然的有限狀態壓縮。
def mod3_dfa(s: str) -> bool:
state = 0 # 0,1,2 = 餘數
for ch in s:
state = (2 * state + int(ch)) % 3 # 左移一位再加新位
return state == 0
print(mod3_dfa("1001")) # 9 % 3 == 0 → True
print(mod3_dfa("1010")) # 10 % 3 == 1 → False
定義 2.3(Kleene 星號)#
對語言 \(L\) ,定義
即「零個或多個 \(L\) 中字串的串接」。例如 \(\{a\}^* = \{\epsilon, a, aa, aaa, \dots\}\) 。Kleene 星號是正則表達式的靈魂:grep 的 *、a+、[0-9]* 全部源自這個 1956 年的定義(Kleene 為麥卡洛克–皮茨神經網路模型尋找「可表達的事件」時發明)。
定理 2.4(Kleene 定理,1956)#
正則語言 = DFA 認得的語言 = 正則表達式描述的語言。
(證明方向一:Thompson 構造把正則表達式變 NFA,子集構造變 DFA;方向二:狀態消去法把 DFA 變正則表達式。細節見任何 automata 教科書;本節示範子集構造。)
定理 2.5(DFA 與 NFA 等價:子集構造)#
NFA(允許多條出邊與 \(\epsilon\) 邊)可轉為等價 DFA:DFA 的每個狀態 = NFA 狀態集合。代價是指數膨脹( \(2^n\) 個狀態)。
def nfa_to_dfa(nfa, alphabet):
"""nfa: dict[(state, sym)] -> set[state];回傳 DFA 轉移表"""
start = frozenset([nfa["start"]])
dfa, work = {}, [start]
seen = {start}
while work:
S = work.pop()
for a in alphabet:
T = frozenset().union(*(nfa["trans"].get((s, a), set()) for s in S)) or frozenset()
dfa[(S, a)] = T
if T not in seen:
seen.add(T); work.append(T)
return dfa # 狀態數上限 2^n,實務通常小得多
3. Pumping Lemma:正則語言的極限#
定理 3.1(正則語言的 Pumping Lemma)#
若 \(L\) 是正則語言,則存在 \(p \ge 1\) (pumping 長度,可取為 DFA 狀態數),使得任何 \(w \in L\) 、 \(|w| \ge p\) 都可寫成 \(w = xyz\) 滿足:
- \(|xy| \le p\) ;
- \(|y| \ge 1\) ;
- 對所有 \(i \ge 0\) : \(xy^i z \in L\) 。
證明(鴿籠原理)。 \(w\) 有 \(p\) 個符號,DFA 讀 \(w\) 經過 \(p+1\) 個狀態 \(q_0, \dots, q_p\) ,由鴿籠原理必有 \(q_i = q_j\) ( \(i < j \le p\) )。令 \(y\) 為 \(i\) 到 \(j\) 之間讀入的片段:這段路徑是從 \(q_i\) 回到 \(q_i\) 的迴圈,繞任意次 \(i\) 之後都會回到同一狀態,故 \(xy^iz\) 仍被接受。 \(\blacksquare\)
使用方式(反證工具):要證 \(L\) 不正則,假設它正則、選合適的 \(w\) ,證明無論怎麼 pump 都會掉出 \(L\) 。
範例 3.2( \(\{a^n b^n\}\) 不是正則語言)#
反設正則,pumping 長度 \(p\) 。取 \(w = a^p b^p \in L\) 。由條件 1、2, \(y\) 只含 \(a\) (設 \(|y| = k \ge 1\) )。取 \(i = 0\) : \(xy^0z = a^{p-k}b^p \notin L\) ( \(a\) 的個數 \(< b\) 的個數),與條件 3 矛盾。 \(\blacksquare\)
深意:有限狀態記不住任意深的計數。 \(\{a^nb^n\}\) 需要「數到 \(n\) 為止」的無界記憶——這正是「括號配對」「JSON 嵌套」需要更強機器的根本原因,也是編譯器分成「詞法(正則)」與「語法(上下文無關)」兩個階段的理論依據。
# pumping lemma 的機械化驗證:窮舉所有合法切分都會失敗
def check_not_regular(L, p, w_builder, in_L):
"""對 w = w_builder(p),檢查所有滿足 |xy|<=p, |y|>=1 的切分"""
w = w_builder(p)
cuts = []
for i in range(p + 1): # |xy| <= p
for j in range(1, p - i + 1): # |y| >= 1
x, y, z = w[:i], w[i:i+j], w[i+j:]
bad = any(not in_L(x + y * k + z) for k in range(0, 4))
if bad:
cuts.append((i, j, k := next(kk for kk in range(4)
if not in_L(x + y * kk + z))))
return f"w={w}: 所有切分皆可 pump 出語言 → {L.__name__} 非正則(若 cuts 非空)", cuts[:3]
is_anbn = lambda s: (n := s.count('a')) == s.count('b') and s == 'a'*n + 'b'*n
print(check_not_regular(is_anbn, 4, lambda p: 'a'*p + 'b'*p, is_anbn)[0])
4. 喬姆斯基層級:文法與機器的對位表#
定義 4.1(形式文法)#
文法 \(G = (V, \Sigma, P, S)\) :變數集 \(V\) 、終端符 \(\Sigma\) 、產生規則 \(P\) 、起始符 \(S\) 。 \(\Rightarrow\) 為一步推導, \(L(G) = \{w \in \Sigma^* \mid S \Rightarrow^* w\}\) 。
Chomsky 依產生規則的形式限制分四層(1956 年原論文是為自然語言;1959 年論文才與自動機對上):
| 層級 | 文法類型 | 規則限制 | 自動機 | 語言例子 | 實務 |
|---|---|---|---|---|---|
| 3 | 正則 | \(A \to aB\) 或 \(A \to a\) | DFA/NFA | \(\{aw\}\) 、數字字面量 | lex、grep |
| 2 | 上下文無關 | \(A \to \alpha\) (左邊單一變數) | 下推自動機(PDA) | \(\{a^nb^n\}\) 、平衡括號 | 編譯器 parser、YACC |
| 1 | 上下文有關 | \(\alpha \to \beta\) 、 $ | \alpha | \le | \beta |
| 0 | 無限制 | 任意 | 圖靈機 | 所有可枚舉語言 | 理論極限 |
層級嚴格遞升: \(\text{正則} \subsetneq \text{CFL} \subsetneq \text{CSL} \subsetneq \text{RE}\) ,每一層的嚴格性都由 pumping 類論證(正則的 pumping lemma、CFL 的疊代引理)證明。
範例 4.2(CFG 描述平衡括號)#
下推自動機用一個堆疊數括號深度:看到 ( 壓入、) 彈出,空堆疊且輸入盡頭即接受。堆疊就是 DFA 缺的那份無界記憶——機器能力每升一級,記憶結構升一級:無記憶 → 堆疊 → 有限帶。
# Type 2 語言的辨識:平衡括號(模擬 PDA 的堆疊)
def balanced(s: str) -> bool:
depth = 0
for c in s:
if c == '(': depth += 1 # push
elif c == ')':
depth -= 1 # pop
if depth < 0: return False
return depth == 0
print(balanced("(()())"), balanced("())(")) # True False
5. 章末小結#
| 年代 | 人物 | 貢獻 | 驅動 |
|---|---|---|---|
| 1931 | Gödel | 不完備定理 | 逼出「機械化證明」的極限問題 |
| 1936 | Turing | 圖靈機、停機問題、通用機 | 「可計算」的定義、程式=資料 |
| 1943 | Kleene/McCulloch–Pitts | 神經網路事件 | 正則表達式的起源 |
| 1956 | Chomsky | 文法層級 | 語言學 → 編譯理論 |
| 1956 | Kleene | 正則表達式 = DFA | grep、詞法分析 |
| 1959 | Chomsky | 層級對應自動機 | 編譯器架構定型 |
核心因果鏈:Hilbert 程式 → Gödel 不完備 → Turing 機器與不可判定性 → Kleene 正則語言 ↔ DFA → Chomsky 層級 ↔ 四種自動機 → pumping lemma 畫出能力邊界 → 編譯器、正則引擎、靜態分析的理論地基。
三個帶走的洞見:
- 狀態數 = 記憶量:DFA 能認什麼語言,取決於「歷史能否壓縮成有限摘要」(同餘、模運算是天然壓縮器)。
- pumping 類論證是通用武器:有限記憶必有重複,重複即可 pump——同一招對付正則(鴿籠)與 CFL(疊代引理)。
- 停機問題是所有「完美工具」的天花板:任何非平凡語意性質都不可判定(Rice 定理),這解釋了為什麼 linter、防毒、AI 對齊都只能做保守近似。
6. 下一步#
本章證明了「有些東西算不了」(停機問題),但沒有量化「算得了的東西有多難」——停機 vs. 多項式時間 vs. 指數時間的階梯,以及 \(P \overset{?}{=} NP\) 、NP-完備性(Cook 1971、Karp 1972)、與 07.3 單向函數的關係,將在第 10 章計算複雜度中展開。到那裡,我們會看到:數論(07 章)提供難題、群論(08 章)提供結構、自動機(本章)提供計算模型——三條線終於匯流成現代密碼學與演算法理論的全圖。