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

語言上的運算與克林星號

語言不過是一組字串的集合,所以我們可以像組合集合一樣組合語言——而再加上一個特別的運算「克林星號」,就能用有限的描述命名出無限的語言。來認識整門學科後續所倚賴的這套工具。

為什麼要組合語言?

從上一篇你已經知道,一個語言不是什麼神祕的東西,它就是某個字母表 Sigma(希臘字母 Σ,也就是我們固定允許使用的符號池)上的一組字串的集合。這個簡單想法之所以強大,正是因為它讓我們能沿用對集合所知的一切。如果語言是集合,那我們就能把兩個語言黏在一起、讓它們重疊、或把它們疊起來——就跟對普通集合取聯集、交集一樣。這些組合方式稱為語言上的運算,我們就是靠它把大而複雜的語言,從小而簡單的語言拼裝出來。

可以把它想成下廚。你不會靠逐一列出每個分子來寫食譜;你會用幾樣食材和幾個動作(切、拌、烤)來描述一道菜。語言上的運算就是那些動作。其中三個是主力:聯集串接克林星號。前兩個直接來自集合論與字串黏接,你都已經見過。第三個才是本篇真正全新的概念,也正是它悄悄地解鎖了「無限」。

三個主力運算

固定同一個字母表上的兩個語言 L 與 M。它們的聯集,寫成 L ∪ M,就是所有屬於 L 或屬於 M(或兩者皆是)的字串——就是普通的集合聯集,沒有意外。它們的交集 L ∩ M 則是同時屬於兩者的字串。它們的表現完全就跟你已熟悉的集合運算一樣,因為語言本來就是集合。唯一要記得的是:被取聯集的元素是字串,不是數字。

語言的串接就比較有意思。串接 L · M 是這樣一組字串:從 L 拿一個字串、從 M 拿一個字串,按這個順序黏起來所能得到的每一個字串:L · M = { x y :x 屬於 L 且 y 屬於 M }。所以若 L = {a, ab}、M = {c, dd},則 L · M = {ac, add, abc, abdd}。注意我們把每個 x 和每個 y 兩兩配對——二乘二的選擇給出四個結果。語言的串接,其實就是字串串接被「抬升」到整組集合的版本。

克林星號:為無限命名

串接把兩個語言黏一次。但如果我們想把一個語言和它自己黏起來,而且想黏幾次就黏幾次——零次、一次、兩次、一百次呢?這就是克林星號,用單一個星號寫成 L*。讀作「從 L 取出零個或多個字串,串接在一起」。形式上,L* 是「L 與自己串接 k 次」對每個從 0 起算的 k 取聯集。k = 0 這個情形特別、而且是刻意的:什麼都不串接會得到空字串,所以不論 L 是什麼,epsilon 永遠都在 L* 裡。

L = { ab }

L^0 = { epsilon }          (glue zero copies)
L^1 = { ab }
L^2 = { abab }
L^3 = { ababab }
...
L*  = L^0 U L^1 U L^2 U ... = { epsilon, ab, abab, ababab, ... }

L+  = L^1 U L^2 U L^3 U ...  = { ab, abab, ababab, ... }   (one or more; drops epsilon)
L* 對 L 的每一個冪次取聯集(含第零次,它貢獻了 epsilon);L+ 一樣,只是從一個拷貝開始算起。

有兩點值得強調。第一,即使 L 是有限的,L* 通常是無限的——{ab}* 就已經包含無限多個字串。用有限的描述命名一個無限的集合:這正是全部的魔法所在,也是為什麼有限的機器——自動機——能辨識無限的語言。第二,它有個近親,加號運算子 L+,意思是「一個或多個」而非「零個或多個」。唯一的差別在於 epsilon 是否被納入:L* = L+ ∪ {epsilon}。如果 epsilon 本來就在 L 裡,那 L* 和 L+ 就相等。

範例演練與幾點誠實的提醒

讓我們用這套工具做點真實的東西。取 Sigma = {a, b}。假設我們想要這樣一個語言:所有以一個 a 開頭、後面接任意個 b 的字串:a、ab、abb、abbb,依此類推。我們可以把它精確命名為 {a} · {b}*。從左讀到右:一個 a,黏上零個或多個 b。其中 {b}* 這部分貢獻了 epsilon、b、bb、bbb……;在每一個前面黏上一個 a,恰好得到 a、ab、abb、abbb……。不需要含糊地說「依此類推」——星號替我們把無限的工作,用有限的方式做完了。

  1. 從原子語言出發:{a}(一個字串)與 {b}(一個字串)。
  2. 對 {b} 套上星號得到 {b}* = {epsilon, b, bb, bbb, ...}——現在是無限的了。
  3. 在前面串接 {a}:{a} · {b}* 讓那個孤單的 a 與其中每一個字串配對。
  4. 結果恰好就是 {a, ab, abb, abbb, ...},由一個有限的運算式描述出來。

在繼續之前說幾句誠實話。串接的引數順序很重要:L · M 一般不等於 M · L,就像「ab」不等於「ba」。星號不是可有可無的裝飾——L* 即使一個拷貝都不取,仍會給你 epsilon,所以加了星號的語言永遠不會是空的。也別把「無限」和「複雜」混為一談:{b}* 是無限的,卻極其簡單。反過來,一個正規語言不一定要是無限的;{a, b} 是正規的,而且恰好只有兩個字串。正規不代表無限,無限也不代表不正規。

為什麼這套工具是後續一切的根基

聯集、串接和星號不只是方便而已——它們正是正規表示式與正規語言的構件,也就是你將要攀登的喬姆斯基階層的第一級。稍後你會看到一個漂亮的事實:一個語言能用這三種運算命名出來,當且僅當有一台有限自動機能辨識它。所以現在精通語言上的運算,就是在精通你日後將打造的、最簡單機器的「文法」。

還有一個更深的理由值得在意。回想問題可以被編碼成語言這個想法:一個是非問題變成「這個輸入字串屬不屬於『答案為是』的那些實例所構成的語言?」。一旦問題成了語言,要求電腦去解它就變成了成員資格問題——某個給定字串屬不屬於某個給定語言?於是,語言上的運算就成了問題上的運算:組合、限制、轉換我們要求機器回答的那些問題本身。把這些工具練成反射,後續整門學科讀起來就像你早已會說的語言裡的一句句話。