チューリングマシンによる回文判定:シミュレーターで追う動作ステップ
はじめに
計算機科学における基本的な概念の一つに、ある文字列が「回文(Palindrome)」であるかを判定するという問題があります。回文とは、逆から読んでも同じになる文字列のことです(例:「madam」「racecar」「しんぶんし」)。この回文判定は、様々なアルゴリズムやデータ構造の学習において基本的な例題として扱われます。
本記事では、この回文判定問題をチューリングマシンでどのように実現するかを解説し、その具体的な動作ステップをシミュレーターで追体験することで、チューリングマシンの動作原理やアルゴリズムの実装方法への理解を深めることを目的とします。
チューリングマシンの回文判定アルゴリズム
チューリングマシンで文字列が回文であるかを判定するための一般的なアルゴリズムは、入力文字列の両端から一文字ずつ読み込み、それらが一致するかを確認していく方法です。一致すれば次の内側の文字に進み、これを繰り返します。もし途中で一致しない文字が見つかれば、その文字列は回文ではないと判断できます。全ての文字のペアが一致し、判定が完了すれば、その文字列は回文であると判断できます。
このアルゴリズムをチューリングマシンの動作として具体化するには、以下のステップを考慮します。
- 左端の記号を読み込む: テープの一番左端にある記号を読み取り、何らかの特殊な記号(例えば空白記号やマーク用の記号)に置き換えます。これにより、読み取った位置をマークします。
- 右端の記号を探す: ヘッドをテープの右方向へ移動させ、文字列の末尾(最初の空白記号の直前)まで移動します。
- 右端の記号を読み込む: 文字列の末尾の記号を読み取り、その記号を特殊な記号(例えば空白記号や、ステップ1で使用した記号とは別のマーク用の記号)に置き換えます。
- 記号の比較: ステップ1で読み取った記号とステップ3で読み取った記号を比較します。
- 一致した場合: ヘッドを再びテープの左方向へ移動させ、ステップ1でマークした位置のすぐ右隣まで戻ります。そして、次の文字ペアの比較のためにステップ1に戻ります。
- 一致しない場合: その文字列は回文ではないと判断し、非受理状態(または拒否状態)へ遷移して停止します。
- 判定の完了: ステップ1と3で読み込む文字ペアがなくなるまでこのプロセスを繰り返します。例えば、テープ上の全ての文字が特殊な記号に置き換えられた場合などです。この状態になれば、全てのペアが一致したことになり、その文字列は回文であると判断し、受理状態へ遷移して停止します。
このアルゴリズムを実現するためには、チューリングマシンは現在の状態(どのステップを実行中か)と、左端で読み取った記号の種類を記憶しておく必要があります。状態遷移図を設計する際には、これらの情報を状態としてエンコードすることになります。
シミュレーターでの動作例:文字列 "aba" の回文判定
ここでは、入力文字列 "aba" を例に、上記のアルゴリズムに基づいたチューリングマシンの動作をシミュレーターで追ってみましょう。チューリングマシンのアルファベットは {a, b, □, X, Y} とし、□は空白記号、XとYはマーク用の記号とします。初期状態を q0
とし、受理状態を q_accept
、拒否状態を q_reject
とします。
簡単化のため、具体的な遷移規則の一部と、それに対応するシミュレーターでの動作ステップを示します。
ステップ1: 左端の 'a' を処理
- 初期状態:
q0
, テープ:a b a □ ...
(ヘッドは左端の 'a' に位置) - 遷移規則例:
(q0, 'a') -> (q_read_a, 'X', R)
(状態q0
で 'a' を読み込んだら、'X' に書き換え、右に移動し、状態をq_read_a
にする) - シミュレーターでの動作:
- ヘッドが左端の 'a' を読み込みます。
- テープの 'a' が 'X' に書き換えられます。
- ヘッドが右に移動します。
- マシンの状態が
q_read_a
に遷移します。
- テープの状態:
X b a □ ...
(ヘッドは 'b' に位置)
ステップ2: 右端の 'a' を探して処理('a' と比較)
- 現在の状態:
q_read_a
, テープ:X b a □ ...
(ヘッドは 'b' に位置) - 右端への移動: 状態
q_read_a
では、文字列の末尾(最初の □ の直前)までヘッドを右に移動し続けます。例えば(q_read_a, 'b') -> (q_read_a, 'b', R)
,(q_read_a, 'a') -> (q_read_a, 'a', R)
といった遷移です。 - 右端の検出: □ を読み込んだら、その直前の文字が右端の文字です。右に移動したので、ヘッドを左に戻して右端の文字を読み込みます。例えば
(q_read_a, '□') -> (q_find_right_a, '□', L)
といった遷移で、右端の文字を探す状態q_find_right_a
に遷移します。 - 右端文字の読み込みと比較: ヘッドは 'a' に戻ってきました。状態は
q_find_right_a
です。ここで読み込んだ文字が、左端で読み込んだ記号 ('a') と一致するかを確認します。 - 遷移規則例:
(q_find_right_a, 'a') -> (q_match, 'Y', L)
(状態q_find_right_a
で 'a' を読み込んだら、'Y' に書き換え、左に移動し、状態をq_match
にする) - シミュレーターでの動作:
- ヘッドが右へ移動し、テープ上の最初の □ を検出します。
- ヘッドが左へ移動し、右端の 'a' に戻ります。
- ヘッドが 'a' を読み込みます。左端で読み込んだ記号('a')と一致します。
- テープの 'a' が 'Y' に書き換えられます。
- ヘッドが左に移動します。
- マシンの状態が
q_match
に遷移します。
- テープの状態:
X b Y □ ...
(ヘッドは 'b' に位置)
ステップ3: 左端に戻る
- 現在の状態:
q_match
, テープ:X b Y □ ...
(ヘッドは 'b' に位置) - 左端に戻る: 状態
q_match
では、次の左端の文字を探すために、最初のマーク記号 ('X' または 'Y') の直後までヘッドを左に移動します。例えば(q_match, 'b') -> (q_match, 'b', L)
,(q_match, 'Y') -> (q_match, 'Y', L)
といった遷移です。 - 左端の検出: 'X' を読み込んだら、その直後の文字が次の左端の文字です。右に移動して次の処理に移ります。例えば
(q_match, 'X') -> (q0, 'X', R)
といった遷移で、次のペア処理のための初期状態q0
に戻ります。 - シミュレーターでの動作:
- ヘッドが左へ移動し、マーク記号 'X' を検出します。
- ヘッドが右へ移動し、次の文字 'b' に位置します。
- マシンの状態が
q0
に遷移します。
- テープの状態:
X b Y □ ...
(ヘッドは 'b' に位置)
ステップ4: 残りの文字 'b' を処理
- 現在の状態:
q0
, テープ:X b Y □ ...
(ヘッドは 'b' に位置) - 遷移規則例:
(q0, 'b') -> (q_read_b, 'X', R)
(状態q0
で 'b' を読み込んだら、'X' に書き換え、右に移動し、状態をq_read_b
にする) - シミュレーターでの動作:
- ヘッドが 'b' を読み込みます。
- テープの 'b' が 'X' に書き換えられます。
- ヘッドが右に移動します。
- マシンの状態が
q_read_b
に遷移します。
- テープの状態:
X X Y □ ...
(ヘッドは 'Y' に位置)
ステップ5: 右端の 'b' を探して処理('b' と比較)
- 現在の状態:
q_read_b
, テープ:X X Y □ ...
(ヘッドは 'Y' に位置) - 右端への移動: 状態
q_read_b
では、□ までヘッドを右に移動します。(q_read_b, 'Y') -> (q_read_b, 'Y', R)
といった遷移です。 - 右端の検出: □ を読み込んだら、その直前の文字が右端の文字です。例えば
(q_read_b, '□') -> (q_find_right_b, '□', L)
といった遷移で、右端の文字を探す状態q_find_right_b
に遷移します。 - 右端文字の読み込みと比較: ヘッドは 'Y' に戻ってきました。状態は
q_find_right_b
です。ここで読み込んだ記号が、左端で読み込んだ記号 ('b') と一致するかを確認します。しかし、右端で読み込んだ記号は 'Y' です。これは文字列が回文である場合は、既にマークされているはずだからです。 - シミュレーターでの動作:
- ヘッドが右へ移動し、 □ を検出します。
- ヘッドが左へ移動し、 'Y' に戻ります。
- ヘッドが 'Y' を読み込みます。このアルゴリズムでは、回文の場合は両端の文字が全てマークされている状態になることを期待します。つまり、マーク記号を読み込んだら、それは残りの文字が存在しないか、全て処理済みであることを意味します。
- 判定完了(受理状態へ): 両端からペアが見つからなくなり、残った部分が空または中央の1文字(奇数長の場合)だけであれば、回文と判定できます。これは、左右からの探索が出会い、マークされた記号のみが残った状態で、次の左端文字を探そうとしてマーク記号や □ を読み込んだ場合に受理状態へ遷移する遷移規則によって実現できます。例えば、左端のマーク記号 'X' を読み込んだ状態で、右に移動しようとした際に □ や他のマーク記号を読み込む遷移
(q0, 'X') -> (q_accept, 'X', R)
や、真ん中の文字処理後に □ を読み込む遷移などです。
"aba" の例では、最初の 'a' と最後の 'a' が一致しマークされ、次に中央の 'b' が処理されます。残りの文字ペアがない状態で処理が完了し、受理状態 q_accept
へ遷移して停止します。
拒否状態への遷移例:文字列 "abc"
入力が "abc" の場合、最初の 'a' と最後の 'c' を比較する際に一致しません。
* 初期状態: q0
, テープ: a b c □ ...
* ステップ1: 左端の 'a' を読み込み、'X' に書き換え、右に移動。状態 q_read_a
。
* テープ: X b c □ ...
(ヘッドは 'b' に位置)
* ステップ2: 右端の 'c' を探し、状態 q_find_right_a
で 'c' を読み込む。
* ヘッドは 'c' に位置。状態 q_find_right_a
。
* 比較: 状態 q_find_right_a
は、左端で 'a' を読み込んだことを記憶しています。右端で 'c' を読み込みました。'a' と 'c' は一致しません。
* 遷移規則例: (q_find_right_a, 'c') -> (q_reject, 'c', N)
(一致しない場合、その文字をそのままにし、ヘッドを動かさずに、拒否状態 q_reject
へ遷移して停止)
* シミュレーターでの動作:
* ヘッドが 'c' を読み込み、左端の 'a' と比較します。
* 一致しないため、拒否状態 q_reject
に遷移します。
* マシンが停止します。
* テープの状態: X b c □ ...
(ヘッドは 'c' に位置)
このように、チューリングマシンの状態遷移とテープ操作を設計することで、複雑なアルゴリズムも実現できます。シミュレーターを使うことで、これらの抽象的な動作を視覚的に追いかけ、アルゴリズムがどのようにステップごとに実行されるかを具体的に理解することが可能になります。
シミュレーター活用のポイント
シミュレーターで回文判定のチューリングマシンを構築し、動作を追う際には、以下の点に注意すると理解が深まります。
- 状態の役割: 各状態がアルゴリズムのどのステップ(例:左端を探す、右端を探す、比較するなど)に対応しているかを確認します。左端で読み込んだ記号の種類を区別するために、状態名にその情報をエンコードしている場合が多いです(例:
q_read_a
,q_read_b
)。 - テープ記号の役割: 空白記号(□)が文字列の区切りを示す役割、マーク記号(X, Yなど)が既に処理済みの位置を示す役割を果たしていることを理解します。
- ヘッドの動き: ヘッドが左右に移動する理由(左端や右端を探す、処理済み位置の次に戻るなど)を、アルゴリズムのステップと関連付けて理解します。
- 遷移規則の設計: どのような「現在の状態」で「どのような記号」を読み込んだら、「次の状態」に遷移し、「テープに何と書き込み」、「ヘッドをどちらに動かすか」という規則が、アルゴリズムの各ステップをどのように実現しているかを詳細に確認します。
まとめ
本記事では、チューリングマシンによる回文判定アルゴリズムの基本的な考え方と、シミュレーターでその動作を追う際のポイントを解説しました。両端から一文字ずつ比較し、マークしながら内側へ進んでいくというアルゴリズムが、チューリングマシンの状態遷移とテープ操作によってどのように実現されるかを、具体的な動作例を通じて示しました。
チューリングマシンは非常にシンプルなモデルですが、このような文字列処理をはじめとする多様な計算を実行できる能力を持っています。ぜひシミュレーターを活用し、様々な入力文字列に対するマシンの動作を観察することで、チューリングマシンの計算能力と、アルゴリズムをそのモデル上で具体化する方法について、さらに理解を深めていただければ幸いです。