@
之前學習編譯原理的時候老師有講過子集構造法,當時我以為自己聽懂了,信心滿滿。可是這兩天我做了一些題目,發現自己實際上還是太嫩了,學習浮於表面。之后又重新看了龍書和虎書,對子集構造法有了更深層次的了解。特此發出一篇文章分享我的經驗。
1 概念
概念是我們學習編譯原理的重中之重,雖然他很晦澀難懂,但我有必要將其放在最開始。
1.1 虎書概念
虎書的概念更偏向於理論化,我當時看的時候一頭霧水,但是不要擔心,之后會一點一點解釋的。
首先,我們形式化定義\(\epsilon\)閉包如下:
- \(edge(s,c)\):狀態\(s\)沿着標有\(c\)的邊可到達的所有NFA狀態的集合;
- \(closure(S)\): 對於狀態集合\(S\),從\(S\)出發,只通過\(\epsilon\)邊可以達到的狀態集合;
- 這種經過\(\epsilon\)邊的概念可以用數學方法表述,即\(closure(S)\)是滿足如下條件的最小集合\(T\):
\(T=S \cup \left( \bigcup_{s \in T}edge(s,\epsilon) \right)\) - 我們可以用迭代法來算出\(T\):
\(T \leftarrow S \\ repeat \ T' \leftarrow T \\ \qquad \quad T \leftarrow T' \cup \left( \bigcup_{s \in T'}edge(s,\epsilon) \right) \\ until \ T=T'\)
- 這種經過\(\epsilon\)邊的概念可以用數學方法表述,即\(closure(S)\)是滿足如下條件的最小集合\(T\):
解釋一下:當我們位於一個狀態集合\(S\),\(S\)里任意狀態經過若干\(\epsilon\)能夠到達的狀態,都將包含在 \(closure(S)\) 里。
- 龍書里將這個操作定義為\(\epsilon-closure(T)\)(\(T\)為狀態集合)。
現在,假設我們位於由NFA狀態\(s_i,s_k,s_l\)組成的集合\(d= \lbrace s_i,s_k,s_l \rbrace\)中。從\(d\)中的狀態出發,輸入符號\(c\),將到達NFA新的狀態集;我們稱這個狀態集為\(DFAedge(d,c)\):
- \(DFAedge(d,c)=closure \left( \bigcup_{s \in d}edge(s,c) \right)\)
解釋一下:將遍歷集合\(d\)中的所有狀態,得到 \(d\) 關於\(T=edge(s,c)\)的狀態集,並對 \(T\) 求 \(closure(T)\),得到的即為\(DFAedge(d,c)\)。簡而言之,就是從一個狀態集,經過一個輸入到達的狀態集為 \(T'=DFAedge(d,c)\)。
利用\(DFAedge\)能更形式化地寫出NFA模擬算法。如果初態是\(s_1\),輸入字符串是\(c_1,...,c_k\),則算法為:
- \(d \leftarrow closure( \lbrace s_1 \rbrace ) \\ for \ i \leftarrow 1 \ to \ k \\ \quad d \leftarrow DFAedge(d,c_i)\)
有了\(closure\)和\(DFAedge\)算法,就能構造出DFA,DFA的狀態\(d_1\)就是\(closure(s_1)\)。抽象而言,如果\(d_j=DFAedge(d_i,c)\)則存在一條從 \(d_i\) 到 \(d_j\) 的標記為 \(c\) 的邊。令 \(\Sigma\) 是字母表:
- \(states[0] \leftarrow \lbrace \rbrace; \qquad states[1] \leftarrow closure(\lbrace s_1 \rbrace) \\ p\leftarrow 1; \qquad j \leftarrow 0 \\ while \ j \leq p \\ \ \ \ foreach \ c \in \Sigma \\ \qquad e \leftarrow DFAedge(states[j],c) \\ \qquad if \ e =states[i] \ for \ some \ i \leq p \\ \qquad \quad \ then \ trans[j,c] \leftarrow i \\ \qquad \quad \ else \ p \leftarrow p+1 \\ \qquad \qquad \quad \, states[p] \leftarrow e \\ \qquad \qquad \quad \, trans[j,c] \leftarrow p \\ \; \; j \leftarrow j+1\)
解釋一下:\(state[]\)代表了最終DFA的一個狀態所對應的NFA狀態集,\(s_1\)為初始狀態,\(closure(\lbrace s_1 \rbrace)\)代表了初始狀態\(s_1\)的閉包。上文中的代碼實際上和龍書的代碼一個意思,龍書的代碼更加簡單直白,所以這里可以跳過。等看完下面的龍書再回頭來看
1.2 龍書概念
個人認為龍書的概念更加通俗易懂,但是由於沒有數學公式的歸納,導致理論基礎不扎實,有點慌。所以推薦兩本書一起看。
首先,是概念:
- 子集構造法的基本思想是讓構造得到的DFA的每個狀態對應NFA的一個狀態集合。DFA在讀入\(a_1a_2...a_n\)之后到達的狀態應該對應於相應的NFA從開始狀態出發,沿着以\(a_1a_2...a_n\)為邊的路徑能達到的狀態的集合。
解釋一下:概念很直觀哈,我就不解釋了^_^
接着,是算法:
- 輸入:一個NFA N
- 輸出:一個接受同樣語言的DFA D
- 方法:我們為算法 D 構造一個轉換表\(Dtran\)。D的每個狀態是一個NFA集合,構造\(Dtran\),使得 D “並行地”模擬 N 在遇到一個給定輸入串可能執行的所有動作。下面我們給出一些函數的定義:
| 操作 | 描述 |
|---|---|
| \(\epsilon - closure(s)\) | 能夠從NFA狀態\(s\)開始只通過\(\epsilon\)轉換到達的NFA狀態集合 |
| \(\epsilon - closure(T)\) | 能夠從\(T\)中某個NFA狀態\(s\)開始只通過\(\epsilon\)轉換到達的NFA狀態集合,即 \(\bigcup_{s \in T} \epsilon -closure(s)\) |
| \(move(T,a)\) | 能夠從 \(T\) 中某個狀態 \(s\) 出發通過標號為 \(a\) 的轉換到達的NFA狀態的集合 |
- 在讀入第一個符號之前,N可以位於集合\(\epsilon-closure(s_0)\)中的任何狀態上 ,其中,\(s_0\)是 N 的開始狀態。
- 下面進行回歸納:假定N在讀入輸入串\(x\)之后可以位於集合T的任意狀態上。如果下一個輸入符號是 \(a\),那么N可以立即移動到集合\(move(T,a)\)中的任何狀態。然而,N 可以讀入\(a\)之后再執行幾個\(\epsilon\)轉換,因此 N 在讀入\(xa\)之后可以位於\(\epsilon-closure(move(T,a))\)中的任意狀態上。接着我們可以構造出轉換函數\(Dtran\):
- 一開始,\(\epsilon-clusure(s_0)\)是\(Dstates\)的唯一狀態,且它未標記(請注意,“標記”是非常重要的概念);
- \(while(在Dstates中有一個未標記的狀態T) \lbrace \\ \quad \quad \ 給T加上標記; \\ \quad \quad \ for(每個輸入符號a) \lbrace \\ \qquad \qquad \quad U=\epsilon-clusure\left(move(T,a) \right) \\ \qquad \qquad \quad if(U不在Dstates中) \\ \qquad \qquad \qquad \qquad \, 將U加入Dstates中,且不加標記; \\ \qquad \qquad \quad Dtran[T,a]=U \\ \quad \quad \ \rbrace \\ \rbrace\)
解釋一下:這部分代碼和虎書上的代碼意思相近,這個更好理解。算法里的\(Dtran[T,a]=\epsilon-clusure\left(move(T,a) \right)\)每個\(Dtran[T,a]\)都可能是DFA的一個狀態。
2 舉個例子解釋
- 題目:給定一個正則表達式\((a|b)^*abb\)的NFA,我們使用子集構造法構造DFA。

- 解法:首先,我們分析得出,NFA的初始為狀態0。因而初始狀態集\(A=\epsilon-closure(0)=\lbrace0,1,2,4,7 \rbrace\)。
- \(A\)被加上標記,對於輸入符號\(a,b\),分別求出:
\(a:B=\epsilon-closure(move(A,a))=\lbrace1,2,3,4,6,7,8 \rbrace \\b:C=\epsilon-closure(move(A,b))=\lbrace1,2,4,5,6,7 \rbrace\) - \(B,C\)都沒有被標記,因而將\(B,C\)依次加上標記,對於輸入符號\(a,b\),分別求出:
\(a:B=\epsilon-closure(move(B,a))=\lbrace1,2,3,4,6,7,8 \rbrace \\b:D=\epsilon-closure(move(B,b))=\lbrace1,2,4,5,6,7,9 \rbrace\)
\(a:B=\epsilon-closure(move(C,a))=\lbrace1,2,3,4,6,7,8 \rbrace \\b:C=\epsilon-closure(move(C,b))=\lbrace1,2,4,5,6,7 \rbrace\) - 現在只剩\(D\)沒有加標記,因而給\(D\)加上標記,對於輸入符號\(a,b\),分別求出:
\(a:B=\epsilon-closure(move(D,a))=\lbrace1,2,3,4,6,7,8 \rbrace \\b:E=\epsilon-closure(move(D,b))=\lbrace1,2,4,5,6,7,10 \rbrace\) - 還剩一個\(E\)沒有標記,因而給\(E\)加上標記,對於輸入符號\(a,b\),分別求出:
\(a:B=\epsilon-closure(move(E,a))=\lbrace1,2,3,4,6,7,8 \rbrace \\b:C=\epsilon-closure(move(E,b))=\lbrace1,2,4,5,6,7 \rbrace\) - 所有構造出來的集合都已經被標記,構造完成!\(A,B,C,D,E\)為五個不同狀態:
\(A=\lbrace0,1,2,4,7 \rbrace \\ B=\lbrace1,2,3,4,6,7,8 \rbrace \\ C=\lbrace1,2,4,5,6,7 \rbrace \\ D=\lbrace1,2,4,5,6,7,9 \rbrace \\ E=\lbrace1,2,4,5,6,7,10 \rbrace\) - 接着就是根據狀態來畫圖了,最好先畫好狀態表:

- \(A\)被加上標記,對於輸入符號\(a,b\),分別求出:
解釋一下:由此可知,\(A\)通過\(a\),連到\(B\),以此類推。就可以做出DFA圖了^_^

3 如何最小化DFA的狀態數量
很簡單,如果開始於\(s_1\)的機器接收字符串\(\sigma\),始於\(s_2\)的和始於與\(s_1\)接收的串相同,並到達相同狀態,且兩個狀態集同為終態或者非終態,那么\(s_1,s_2\)是等價的。我們可以把指向\(s_2\)的連線全部指向\(s_1\),並刪除\(s_2\),反之亦然。
- 舉個書上的例子:

- 圖中的\(\lbrace 5,6,8,15 \rbrace,\lbrace 6,7,8 \rbrace\)是等價的,還有\(\lbrace 10,11,13,15 \rbrace,\lbrace 11,12,13 \rbrace\)也是等價的。
Tips:在判斷是否等價前,我們要先判斷是否為死狀態哦(1.不能到達終態 2.從開始沒有路徑指向這個狀態)。
4 總結
NFA轉DFA知識總結就到這里,有什么問題請留言,有錯誤請批評指正,非常感謝您的閱讀。
