Złożoność obliczeniowa/Wykład 2: Inne modele dla złożoności
Wprowadzenie
W poprzednim rozdziale przedstawiliśmy bardzo prosty, ale zarazem potężny model obliczeń - maszynę Turinga. Przekonaliśmy się, że można na niej zaimplementować proste operacje takie jak dodawanie; nietrudno uwierzyć w to, że za jej pomocą można rozwiązywać również dużo bardziej skomplikowane problemy programistyczne.
Maszyna Turinga ma jednak zasadniczą wadę - programowanie na niej jest bardzo mało wygodne. W tym module poznamy dwa modele obliczeń dużo bardziej przypominające wygodne współczesne mikrokomputery takie jak ZX Spectrum.
Maszyna RAM
Maszyna RAM (ang. Random Access Machine - maszyna o dostępie swobodnym) jest modelem obliczeń bardziej złożonym od maszyny Turinga. Składa się ona z programu - skończonego ciągu rozkazów definiujących jej działanie - oraz z potencjalnie nieograniczonego zbioru rejestrów, z których każdy może przechowywać dowolną liczbę naturalną. Rejestry indeksujemy liczbami naturalnymi, a więc ich wartości będziemy zapisywać jako . Rejestr o indeksie 0 zwyczajowo nazywany jest akumulatorem i ma pewne specjalne (opisane poniżej) własności. Zbiór rejestrów zazwyczaj kolektywnie nazywamy pamięcią operacyjną lub po prostu pamięcią.
Oprócz programu i pamięci operacyjnej maszyna RAM posiada również dwie taśmy: wejściową i wyjściową. Podobnie jak taśmy maszyny Turinga składają się one z komórek, mogących zawierać symbole pewnego skończonego alfabetu; w przypadku maszyny RAM na taśmach wejściowej i wyjściowej znajdują się liczby naturalne z zakresu , gdzie to liczba ustalona z góry dla danej maszyny. Różnicą w stosunku do maszyny Turinga jest to, że po dokonaniu zapisu na komórce taśmy wyjściowej głowica przesuwana jest w ustalonym kierunku (dla ustalenia uwagi w prawo) i nie można już do tej komórki powrócić; taśma wyjściowa nie może zatem służyć jako miejsce wykonywania obliczeń. To na programiście spoczywa odpowiedzialność zapisania fragmentów wyjścia w bezpiecznym miejscu, jeżeli planuje w przyszłości z nich skorzystać.
Zestaw instrukcji
W przeciwieńswie do maszyny Turinga, gdzie wszystkie wykonywane operacje były tego samego typu, maszyna RAM posiada stosunkowo złożony zestaw instrukcji. Warto przy tym zauważyć, że zestaw podany poniżej jest jednym z wielu możliwych zestawów instrukcji, które można znaleźć w literaturze. Zestawy te są zasadniczo równoważne (tzn. program napisany dla jednego z nich można przetłumaczyć na równoważny program dla innego zestawu instrukcji), mogą jednak wystąpić pewne niuanse przy rozważaniu złożoności obliczeniowej.
Kod | Symbol | Argument | Operacja | |
---|---|---|---|---|
1 | LOAD | =n | ||
2 | LOAD | n | ||
3 | LOAD | n | ||
4 | STORE | n | ||
5 | STORE | n | ||
6 | READ | n | ||
7 | READ | n | ||
8 | WRITE | n | ||
9 | WRITE | n | ||
10 | NEXT | przesunięcie głowicy taśmy wejściowej w prawo | ||
11 | PREV | przesunięcie głowicy taśmy wejściowej w lewo | ||
12 | ADD | =n | ||
13 | ADD | n | ||
14 | ADD | n | ||
15 | SUB | =n | ||
16 | SUB | n | ||
17 | SUB | n | ||
18 | MUL | =n | ||
19 | MUL | n | ||
20 | MUL | n | ||
21 | DIV | =n | ||
22 | DIV | n | ||
23 | DIV | n | ||
24 | JUMP | n | skok do instrukcji o numerze n | |
25 | JZERO | n | skok do instrukcji o numerze n gdy | |
26 | JNZERO | n | skok do instrukcji o numerze n gdy | |
27 | HALT | zakończenie działania maszyny |
W powyższej tabeli symbol I oznacza zawartość komórki taśmy wejściowej, znajdującej się pod głowicą. Analogicznie O to zawartość odpowiedniej komórki na taśmie wyjściowej.
Działanie maszyny RAM
W chwili początkowej konfiguracja maszyny wygląda następująco:
- wszystkie rejestry mają wartość 0,
- na taśmie wejściowej umieszczone jest słowo wejściowe; głowica taśmy wejściowej ustawiona jest na pierwszej (skrajnie lewej) komórce słowa wejściowego,
- licznik rozkazów wskazuje na pierwszą instrukcję programu.
W kolejnych krokach maszyna wykonuje instrukcje wskazywane przez licznik rozkazów. W przypadku instrukcji 1-23 po wykonaniu operacji na rejestrach lub taśmach licznik rozkazów zwiększany jest o jeden. W przypadku instrukcji skoku licznik rozkazów modyfikowany jest zgodnie z wartością argumentu (lub zwiększany o jeden, gdy skok jest warunkowy i warunek nie jest spełniony). Wykonanie instrukcji HALT kończy działanie programu. Dodatkowo instrukcja WRITE przesuwa głowicę taśmy wyściowej o jedną komórkę w prawo, a instrukcje NEXT i PREV przesuwają odpowiednio głowicę taśmy wejściowej.
Można się w tym momencie zastanawiać, co się stanie, gdy maszyna wykona ostatnią instrukcję programu i nie będzie to instrukcja HALT ani instrukcja skoku; albo też co się stanie, gdy jedna z instrukcji reprezentuje skok za ostatni rozkaz programu. Dla porządku przyjmiemy, że program zachowa się tak, jakby wykonał instrukcję HALT. Można też jednak przyjąć, że program dopuszczający taką sytuację jest niepoprawnie napisany i należy go poprawić - co nie jest zadaniem trudnym.
Wynikiem działania programu dla zadanego słowa wejściowego nazywamy ciąg liczb wypisanych na taśmę wyjściową od rozpoczęcia działania programu aż do wykonania instrukcji HALT.
Przykład 2.2
Poniższy program liczy największy wspólny dzielnik dwóch liczb naturalnych podanych na wejściu:
1. READ 1 2. NEXT 3. READ 2 4. LOAD 2 5. JZERO 19 6. SUB 1 7. JNZERO 12 8. LOAD 1 9. SUB 2 10. STORE 1 11. JUMP 4 12. LOAD 2 13. STORE 3 14. LOAD 1 15. STORE 2 16. LOAD 3 17. STORE 1 18. JUMP 4 19. WRITE 1 20. HALT
Jest on generalnie równoważny poniższemu programowi w języku Pascal:
function gcd(a:integer; b:integer):integer; var c:integer; begin while b <> 0 do {linie 4-5} begin if a >= b then {linie 6-7} a := a - b {linie 8-10} else begin c := b; {linie 12-13} b := a; {linie 14-15} a := c; {linie 16-17} end; end; gcd := a; end;
Rozpoznawanie języków
Większość problemów omawianych w ramach tego kursu jest natury decyzyjnej - to znaczy dla zadanego słowa wejściowego program ma udzielić odpowiedzi tak lub nie. Warto zatem umówić się, w jaki sposób maszyna ma nam zakomunikować swoją odpowiedź.
Definicja 2.3
Mówimy, że maszyna RAM akceptuje słowo wejściowe w wtedy i tylko wtedy, gdy po uruchomieniu jej ze słowem w umieszczonym na taśmie wejściowej wypisuje ona na taśmę wyjściową dokładnie jedną liczbę - 1 - i zatrzymuje się po skończonej ilości kroków. Językiem akceptowanym przez maszynę RAM nazywamy zbiór wszystkich skończonych słów akceptowanych przez tę maszynę.
Ćwiczenie 2.4
Uzasadnij, dlaczego żądamy, żeby rejestry maszyny RAM mogły przechowywać dowolnie duże liczby naturalne.
Adresowanie pośrednie jest kluczową cechą maszyny RAM; poniższe ćwiczenie ma na celu zaznajomienie Cię z jego działaniem.
Ćwiczenie 2.5
Zasymuluj program maszyny Turinga z przykładu 2 w module 1 (realizujący dodawanie dwóch liczb unarnych) na maszynie RAM. Oblicz, ile najwięcej kroków wykonuje skonstruowana przez Ciebie maszyna RAM podczas jednego kroku symulowanej maszyny Turinga. Jeśli liczba takich kroków nie jest ograniczona, popraw maszynę w ten sposób, aby ta liczba była ograniczona.
Powyższe ćwiczenie ilustruje ważną własność - każdy program dla jednotaśmowej, jednostronnie ograniczonej (czyli posiadającej nieskończoną taśmę tylko w jednym kierunku) maszyny Turinga można w prosty sposób przekształcić na równoważny program maszyny RAM. Łatwo się przekonać, że maszyna Turinga z obustronnie nieograniczoną taśmą nie jest potężniejsza od maszyny z taśmą jednostronnie ograniczoną; aby przekształcić program napisany dla pierwszej z nich wystarczy umówić się, że komórki o nieparzystych numerach będą reprezentowały komórki oryginalnej maszyny o ujemnych numerach, a komórki o parzystych numerach będą reprezentowały komórki oryginalnej maszyny o numerach dodatnich; dodatkowo należy w dość prosty sposób zmodyfikować program.
Złożoność obliczeniowa w modelu RAM
Zastanówmy się teraz, w jaki sposób można określić czas i pamięć zużyte w trakcie działania maszyny RAM. Pierwszy - zapewne wyglądający na najbardziej naturalny - sposób definiowania tych wielkości to tak zwane kryterium jednostajne. Jego założenia są następujące:
- uznajemy, że każdy rozkaz maszyny jest wykonywany w pewnym z góry ustalonym, stałym czasie,
- uznajemy, że miarą pamięci "zużytej" przez program w trakcie działania jest ilość rejestrów, do których program się odwoływał (w postaci odczytu lub zapisu).
Takie podejście po chwili zastanowienia nie wydaje się jednak właściwe; co gorsza - założenie o wykonywaniu operacji arytmetycznych na dowolnie dużych liczbach w stałym czasie nie jest obecnie możliwe do zrealizowania, i nie wygląda na to, aby mogło zostać zrealizowane również w przyszłości.
Będziemy zatem stosować inne miary dla czasu i pamięci, zwane kryterium logarytmicznym. Jego założenia są następujące:
- czas wykonania operacji na liczbie jest proporcjonalny do . Czas wykonania operacji na dwóch liczbach i jest proporcjonalny do (oczywiście trzeba pamiętać, że logarytm liczby jest niezdefiniowany; koszt wykonania operacji w tym przypadku równy jest ),
- jeżeli operacja używa adresowania pośredniego, to czas dostępu do -tej komórki jest proporcjonalny do ,
- do czasu wykonania każdej operacji doliczana jest pewna stała wielkość, mogąca reprezentować koszt pobrania i rozpoznania instrukcji. Chodzi tutaj głównie o to, by programy wykonujące same instrukcje skoku lub trywialne (czyli w tym przypadku operujące na samych zerach) operacje arytmetyczne miały niezerowy czas działania,
- jako miarę pamięci używanej przez program w danym momencie uznajemy
sumę logarytmów wartości wszystkich rejestrów, do których odwoływał się program w trakcie swojego dotychczasowego działania.
Tak zdefiniowane miary ilości zasobów dużo lepiej odpowiadają rzeczywistości współczesnych komputerów - do zapisania w pamięci liczby potrzebujemy cyfr w systemie binarnym, natomiast dodanie dwóch liczb i zajmuje czas . Niedoszacowany może się wydawać czas mnożenia i dzielenia (jest taki sam jak dodawania), jednak, jak się okaże, dla naszych celów taka definicja zupełnie wystarczy.
Porównanie czasu działania maszyny Turinga i maszyny RAM
Umówmy się, że czas działania określamy tylko dla maszyn z własnością stopu - zarówno w przypadku maszyn Turinga i maszyn RAM.
W jednym z poprzednich ćwiczeń symulowaliśmy działanie maszyny Turinga na maszynie RAM; stwierdziliśmy, że liczba kroków wykonywanych przez maszynę RAM jest proporcjonalna do liczby kroków wykonanych przez symulowaną maszynę Turinga. Jeżeli zatem określa największą ilość kroków wykonywaną przez daną maszynę Turinga przy słowie wejściowym o długości , a jest analogicznie zdefiniowaną maksymalną ilością kroków dla maszyny RAM, to zachodzi następująca własność:
W rzeczywistości w tym momencie powinniśmy zapisać:
Skąd się wzięło ? Otóż maszyna Turinga może dać odpowiedź jeszcze przed przeczytaniem całego słowa wejściowego. Maszyna RAM, skonstruowana w zaprezentowany we wspomnianym ćwiczeniu sposób, symulująca działanie maszyny Turinga, przed rozpoczęciem symulacji przepisze całe słowo wejściowe do rejestrów, po czym dopiero rozpocznie właściwe działanie. Można jednak łatwo wyobrazić sobie schemat, w którym maszyna RAM sprowadza znaki z wejścia niejako "na żądanie", zapamiętując jednocześnie liczbę sprowadzonych wcześniej znaków. Taki schemat postępowania sprawia, że własność:
naprawdę zachodzi.
Zauważmy, że powyższa definicja funkcji czasu dokładnie odpowiada definicji złożoności czasowej przy zastosowaniu kryterium jednostajnego. Warto się zastanowić, jak będzie wyglądała złożoność obliczeniowa w przypadku zastosowania kryterium logarytmicznego.
Ćwiczenie 2.8
Oszacuj czas działania maszyny RAM, symulującej działanie maszyny Turinga, przy zastosowaniu kryterium logarytmicznego.
Symulowanie maszyny RAM na wielotaśmowej maszynie Turinga
Wyniki z poprzedniego paragrafu nie są specjalnie zaskakujące; gołym okiem widać, że maszyna RAM jest narzędziem co najmniej równie potężnym jak maszyna Turinga. W tym paragrafie zajmiemy się zagadnieniem odwrotnym - pokażemy, że maszynę RAM da się symulować na maszynie Turinga i oszacujemy wydajność takiej symulacji.
Aby tego jednak dokonać, uprościmy sobie najpierw definicję maszyny RAM. Będziemy zatem tylko rozpatrywać maszyny RAM spełniające następujące warunki:
- wejście i wyjście stanowią słowa nad alfabetem ,
- maszyna nie używa instrukcji MUL ani DIV,
- jedynymi operacjami, dopuszczającymi adresowanie pośrednie, są instrukcje LOAD i STORE.
Maszynę RAM będziemy symulować na wielotaśmowej maszynie Turinga, posiadającej:
- taśmę wejściową nad alfabetem ,
- taśmę wyjściową nad alfabetem ,
- cztery taśmy robocze nad alfabetem .
Maszyna Turinga będzie symulowała po kolei rozkazy maszyny RAM, przy czym zakładamy, że na początku symulacji każdego rozkazu głowice taśm roboczych znajdują się w takim położeniu, jak na początku działania programu. Taśma robocza nr 1 będzie służyła do reprezentacji aktualnej zawartości rejestrów maszyny RAM. Taśma robocza nr 2 będzie służyła do przechowywania jednego adresu komórki. Taśmy robocze 3 i 4 będą służyły do wykonywania operacji arytmetycznych. Najciekawiej wygląda taśma nr 1; sposób reprezentacji pamięci RAM jest przedstawiony na poniższym schemacie:
Parser nie mógł rozpoznać (SVG (MathML może zostać włączone przez wtyczkę w przeglądarce): Nieprawidłowa odpowiedź („Math extension cannot connect to Restbase.”) z serwera „https://wazniak.mimuw.edu.pl/api/rest_v1/”:): {\displaystyle \verb''\$011*01*11*101**1011\$} .
Na taśmie znajdują się pary liczb, zapisane w postaci binarnej, zaczynając od najmniej znaczącej cyfry. Powyższy zapis mówi, że:
- rejestr 6 ma wartość 2,
- rejestr 3 ma wartość 5,
- rejestr 0 ma wartość 13 (zakładamy, że reprezentacja binarna liczby 0 ma 0 cyfr).
Prześledźmy teraz działanie maszyny dla operacji ADD n (przypominamy, że nie jest tutaj stosowane adresowanie pośrednie, zatem jest stałą określoną w czasie pisania programu). Maszyna Turinga zachowa się w następujący sposób:
- na taśmie numer 2 zapisze ciąg symboli , po czym przesunie głowicę tej taśmy do położenia początkowego,
- przesuwając się w prawo na taśmie nr 1, maszyna będzie szukać reprezentacji komórki o numerze ; w tym celu będzie porównywać numery komórek, zapisane na taśmie nr 1, z adresem zapisanym na taśmie nr 2. Jeśli adresy się nie będą zgadzały maszyna wycofa głowicę taśmy nr 2 do położenia początkowego, zignoruje zawartość komórki na taśmie nr 1 i przejdzie do następnej pary adres-zawartość,
- jeżeli maszyna znajdzie odpowiedni adres, to przepisze zawartość komórki na taśmę nr 3. W przeciwnym wypadku - gdy dojdzie do symbolu - zapisze na taśmie nr 3 reprezentację liczby 0,
- podobna procedura zostanie zastosowana do przekopiowania na taśmę nr 4 zawartości akumulatora (rejestru o numerze 0),
- maszyna doda liczbę zawartą na taśmie nr 3 do liczby z taśmy nr 4 za pomocą algorytmu pisemnego dodawania liczb binarnych,
- maszyna zapisze wynik dodawania na taśmie nr 1.
Ostatni krok może nastręczyć pewnych problemów: nie wiemy, co powinniśmy zrobić, jeśli wynik dodawania zajmuje więcej bitów niż oryginalna liczba zawarta w akumulatorze. Rozwiązanie w tym przypadku jest bardzo brutalne: nasza maszyna po prostu zapisze wynik na samym końcu taśmy nr 1; jeżeli po drodze na koniec taśmy napotka parę adres-wartość, odpowiadającą akumulatorowi, to zarówno adres jak i wartość oraz występujący między nimi separator Parser nie mógł rozpoznać (SVG (MathML może zostać włączone przez wtyczkę w przeglądarce): Nieprawidłowa odpowiedź („Math extension cannot connect to Restbase.”) z serwera „https://wazniak.mimuw.edu.pl/api/rest_v1/”:): {\displaystyle \'\*\'} zamaże, wpisując w ich miejsca symbole . Na poniższym schemacie przedstawiony jest stan taśmy nr 1 po wykonaniu operacji ADD 3 (dodanie do akumulatora zawartości komórki 3):
Parser nie mógł rozpoznać (SVG (MathML może zostać włączone przez wtyczkę w przeglądarce): Nieprawidłowa odpowiedź („Math extension cannot connect to Restbase.”) z serwera „https://wazniak.mimuw.edu.pl/api/rest_v1/”:): {\displaystyle \verb''\$011*01*11*101**\%\%\%\%**01001\$} .
Operacja nie jest dużo bardziej skomplikowana; trzeba znaleźć zawartość komórki , następnie znaleźć zawartość komórki , a na końcu zapisać wynik w akumulatorze (w razie potrzeby zamazując jego poprzednią zawartość).
Oszacujemy teraz czas potrzebny do zasymulowania maszyny RAM. Wykonanie operacji arytmetycznych (pomijając czas dostępu do pamięci) zajmuje czas , gdzie i to argumenty operacji. Operacja kopiowania zawartości jednego rejestru do drugiego zajmuje czas , gdzie to kopiowana wartość. Stwierdzamy zatem, że samo wykonanie operacji na maszynie Turinga jest "wolniejsze" od wykonania tych operacji na maszynie RAM o stały czynnik. Innymi słowy, jeśli to czas wykonania programu dla danych o wielkości na maszynie RAM, a to czas wykonania (pomijając dostępy do pamięci) tych samych operacji na maszynie Turinga, to zachodzi następująca własność:
Pozostaje zatem kwestia oszacowania, ile zajmuje wyszukiwanie (i ewentualne zamazywanie) komórek na taśmie nr 1. Łatwo jednak zauważyć, że w każdym momencie symulacji ilość symboli na taśmie 1 jest co najwyżej proporcjonalna do czasu działania symulowanej maszyny RAM. Zatem czas łącznie spędzony przez maszynę Turinga na dostępy do pamięci można oszacować w następujący sposób:
Zatem łączny czas działania maszyny Turinga - a więc czas dostepu do pamięci plus czas właściwych operacji - spełnia własność:
Ćwiczenie 2.9
Oszacuj zależność między złożonością czasową programu dla maszyny RAM a złożonością czasową programu maszyny Turinga, symulującego działanie maszyny RAM, przy założeniu, że program maszyny RAM może używać wszystkich rozkazów.
Powyższe paragrafy pokazały zależność pomiędzy złożonościami czasowymi równoważnych programów dla maszyn RAM i Turinga. Taką zależność pomiędzy modelami obliczeń nazywamy Parser nie mógł rozpoznać (SVG (MathML może zostać włączone przez wtyczkę w przeglądarce): Nieprawidłowa odpowiedź („Math extension cannot connect to Restbase.”) z serwera „https://wazniak.mimuw.edu.pl/api/rest_v1/”:): {\displaystyle \textit{wielomianową równoważnością}} .
Definicja 2.10
Mówimy, że modele obliczeń i są wielomianowo równoważne (w sensie złożoności czasowej) wtedy i tylko wtedy, gdy dla dowolnego języka (gdzie jest skończonym alfabetem):
- istnienie programu dla maszyny modelu o złożoności czasowej implikuje istnienie programu dla maszyny modelu o złożoności czasowej , gdzie jest wielomianem,
- zachodzi własność symetryczna.
Wielomianowa równoważność sprawia, że jeśli dla danego problemu na maszynie jednego typu istnieje algorytm o wielomianowej złożoności czasowej, to również na drugiej maszynie istnieje algorytm o wielomianowej złożoności czasowej.
Porównanie złożoności pamięciowej w modelach obliczeniowych
Do tej pory mało uwagi poświęcaliśmy złożoności pamięciowej w obu poznanych modelach obliczeniowych. Spróbujmy zatem analogicznie jak poprzednio oszacować pamięć zużytą przez równoważne programy dla maszyny RAM i maszyny Turinga. Ponownie w przypadku maszyny RAM będziemy używali kryterium logarytmicznego.
Łatwo zauważyć, że podczas symulacji maszyny Turinga na maszynie RAM jedynym rejestrem, którego zawartość nie będzie z góry ograniczona przez pewną stałą, jest rejestr zawierający pozycję głowicy. Jego zawartość będzie miała rozmiar co najwyżej logarytmicznie zależny od ilości komórek zużytych na maszynie Turinga. Koszt każdego pozostałego użytego rejestru -- służącego do symulacji komórki maszyny Turinga -- będzie ograniczony przez stałą zależną od alfabetu symulowanej maszyny. Sumaryczna ilość pamięci zużytej przez maszynę RAM będzie zatem liniowo zależna od ilości pamięci zużytej przez maszynę Turinga.
Przejdźmy teraz do symulacji maszyny RAM na maszynie Turinga. W podanej wcześniej procedurze symulacji zachowywaliśmy się dość niechlujnie -- jeżeli symulowana maszyna RAM zapisywała nową wartość do używanego wcześniej rejestru, maszyna Turinga po prostu zamazywała poprzednią wartość i zapisywała nową wartość na koniec taśmy. Można tą procedurę jednak łatwo zmodyfikować -- zamiast zamazywania poprzedniej wartości można przesunąć wszystkie kolejne zapisane na taśmie wartości rejestrów do zwolnionych komórek maszyny Turinga, po czym zapisać nową wartość rejestru na końcu taśmy. Modyfikacja ta sprawi, że ilość komórek używanych przez maszynę Turinga będzie liniowo zależna od ilości pamięci używanej przez symulowaną maszynę RAM; co ważne, czas działania maszyny Turinga nadal będzie wielomianowo zależny od czasu działania maszyny RAM.
Widzimy zatem, że w przypadku złożoności pamięciowej podobieństwo maszyny Turinga i maszyny RAM jest jeszcze silniejsze niż w przypadku złożoności czasowej. Dwie powyższe własności sprawiają, że w dalszej części tego wykładu będziemy używać tych dwóch modeli zamiennie -- dla naszych celów będą one równoważne.
Obwody logiczne
Rozważymy teraz model obliczeń w znaczący sposób różniący się od dwóch poprzednio opisanych -- obwody logiczne. Obwód logiczny można formalnie zdefiniować jako acykliczny graf skierowany o następujących własnościach:
- każdy wierzchołek jest wierzchołkiem wejściowym, wierzchołkiem wyjściowym, koniunkcją, alternatywą albo negacją,
- w zależności od typu wierzchołka spełnione są ograniczenia na jego stopnie wejściowe i wyjściowe:
- wierzchołek wejściowy - stopień wejściowy 0,
- wierzchołek wyjściowy - stopień wejściowy 1, stopień wyjściowy 0,
- alternatywa i koniunkcja - stopień wejściowy 2,
- negacja - stopień wejściowy 1.
Węzły wewnętrzne - koniunkcję, alternatywę i negację - zwyczajowo nazywa się bramkami. Ich semantyka zgodna jest z powszechnie przyjętym w logice zwyczajem. Załóżmy, że obwód logiczny ma wejść i wyjść, z pewnym ustalonym na nich porządkiem. Możemy wtedy patrzeć na układ logiczny jako na funkcję następującego typu:
Ćwiczenie 3.1
Skonstruuj układ logiczny, obliczający funkcję XOR.
Stopień skomplikowania układu można mierzyć na dwa sposoby:
- poprzez ilość bramek w układzie logicznym,
- poprzez ilość bramek na najdłuższej ścieżce występującej w układzie (głębokość układu logicznego).
W dalszej części tego rozdziału będziemy się zajmować pierwszą z tych miar.
W odróżnieniu od dwóch poprzednich modeli, każdy układ logiczny ma z góry określoną wielkość wejścia i wyjścia, a co za tym idzie, skończoną liczbę możliwych zestawów danych wejściowych. Dla takiej rodziny układów będziemy określać miarę złożoności ilościowej.
Definicja 3.2 [Miara złożoności ilościowej]
Niech będzie rodziną układów logicznych taką, że dla każdej liczby naturalnej rodzina zawiera dokładnie jeden układ o węzłach wejściowych. Miarą złożoności ilościowej nazwiemy wtedy funkcję , która każdej liczbie naturalnej przyporządkowuje liczbę bramek zawartych w jedynym układzie z , posiadającym wejść.
Pokażemy teraz, jaki jest związek między złożonością czasową obliczenia na maszynie Turinga (rozpoznającej pewien język ) a złożonością czasową rodziny układów logicznych rozpoznających ten sam język. W tym celu zasymulujemy działanie jednotaśmowej maszyny Turinga dla słów wejściowych o ustalonej długości.
Załóżmy zatem, że maszyna Turinga rozpoznaje język , jej alfabet taśmowy zawiera elementów, a zbiór stanów zawiera elementów. Załóżmy dodatkowo, że maszyna w trakcie swojego działania nie musi w każdym ruchu przesuwać głowicy - może ją pozostawiać w miejscu; ponadto zażądajmy, by maszyna po ustaleniu wyniku działania wracała do położenia początkowego i przechodziła do stanu akceptującego (a dokładniej, żeby się w tym stanie "zapętlała"). Łatwo zauważyć, że każdą maszynę można "zmusić" do tego, aby spełniała te założenia, nie zwiększając jej czasu działania więcej niż dwukrotnie. Złożoność czasową tej maszyny oznaczmy jako .
Ustalmy teraz pewną długość słowa wejściowego - ; ponieważ znamy złożoność czasową maszyny , możemy jeszcze przed rozpoczęciem działania stwierdzić, ile maksymalnie może jej zająć rozwiązywanie problemu decyzyjnego i ile najwięcej może zużyć miejsca podczas tego procesu. Skonstruujemy zatem następujący układ logiczny, z wejściami, symulujący maszynę - jego schemat przedstawiony jest na poniższym rysunku:
Na powyższym schemacie prostokąty reprezentują pewne podukłady logiczne. "Wyjścia" (czyli wartości węzłów na "dolnej" warstwie) prostokątów na -tej warstwie reprezentują konfigurację maszyny po krokach. Wyjście -tego prostokąta na -tej warstwie składa się z węzłów (bitów). Pierwsza część wyjścia oznacza symbol, który znajduje się w -tej komórce taśmy maszyny Turinga po krokach. Pozostała część mówi, czy w -tym kroku głowica znajduje się nad -tą komórką i jeśli tak, w jakim stanie znajduje się maszyna (robimy to dodając jeden "sztuczny" stan, mówiący, że w danym miejscu nie ma głowicy; stąd na zapisanie stanu potrzebne jest bitów). Zastanówmy się teraz, jakie powinno być zachowanie typowego "prostokąta" (oznaczonego symbolem A). Wynik działania tego podukładu zależy tylko i wyłącznie od wyników działania trzech podukładów z poprzedzającej go warstwy; jego funkcja jest zdefiniowana przez zachowanie maszyny Turinga, to znaczy:
- jeśli żadna z trzech komórek poprzedniej warstwy nie zawierała głowicy, to należy przepisać symbol taśmy z prostokąta położonego bezpośrednio nad rozpatrywanym układem,
- jeśli prostokąt położony bezpośrednio nad rozpatrywanym układem posiadał symulowaną głowicę, to należy odpowiednio zmodyfikować otrzymany od niego symbol taśmowy i - w zależności od tego czy głowica się przesuwa, czy nie - zapisać stan symulowanej maszyny lub zapisać sztuczny stan "brak głowicy",
- analogiczne zasady (oczywiście odpowiednio zmodyfikowane) dla sytuacji, gdy głowica na warstwie o numerze znajdowała się w jednej z dwóch sąsiadujących z rozpatrywaną komórek.
Prostokąty A reprezentują więc pewne z góry określone funkcje typu . Wiedząc, że układ operatorów jest układem funkcjonalnie pełnym, można stwierdzić, że prostokąt A jest implementowany przez określoną z góry, stałą liczbę bramek (oznaczmy ją ; stała ta jest zależna tylko od funkcji przejścia - w szczególności nie jest w żaden sposób zależna od ). Rozumując analogicznie, możemy też stwierdzić, że prostokąty B, C, D i E reprezentują pewne funkcje logiczne i możemy je zaimplementować z użyciem co najwyżej bramek.
Pozostaje nam tylko ustalić, jak obliczamy wartość wyjścia układu logicznego; tutaj postępowanie jest proste z powodu wymagań, jakie nałożyliśmy na maszynę: maszyna akceptuje słowo wtedy i tylko wtedy, gdy po krokach głowica znajduje się w pozycji początkowej i maszyna jest w ustalonym stanie akcpetującym. Oba te warunki można sprawdzić na podstawie wyjścia podukładu reprezentowanego przez środkowy prostokąt -tej warstwy.
Przedstawiliśmy zatem sposób, w jaki dla algorytmu o złożoności czasowej można skonstruować rodzinę układów logicznych o:
bramkach.
Wniosek 3.3
Jeżeli dla określonego problemu decyzyjnego istnieje algorytm dla maszyny Turinga o wielomianowej złożoności czasowej, to istnieje rodzina układów logicznych o wielomianowej złożoności ilościowej, rozwiązująca ten sam problem decyzyjny.
Kontrapozycja tego wniosku jest czasami używana do pokazywania, że dla danego problemu nie istnieje algorytm wielomianowy.