JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

非確定性:猜測與分身

DFA 每讀一個符號,都必須鎖定唯一的下一個狀態。如果一台機器能分裂成許多分身,同時試遍每一條路,只要有任何一個分身成功就接受呢?這就是非確定性——一個思考工具,而不是魔法硬體。

上一階梯留下的缺口

在上一階梯,你建造並運行了確定型有限自動機——一個像旋轉閘門的控制器,除了目前的狀態外什麼都不記得,且每讀一個輸入符號就遵循恰好一條規則 delta(q, a) = p(在狀態 q、讀到符號 a 時,移動到狀態 p)。定義它的關鍵字是確定性:從任一狀態出發,每個符號都指向唯一一個下一狀態,所以機器整趟運行是一條單一、完全被決定的路徑。你也認識了它的根本極限——有限記憶體無法無上限地計數,這正是為什麼 a^n b^n 落在正規語言之外。

確定性極為可預測,但它會讓設計機器變得痛苦。假設你想恰好接受字母表 Sigma = {a, b} 上 a b b 結尾的字串。一個人從左讀到右時會不斷猶豫:「這個 a 是最後那段 a b b 的開頭,還是只是中間隨便一個 a?」DFA 不能猶豫——它必須當下就決定,不准偷看後面。本階梯要介紹一台被允許猶豫的機器:它會把自己分裂,平行地探索好幾種未來。

猜測與分身:核心想法

一台非確定型有限自動機,也就是 NFA,外觀幾乎和 DFA 一樣,但它的轉移規則在一個大膽的地方放寬了:從某個狀態出發、讀到某個符號時,它可以指向零個、一個或多個下一狀態。於是規則不再是 delta(q, a) = p(單一狀態),而變成轉移到一組狀態:delta(q, a) = {p1, p2, ...},甚至可能是空集合。請把這幅畫面記在腦中——分身。每當機器面臨好幾個選擇時,就想像它分裂成一個選擇配一個分身,而每個分身都各自獨立地讀完剩下的輸入。

這帶出一個顯而易見的問題:既然有許多分身在跑,由誰決定機器是否接受?這條規則——NFA 的接受條件——是極度樂觀的:只要至少有一個分身停在接受狀態,NFA 就接受這個字串。換句話說,只要存在某一條逐個符號選擇下來的路徑落在接受狀態,就接受。唯有當每個分身都死掉(走進空的下一狀態集合)或停在非接受狀態時,這個字串才會被拒絕。一條成功的路徑就足以致勝。

一個小例子:以 a b b 結尾的字串

看看分身如何化解設計者的頭痛。我們要 {a, b} 上所有以 a b b 結尾的字串。建一台四狀態的 NFA,狀態為 q0、q1、q2、q3,起始狀態 q0,只有 q3 是接受狀態。從 q0 讀到 a 時放兩條轉移:一條 a 自迴圈回到 q0(這個分身說「我還在略過中間的雜訊」),另一條 a 到 q1(這個分身「最後的 a b b 就從這裡開始」)。其餘只是驗證這個猜測:q1 讀 b 到 q2,q2 讀 b 到 q3。讀到任何其他符號時,猜測中的那個分身根本沒有可走的轉移,於是死掉。

NFA for "ends in a b b"   (start = q0, accept = q3)

  state |   on a        |   on b
  ------+---------------+-----------
   q0   | {q0, q1}      | {q0}
   q1   | { }           | {q2}
   q2   | { }           | {q3}
   q3   | { }           | { }

Run on input  a b a b b   (track the SET of live clones):

  start         : {q0}
  read a        : {q0, q1}        (q0 stayed, and guessed -> q1)
  read b        : {q0, q2}        (q1 -> q2 ; q0 looped ; guess q1 had no... wait)
  read a        : {q0, q1}        (q2 had no 'a' move and died; q0 stayed+guessed)
  read b        : {q0, q2}        (q1 -> q2 ; q0 loops)
  read b        : {q0, q3}        (q2 -> q3 ; q0 loops)

  q3 is in the final set  ==>  ACCEPT  (some clone reached q3)
這台 NFA 同時為最後那段 a b b 嘗試每一個起點;我們之所以接受,是因為恰好在輸入結束時有一個分身落在 q3。

關於那段追蹤,有兩點要誠實說明。第一,當狀態 q2 讀到 a 時,它的目標是空集合,所以那個特定的分身就直接消失了——沒有錯誤、沒有當機,它只是不再是一條活路徑;這就是 NFA 默默放棄錯誤猜測的方式。第二,你其實不會真的跑無限多個分身;模擬 NFA 的實際做法,是在每一步追蹤「此刻可能有活分身佔據」的那一組狀態。這種把所有路徑平行探索看成一組狀態的觀點,正是我們在最後遇到的那個轉換法的種子。

為什麼非確定性不是作弊

這感覺像作弊:機器彷彿魔法般「知道」最後那段 a b b 從哪裡開始。但這裡沒有神諭,也沒有運氣。理解 NFA 的乾淨方式是猜測再驗證:想像機器在每個岔路(這裡是最後的模式從哪裡開始)都一個選擇,接著確定性地一路讀下去驗證這個猜測。接受規則只是說:若存在某個能被驗證成功的猜測,這個字串就屬於該語言。沒有任何東西是靠機率算出來的;我們是用「存在」這個詞,對所有可能的猜測作量化。

NFA 還有一項靈活之處,我們在這裡只先點名,留到下一篇展開:一條完全不消耗任何輸入符號的轉移,寫在空字串 epsilon(希臘字母 ε)上。這樣的ε-轉移讓一個分身能「免費」漂移到另一個狀態,於是把較小的機器黏接起來變得毫不費力。現在你只要知道它存在即可;用來馴服它的ε-閉包,正是緊接著的下一個主題。

能力相同,機器更小——以及一個警告

這裡有一個讓整座階梯既美麗又有點出人意料的結論:非確定性並不會多給你任何識別能力。NFA、ε-NFA 與 DFA 三者識別的,恰好是同一類語言——正規語言。這就是NFA 與 DFA 等價定理。所以 NFA 同樣永遠識別不了 a^n b^n;非確定性只買到精簡與設計上的便利,買不到觸及範圍。容易的那個方向是:每台 DFA 本來就已經是一台 NFA;令人驚奇的方向則是:每台 NFA 都能被攤平回某台 DFA。

把 NFA 攤平成 DFA 的訣竅,就是子集構造法(也叫冪集構造法),而你在上面那段追蹤裡已經看見了它的心跳。既然模擬 NFA 就等於追蹤「可能有活分身佔據的那一組狀態」,那就乾脆把每一個這樣的集合當成一台全新 DFA 的單一狀態。完整流程我們會用一整個階梯來跑,但這四步的骨架短到現在就可以先嚐一口。

  1. DFA 的起始狀態,就是只含 NFA 起始狀態的那個集合(之後會被 ε-轉移擴張)。
  2. 對目前的集合 S 與符號 a,下一個 DFA 狀態就是把每個 q 屬於 S 的 delta(q, a) 取聯集——正是「任一活分身讀到 a 能去哪裡」。
  3. 持續產生新的可達集合,直到不再冒出新集合為止;這一定會結束,因為子集只有有限多個。
  4. 一個 DFA 狀態(一個集合)是接受狀態,恰好當它含有至少一個 NFA 的接受狀態時——正好對應「只要有分身在接受狀態就接受」。

這裡有一個你該帶往後面的誠實陷阱。一台有 n 個狀態的 NFA 有 2^n 個可能的子集,所以做出來的 DFA 在最壞情況下可能多達 2^n 個狀態——也就是令人聞之色變的指數爆炸。這不只是理論上的擔憂:確實有些真實語言,其最小的 NFA 約有 n 個狀態,而每一台 DFA 都需要大約 2^n 個。所以這兩種模型在能力上相等,在大小上卻可能天差地遠;而這個取捨——用小而好設計的 NFA 來設計,再(有時昂貴地)付出代價去確定化——正是本階梯留給你的實用教訓。