Maszyny Turinga i obliczalność

Wstęp

W tym wykładzie zajmiemy się pojęciem obliczalności. Poznamy maszyny Turinga, model teoretyczny opisują pojęcie obliczalności, równy pod względem siły wyrazu wszelkiego rodzaju językom programowania i komputerom.

Pojęcie obliczalności było badane zanim narodziła się informatyka, a nawet zanim powstały pierwsze komputery, bo było to już w latach 30-tych ubiegłego wieku. W ramach badań nad formalizacją podstaw matematyki, badano jakie zbiory formuł można uzyskać na drodze mechanicznego przepisywania napisów zgodnego z określonymi zasadami. Prowadziło to do sformalizowania intuicji dotyczącej tego co można ,,automatycznie obliczyć''. Pojawiło się wiele modeli formalizujących takie intuicyjne pojęcie obliczalności. W śród nich można wymienić maszyny Turinga, rachunek $\lambda$, czy systemy Posta. W tym wykładzie zajmiemy się maszynami Turinga, gdyż spośród tych modeli są one najbliższe komputerom.

Jak się okazało, wszystkie te formalizmy są sobie równoważne -- w każdym z nich można symulować każdy z pozostałych formalizmów. Zostało to ujęte w tzw. hipotezie Church'a, mówiącej, że wszystkie te modele opisują to samo intuicyjne pojęcie obliczalności. Oczywiście nie jest to formalne twierdzenie, które można by udowodnić, gdyż dotyczy ona intuicji, ale jak dotąd nic nie podważyło słuszności tej hipotezy.

Uniwersalność

Formalizmy modelujące pojęcie obliczalności nie tylko są sobie nawzajem równoważne, ale też w każdym z nich można stworzyć model uniwersalny -- taki, który potrafi symulować wszystkie inne modele danego rodzaju (na podstawie ich opisu). W szczególności istnieje uniwersalna maszyna Turinga, która potrafi symulować działanie dowolnej innej maszyny Turinga na podstawie jej opisu.

W pierwszym momencie, pojęcie uniwersalności może być niezrozumiałe lub zadziwiające. Natomiast dzisiaj jest ono prawie oczywiste. Zamiast mówić o maszynach Turinga wybierzmy dowolny język programowania. Uniwersalny program to nic innego jak interpreter danego języka programowania napisany w nim samym. (Jeśli czytelnik zna język Scheme, to zapewne zetknął się z interpreterem Scheme'a napisanym w Scheme'ie.) Interpretery może nie są tak popularne jak kompilatory. Natomiast np. kompilator C++ napisany w C++ jest czymś naturalnym, a w pewnym sensie jest to uniwersalny program w C++. Za jego pomocą można uruchomić każdy inny program w C++.

Maszyny Turinga

Maszyna Turinga przypomina skrzyżowanie automatu skończonego z magnetofonem szpulowym. Tak jak automat stosowy to automat skończony wyposażony w dodatkową pamięć w postaci stosu, tak maszyna Turinga to automat skończony wyposażony w dodatkową pamięć w postaci nieskończonej taśmy, na której można zapisywać i odczytywać informacje.
\includegraphics{rys-14-a}
Taśma ta jest podzielona na klatki. W każdej klatce można zapisać jeden symbol z ustalonego alfabetu. Nad taśmą przesuwa się głowica, którą steruje automat skończony. W jednym kroku automat odczytuje znak zapisany pod głowicą, może w tym miejscu zapisać inny znak, po czym może przesunąć głowicę w lewo lub prawo. Oczywiście automat sterujący głowicą w każdym kroku zmienia również swój stan.

Taśma ma początek, lecz nie ma końca -- jest nieskończona. W pierwszej klatce taśmy jest zapisany specjalny znak, tzw. lewy ogranicznik. Jeżeli głowica znajduje się nad lewym ogranicznikiem, to nie może go zamazać ani przesunąć się na lewo od niego. Na początku, zaraz za lewym ogranicznikiem, zapisane jest słowo, które stanowi dane wejściowe dla maszyny Turinga. Oczywiście słowo to jest skończone. Za tym słowem taśma wypełniona jest w nieskończoność specjalnymi pustymi symbolami, tzw. balnk'ami. Blank'i będziemy oznaczać przez $\sqcup $.

Będziemy tutaj rozważać jedynie deterministyczne maszyny Turinga, tzn. takie, w których automat sterujący głowicą jest deterministyczny. W momencie uruchomienia maszyny Turinga, głowica znajduje się nad pierwszą klatką taśmy (nad lewym ogranicznikiem) i jest w stanie początkowym. Maszyna ma dwa wyróżnione stany: akceptujący i odrzucający. Działa ona tak długo, aż znajdzie się w jednym z tych dwóch stanów. Jeśli jest to stan akceptujący, to słowo początkowo zapisane na taśmie zostało zaakceptowane, a w przeciwnym przypadku zostało odrzucone. Oczywiście jest możliwe, że maszyna Turinga nigdy nie znajdzie się w żadnym z tych stanów. Wówczas jej obliczenie trwa w nieskończoność i mówimy, że się zapętliło. Tak więc dla danego słowa maszyna Turinga może zrobić jedną z trzech rzeczy: zaakceptować to słowo, odrzucić je lub zapętlić się.

Nieznacznie zmieniając opisany model maszyny Turinga możemy go użyć do modelowania dowolnych obliczeń, a nie tylko akceptowania języków, czy problemów decyzyjnych. W tym celu wystarczy, że zamiast dwóch stanów: akceptującego i odrzucającego, mamy jeden stan końcowy. Wówczas to co jest zapisane na taśmie (z wyjątkiem lewego ogranicznika i blank'ów) w momencie gdy maszyna osiągnie stan końcowy, stanowi wynik obliczeń.

Definicja

Maszyna Turinga to dowolna taka dziewiątka $M = \angles{Q, \Sigma, \Gamma, \vdash, \sqcup , \delta, s, t, r}$, że:

Definicja

Niech $M = \angles{Q, \Sigma, \Gamma, \vdash, \sqcup , \delta, s, t, r}$ będzie ustaloną maszyną Turinga. Konfiguracja maszyny M, to dowolna taka trójka:

\begin{displaymath}
\angles{q,\vdash \alpha,k}\in Q \times \Gamma^* \times {\cal N}
\end{displaymath}

że $\vert\alpha\vert \ge k$. W konfiguracji $\angles{q,\alpha,k}$ q to stan maszyny, $\vdash\alpha$ reprezentuje zawartość taśmy (uzupełnioną w nieskończoność blank'ami), a k to pozycja głowicy.

Konfiguracja początkowa maszyny M dla słowa $x \in \Sigma^*$ to $\angles{s, \vdash x, 0}$.

Definicja

Niech $M = \angles{Q, \Sigma, \Gamma, \vdash, \sqcup , \delta, s, t, r}$ będzie daną maszyną Turinga. Na konfiguracjach maszyny Turinga definiujemy relację $\to \subseteq (Q \times \Gamma^* \times {\cal N})^2$. Relacja ta opisuje przejścia między konfiguracjami odpowiadające pojedynczym krokom w obliczeniach M. Jest to najmniejsza relacja, która spełnia podane poniżej warunki.

Niech $\angles{q,\vdash\alpha,k}$ będzie konfiguracją i niech $q \neq t$, $q \neq r$, $\vdash\alpha = a_0a_1a_2\dots a_n$, $n \ge k$. Niech $\delta(q,a_k) = \angles (q', a', x)$. Jeśli x=L (czyli głowica przesuwa się w lewo), to mamy:

\begin{displaymath}
\angles{q,\vdash\alpha,k} \to
\angles{q',a_0a_1\dots a_{k-1}a'a_{k+1}\dots a_n,k-1}
\end{displaymath}

Jeśli x=P i k < n (czyli głowica przesuwa się w prawo, ale w obrębie $\vdash\alpha$), to mamy:

\begin{displaymath}
\angles{q,\vdash\alpha,k} \to
\angles{q',a_0a_1\dots a_{k-1}a'a_{k+1}\dots a_n,k+11}
\end{displaymath}

Jeśli zaś x=P i k=n (czyli głowica jest na prawym końcu $\vdash\alpha$ i przesuwa się w prawo), to mamy:

\begin{displaymath}
\angles{q,\vdash\alpha,k} \to
\angles{q',a_0a_1\dots a_{k-1}a'a_{k+1}\dots a_n\sqcup ,k+11}
\end{displaymath}

Przez $\to^*$ oznaczamy domknięcie zwrotnio przechodnie relacji $\to$.

Relacja $\to^*$ opisuje do jakich konfiguracji możemy dojść w wyniku obliczenia. Konfiguracje akceptujące, to te, które zawierają stan t, a odrzucające to te, które zawierają stan r.

Definicja

Niech $M = \angles{Q, \Sigma, \Gamma, \vdash, \sqcup , \delta, s, t, r}$ będzie daną maszyną Turinga. Język akceptowany przez M składa się z tych słów, dla których obliczenie maszyny prowadzi do konfiguracji akceptującej:

\begin{displaymath}
L(M) = \{x \in \Sigma^* :
\exists_{\alpha \in \Gamma^*, k...
...
\angles{s, \vdash x, 0} \to^* \angles{t, \vdash\alpha, k}\}
\end{displaymath}

Języki częściowo obliczalne i obliczalne

Definicja

Powiemy, że język $A \subseteq \Sigma^*$ jest częściowo obliczalny, jeżeli istnieje taka maszyna Turinga M, że A= L(M).
Wobec języków częściowo obliczalnych używa się też określeń częściowo rozstrzygalny i rekurencyjnie przeliczalny. Wszystkie te określenia oznaczają to samo pojęcie.

Zauważmy, że żeby maszyna Turinga M akceptowała język A, dla wszystkich słów z tego języka musi zatrzymywać się w stanie akceptującym, natomiast dla słów spoza języka A nie musi zatrzymywać się w stanie odrzucającym -- może również się zapętlać. Intuicyjnie odpowiada to istnieniu programu komputerowego, który dla słów z języka A potrafi potwierdzić ich przynależność do języka, natomiast dla słów spoza tego języka wcale nie musi potrafić zaprzeczyć ich przynależności do języka, lecz może się zapętlić. Stąd słowo ,,częściowo'' w nazwie.

Istnieje równoważna definicja języków częściowo obliczalnych, która mówi, że język A jest częściowo obliczalny, gdy istnieje taki program komputerowy, który wypisuje (w pewnej kolejności) wszystkie słowa należące do A. Jeżeli A jest nieskończony, to oznacza to, że każde słowo należące do A kiedyś zostanie wypisane.

Definicja

Powiemy, że język $A \subseteq \Sigma^*$ jest obliczalny, jeżeli istnieje taka maszyna Turinga M, że A= L(M) i dla każdego $x \in \Sigma^*$ maszyna M albo akceptuję x, albo go odrzuca.


\begin{fact}
Każdy język bezkontekstowy jest obliczalny.
\end{fact}

Fakt ten wynika stąd, że algorytm CYK (dla danej gramatyki bezkontekstowej) można zaimplementować w postaci maszyny Turinga. Ponieważ algorytm ten zawsze się zatrzymuje, więc i taka maszyna nie będzie się zapętlać.

Przykład

Pokazaliśmy poprzednio, że język $\{a^nb^nc^n : n \ge 0 \}$ nie jest bezkontekstowy. Zapewne dla większości Czytelników napisanie programu, który wczytuje napis i sprawdza czy jest on postaci anbncn nie sprawiłoby kłopotu. Pokażemy, jak skonstruować maszynę Turinga, która to robi. Nie podamy jej formalnej definicji (gdyż byłoby to nudne), ale opiszemy jej sposób działania. Oto przykładowa początkowa konfiguracja:

\begin{displaymath}
\begin{array}{\vert c\vert c\vert c\vert c\vert c\vert c\ve...
...}{}&
\multicolumn{1}{c}{}&
\multicolumn{1}{c}{}
\end{array} \end{displaymath}

  1. Nasza maszyna najpierw czyta słowo podane na wejściu i sprawdza czy pasuje ono do wzorca a*b*c*, przesuwając głowicę w prawo, aż do napotkania pierwszego blanka. Jeżeli słowo na wejściu nie pasuje do wzorca, to odrzucamy je. Dodatkowo, jeżeli na wejściu jest dane słowo puste, to od razu je akceptujemy.

    \begin{displaymath}
\begin{array}{\vert c\vert c\vert c\vert c\vert c\vert c\ve...
...ulticolumn{1}{c}{\uparrow}&
\multicolumn{1}{c}{}
\end{array} \end{displaymath}

  2. W miejsce pierwszego blank'a wstawiamy prawy ogranicznik.

    \begin{displaymath}
\begin{array}{\vert c\vert c\vert c\vert c\vert c\vert c\ve...
...ulticolumn{1}{c}{\uparrow}&
\multicolumn{1}{c}{}
\end{array} \end{displaymath}

  3. Następnie przejeżdżamy głowicą w lewo zamieniając pierwszy napotkany znak a, b i c na blanki. Jeżeli któregoś ze znaków zabrakło, to odrzucamy. Oznacza to, że znaków a, b i c nie było po równo.

    \begin{displaymath}
\begin{array}{\vert c\vert c\vert c\vert c\vert c\vert c\ve...
...}{}&
\multicolumn{1}{c}{}&
\multicolumn{1}{c}{}
\end{array} \end{displaymath}

  4. Przesuwamy głowicę w prawo, aż do prawego ogranicznika, sprawdzając czy między ogranicznikami są jakieś znaki inne niż blanki.

    \begin{displaymath}
\begin{array}{\vert c\vert c\vert c\vert c\vert c\vert c\ve...
...ulticolumn{1}{c}{\uparrow}&
\multicolumn{1}{c}{}
\end{array} \end{displaymath}

    W takim przypadku skaczemy do kroku 3 i powtarzamy dwa ostatnie kroki tak długo, aż między ogranicznikami pozostaną same blank'i.

    \begin{displaymath}
\begin{array}{\vert c\vert c\vert c\vert c\vert c\vert c\ve...
...}{}&
\multicolumn{1}{c}{}&
\multicolumn{1}{c}{}
\end{array} \end{displaymath}


    \begin{displaymath}
\begin{array}{\vert c\vert c\vert c\vert c\vert c\vert c\ve...
...ulticolumn{1}{c}{\uparrow}&
\multicolumn{1}{c}{}
\end{array} \end{displaymath}


    \begin{displaymath}
\begin{array}{\vert c\vert c\vert c\vert c\vert c\vert c\ve...
...}{}&
\multicolumn{1}{c}{}&
\multicolumn{1}{c}{}
\end{array} \end{displaymath}

  5. Gdy w końcu między ogranicznikami zostaną same blank'i, akceptujemy.

    \begin{displaymath}
\begin{array}{\vert c\vert c\vert c\vert c\vert c\vert c\ve...
...ulticolumn{1}{c}{\uparrow}&
\multicolumn{1}{c}{}
\end{array} \end{displaymath}

[Prezentacja multimedialna: animacja powyższej maszyny.]

Pokazaliśmy właśnie, że język $\{a^nb^nc^n : n \ge 0 \}$ jest obliczalny, choć wiemy, że nie jest bezkontekstowy. Wynika stąd następujący fakt:
\begin{fact}
Klasa języków obliczalnych zawiera ściśle klasę języków
bezkontekstowych.
\end{fact}

Czy każdy język jest częściowo obliczalny? Zdecydowanie nie! Wynika to stąd, że (dla ustalonego alfabetu wejściowego $\Sigma$) maszyn Turinga (z dokładnością do izomorfizmu) jest przeliczalnie wiele. Być może bardziej intuicyjne jest równoważne stwierdzenie, że w dowolnym języku programowania istnieje przeliczalnie wiele programów. Wszak każdy program to tylko słowo, a słów (nad ustalonym alfabetem $\Sigma$) jest przeliczalnie wiele. Natomiast języki to zbiory słów, czyli wszystkich możliwych języków (nad ustalonym alfabetem $\Sigma$) jest $2^{\vert\Nat\vert} > \vert\Nat\vert$. Z drugiej strony, każda maszyna Turinga czy program komputerowy akceptuje jeden określony język. Tak więc maszyn Turinga czy programów komputerowych jest za mało, żeby każdy język był częściowo obliczalny.

Powyższe rozumowanie jest poprawne, ale niekonstruktywne -- nie dostarcza nam żadnego przykładu języka, który nie byłby częściowo obliczalny czy obliczalny. Dalej poznamy przykład takiego języka.

Wariacje na temat maszyn Turinga

W literaturze można spotkać różne warianty maszyn Turinga: maszyny z taśmą nieskończoną w obydwie strony, maszyny w taśmą wielościeżkową, maszyny z wieloma taśmami i głowicami, czy maszyny z dwuwymiarową płaszczyzną zamiast taśmy. Dalej w naszych rozważaniach czasem będzie nam wygodnie założyć, że maszyny Turinga posiadają któreś z tych rozszerzeń. Wszystkie te modle są równoważne przedstawionym tutaj maszynom Turinga. Nie będziemy tego formalnie dowodzić, ale krótko naszkicujemy odpowiednie konstrukcje.

Powiedzmy, że chcielibyśmy mieć maszynę wyposażoną w k ścieżek, przy czym na i-tej ścieżce są zapisane znaki z $\Gamma_i$. Głowica będąc w jednym położeniu może odczytać i zapisać równocześnie znaki na wszystkich ścieżkach. Tak naprawdę jest to równoważne klasycznej maszynie Turinga z alfabetem ścieżkowym $\Gamma_1 \times \Gamma_1 \times \cdots \times \Gamma_k$.

Jeżeli chcemy, aby taśma była nieskończona w obie strony, to wystarczy, że złożymy ją (w miejscu rozpoczęcia wejścia) na pół i skleimy w taśmę dwuścieżkową. Taka maszyna będzie w każdym kroku uwzględniać i modyfikować tylko jedną z dwóch ścieżek. Dodatkowo więc automat sterujący głowicą musi pamiętać nad którą ścieżką znajduje się głowica.

Jeśli chcielibyśmy mieć wiele taśm i głowic, to symulujemy to na maszynie z jedną głowicą i wieloma ścieżkami. Potrzebujemy taśmy o dwukrotnie większej liczbie ścieżek. Połowa z nich będzie nam służyć do przechowywania zawartości taśmy symulowanej maszyny, a połowa będzie przeznaczona na znaczniki wskazujące położenia odpowiednich głowic. Oczywiście symulacja jednego kroku takiej maszyny może wymagać przejrzenia większego fragmentu taśmy i trwać odpowiednio długo. Niemniej jest to wykonalne.

Podsumowanie

W tym wykładzie poznaliśmy maszyny Turinga -- model obliczeniowy równoważny programom komputerowym. Poznaliśmy też definicje klas języków obliczalnych i częściowo obliczalnych, które zbadamy bliżej w kolejnym wykładzie.

Skorowidz

Praca domowa

  1. Opisz maszynę Turinga akceptującą język $\{ww : w \in \{a,b\}\}$. Maszyna ta nie może zapętlać się. Zasymuluj jej działanie dla słowa abbabb.