Złożoność obliczeniowa/Wykałd 3: Klasy złożoności obliczeniowej: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Pitab (dyskusja | edycje)
Pitab (dyskusja | edycje)
Nie podano opisu zmian
 
(Nie pokazano 3 wersji utworzonych przez 2 użytkowników)
Linia 1: Linia 1:
==Klasy złożoności czasowej i pamięciowej==


W poprzednich modułach zostały wprowadzone maszyny Turinga oraz
zdefiniowane pojęcie problemu obliczeniowego. Przypomnijmy, że
problem obliczeniowy to dla nas język, czyli zbiór słów. Poznaliśmy
także szczegółowo maszynę w wersji deterministycznej i
niedeterministycznej oraz jej miarę złożoności czasowej i
pamięciowej w każdej z wersji. W tym module zajmiemy się
klasyfikacją języków przy pomocy maszyn. W naszych dalszych
rozważaniach przyjmujemy model obliczeń w postaci maszyny Turinga o
<math>k</math> taśmach.
Klasa złożoności obliczeniowej to zbiór problemów (języków) spełniających określone kryterium.
Najbardziej podstawowe kryteria, tzn.
czas i pamięć potrzebne do klasyfikacji języka dają nam podstawowe klasy złożoności:
{{definicja|[<math>\textsc{TIME}(f(n))</math> ]||
Poprzez <math>\textsc{TIME}(f(n))</math> oznaczamy zbiór języków <math>L</math> takich, że są akceptowane przez deterministyczną maszynę Turinga <math>M</math> o złożoności
czasowej <math>f(n)</math>.
}}
{{definicja|[<math>\textsc{SPACE}(f(n))</math>]||
Poprzez <math>\textsc{SPACE}(f(n))</math> oznaczamy zbiór języków <math>L</math> takich, że są akceptowane przez deterministyczną maszynę Turinga <math>M</math> o złożoności
pamięciowej <math>f(n)</math>.
}}
Stosowne klasy można też zdefiniować dla niedeterministycznych maszyn:
{{definicja|[<math>\textsc{NTIME}(f(n))</math>]||
Poprzez <math>\textsc{NTIME}(f(n))</math> oznaczamy zbiór języków <math>L</math> takich,
że są akceptowane przez niedeterministyczną maszynę Turinga <math>M</math> o
złożoności czasowej <math>f(n)</math>.
}}
{{definicja|[<math>\textsc{NSPACE}(f(n))</math>]||
Poprzez <math>\textsc{NSPACE}(f(n))</math> oznaczamy zbiór języków <math>L</math> takich, że są akceptowane przez niedeterministyczną maszynę Turinga <math>M</math> o złożoności
pamięciowej <math>f(n)</math>.
}}
==Twierdzenia o liniowym przyspieszaniu i kompresji pamięci==
Pierwsze dwa twierdzenia możemy nazwać "teoretycznymi podstawami"
notacji <math>O</math>. Pokażemy bowiem, że w obu powyższych definicjach klas
funkcja <math>f(n)</math> może być rozpatrywana z dokładnością do stałej, ze
względu na specyfikę samego modelu obliczeń. Zostały one udowodnione
w latach 60 przez pionierów badań nad klasami złożoności, laureatów
nagrody Turinga z roku 1993, Jurisa Hartmanisa i Richarda Stearnsa.
{{twierdzenie|[twierdzenie o liniowym przyspieszaniu]||  Jeśli język <math>L</math> jest rozpoznawany przez maszynę <math>M</math> o złożoności czasowej <math>f(n)</math> to może
być rozpoznany przez maszynę <math>M'</math> o złożoności czasowej <math>f'(n)=\epsilon f(n) + (1+\epsilon) n</math>, gdzie <math>\epsilon>0</math>.
}}
{{dowod|:||
Idea dowodu jest oparta na powiększeniu alfabetu maszyny, a tym samym wielkości "słowa" maszyny oraz liczby jej stanów.
Odpowiada to w praktyce podniesieniu technologii
wytwarzania komputerów. Na rysunku przedstawiono schematycznie zamianę 8 komórek pamięci na jedną większą nowej maszyny:
[[ZO-3-1-rys.jpg (Liniowe przyspieszanie)]]
Nasza nowa maszyna o powiększonym alfabecie rozpoczyna działanie od skompresowania słowa wejściowego na drugiej taśmie i zamiany ról taśmy drugiej i wejściowej. Następnie
głowica powraca na początek słowa i rozpoczyna obliczenia.
W każdym kroku obliczeń <math>M'</math> odczytuje bieżącą komórkę oraz dwie sąsiednie (na wszystkich taśmach), a następnie symuluje zachowanie <math>M</math> w obszarze tych trzech komórek dzięki
zwiększonej stosownie liczbie stanów.
Jako efekt następuje modyfikacja bieżącej komórki i komórek sąsiednich oraz stosowne przesunięcie głowicy.
Policzmy, czy taka symulacja rzeczywiście może być opłacalna. Załóżmy, że każda z komórek <math>M'</math> odpowiada <math>c</math> komórkom maszyny <math>M</math>, gdzie <math>c</math> - stopień
kompresji, który ostatecznie ustalimy na końcu (będzie on oczywiście zależał od <math>\epsilon</math> i <math>M</math>). Alfabet nowej maszyny <math>\Gamma'</math> to alfabet starej maszyny <math>\Gamma</math>
powiększony o symbole postaci <math>\Gamma^c</math>, które zapewniają nam zapis <math>c</math> symboli <math>M</math> w jednym symbolu <math>M'</math>.
W pierwszym etapie <math>M'</math> musi dokonać kompresji, czyli odczytać <math>c</math> symboli i zapisać 1 symbol skompresowany. Do tego celu wystarczy jej istnienie
około <math>|\Gamma^c|</math> dodatkowych stanów. W ostatnim skompresowanym symbolu dokładamy symbole puste dla wyrównania.
Czas działania tego etapu to około <math>n+\lceil n/c \rceil</math> (odczyt związany z kompresją + powrót).
W drugim etapie dokonujemy symulacji. Najpierw odczytujemy "obszar trzech" komórek na każdej taśmie co zajmuje 4 kroki <math>M'</math>:
[[ZO-3-2-rys.jpg (Obszar trzech)]]
Zapamiętujemy odczyt dzięki stosownemu powiększeniu liczby stanów <math>M'</math>, które wykonujemy podobnie jak przy kompresji. Tym razem nowy zbiór stanów powiększamy o około <math>\Gamma^{3c}</math>
(trzeba jeszcze pamiętać o zapamiętaniu pozycji każdej głowic wewnątrz obszarów, te szczegóły techniczne pomijamy).
W tym momencie mamy pełną wiedzę kiedy maszyna <math>M</math> opuści "obszar trzech" i jak zostanie on zmodyfikowany.
Oczywiście jeśli w tym czasie maszyna <math>M</math> kończy działanie, to tak robi również <math>M'</math>. W przeciwnym wypadku "obszar trzech" jest modyfikowany kolejnymi 4 ruchami
<math>M'</math> i cykl rozpoczyna się od nowa. Wiemy jednak, że aby <math>M</math> opuściła obszar trzech komórek <math>M'</math> to musi wykonać przynajmniej <math>c</math> ruchów.
Przyspieszyliśmy zatem działanie przynajmniej o czynnik <math>c/8</math>.
Podsumowując koszt czasowy to <math>f'(n)=n+\lceil n/c \rceil+\lceil 8f(n)/c \rceil<(1+\epsilon)n + \epsilon f(n)</math>, jeśli <math>c=\lceil 8/\epsilon \rceil</math>,
dla odpowiednio dużych <math>n</math> (dla małych
<math>n</math> można stosownie rozbudować maszynę).
}}
Twierdzenie nie ma zastosowania dla subliniowych funkcji złożoności, jednak maszyny, które nie czytają całego wejścia wydają się mało interesujące.
W przypadku liniowej funkcji złożoności oznacza to, że stała może być dowolnie bliska 1.
Analogicznie twierdzenie zachodzi dla złożoności pamięciowej:
{{cwiczenie|[twierdzenie o liniowej kompresji pamięci]||
Jeśli język <math>L</math> jest rozpoznawany przez maszynę <math>M</math> o złożoności pamięciowej <math>f(n)</math> to może być rozpoznany przez maszynę <math>M'</math> o złożoności pamięciowej <math>f'(n)=\epsilon f(n)</math>, gdzie <math>\epsilon>0</math>.
}}
<div class="mw-collapsible mw-made=collapsible mw-collapsed">Wskazówka: <div class="mw-collapsible-content" style="display:none">Skorzystaj z dowodu twierdzenia o liniowym przyspieszaniu.
</div>
</div>
<div class="mw-collapsible mw-made=collapsible mw-collapsed">Rozwiązanie: <div class="mw-collapsible-content" style="display:none">Dowód przebiega dokładnie tak samo jeśli chodzi o ideę. Wystarczy tylko dokonać obliczenia zużycia pamięci. Każda komórka nowej maszyny <math>M'</math> pamięta <math>c</math> symboli maszyny <math>M</math>, więc koszt pamięciowy to <math>f'(n)=\lceil f(n)/c \rceil<\epsilon f(n)</math>, jeśli <math>c=\lceil 1/\epsilon \rceil</math>,
dla odpowiednio dużych <math>n</math> (ponownie dla małych <math>n</math> można stosownie rozbudować maszynę).
</div>
</div>
Na koniec ciekawostka dotycząca przyspieszania maszyn.
Mając dany język <math>L</math> poszukujemy najszybszego algorytmu, który go akceptuje.
Okazuje się, że jest język, dla którego nie istnieje algorytm asymptotycznie najszybszy!
Autorem tego przeczącego intuicji twierdzenia jest Manuel Blum, laureat nagrody Turinga z roku 1995.
{{twierdzenie|[twierdzenie Bluma o przyspieszaniu]||
Istnieje język <math>L</math>, taki, że jeśli jest akceptowany przez maszynę Turinga o złożoności czasowej <math>f(n)</math>, to jest
również akceptowany, przez maszynę Turinga o złożoności czasowej <math>\textnormal{log}(f(n))</math>.
}}
==Relacje między klasami, twierdzenie Savitcha==
Teraz jesteśmy gotowi do wprowadzenia podstawowych klas złożoności, w których funkcje są wyłącznie asymptotyczne:
* Klasa P = <math>\textnormal{TIME}(n^k) = \bigcup_{j>0} \textnormal{TIME}(n^j)</math>, to klasa tych języków, które mogą być akceptowane w deterministycznym czasie wielomianowym,
* Klasa NP = <math>\textnormal{NTIME}(n^k) = \bigcup_{j>0} \textnormal{NTIME}(n^j)</math>, to klasa tych języków, które mogą być akceptowane w niedeterministycznym czasie wielomianowym,
* Klasa EXP = TIME(<math>2^{n^k}</math>), to klasa tych języków, które mogą być akceptowane w deterministycznym czasie wykładniczym,
* Klasa NEXP = NTIME(<math>2^{n^k}</math>), to klasa tych języków, które mogą być akceptowane w niedeterministycznym czasie wykładniczym.
dla klas pamięciowych:
* Klasa L = SPACE(log<math>n</math>), to klasa tych języków, które mogą być akceptowane w deterministycznej pamięci logarytmicznej,
* Klasa NL = NSPACE(log<math>n</math>), to klasa tych języków, które mogą być akceptowane w niedeterministycznej pamięci logarytmicznej,
* Klasa PSPACE = SPACE(<math>n^k</math>), to klasa tych języków, które mogą być akceptowane w deterministycznej pamięci wielomianowej,
* Klasa NPSPACE = NSPACE(<math>n^k</math>), to klasa tych języków, które mogą być akceptowane w niedeterministycznej pamięci wielomianowej,
* Klasa EXPSPACE = SPACE(<math>2^{n^k}</math>), to klasa tych języków, które mogą być akceptowane w deterministycznej pamięci wykładniczej,
* Klasa NEXPSPACE = NSPACE(<math>2^{n^k}</math>), to klasa tych języków, które mogą być akceptowane w niedeterministycznej pamięci wykładniczej.
Teraz zajmiemy się relacjami pomiędzy poszczególnymi klasami złożoności. Najbardziej podstawowe zależności, łączące czas, pamięć i niedeterminizm to:
* <math>\textnormal{TIME}(f(n))\subseteq\textnormal{NTIME}(f(n))</math>, gdyż z definicji, każda maszyna deterministyczna jest maszyną niedeterministyczną,
* <math>\textnormal{SPACE}(f(n))\subseteq\textnormal{NSPACE}(f(n))</math>, jak wyżej,
* <math>\textnormal{TIME}(f(n))\subseteq\textnormal{SPACE}(f(n))</math>, gdyż maszyna nie może zapisać więcej komórek niż wynosi jej czas działania,
* <math>\textnormal{NTIME}(f(n))\subseteq\textnormal{NSPACE}(f(n))</math>, jak wyżej,
* <math>\textnormal{NTIME}(f(n))\subseteq\textnormal{TIME}(c^{f(n)})</math>, na podstawie twierdzenia z modułu pierwszego o symulacji
maszyny niedeterministycznej przez maszynę deterministyczną.
Aby powiedzieć więcej o relacjach pomiędzy klasami musimy narzucić pewne rozsądne ograniczenie na funkcję złożoności <math>f(n)</math>. Powiemy, że <math>f(n)</math>
jest ''konstruowalna pamięciowo'', gdy <math>f(n)\geqslant \textnormal{log}n</math> oraz istnieje deterministyczna maszyna Turinga, która mając na wejściu <math>n</math> zapisane unarnie potrafi
zużyć dokładnie <math>f(n)</math> komórek pamięci i zatrzymać się.
Zawężamy się w ten sposób do funkcji <math>f(n)\geqslant \textnormal{log}n</math>, lecz mniejszych złożoności nie będziemy tutaj rozważać (mimo, iż można).
Warto dodać, że jeśli maszyna działa w pamięci <math>o(\textnormal{log log}n)</math> to działa w pamięci stałej.
Okazuje się, że większość interesujących funkcji spełnia tą własność. Jest to także własność zamknięta ze względu na dodawanie, mnożenie i potęgowanie.
{{cwiczenie|||
Pokaż, że funkcje <math>\lceil \textnormal{log}n \rceil</math>, <math>n^k</math>, <math>2^n</math> są konstruowalne pamięciowo.
}}
<div class="mw-collapsible mw-made=collapsible mw-collapsed">Wskazówka: <div class="mw-collapsible-content" style="display:none"> Zastosuj definicję i skonstruuj stosowne maszyny.
</div>
</div>
<div class="mw-collapsible mw-made=collapsible mw-collapsed">Rozwiązanie: <div class="mw-collapsible-content" style="display:none"> Dowód konstruowalności pamięciowej funkcji
<math>\lceil \textnormal{log}n \rceil</math> opieramy na implementacji klasycznego licznika binarnego. Długość zapisu liczby binarnej <math>n</math> to właśnie <math>\lceil \textnormal{log}n \rceil</math>.
Dla funkcji wielomianowej wykonujemy serię prostych działań arytmetycznych polegających na mnożeniu i dodawaniu liczb naturalnych zapisanych binarnie.
W przypadku funkcji wykładniczej wystarczy zaprogramować maszynę, aby <math>n</math> razy dokonała podwojenia liczby zużytych komórek.
</div>
</div>
Poniżej przedstawiamy twierdzenie, które zachodzi, jeśli nie narzucimy dodatkowego warunku na funkcję złożoności. Wprowadzając go, chcemy uniknąć podobnych
sytuacji:
{{twierdzenie|[twierdzenie o luce]|| Istnieje funkcja rekurencyjna <math>f(n)</math> taka, że <math>\textnormal{TIME}(f(n))=\textnormal{TIME}(2^{f(n)})</math>.
}}
{{dowod|:||
Przedstawimy bardzo specyficzną definicję funkcji <math>f(n)</math>, dla której każda maszyna na słowie o długości <math>n</math> działa w czasie co najwyżej <math>f(n)</math> lub
działa przynajmniej <math>2^{f(n)}+1</math> kroków lub pętli się. W ten sposób pokażemy stosowną równość klas. Dowód opiera się na bardzo użytecznej i często stosowanej
technice przekątniowej.
Będziemy rozważać wszystkie możliwe maszyny w pewnej ustalonej kolejności <math>M_1, M_2, \ldots</math> wynikającej np. z leksykograficznego porządku na ich kodach zdefiniowanych w
module pierwszym. Ponieważ każda maszyna może być opisana skończonym słowem, więc wygenerowanie takiego ciągu wszystkich maszyn jest wykonalne.
Zdefiniujmy relację binarną <math>P(i,k)</math> w ten sposób, by była spełniona, gdy każda maszyna od <math>1</math> do <math>i</math> działając na dowolnym słowie o długości
<math>i</math> działa w czasie co najwyżej <math>k</math> lub działa przynajmniej <math>2^k+1</math> kroków lub pętli się. Tą relację jesteśmy w stanie obliczyć poprzez stosowną symulację maszyn <math>M_1, \ldots, M_i</math>
na wszystkich słowach długości <math>i</math> przez co najwyżej <math>2^k+1</math> kroków (oczywiście jest to dosyć czasochłonne), tym samym ewentualne pętlenie się maszyn nie stanowi
przeszkody.
Teraz jesteśmy gotowi do zdefiniowania <math>f(n)</math>. Ustalmy <math>n</math>. Zauważmy, że <math>P(n,k)</math> musi być prawdziwa dla pewnego <math>k</math>. Dzieje się tak dlatego, gdyż wraz
ze wzrostem <math>k</math> zmieniamy zabroniony obszar czasu działania maszyn od <math>1</math> do <math>n</math>. Liczba słów które testujemy jest jednak ograniczona - są to wszystkie słowa o długości dokładnie <math>n</math> dla tych maszyn. Aby <math>P(n,k)</math> nie było prawdą to czas działania maszyny
na słowie musi trafić do obszaru zabronionego, co wobec ustalonej liczby słów i zwiększania <math>k</math> spowoduje, że <math>P(n,k)</math> w końcu będzie prawdą. Definiujemy wartość <math>f(n)</math> jako najmniejsze takie <math>k</math>.
Weźmy dowolny język <math>L\in\textnormal{TIME}(2^{f(n)})</math>. Jest on akceptowany przez maszynę, którą oznaczmy <math>M_j</math> (w naszym porządku ustalonym w pierwszej
części). Maszyna ma złożoność <math>2^{f(n)}</math>. Weźmy dowolne słowo o długości <math>l\geqslant j</math>. Wiemy, że <math>P(l,f(l))</math> jest spełnione, a tym
samym maszyna <math>M_j</math> działa w czasie co najwyżej <math>f(l)</math> (bo więcej niż <math>2^{f(l)}</math> nie może z definicji klasy). Zatem <math>L\in\textnormal{TIME}(f(n))</math>.
Pominęliśmy działanie na słowach krótszych niż <math>j</math>, jednakże jest to stała liczba słów, które łatwo zaakceptować w czasie rzędu ich długości
po prostu wbudowując ich przynależność do <math>L</math> w maszynę.
}}
W literaturze rozważa się wiele wersji "normujących" dopuszczalne funkcji złożoności, np. właściwie funkcje złożoności lub funkcje uczciwe.
Różnice między nimi są dosyć techniczne.
Przyjmijmy zatem, że funkcje złożoności <math>f(n)</math> są konstruowalne pamięciowo.
Przeanalizujmy teraz możliwe konfiguracje maszyny Turinga <math>M</math>, które tworzą tzw. graf przejść maszyny:
{{cwiczenie|||
W jak wielu konfiguracjach może znaleźć się maszyna Turinga o złożoności pamięciowej
<math>f(n)</math> (konstruowalnej pamięciowo) przeprowadzając obliczenie na słowie o długości <math>n</math>?
}}
<div class="mw-collapsible mw-made=collapsible mw-collapsed">Wskazówka: <div class="mw-collapsible-content" style="display:none"> Policz liczbę stanów maszyny, położeń głowic i zawartości taśm.
</div>
</div>
<div class="mw-collapsible mw-made=collapsible mw-collapsed">Rozwiązanie: <div class="mw-collapsible-content" style="display:none"> Liczba możliwych konfiguracji to oczywiście iloczyn:
* liczby stanów <math>|Q|</math>,
* położeń głowic na wszystkich taśmach, jest rzędu <math>f(n)^k</math> jednak nie mniej niż <math>n</math> (dokładny wynik zależy od przyjętych wyróżnień taśmy wejściowej i wyjściowej),
* zawartości taśm, czyli <math>|\Gamma|^{k f(n)}</math>.
</div>
</div>
Razem możemy to ograniczyć przez <math>c^{f(n)}</math>, dla pewnego <math>c</math> zależnego tylko od maszyny oraz korzystając z założenia, że
<math>f(n)\geqslant \textnormal{log}n</math>, gdyż jest konstruowalna pamięciowo.
Teraz jesteśmy gotowi do wypowiedzenia kolejnych interesujących relacji pomiędzy wprowadzonymi klasami:
* <math>\textnormal{SPACE}(f(n))\subseteq\textnormal{TIME}(c^{f(n)})</math>, ze względu na fakt, iż liczba możliwych konfiguracji maszyny o złożoności pamięciowej <math>f(n)</math>,
co pokazaliśmy przed chwilą wynosi <math>c^{f(n)}</math>, zatem maszyna, która się nie pętli może zostać
zasymulowana przez maszynę działającą co najwyżej tak długo. W przeciwnym wypadku wpadła by w nieskończoną pętlę.
* <math>\textnormal{NTIME}(f(n))\subseteq\textnormal{SPACE}(f(n))</math>, gdyż maszyna deterministyczna może zasymulować działanie maszyny niedeterministycznej.
Wystarczy wygenerować po kolei każdy z ciągów <math>f(n)</math> niedeterministycznych wyborów (tu korzystamy z pamięciowej konstruowalności), których musi ona dokonać w trakcie obliczeń.
Następnie dokonujemy już deterministycznej symulacji obliczeń przez <math>f(n)</math> kroków. Wszystkie te operacje można dokonać w dostępnej pamięci <math>f(n)</math>, gdyż
każdy z ciągów niedeterministycznych wyborów możemy symulować w tej samej pamięci,
* <math>\textnormal{NSPACE}(f(n))\subseteq\textnormal{TIME}(c^{f(n)})</math>, ponownie opierając się na symulacji maszyny.
Jak poprzednio liczba wszystkich konfiguracji wynosi <math>c^{f(n)}</math>, jednak tym razem przejścia pomiędzy poszczególnymi
konfiguracjami tworzą graf. Wystarczy jednak obliczyć, czy istnieje ścieżka od konfiguracji początkowej do konfiguracji końcowej, co może być
obliczone w czasie wielomianowym ze względu na rozmiar grafu, a zatem w czasie asymptotycznym <math>c^{f(n)}</math>.
Dzięki powyższym relacjom możemy wypisać kilka podstawowych zależności pomiędzy wprowadzonymi klasami:
{{cwiczenie|||
Uzasadnij każdą z poniższych relacji:
* <math>\textnormal{L}\subseteq\textnormal{NL}\subseteq\textnormal{P}\subseteq\textnormal{NP}\subseteq\textnormal{PSPACE}</math>
* <math>\textnormal{PSPACE}\subseteq\textnormal{NPSPACE}\subseteq\textnormal{EXP}\subseteq\textnormal{NEXP}\subseteq\textnormal{EXPSPACE}\subseteq\textnormal{NEXPSPACE}</math>
}}
<div class="mw-collapsible mw-made=collapsible mw-collapsed">Wskazówka: <div class="mw-collapsible-content" style="display:none"> Skorzystaj z poznanych już własności klas oraz własności funkcji logarytmicznej, wielomianowej i wykładniczej.
</div>
</div>
<div class="mw-collapsible mw-made=collapsible mw-collapsed">Rozwiązanie: <div class="mw-collapsible-content" style="display:none"> Wszystkie zawierania pochodzą z wypisanych wcześniej ogólnych zależności. Nietrywialne, to:
* <math>\textnormal{NL}\subseteq\textnormal{P}</math>, korzystamy z faktu, że <math>\textnormal{NSPACE}(f(n))\subseteq\textnormal{TIME}(c^{f(n)})</math>, w tym wypadku
mamy bowiem <math>c^{\textnormal{log}n}=n^k</math>,
* <math>\textnormal{NPSPACE}\subseteq\textnormal{EXP}</math>, również korzystamy z faktu, że <math>\textnormal{NSPACE}(f(n))\subseteq\textnormal{TIME}(c^{f(n)})</math>, w tym wypadku
mamy bowiem <math>c^{n^k}=2^{n^{k'}}</math>.
</div>
</div>
Przyjrzyjmy się bliżej pierwszej serii relacji z poprzedniego ćwiczenia i zobrazujmy ją na rysunku:
[[ZO-3-3-rys.jpg(Relacje pomiędzy podstawowymi klasami)]]
<center><math>\textnormal{L}\subseteq\textnormal{NL}\subseteq\textnormal{P}\subseteq\textnormal{NP}\subseteq\textnormal{PSPACE}</math></center>
W następnej części z twierdzenia o hierarchii pamięciowej dowiemy się, że <math>L\varsubsetneq \textnormal{PSPACE}</math>. Tym samym wiemy, że
pierwszy i ostatni element są różne. Jedną z najbardziej fascynujących rzeczy w teorii złożoności jest fakt, że przynajmniej
jedno z czterech powyższych zawierań musi być ścisłe, jednakże o ''żadnym z nich'' tego nie wiadomo! Najsłynniejszy fragment to
oczywiście pytanie o zawieranie pomiędzy P i NP.
Ostatnią i najciekawszą relacją pomiędzy klasami jest ta odkryta przez Savitcha, mówiąca o niewielkiej przewadze niedeterministycznej złożoności pamięciowej.
Przypomnijmy, że do tej pory wiemy poprzez połączenie dwóch wymienionych własności, że <math>\textnormal{NSPACE}(f(n))\subseteq\textnormal{SPACE}(c^{f(n)})</math>. Okazuje
się, że zachodzi twierdzenie dużo silniejsze:
{{twierdzenie|[twierdzenie Savitcha]||
Jeśli <math>f(n)</math> jest konstruowalna pamięciowo, to <math>\textnormal{NSPACE}(f(n))\subseteq\textnormal{SPACE}(f^2(n))</math>.
}}
{{dowod|:||
Wspominaliśmy już o tym, że kluczowym elementem symulacji maszyny niedeterministycznej jest sprawdzanie czy istnieje ścieżka od konfiguracji początkowej do końcowej maszyny.
Problem sprawdzania czy dwa wierzchołki w grafie są połączone ścieżką jest znany jako REACHABILITY. Savitch udowodnił, że
REACHABILITY należy do SPACE(<math>\textnormal{log}^2n</math>), gdzie <math>n</math> to rozmiar grafu.
W naszym wypadku rozmiar grafu to liczba konfiguracji, czyli <math>c^{f(n)}</math>, zatem
REACHABILITY wymaga czasu <math>\textnormal{log}^2(c^{f(n)})=O(f(n))^2</math>, co da nam tezę.
Od tej pory przyjmijmy, że graf ma <math>n</math> wierzchołków.
Wprowadźmy pomocniczą funkcję <math>\textnormal{PATH}(x,y,i)</math> która ma zwrócić 1, gdy istnieje ścieżka od <math>x</math> do <math>y</math> w grafie zawierająca co najwyżej <math>i</math>
wierzchołków. Nasze pytanie możemy przeformułować jako <math>PATH(b,e,n)</math>, gdzie <math>b</math> to stan początkowy, <math>e</math> końcowy, a <math>n</math> to
maksymalna długość ścieżki w grafie konfiguracji, czyli liczba wszystkich wierzchołków.
Obliczymy <math>\textnormal{PATH}(x,y,i)</math> w sposób rekurencyjny. Jeśli <math>i=1</math> to sprawdzamy bezpośrednie przejście od konfiguracji <math>x</math> do <math>y</math>.
W przeciwnym wypadku najpierw wybieramy dowolny wierzchołek jako kandydata na wierzchołek pośredni na ścieżce pomiędzy <math>x</math> i <math>y</math>, który
oznaczmy przez <math>t</math>:
[[ZO-3-4-rys.jpg(Wierzchołek pośredni)]]
Potem wywołujemy rekurencyjnie <math>\textnormal{PATH}(x,t,i/2)</math> oraz <math>\textnormal{PATH}(t,y,i/2)</math>. Jeśli obie ścieżki istnieją to zwracamy 1,
w przeciwnym wypadku próbujemy kolejny wierzchołek <math>t</math>. Nie musimy się martwić o czas, tylko pamięć, więc policzmy ile jest nam potrzebne.
Głębokość rekurencji wyniesie <math>\textnormal{log}n</math>, ze względu na fakt iż na każdym poziomie zmniejszamy długość o czynnik 2.
Aby zapamiętać wywołanie rekurencyjne (stos rekurencyjny) potrzebujemy pamiętać <math>x</math>, <math>y</math>, bieżące <math>t</math> oraz <math>i</math>. Wszystkie te liczby
w zapisie binarnym potrzebują <math>\textnormal{log}n</math> pamięci. Razem potrzebujemy <math>O(\textnormal{log}^2n)</math> pamięci.
}}
Na podstawie twierdzenia Savitcha wiemy, że niektóre z poznanych klas są sobie równe:
* PSPACE = NPSPACE,
* EXPSPACE = NEXPSPACE.
czyli, że niedeterminizm w złożoności pamięciowej dla większych funkcji nic nie daje.
Dla klas złożoności czasowej takiej wiedzy nie posiadamy!
===Dopełnienia klas===
W tym rozdziale przyjrzymy się dopełnieniom języków i klas. Jeśli <math>L</math> jest językiem to przez <math>\overline{L}=\sum^*\setminus L</math>
oznaczamy jego dopełnienie. W przypadku problemów decyzyjnych ''dopełnienie'' (ang. COMPLEMENT) to problem decyzyjny,
w którym odpowiedzi są odwrócone.
Jeśli rozważymy SAT, w którym pytamy czy formuła może zostać spełniona,
to jego dopełnienie to SAT COMPLEMENT. Jest to problem bardzo blisko spokrewniony z TAUTOLOGY, w którym pytamy czy każde wartościowanie
formuły <math>\phi</math> ją spełnia. SAT COMPLEMENT to pytanie, czy formuła nie ma wartościowań spełniających, co
jest równoważne temu, że <math>\neg \phi</math> jest formułą zawsze spełnioną, czyli jest tautologią logiczną.
COMPLEMENT nie jest ściśle dopełnieniem języka, gdyż <math>\overline{SAT}</math> zawiera także wszystkie słowa, które nie są poprawnymi
opisami formuł. Te słowa nie stanowią jednak problemu w rozpoznawaniu.
Zdefiniujemy teraz pojęcie dopełnienia klasy złożoności. Przypomnijmy, że klasy złożoności składają się z języków.
Jeśli <math>C</math> jest dowolną klasą złożoności to przez <math>\textnormal{co}C</math>
oznaczamy jej ''dopełnienie'', które jest złożone z dopełnień języków z klasy <math>C</math>.
Zauważmy od razu, że jeśli <math>C</math> jest klasą deterministyczną, to <math>\textnormal{co}C=C</math> ze względu na fakt, iż
maszyna deterministyczna, która akceptuje język <math>L\in C</math> po zamianie rolami stanu akceptującego i odrzucającego
stanie się maszyną akceptującą język <math>\overline{L}</math>.
W module dotyczącym pamięci logarytmicznej dowiemy się, że klasy niedeterministycznej złożoności pamięciowej
również zamknięte są na dopełnienia, natomiast w przypadku klas niedeterministycznych złożoności
czasowej ''nie wiemy'' jakie są relacje pomiędzy nimi i jest to problem otwarty.
===Twierdzenia o hierarchii czasowej i pamięciowej===
W tej części poznamy dwa ważne twierdzenia, które wprowadzają pojęcia hierarchii czasowej i pamięciowej, tzn. pokażemy, że
większe złożoności rzeczywiście istotnie pozwalają akceptować więcej języków.
{{twierdzenie|[twierdzenie o hierarchii pamięciowej]||
Jeśli <math>f(n)</math> jest konstruowalna pamięciowo oraz <math>g(n) \in o(f(n))</math> (czyli rośnie asymptotycznie wolniej) to
<math>\textnormal{SPACE}(g(n)) \subsetneq  \textnormal{SPACE}(f(n))</math>.
}}
{{dowod|:||
Pokażemy, że istnieje język <math>L\in\textnormal{SPACE}(f(n))</math> taki, że <math>L\notin\textnormal{SPACE}(g(n))</math>.
Aby zapewnić pierwszy warunek, skonstruujemy maszynę <math>M</math> dla <math>L</math>, która ma złożoność pamięciową <math>f(n)</math>.
Drugi warunek zapewnimy pokazując, że żadna z maszyn o złożoności pamięciowej <math>g(n)</math> nie akceptuje <math>L</math>.
Maszyna <math>M</math> działa następująco. Gdy na wejściu dostanie słowo o długości <math>n</math> to w pierwszym etapie
oznakowuje <math>f(n)</math> komórek pamięci na każdej z taśm. Będziemy bowiem symulować inne maszyny i chcemy się upewnić, że
nie zużywają one więcej niż <math>f(n)</math> pamięci. Możemy to zrobić dzięki konstruowalności pamięciowej <math>f(n)</math>.
Teraz następuje część przekątniowa:
[[ZO-3-5-rys.jpg(Maszyna przekątniowa)]]
W drugim etapie maszyna <math>M</math> interpretuje słowo <math>x</math> z wejścia jako kod maszyny <math>M'</math>. Maszyna <math>M</math> odrzuca słowa, które nie są poprawnymi kodami.
Gdy kod jest poprawny, to rozpoczyna się symulacja <math>M'</math> na tym samym słowie <math>x</math>. Jeśli symulacja
przekroczy oznakowaną pamięć <math>f(n)</math> to maszyna <math>M</math> również odrzuca słowo <math>x</math>. Jeśli maszyna <math>M'</math> zaakceptuje <math>x</math>
to maszyna <math>M</math> odrzuca <math>x</math> i jeśli <math>M'</math> odrzuca <math>x</math> to <math>M</math> je akceptuje.
Maszyna <math>M</math> ma złożoność pamięciową <math>f(n)</math>, więc <math>L=L(M)\in\textnormal{SPACE}(f(n))</math>.
Zastanówmy się, czy język <math>L</math> należy do klasy <math>\textnormal{SPACE}(g(n))</math>. Wtedy jest akceptowany przez
maszynę <math>M'</math> o złożoności <math>g(n)</math>. Zastanówmy się co dzieje się, gdy <math>M'</math> dostanie na wejściu swój kod <math>\textnormal{<}M'\textnormal{>}</math>.
Ponieważ <math>M'</math> akceptuje język w czasie <math>g(n)</math>, więc i na słowie <math>x</math> działa w takim czasie. Załóżmy, że
odrzuca <math>x</math>. Wtedy maszyna <math>M</math> zaakceptuje <math>x</math>, gdyż symulacja którą ona przeprowadza, tzn. działanie <math>M'</math> na <math>x</math>
może zostać przeprowadzona, jako że <math>g(n)</math> jest asymptotycznie mniejsze niż <math>f(n)</math> (pomijamy szczegóły techniczne, które pojawią się dla małych <math>n</math>).
Analogicznie, gdy <math>M'</math> akceptuje <math>x</math> to <math>M</math> je odrzuca, czyli <math>M'</math> nie może akceptować <math>L</math>, gdyż myli się na <math>x=\textnormal{<}M'\textnormal{>}</math>.
Otrzymujemy sprzeczność, stąd <math>L \notin \textnormal{SPACE}(g(n))</math>.
}}
A teraz pora na analogiczne twierdzenie o hierarchii czasowej.
Potrzebne jest nam do niego dodatkowe ograniczenie na funkcję złożoności. Powiemy, że <math>f(n)</math>
jest ''konstruowalna czasowo'', gdy <math>f(n)\geqslant n\textnormal{log}n</math> oraz istnieje deterministyczna maszyna Turinga, która mając na wejściu <math>n</math> zapisane unarnie potrafi
działać przez dokładnie <math>f(n)</math> kroków i zatrzymać się. Również w tym wypadku większość znanych funkcji jest konstruowalna.
{{twierdzenie|[twierdzenie o hierarchii czasowej]||
Jeśli <math>f(n)</math> jest konstruowalna czasowo oraz <math>g(n) \in o(f(n)/\textnormal{log}f(n))</math> to
<math>\textnormal{TIME}(g(n)) \subsetneq  \textnormal{TIME}(f(n))</math>.
}}
Dowód jest przedmiotem ćwiczenia końcowego. Teraz możemy wyciągnąć kilka ważnych wniosków o ''silnym'' zawieraniu się klas złożoności:
* <math>\textnormal{SPACE}(n^{\epsilon_1})\subsetneq\textnormal{SPACE}(n^{\epsilon_2})</math>, gdy <math>0\leqslant\epsilon_1 < \epsilon_2</math>, z własności funkcji wielomianowej,
* <math>\textnormal{TIME}(n^{\epsilon_1})\subsetneq\textnormal{TIME}(n^{\epsilon_2})</math>, gdy <math>1\leqslant\epsilon_1 < \epsilon_2</math>, jak wyżej,
* <math>\textnormal{L}\subsetneq\textnormal{PSPACE}</math>, gdyż logarytm rośnie wolniej niż funkcja wielomianowa,
* <math>\textnormal{P}\subsetneq\textnormal{EXP}</math>,  korzystamy z własności, że każdy wielomian rośnie wolniej niż funkcja subwykładnicza <math>n^{\textnormal{log}n}</math> a ta z kolei rośnie wolniej, niż każda funkcja wykładnicza.
* <math>\textnormal{PSPACE}\subsetneq\textnormal{EXPSPACE}</math>, jak wyżej.
Widzimy zatem, że klasa P, pojmowana jako zbiór praktycznie rozwiązywalnych problemów również podlega hierarchii. Istnieją w niej języki
które są akceptowane w czasie <math>n^{10000}</math> natomiast w <math>n^{9999}</math> już nie. To sprawia, że należy patrzeć na praktyczność klasy P
z pewną ostrożnością.
==Ćwiczenia dodatkowe==
{{cwiczenie|||
Udowodnij twierdzenie o hierarchii czasowej:
Jeśli <math>f(n)</math> jest konstruowalna czasowo oraz <math>g(n) \in o(f(n)/\textnormal{log}f(n))</math> to
<math>\textnormal{TIME}(g(n)) \subsetneq  \textnormal{TIME}(f(n))</math>.
}}
<div class="mw-collapsible mw-made=collapsible mw-collapsed">Wskazówka: <div class="mw-collapsible-content" style="display:none"> Przeprowadź rozumowanie podobne jak w dowodzie twierdzenia o hierarchii pamięciowej.
</div>
</div>
<div class="mw-collapsible mw-made=collapsible mw-collapsed">Rozwiązanie: <div class="mw-collapsible-content" style="display:none"> Jak w dowodzie twierdzenia o hierarchii pamięciowej skonstruujemy maszynę <math>M</math>, której język <math>L=L(M)</math>
należy do <math>\textnormal{TIME}(f(n))</math>, ale nie należy do <math>\textnormal{TIME}(g(n))</math>.
Konstruujemy <math>M</math> tak, że bierze <math>x</math> z wejścia i traktuje jako kod maszyny. Następnie
symuluje jej działanie nie jak poprzednio w pamięci <math>f(n)</math> lecz w czasie <math>f(n)</math>.
Aby jednak dokonać takiej symulacji maszyna <math>M</math> musi zliczać liczbę wykonanych kroków
symulacji. Symulacja każdego kroku zajmuje teraz <math>\textnormal{log}(f(n))</math> potrzebnych do zaimplementowania
licznika. Stąd można zasymulować każdą maszynę o złożoności asymptotycznie mniejszej niż <math>f(n)\textnormal{log}f(n)</math>
a tym samym wykazać przy pomocy przekątniowej metody, że język <math>M</math> jest różny od języka każdej z tych maszyn.
</div>
</div>
{{cwiczenie|||
Udowodnij następujące fakty:
* <math>\textnormal{NTIME}(n)\subsetneq \textnormal{PSPACE}</math>
* <math>\textnormal{TIME}(2^n) = \textnormal{TIME}(2^{n+1})</math>
* <math>\textnormal{TIME}(2^n)\subsetneq \textnormal{TIME}(2^{2n})</math>
}}
<div class="mw-collapsible mw-made=collapsible mw-collapsed">Wskazówka: <div class="mw-collapsible-content" style="display:none"> Użyj poznanych twierdzeń o przyspieszaniu i hierarchii.
</div>
</div>
<div class="mw-collapsible mw-made=collapsible mw-collapsed">Rozwiązanie: <div class="mw-collapsible-content" style="display:none">
Ad. 1:
Wiemy, że <math>\textnormal{NTIME}(n) \subseteq \textnormal{SPACE}(n)</math>.
Następnie na podstawie twierdzenia o hierarchii pamięciowej <math>\textnormal{SPACE}(n) \subsetneq \textnormal{PSPACE}</math>, co kończy dowód.
Ad. 2:
Oczywiście <math>\textnormal{TIME}(2^n) \subseteq \textnormal{TIME}(2^{n+1})</math>.
Weźmy teraz dowolny język <math>L</math> należący do klasy <math>\textnormal{TIME}(2^{n+1})</math>. Istnieje zatem maszyna działająca w czasie <math>2^{n+1}</math>.
Z twierdzenia o przyspieszaniu wiemy, że istnieje inna maszyna <math>M'</math>
akceptująca ten sam język <math>L</math>, ale działająca w czasie <math>2^{n+1}/4</math>, gdy zastosujemy <math>\epsilon = 1/4</math> (czynnik liniowy
obok funkcji wykładniczej jest nieistotny). Lecz <math>2^{n+1}/4=2^n/2</math>, a zatem <math>L</math> należy do <math>\textnormal{TIME}(2^n)</math> co dowodzi tezy.
Ad. 3:
Zastosujemy twierdzenie o hierarchii czasowej dla funkcji <math>f(n)=2^{2n}</math> oraz <math>g(n)=2^n</math>.
Musimy tylko wykazać, że funkcje spełniają założenia twierdzenia. Mamy jednak <math>f(n)/\textnormal{log}(f(n))=2^{2n}/2n=(2^n/2n)2^n</math>
zatem dominuje asymptotycznie <math>2^n</math> co kończy dowód.
</div>
</div>
==Testy końcowe==

Aktualna wersja na dzień 17:51, 8 sie 2006