Skip to the content.

02. 正規表現から NFA を作る

木製レールを組み立てるBobとAlice、見守るリス

01 章 で見た NFA は、天下りに与えられたものだった。 しかしこれは正規表現から機械的に作れる。規則は 4 つしかない。

NFA の構成規則

記号の付いた矢印は入力を 1 文字読んで移ることを、ε の矢印は入力を読まずに移れることを表す。 丸はすべて状態である。

図は a と b で描いてあるが、この a や b の位置には、 正規表現の一部がかたまりで入ってよい。 実際④は (a|b)* を描いたもので、真ん中の 2 つの丸とそれを結ぶ a b は③そのものである。 つまり 4 つの規則は、小さい NFA を部品にして、一回り大きい NFA を作る手順になっている。

規則 やること
① 記号 a 状態を 2 つ作り、間に a の遷移を 1 本引く
② 連接 ab a の終わりの状態を、そのまま b の始まりの状態として使う
③ 選択 a\|b 同じ状態から a と b の 2 本を引き、行き先を 1 つの状態にまとめる
④ 繰り返し (a\|b)* 繰り返す部分 a\|b の前後に状態を置き、図のように ε を 4 本引く

④ の ε 4 本が、01 章の NFA にあった 1→2 1→4 3→2 3→4 に対応している。 この 4 本がそれぞれ何をしているかは、後で組み立てるときに見る。 ε を使うのはこの規則だけで、②③ は状態をまとめるだけで済ませている。

ここで扱う正規表現は、この 4 つの規則だけで組み立てられるものとする。 ただし、5 つめの規則を 05 章 で加える。?(0 回か 1 回)で、 これも同じやり方で足せる。a(a|b)*bb には出てこないので、ここから先では使わない。

Thompson の構成法では、③ の選択にも新しい状態を 2 つ置いて ε を 4 本引くので、 状態数はもっと増える。①②④ は Thompson と同じ形である。 ③ だけ状態を置かず、両方の枝を同じ状態から出して行き先をまとめたのは、 01 章の図と一致させるため。どちらでも受理する文字列は変わらない。

a(a|b)*bb を組み立てる

a(a|b)*bb は、a、(a|b)*、b、b の 4 つを連接したものである。 このうち (a|b)* は (a|b) の繰り返しで、(a|b) は a と b の選択になっている。 一番内側から順に規則を当てはめていく。

状態の名前は、できあがる図に合わせて先に付けておく。

手順 1: a|b に③を当てはめる

状態を 2 つ用意し、片方からもう片方へ a と b の 2 本を引く。図③そのものである。 この 2 つを 2、3 と呼ぶ。

a|b に対応する NFA

a でも b でも、1 文字読んで 2 から 3 へ移る。これが「選択」の形である。

手順 2: (a|b)* に④を当てはめる

手順 1 でできた部品(a|b の部分)が、繰り返される本体である。 その前と後ろに状態を 1 つずつ足し、前を 1、後ろを 4 とする。そして ε を 4 本引く。

(a|b)* に対応する NFA

灰色が手順 1 で作った本体である。オレンジの 4 本がそれぞれ、 本体に入る、本体を飛ばす(0 回の場合)、 本体の終わりから戻る(もう 1 回繰り返す)、本体から抜ける、を担当している。

「0 回以上の繰り返し」という意味が、この 4 本にそのまま出ている。 0 回なら「飛ばす」、1 回なら「入る」→本体→「抜ける」、2 回以上なら途中で「戻る」を使う。

これで (a|b)* に対応する部品ができた。始まりは 1、終わりは 4 である。

手順 3: 先頭の a を①で作り、②で繋ぐ

①で状態を 2 つ作り、a の遷移を 1 本引く。 その終わりの状態を、手順 2 の部品の始まり 1 と同じ状態にする。これが②の連接である。 残った始まりの状態が全体の初期状態なので i と呼ぶ。

先頭の a を連接したところ

手順 4: 残りの b、b を②で繋ぐ

同じように、手順 2 の部品の終わり 4 を始まりとして b の遷移を引き、新しい状態 5 へ。 さらに 5 から b で f へ。最後の f が全体の受理状態である。

末尾の bb を連接したところ

できあがり

引いた遷移は全部で 9 本になる。

(i)-- a -->(1)        手順 3
(1)-- ε -->(2)        手順 2 の「入る」
(1)-- ε -->(4)        手順 2 の「飛ばす」
(2)-- a -->(3)        手順 1 の選択
(2)-- b -->(3)        手順 1 の選択
(3)-- ε -->(2)        手順 2 の「戻る」
(3)-- ε -->(4)        手順 2 の「抜ける」
(4)-- b -->(5)        手順 4
(5)-- b -->((f))      手順 4

図にすると、01 章に載せた NFA そのものである。

正規表現 a(a|b)*bb に対応する NFA

状態の名前 i, 1, 2, ..., f は、初期状態から辺をたどっていった順に振ったものである。

つまり 01 章の NFA は、この 4 つの規則を a(a|b)*bb に当てはめた結果である。 正規表現さえあれば、NFA は必ず作れる。

この NFA の問題

こうして作った NFA には、ε 遷移が 1→2 1→4 3→2 3→4 の 4 本ある。 これが動かすときにそのまま困り事になる。 abb を入力して、i から順にたどってみる。

i --a--> 1    ここまでは一本道
1 --ε--> 2 か 4 か?   入力を読まずに 2 手に分かれる

4 を選べば b、b と読んで f に着き、受理できる。 2 を選べば b を読んで 3 に進み、そこからまた 2 か 4 かで分かれる。

入力のどこを読んでいるかに関係なく分岐が起きるので、 どちらが正解かはその場では分からない。分かるのは最後まで読み終えてからである。 素直に実装すれば、片方を試して駄目ならもう片方に戻る——後戻り(バックトラック)が要る。 分岐は入れ子にもなるので、入力が長くなると試す組み合わせは急激に増える。

欲しいのは、今いる状態と次の 1 文字だけで行き先がただ 1 つに決まる機械である。 それなら分岐しようがなく、入力を頭から 1 回なぞるだけで済む。

では、どうすれば ε 遷移を消して、そういう形にできるか。 これが 03 章 の話である。