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 ,
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.
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++.
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 .
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ń.
Konfiguracja początkowa maszyny M dla słowa
to
.
Niech
będzie konfiguracją i niech
,
,
,
.
Niech
.
Jeśli x=L (czyli głowica przesuwa się w lewo), to mamy:
Relacja 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.
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.
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ć.
Pokazaliśmy właśnie, że język
jest obliczalny, choć wiemy, że nie jest bezkontekstowy.
Wynika stąd następujący fakt:
Czy każdy język jest częściowo obliczalny?
Zdecydowanie nie!
Wynika to stąd, że (dla ustalonego alfabetu wejściowego
) 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
) jest przeliczalnie wiele.
Natomiast języki to zbiory słów, czyli wszystkich możliwych
języków (nad ustalonym alfabetem
)
jest
.
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.
Powiedzmy, że chcielibyśmy mieć maszynę wyposażoną w k ścieżek,
przy czym na i-tej ścieżce są zapisane znaki z .
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
.
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.