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

上下文無關語言的幫浦引理

夠長的上下文無關字串總藏著一種可重複的結構——兩段一起同步成長的片段。我們會拆解為什麼剖析樹逼出這個事實,以及它如何成為證明某語言「不是」上下文無關的工具。

回想正規語言的幫浦引理——這次改用樹來做

回到正規語言的世界時,你用幫浦引理證明了 a^n b^n 不是正規語言。那個想法其實是鴿籠原理的偽裝:一台 DFA 只有有限多個狀態,所以在夠長的執行裡,它必定造訪某個狀態兩次,而這兩次造訪之間的迴圈可以被重複——也就是幫浦——生出新的被接受字串。這就逼住了任何字串無法容忍這種迴圈的語言。本階梯前一篇磨利了一個相關事實:上下文無關語言在某些運算下封閉,卻在交集或補集下封閉,頭號反例正是 a^n b^n c^n。現在我們要打造真正能證明 a^n b^n c^n 不是上下文無關的工具。

對正規語言有效的訣竅——重複一個狀態——無法直接搬過來,因為下推自動機還多了一個無界的堆疊,所以「同一狀態出現兩次」不再代表「同一情境出現兩次」。取而代之,上下文無關語言的幫浦引理改而對著剖析樹推理,也就是文法賦予字串的結構。一個文法只有有限多個變數(非終端符號)。若字串很長,它的剖析樹一定很高;若樹很高,某條從根到葉的路徑一定會重複某個變數。那個重複的變數就是新的鴿籠,而兩份副本之間的那塊樹,正是我們可以幫浦的部分。

引理怎麼說:五段,其中兩段一起幫浦

陳述如下。對每個上下文無關語言 L,存在一個常數 p,稱為幫浦長度,使得 L 中每個長度 |s| 至少為 p 的字串 s 都能切成段,s = u v x y z,並滿足三個條件。第一,|v y| 至少為 1——兩段可幫浦的部分至少有一段非空,所以我們不是在幫浦空無一物。第二,|v x y| 至多為 p——可幫浦的中段保持有界,這把那幾段釘在彼此附近。第三,也是核心:對每個 i = 0, 1, 2, …,字串 u v^i x y^i z 也在 L 裡。注意 v 與 y 帶著相同的指數 i:它們同步地一起變大變小。

把它和正規語言的引理比一比:後者把字串切成三段 x y z,幫浦單一中段 y。從一段可幫浦片段躍升到兩段,正是上下文無關語言的全部性格。堆疊以巢狀、成對的方式配對東西——進去時推入、出來時彈出——所以當某物重複時,它是以配對的一對在重複,就像多一層巢狀的左右括號。這正是為什麼 a^n b^n 上下文無關的(一段 a 的 v 和一段 b 的 y 配對),而 a^n b^n c^n 不是:兩段可幫浦片段無法同時讓三個獨立的計數保持相等。

為什麼成立:一棵高的剖析樹一定重複某個變數

證明是對剖析樹做的一個乾淨的鴿籠論證,值得一看,因為這個證明的結構就是引理的意義。把文法化成每個節點至多有固定數目子節點的形式(喬姆斯基正規形就做到這點:每條規則是 A → B C 或 A → a,所以剖析樹是二元的)。一棵高度為 h 的二元樹至多有 2^h 片葉子。所以若字串 s 夠長——比 2^(變數個數) 還長——它的剖析樹高度必定超過變數個數,因而某條從根到葉的路徑長到足以重複某個變數,比方說 A 在同一條路徑上出現兩次。

想像那兩份 A 一個套在另一個裡面。較低的 A 推導出字串的某一塊——叫它 x,是最內層的產出。較高的 A 推導出更大的一塊,包含 x 加上它左右兩側的材料:左邊的材料是 v,右邊的材料是 y。較高的 A 之前的全部是 u,之後的全部是 z。於是 s = u v x y z。因為兩份副本都是同一個變數 A,掛在較高 A 底下的子樹與掛在較低 A 底下的子樹可以互換:A 能推導出 v A y,A 也能推導出 x。我們愛接幾次就接幾次。

Two copies of A on one path:

              S
             /
            A        <- upper A: derives  v x y
          / | \
         v  A  y      <- lower A: derives  x
            |
            x

  A => v A y      (the loop we can repeat)
  A => x          (the base, used to stop)

  i=0:  drop the loop ->  u x z           (pump down)
  i=1:  original       ->  u v x y z
  i=2:  loop twice     ->  u v v x y y z   (pump up)
  general:             ->  u v^i x y^i z   for all i >= 0
重複的變數 A 給出一個迴圈 A => v A y 與一個基底 A => x。把迴圈接 i 次,就讓 v 與 y 一起被幫浦;只用基底(i=0)則把它們幫浦掉。

這就是那些條件的由來,一次全到齊。被幫浦的兩段恰好是 v 與 y,因為它們夾住內層子樹——所以它們總是以配對的形式一起成長。只要我們選的文法沒有無用的 epsilon(空字串)或單位規則的繞路,就保證較高的 A 確實比較低的 A 多產生一些東西,因而其中至少一段非空(|v y| 至少為 1)。而 |v x y| 保持至多為 p,是因為我們在那條路徑上挑的是最低的那個重複變數,使其子樹維持得很小。整個引理就是這一張圖,仔細讀就懂了。

運用它:對手遊戲與 a^n b^n c^n

要運用這個引理,你玩的是和正規語言情形相同的對手遊戲,只是現在有五段。你不能選 p,也不能選切法;這兩件都由對手決定。你只能選字串 s(聰明地,用 p 來表示)以及幫浦指數 i(用來逼出矛盾)。如果無論對手如何在遵守那兩個邊條件下把 s 切成 u v x y z,總有某個 i 的選擇把 u v^i x y^i z 踢出 L,那麼 L 就不可能是上下文無關的。技巧在於選 s,讓每一種合法的切法都注定失敗。

  1. 為導出矛盾,假設 L = { a^n b^n c^n : n >= 0 } 是上下文無關的,因此它有一個幫浦長度 p。
  2. 選取見證字串 s = a^p b^p c^p。它的長度是 3p,遠超過 p,所以引理適用於它。
  3. 讓對手把 s 切成 s = u v x y z,其中 |v y| 至少為 1 且 |v x y| 至多為 p。因為中段 v x y 長度至多為 p,它太短,無法橫跨全部三個字母區段——它至多只能碰到 a、b、c 三個字母中的兩種。
  4. 向上幫浦到 i = 2,得到 u v^2 x y^2 z。幫浦加入了 v 與 y 的副本,而它們合起來至多落在三個區段中的兩個,因此它至多只提高三個字母裡兩種的計數。
  5. 第三個字母的計數毫髮無傷,所以三個計數再也無法全部相等。因此新字串不再是 a^n b^n c^n 的形式,與它本應屬於 L 矛盾。(若某種切法混了順序,例如 v 同時含 a 與 b,會給出更糟的字串如 …abab…,照樣不在 L 裡。)故 L 不是上下文無關的。

停下來體會為什麼這對 a^n b^n c^n 有效,對先前的 a^n b^n 卻不行。只有兩個字母時,兩段可幫浦片段就夠了:v 可以待在 a 之間、y 待在 b 之間,讓兩個計數一起提高,所以 a^n b^n 撐過每一次幫浦,是上下文無關的。有三個字母時,兩段至多讓兩個區段同步;第三個就飄走。一個語言必須保持相等的獨立量的數目,對上引理只允許的兩段幫浦,就是那條分界線。這也正是為什麼a^n b^n c^n 標誌著上下文無關語言的邊界

當兩段幫浦還不夠:奧格登引理

有時基本的幫浦引理太鈍。設想某個語言,它的長字串恰好含有一大塊好對付的區域——比方一長串某個無害的字母——對手可以把整個 v x y 視窗停在裡頭,只幫浦那段無害的字串,永遠不去動到你在意的部分。於是每次幫浦都留在 L 裡,基本引理什麼也揭不出來。字串雖長,但它的長度並沒有逼迫迴圈落在會痛的地方。

奧格登引理正是針對這個情況的更銳利工具。它讓你先把至少 p 個你選的位置標記為「特別」的,然後保證夾在它們之間被幫浦的片段 v 與 y 至少含有一個被標記的位置——迴圈被逼著穿過你標記的那個點,而不是溜進某個方便的填充區。基本幫浦引理就是把每個位置都標記起來的奧格登引理。有了標記,你還能證明某些語言是本質歧義的,這是樸素的引理辦不到的事。