生成函數 (II) – 關於 RegExp 的妙用

之前說要讀 Analytic Combinatorics 然後寫心得,後來卻跳槽去看代數拓撲,斷尾了。作為一個半途而廢的勞山道士,還是學到了一些粗淺卻酷炫的穿牆術,在此作一些心得分享。內容主要是

  1. 免思考,機械化寫出 regexp 的生成函數,進而回答「regexp 所表示的語言中,長度為 的字串有幾個?」
  2. 初步判斷生成函數係數的漸進複雜度

這兩個手法看起來很酷炫,又在本書 Part I 與 Part II 的第一個章目,的確像是《聊齋誌異》中《勞山道士》的「穿牆術」,很有淺嘗即止的味道。

RegExp 的生成函數

形式語言的生成函數

寫出一個正規表達式(RegExp),就代表一個正規語言 。如果 中長度為 0 的字串有 個、長度為 1 的有 個、長度為 n 的有 個,則關於 的多項式

稱為正規語言 的生成函數,其第 n 項係數的值是該語言中長度為 n 的字串的數量。

正規語言的建構

雖然在程式語言中,正規表達式有許多花花綠綠的寫法,但是他們都可以由以下三種 regular constructions 來延伸,這裡先明確的寫出來:

聯集

聯集是「或」的概念,用直槓 | 來表達。正規表達式 (a|bb) 可以 match 字串 abb

串接

串接是將兩個字串接在一起,寫法是直接接起來。正規表達式 (a|bb)c 代表「abb 後面要接一個 c」 才 match。

Sequence

Sequence 是指某個字串重複出現,出現次數可以從零次到任意多次,用星號 * 來表達。正規表達式 (a|bb)* 可以 match 空字串、aaaabbabb 等。

以上三個就是基本的 regular construction。能夠只以 regular construction 建構的語言稱為 regular language。

生成函數的建構

三種 regular construction 都對應到一種生成函數的運算:| 號對應函數相加的動作,串接是相乘,* 則是把函數 寫成 。以下一一詳細解釋:

聯集與相加

現在有兩個正規語言

,其生成函數分別為

,則他們聯集的新語言 (a|bb) ,含有長度為一、二的字串各一個,故生成函數為

恰好是 的和。我們觀察到「正規語言的聯集,對應到生成函數的相加」。

這裡有個但書:語言 的聯集能表示成生成函數相加,前提是他們互斥。上面的例子如果 (a|bb) ,生成函數的相加就會出錯。

Correspondence 1:正規語言的聯集,其對應的生成函數相加,但被聯集的兩個語言需互斥。

串接與相乘

Correspondence 2:正規語言的串接,其對應的生成函數相乘。

舉例來說:(a|bb) 的生成函數是 ,而 c 的生成函數是 ,因此正規語言 (a|bb)c 的生成函數是

Correspondence 2 果然所言不假。

Sequence

Correspondence 3:正規語言 的生成函數是 ,則 的生成函數為

舉例來說:如果正規語言 (a|bb),則 (a|bb)*。我們把這看成

()+(a|bb) + (a|bb)(a|bb)+(a|bb)(a|bb)(a|bb)+…

於是生成函數是

,用無窮等比級數公式化簡得到

Correspondence 3 所說的如出一轍。

舉例

隨便舉一個有點複雜的 RegExp 來試試:(0|(1(01*0)*1))* 所表示的語言中,長度為 4 的字串有幾個?

我們先寫出其生成函數。一個步驟一個步驟做,從裡面開始:

  • (01*0) 對應到 ,化簡為

  • (01*0)* 要再寫一次 sequence construction,變成

  • (1(01*0)*1) 變成

  • (0|(1(01*0)*1)) 變成

  • (0|(1(01*0)*1))* 變成

一個口令一個動作,就這樣寫完了。不管怎麼寫,它總是可以化簡成兩個多項式相除(有理式),畢竟建構過程中不會有開根號什麼的。

Corollary:Regular Language 的 Generating Function 總是有理式。

方才這個生成函數可以化簡為

將其寫成泰勒展開式(過程省略),可以得到 ,所以答案也就是 4 次項係數, 6。

另外(0|(1(01*0)*1))*是從維基百科找的 regexp,其意義為「用二進位表示的三的倍數」。用 4 個 digit 能表示的三的倍數有 0000, 0011, … , 1111,正好有 [16/3]+1 = 6 個,果然計算無誤。

初步判斷生成函數係數的複雜度

一個數列 有漸進複雜度,而生成函數的係數是個數列,所以也可以計算係數複雜度,這部分是 Analytic Combinatorics 的重點。我在這裡寫下一個粗略的手法,說明如何判斷「生成函數係數複雜度的指數項」,但不證明。

手法

生成函數 約分化簡後,其在複數平面上的不連續點之中,距離原點最近的設為 ,則其數列 複雜度的指數項為 .

但有個前提是:原點不能是生成函數的不連續點。

這裡說不連續點其實不太好,應該要說奇點,因為這有複分析的意義

約分化簡是要把類似 的式子直接寫成

所謂「複雜度指數項為 」指的是複雜度為 ,其中 比指數項還小,比方說 ,其指數項就是

舉例

費氏數列

費氏數列的生成函數為 ,其分母在 為零,所以其不連續點中離原點最近的是 ,其絕對值倒數為 ,所以費氏數列複雜度的指數項為 。對照費氏數列的通式

可發現確是如此。

形式語言的例子

方才我們有個判斷 3 的倍數的正規表達式(0|(1(01*0)*1))*,其生成函數為。其分母在 1、-1、0.5 有根,其中 0.5 離原點最近,其倒數為 2,所以複雜度的指數項為

對照起來,能用 個 digit 表示的三的倍數有 個,的確是

關於細節

關於不連續點有一些細節,比方說方才我們常在找分母的實根,當作不連續點。但是這個多項式是可以代入複數進去的,所以如果有虛根的話,虛根到原點的距離也要考慮進去。

之所以要把分式先約分,是因為類似 的式子雖然不連續,但是從複分析的角度,其不連續點可以輕易的被「填補」(解析延拓)。

很多式子雖然都是有理式的形式,可以只看分母的根,但也有些不是。比方說出現根號也會造成不連續點。

分母在原點有根時,使用這手法可能會算錯