Co bylo na wykladzie ?
20 lutego 2008
Wprowadzenie: trudność obliczeniowa -- od rozumienia empirycznego do
modelu matematycznego. Problemów trudnych obliczeniowo
nie trzeba sztucznie konstruować, spotyka się je dość
powszechnie, np. rozkład liczby naturalnej na czynniki
(faktoryzacja).
Postęp technologiczny przyśpiesza czas działania komputerów o
stałą multyplikatywną. Prosty rachunek pokazuje, że to ma znaczenie
przy szybkich algorytmach (największe przy liniowych, przy
wykładniczych -- prawie wcale).
Podstawowy model: maszyna Turinga off
line, tzn. taśma wejściowa jest read only i nie wlicza się do
rozmiaru pamięci (który moze być mniejszy niż liniowy). Konfiguracja maszyny obejmuje
zawartość taśm roboczych i położenie wszystkich głowic. Złożoność pamieciowa maszyny jest
ograniczona przez funkcję S(n), jeśli dla dowolnego wejścia rozmiaru n,
maszyna odwiedza co najwyżej S(n) komórek na wszystkich taśmach
roboczych. Złożoność czasowa
maszyny jest ograniczona przez funkcję T(n), jeśli
dla dowolnego wejścia rozmiaru n, maszyna wykonuje co najwyżej T(n)
kroków. Złożoność pamieciową
S(n) lub czasową T(n) rozpatrujemy zawsze z dokładnością do stałej
multiplikatywnej, bo ewentualnie powiekszając alfabet roboczy możemy
zawsze "skompresować" c S(n) do S(n) (podobnie dla T).
Zauważmy, że
rozmiar konfiguracji maszyny, której złożoność pamięciowa jest
ograniczona przez S(n), jest ograniczony przez
S(n) + O(log n) . Składnik O(log n) odpowiedzialny jest położenie
głowicy na taśmie wejściowej. Można go pominać, jeśli S(n) >=
log (n)
-- na mocy uwagi o kompresowaniu o stałą.
Hipoteza P=/=NP będzie dokładniej omawiana później, ale
"na rozgrzewkę" rozważymy następujący
Problem Levina: czy jest
możliwe, że udowodnimy, że P=NP, ale nie będziemy znali algorytmu
wielomianowego dla problemu SAT?
Otóż, używając numeracji maszyn Turinga można podać taki
algorytm, że jeśli w ogóle istnieje algorytm wielomianowy dla
SAT, to podany algorytm również działa w czasie wielomianowym,
przynajmniej dla przypadków pozytywnych (tzn. gdy formuła jest
spełnialna).
Dla konstrukcji istotne znaczenie ma fakt, że w przypadku problemu SAT
z algorytmu odpowiadającego na pytanie czy istnieje rozwiazanie
(tzn. wartościowanie spełniające formułę), można otrzymać algorytm znajdujący pewne rozwiązanie w
czasie tylko kwadratowo wiekszym. (Nie jest to cecha wszystkich
problemów; np. znamy obecnie wielomianowy algorytm odpowiadający na
pytanie, czy liczba jest złożona, ale
nie znamy algorytmu znajdujacego w czasie wielomianowym nietrywialny
dzielnik liczby złożonej.) Po drugie, dla danego wartościowania
można oczywiście łatwo sprawdzić, czy spełnia ono daną formułę. Nasz
algorytm, dla danej formuły A przegląda kolejne maszyny Turinga
(w ustalonym porządku) i symuluje je "ruchem zygzakowym", aż napotka
wartościowanie spełniające formułę A (o ile jest spełnialna). Jeśli
maszyna powiedzmy nr k produkuje takie wartosciowania w czasie p(n), to
nasz algorytm będzie działał w czasie proporcjonalnym do p^2 (n).
Ograniczenie pamięciowe a problem stopu.
Jeśli złożoność czasowa maszyny Turinga jest ograniczona funkcją T(n),
to z definicji maszyna zatrzymuje się dla każdych danych. Dla
ograniczonej złożonosci pamięciowej nie jest to takie oczywiste. Mamy
jednak
Twierdzenie Spisera Mając
daną maszynę M o złożonosci pamięciowej ograniczonej przez S(n) można
skonstruować maszyne M', która akceptuje ten sam język co M, ma
tę samą złożoność pamięciową, a ponadto zatrzymuje się dla każdego
słowa wejściowego.
Szkic dowodu. Kluczowa
jest obserwacja, że dla danej konfiguracji C rozmiaru K, wszystkie
konfiguracje rozmiaru K, z których
można osiągnąć C tworzą drzewo
(gdzie rodzic wierzchołka etykietowanego konfiguracją D jest
etykietowany jedyną konfiguracją osiagalną z D w jednym
kroku, a etykietą korzenia jest C). Przy pomocy
odpowiednio dobranych stanów i przejść, maszyna M' może
przeszukać to drzewo i sprawdzić, czy M mogłaby osiagnąć konfigurację C
z konfiguracji początkowej -- używając jedynie pamieci K.
Zauważmy, że w danej chwili w pamięci maszyny przechowywana jest tylko
jedna konfiguracja. W szczególności, konfiguracja C, od
której zaczynamy przeszukiwanie, nie jest trzymana w pamięci,
ale zostaje na końcu zrekonstruowana z powrotem.
Zalóżmy na chwilę, że maszyna M', otrzymując na wejściu słowo w,
zna maksymalny rozmiar K konfiguracji osiągalnych przez maszynę M z
konfiguracji początkowej (ze słowem w). Wtedy M' może przeglądać
kolejno wszystkie konfiguracje rozmiaru K i sprawdzać, czy
któraś z nich jest osiagalna z konfiguracji początkowej.
Problem w tym, że nie znamy rozmiaru K ! Można to jednak przezwyciężyć
w podobny sposób.
Zaczynamy od K = 1. Jeśli dla jakiegoś K żadna konfiguracja
akceptująca nie jest osiągalna z poczatkowej w pamieci K, to w
podobny sposób badamy te konfiguracje rozmiaru K, które w
następnym kroku maszyna M "chciałaby powiekszyć" do konfiguracji
rozmiaru K+1 (może to mieć miejsce, gdy ze skrajnie prawej pozycji
maszyna wykona kolejny ruch w prawo).
Jesli któraś z takich konfiguracji jest osiagalna z
początkowej, decydujemy powiekszyć pamięć, K := K+1. W przeciwnym razie
M' kończy obliczenie bez akceptacji. Szczegóły są
ćwiczeniem z programowania maszyn Turinga. Z konstrukcji wynika, że M'
używa nie więcej pamieci niż M.
27 lutego
W niedeterministycznej maszynie Turinga zamiast funkcji mamy relację. Z
danej konfiguracji mamy na ogół wiele możliwych ruchów.
Maszyna M akceptuje słowo w jeśli przynajmniej
jedno obliczenie jest akceptujace. O maszynie
niedeterministycznej mówimy, że działa w czasie ograniczonym
przez T(n) (lub w pamięci ograniczonej przez S(n)), jeśli każde obliczenie na słowie
wejsciowym długości n kończy się w co najwyżej T(n) krokach (lub
wykorzystuje co najwyżej S(n) komórek pamieci).
W ogólności dowolną niedeterministyczną maszynę Turinga można
symulować przez maszyne deterministyczną. Jednak pytanie, na ile
efektywnie można to zrobić, pozostaje głównym nierozwiazanym
zagadnieniem teorii złożonosci.
Dla deterministycznej maszyny M działającej w czasie T(n), dopełnienie
języka L(M) można również rozpoznać w czasie T(n). Dla
maszyny niedeterministycznej przypuszczalnie tak nie jest.
Ma jednak miejsce dość zaskakujący fakt.
Twierdzenie Immermana-
Szelepcsenyi'ego
(1988).
Zakładamy, ze S(n) jest funkcją nie mniejszą niz log n i w pełni pamięciowo konstruowalną,
tzn. istnieje deterministyczna maszyna, która dla dowolnego
wejścia rozmiaru n odwiedza dokładnie
n komórek pamieci (większość "sensownych" funkcji ma tę
własność).
Mając daną niedeterministyczną maszynę Turinga M, która działa w
pamięci ograniczomej przez S(n), można skonstruować maszynę
(niedeterministyczną) M', która działa również w pamięci
S(n) i rozpoznaje dopełnienie języka rozpoznawanego przez M.
W terminach klas złożoności można to zapisać równaniem
NSPACE ( S(n) ) = co-NSPACE ( S(n)
)
Uwaga. Twierdzenie to implikuje
w szczególności, że języki kontekstowe są zamknięte na
dopełnienie, co przez wiele lat stanowiło problem otwarty w teorii
języków formalnych.
Szkic dowodu. Tym razem
kluczową obserwacją jest, że pomocna może być liczba wszystkich
konfiguracji osiągalnych
przez maszynę M z konfiguracji początkowej (w jakimkolwiek obliczeniu).
Idea:
wiedząc, że ta liczba jest K, przeglądamy kolejne konfiguracje badając
(niedeterministycznie), czy są osiągalne. Jeśli znaleźliśmy K
konfiguracji osiągalnych (a więc wszystkie) i żadna nie jest
akceptujaca, to mamy pewność że nasze słowo wejściowe nie jest
akceptowane przez M. Okazuje się, że podobna idea pozwala na
obliczenie liczby konfiguracji osiągalnych w T krokach, jeśli znamy
dokładną liczbę konfiguracji osiągalnych w T-1 krokach (dla T=0
ta liczba jest 1). Znajdując kolejne wartości dla T =
0,1,2,... (aż do ustabilizowania się), znajdujemy potrzebną
wartość K.
Przedstawimy dwa niedeterministyczne algorytmy implementowalne na
niedeterministycznej maszynie Turinga w pamięci S(n). W
algorytmach tych wykorzystujemy swobodnie daną niedeterministyczną
maszynę M. Pierwszy algorytm (program
główny) przyjmuje jako dane słowo w
i liczbę K (binarnie) i ma
tę własność, że o ile się zatrzymuje w stanie innym niż error, to daje poprawną odpowiedź
na pytanie czy M nie akceptuje
w, pod warunkiem że K
jest liczbą
wszystkich konfiguracji osiągalnych przez maszynę M z konfiguracji
początkowej ze słowem
wejściowym w. Drugi algorytm
jest procedurą obliczajacą liczbę K, tzn. o ile
się zatrzymuje w stanie innym niż error,
to wynik jest liczbą wszystkich konfiguracji osiągalnych przez M ze
słowa wyjściowego w (poniżej
mówimy krótko "osiągalnych").
Program główny
wejście: słowo w
długości n
Oblicz (K);
licznik := 0;
Przeglądaj wszystkie konfiguracje C maszyny M rozmiaru S(n) w porzadku
leksykograficznym i wykonuj:
Wykonuj (niedeterministyczne) obliczenie maszyny M
na słowie w
jesli w trakcie obliczenia pojawi się konfiguracja
akceptujaca, to STOP, Odpowiedź NIE (* słowo w jest
akceptowane przez M *)
jeśli pojawi się C, ale C nie jest akceptujace, to
licznik := licznik + 1;
Jesli licznik < K to STOP error
Jesli licznik = K to STOP, Odpowiedź TAK
(* przeszukaliśmy wszystkie konfiguracje osiagalne i nie było
wśród nich akceptującej, zatem w
nie jest akceptowane M *)
Procedura Oblicz (K)
Koniec := false;
Czas := 0;
while not Koniec do
if Czas = 0 then K_e := K := 1 (*
dokladnie jedna konfiguracja osiagalna w 0 krokach *)
else (* K jest
liczbą konfiguracji osiągalnych w =< Czas -- 1 krokach,
K_e w dokładnie Czas -- 1 krokach *)
old_K_e := K_e;
K_e := 0;
for
C := wszystkie konfiguracje maszyny M rozmiaru S(n) przegladane w
porzadku
leksykograficznym do
(* chcemy
sprawdzic, czy C jest osiagalne w dokladnie Czas krokach *)
licznik := 0;
osiagalna := false;
for D :=
wszystkie konfiguracje maszyny M rozmiaru S(n) przegladane w porzadku
leksykograficznym do
Wykonaj
(niedeterministyczne) Czas -- 1 kroków obliczenia maszyny
M na słowie w
(* w jest daną programu
głównego *)
if osiagniętą
konfiguracją jest D then
licznik := licznik +1;
if D -->
C (* C osiagalna z D w jednym kroku *) then osiagalna := true
if osiagalna then K_e := K_e + 1 (* C jest
osiagalna w dokładnie Czas krokach *) else
if licznik < old_K_e then STOP error
(* w pętli "for D..." nie sprawdziliśmy
wszystkich konfiguracji osiągalnych w Czas -- 1 krokach,
więc nie mamy pewności czy C nie jest osiagalna *)
K := K + K_e;
if
K_e = 0 then Koniec := true
(* wszystkie konfiguracje osiagalne są
osiągalne w =< Czas -- 1 krokach, a zatem K jest
liczbą
wszystkich konfiguracji osiągalnych *)
else
Czas := Czas + 1