Formalisace intuitivního pojmu algoritmus Studijní materiál, M.Č. 1. Výpočetní krok 1.1. Začněme s následujícím programem: function y(x: natural number) : natural number; begin y := x + 1; end; Jak dlouho bude tento program počítat? Můžeme říci, že jeho časová složitost je O(1), pokud bychom předpokládali, že máme k disposici počítač, který je schopen v jednom výpočetním kroku, tedy pomocí jediné instrukce, realisovat přičítání jedničky k libovolně velkému číslu. Ve skutecnosti však počítače dokáží v jednom výpočetním kroku pracovat pouze s čísly omezené velikosti, například nanejvýš 2 32. Kdybychom tvrdili, že přičtení jedničky k danému číslu x trvá čas O(1), znamenalo by to, že přičtení jedničky např. k číslům, jejichž dvojkový zápis je 1000 a bude trvat stejně dlouho. 11111111111 1 }{{} 10 101010 -krát Poznámka. Platí (ve dvojkovém zápisu) 1000 + 1 = 1001 a 11111111111 }{{ 1} +1 = 1 00000000000 }{{ 0}. 10 101010 -krát 10 101010 -krát Je vidět, že přičtení jedničky k dlouhému číslu může znamenat velkou práci: v našem příkladu je třeba v paměti přepsat hodně bitů. 1
1.2. Předpokládejme, že počítač representuje ve své paměti přirozená čísla pomocí jejich dvojkového zápisu. Dvojkový zápis čísla x má délku O(log x) bitů. 1.3. Rozeberme detailněji instrukci y := x + 1. Je-li v paměti uloženo např. číslo x = 1151 jako posloupnost bitů 10001111111, můžeme přičtení jedničky realisovat tak, že postupně budeme tuto posloupnost procházet odprava doleva a přepisovat jedničky na nuly do té doby, než narazíme na nulu; tuto nulu přepíšeme na jedničku a skončíme, takže naše posloupnost se změní na 10010000000. Jedině v případě, kdyby posloupnost obsahovala samé jedničky, přepíšeme všechny jedničky na nuly a před ně připojíme novou jedničku. 1.4. Z popsaného postupu je vidět, že v obecném případě budeme muset projít všech O(log x) bitů. Budeme-li za jednu instrukci považovat práci s jedním bitem, ukazuje se, že čím je číslo x větší, tím více času může operace pričtení jedničky trvat. 1.5. Označíme-li n velikost vstupu, tedy počet bitů čísla x, budeme muset v nejhorším případě projít všech těchto n bitů (a pokud by všechny bity byly 1, ještě jeden bit přidat). Náš postup tedy v nejhorším případě bude vyžadovat O(n) kroků, přičemž výpočetním krokem rozumíme libovolnou z následujících operací: 1. prečtení bitu, 2. test, je-li prečtený bit roven 0 nebo 1, 3. přepsání bitu z 1 na 0 nebo z 0 na 1, 4. přechod na sousední bit (vpravo nebo vlevo). Předpokládejme následující model výpočtu: číslo x máme zapsané v paměti, která je uspořádána jako pole. V každé buňce tohoto pole je možné uložit jeden bit. Do paměti je možné přistupovat pomocí ukazatele; ten na začátku výpočtu ukazuje na buňku, kde začíná vstupní posloupnost bitů. 2
1.6. Předpokládejme, že toto pole je na obě strany potenciálně nekonečné. Předpoklad, že toto pole nemá konečný počet buněk, je nutný kvůli tomu, abychom mohli napsat program, který principiálně dokáže pracovat s libovolně velkými čísly. Kdyby toto pole bylo dlouhé jen řekněme 10, 100 (či třeba 10 10 ) buněk, dokázal by libovolný algoritmus, který zapíšeme, zpracovat jen konečně mnoho různých vstupů. My však přinejmenším při analýze složitosti algoritmů budeme vyšetřovat především asymptotické vlastnosti programů, tedy jejich chování v případě, že velikost vstupu postupně roste. 1.7. Příklad. Uvažme, že bychom mohli do paměti zapsat pouze omezeně mnoho, např. jen 10 bitů. Kdybychom na tomto počítači měli zapsat algoritmus, který rozhoduje, zdali číslo na vstupu je prvočíslo (zapsané ve dvojkovém zápisu jako posloupnost bitů), byl by takový program velice jednoduchý: function prvočíslo(x: natural number < 2 11 ) : boolean; begin if x=2 or x=3 or x=5 or x=7 or x=11 or...... or x=2029 or x=2039 then odpověz Ano else odpověz Ne; end; (Kvůli čitelnosti jsme prvočísla zapsali v desítkové soustavě.) Takový program na určování prvočíselnosti nás však patrně příliš neuspokojí. Kdybychom měli omezenou paměť, mohli bychom vždy do ní zapsat jen konečně mnoho vstupů a proto by každý problém mohl být řešen tak, že se probere těchto konečně mnoho možností. Takové programy však nic neříkají o skutečném řešení problému. Aby algoritmus nějaký problém řešil principiálně správně, musí jej řešit bez ohledu na to, jak dlouhé je vstupní zadání. Proto budeme předpokládat, že naše teoretické modely algoritmů mohou pracovat s neomezeně velkými daty. 1.8. Vraťme se k našemu problému přičítání jedničky. Pomocí našich čtyřech instrukcí (které není na tomto místě třeba přesněji formalisovat) lze napsat už téměř program, jehož každá instrukce bude typu 1., 2., 3., nebo 4. Je rozumné říci, že každá tato instrukce bude trvat jednotkový čas, přinejmenším proto, že doba na její provedení není závislá na velikosti (počtu bitů) vstupního čísla x. Abychom mohli řídit chod programu, potřebujeme ješte instrukci určující, 3
kde má program dále pokračovat (což může být instrukce typu známého GOTO). Předpokládejme, že před začátkem výpočtu je v paměti zapsáno vstupní číslo x jako posloupnost bitů (v každé paměťové buňce jeden bit) a že ukazatel do paměti ukazuje na první bit. Všechny ostatní paměťové buňky obsahují speciální symbol λ, který znamená nic. Náš program pak může pracovat takto: (1) začátek (2) čti paměťovou buňku, kam ukazuje ukazatel; (3) obsahuje-li tato buňka 1, posuň ukazatel o jednu buňku vpravo a jdi na (2); (4) obsahuje-li tato buňka 0, posuň ukazatel o jednu buňku vpravo a jdi na (2); (5) obsahuje-li tato buňka λ, posuň ukazatel o jednu buňku vlevo a jdi na (6); (6) čti paměťovou buňku, kam ukazuje ukazatel; (7) obsahuje-li tato buňka 1, přepiš ji na 0, posuň ukazatel o jednu buňku vlevo a jdi na (6) (8) obsahuje-li tato buňka 0, přepiš ji na 1, ukazatel neposouvej a jdi na (10) (9) obsahuje-li tato buňka λ, přepiš ji na 1, ukazatel neposouvej a jdi na (10) (10) konec 1.9. Náš program pracuje takto: na začátku je v paměti zapsáno vstupní číslo jako posloupnost bitů a ukazatel do paměti ukazuje na první bit. Ve všech ostatních paměťových buňkách je prázdno (λ):...λλ10001111111λλ... Instrukce (3) a (4) zajistí, že se ukazatel přesune až za konec vstupního slova:...λλ10001111111λλ... 4
Instrukce (5) posune ukazatel o jeden bit vlevo, tedy na poslední bit vstupního čísla. Všimněme si, že program se už nikdy nevrátí k instrukcím (2) až (4). Přejde do druhé části (6) až (9). Instrukce (7) zajistí, že se budou jedničky přepisovat na nuly a ukazatel se bude posouvat vlevo. Po okamžiku provedení instrukce (8) bude náš program v situaci...λλ10010000000λλ... a skončí. Instrukce (9) se provede pouze v případě, že vstupní slovo sestává ze samých jedniček v tom případě se všechny tyto jedničky instrukcí (8) přepíší na nuly a instrukce (9) před tyto nuly napíše jedničku. 2. Turingův stroj 2.1. Hned na začátku upozorněme, že Turingův stroj je teoretický model algoritmu, nejedná se o žádné reálné technické zařízení. Jedná se o přesnou matematickou formalisaci jinak vágně používaných pojmů krok výpočtu, výpočetní čas programu, paměťová náročnost programu atd. 2.2. Zůstaňme ještě chvíli u programu 1.8. Ten byl rozepsán velmi detailně: přesně jsme popsali manipulaci s každým bitem vstupního čísla. 2.3. Turingův stroj je hypotetické zařízení, které má neomezenou paměť (tzv. pásku), která je analogií našeho vstupního pole z programu 1.8. Je to oboustranně neomezená posloupnost buněk. Do buněk této pásky se zapisují symboly z dané konečné abecedy Σ; v každé buňce může být zapsán jeden symbol s Σ, nebo může být prázdná. (V příkladu 1.8. jsme pracovali s abecedou Σ = {0, 1}.) Buňky, které nejsou obsazeny, obsahují speciální symbol λ Σ ( nic, symbol pro prázdnou buňku). Říkáme, že Turingův stroj pracuje či počítá nad slovem 1 σ z abecedy Σ. To znamená: před začátkem výpočtu je na pásce zapsáno toto slovo a všechna 1 Připomeňme, že abeceda je konečná množina a slovo je konečná posloupnost symbolů z této abecedy. Je-li například abeceda Σ = {0, 1}, slovo nad touto abecedou je třeba posloupnost σ = 000110110. Množina všech slov nad danou abecedou se označuje pomocí hvězdičky, tedy v našem případě Σ. 5
ostatní pole pásky obsahují λ. Hlava Turingova stroje je analogie ukazatele do paměti z části 1.8. Je to hypotetické zařízení, které se může po pásce posouvat a vždy vidí jedinou buňku. V každém kroku výpočtu je možné číst či zapisovat symbol pouze do té buňky, kterou hlava vidí, a v každém kroku výpočtu se hlava může pohnout buď o jednu buňku vpravo, o jednu buňku vlevo anebo zůstat na místě. V příkladu 1.8. program pracoval ve dvou blocích první blok, instrukce (2) až (5), zajišťovaly přesun ze začátku na konec vstupního slova. Druhý blok, instrukce (6) až (9), zajišťovaly vlastní přičtení jedničky. Každý z bloků obsahoval tři instrukce typu: jestliže hlava čte symbol s, pak zapiš symbol na pásku (nebo nezapisuj nic), posuň hlavu vpravo nebo vlevo (nebo ji nech na místě), zůstaň ve stejném bloku anebo přejdi do jiného bloku. Například při instrukcích (3) a (4) program zůstal v prvním bloku, zatímco při instrukci (5) přešel do druhého bloku. Analogií bloků programu jsou v případě Turingových strojů tzv. stavy. Stavy představují vnitřní informaci Turingova stroje o tom, který blok programu se právě provádí. Stavy se označují symboly z nějaké konečné množiny, které se říká stavový prostor a obvykle se označuje symbolem Q. Již jsme řekli, že na začátku výpočtu je na pásce zapsáno vstupní slovo a hlava je na buňce, kde je zapsán první symbol vstupního slova. Stav, ve kterém se výpočet nachází na začátku, nazveme iniciálním stavem a označíme jej q start. Jeden ze stavů bude odpovídat situaci, kdy výpočet úspěšně skončil. To bude stav, ze kterého již výpočet nepokračuje. Nazveme jej přijímajícím stavem a označíme q fin. Instrukce turingových strojů jsou zadány pomocí tzv. přechodové funkce δ. Přechodová funkce je tabulka, která obsahuje instrukce typu Jestliže hlava je na buňce, kde se nachází symbol s Σ {λ} a stroj je ve stavu q Q \ {q fin }, pak proveď výpočetní krok: přejdi do stavu q Q, zapiš do buňky, kde je hlava, symbol s Σ {λ} a proveď 6
posun hlavy o jednu buňku vlevo (označíme ), o jednu buňku vpravo ( ) anebo hlavu neposouvej ( ). Budeme je zapisovat ve tvaru: kde x je jeden ze symbolů,,. δ(q, s) = (q, s, x), 2.4. Shrňme předchozí část. Turingův stroj je zadán pomocí následujících údajů: konečná množina Q (stavový prostor), q start Q (iniciální stav), q fin Q (přijímající stav), konečná množina Σ (abeceda), symbol λ Σ (prázdné pole), přechodová funkce δ, která dvojici (q, s), kde q Q \ {q fin } je ne-přijímající 2 stav, s Σ {λ} je symbol, který je v buňce pásky, kde je hlava, přiřazuje trojici (q, s, x), kde q Q je stav, do kterého stroj přejde, s Σ {λ} je symbol, který se zapíše do buňky pásky, kde se nachází hlava, x {,, } je určení pohybu hlavy na pásce. (Tato funkce nemusí být totální, tj. nemusí být definována pro všechny dvojice (q, s)). 2 Uvidíme, že přijímající stav je speciální stav, který indikuje konec výpočtu. Proto nepřipouštíme, aby výpočet stroje, který vstoupil do přijímajícího stavu, mohl pokračovat; proto funkce δ(q fin, ) je vždy nedefinována. 7
2.5. Šestice údajů z části 2.4. nám už úplně popisuje teoretický model algoritmu. Pamětí je páska, přístup do paměti se realisuje pomocí hlavy. V každé paměťové buňce může být jen některý z konečně mnoha symbolů z abecedy Σ, anebo může být buňka prázdná (λ). Během výpočtu se stroj může nacházet v některém z konečně mnoha stavů. Instrukce jsou zadány prostřednictvím přechodové funkce δ. 2.6. Výpočet Turingova stroje probíhá takto: na začátku výpočtu je na vstupní pásce zapsáno vstupní slovo σ = s 1 s 2 s 3... s n. Hlava je na buňce, kde je první symbol vstupního slova s 1. Stroj se nachází ve stavu q start. V tabulce přechodové funkce vyhledá instrukci (pokud existuje) δ(s 1, q start ) = (s, q, x). Na základě této instrukce přejde do stavu q ; do buňky, kde se nachází hlava, zapíše symbol s a je-li x =, posune hlavu o jednu buňku vlevo, je-li x =, posune hlavu o jednu buňku vpravo, a je-li x =, hlava se nepohne. Další kroky vypadají obdobně. Stroj je ve stavu q a čte symbol λ (pokud bylo x = ), symbol s 1 (pokud bylo x = ) anebo symbol s 2 (pokud bylo x = ). Podle toho vyhledá v tabulce instrukcí δ další instrukci a tak pokračuje ve výpočtu. 2.7. Konec výpočtu nastává tehdy, kdy hlava čte symbol s, stroj je ve stavu q a δ(s, q) není definováno (tj. v tabulce není příslušná instrukce). Pokud q = q fin, řekneme, že výpočet skončil v přijímajícím stavu. 2.8. Příklad. Vraťme se k příkladu přičítání jedničky. Sestavme Turingův stroj T, který dostane na vstup číslo zapsané v binárním zápisu a přičte k němu jedničku. Definujme stroj takto: Q = {Blok 1, Blok 2, Konec} (stavový prostor), q start = Blok 1 (iniciální stav), q fin = Konec (přijímající stav), Σ = {0, 1} (abeceda), λ (prázdné pole všimněme si, že symbol λ Σ), 8
přechodová funkce δ (tabulka instrukcí): (i) δ(blok 1, 1) = (Blok 1, 1, ) (ii) δ(blok 1, 0) = (Blok 1, 0, ) (iii) δ(blok 1, λ) = (Blok 2, λ, ) (iv) δ(blok 2, 1) = (Blok 2, 0, ) (v) δ(blok 2, 0) = (Konec, 1, ) (vi) δ(blok 2, λ) = (Konec, 1, ) Proberme výpočet stroje T, bude-li na začátku mít zapsáno na pásce slovo σ = 10001111111. Výpočet stroje T nad slovem σ označíme T (σ). (Všimněme si, že σ je slovo nad abecedou Σ = {0, 1}. Slova z jiných abeced stroji T na vstup dávat nemůžeme.) Na začátku výpočtu je hlava na buňce s první jedničkou a stroj je ve stavu q start = Blok 1. Použije se proto instrukce (i): stroj zůstane ve stavu Blok 1, do buňky zapíše jedničku (protože tam byla jednička už předtím, nic se nestane) a posune hlavu o jednu buňku vpravo. Tuto skutečnost zachytíme zápisem q start = Blok 1 1 1 Blok 1. Zápis 1 1 znamená, že v buňce byl symbol 1, stroj do buňky zapsal symbol 1 a posunul hlavu vpravo. Je to vlastně zkrácený zápis instrukce (i). Nyní hlava čte druhou buňku, kde je symbol 0; použije se proto instrukce (ii). Stroj zůstane ve stavu Blok 1, zapíše do buňky nulu (protože tam byla již předtím, nic se nestane) a posune hlavu o jednu buňku vpravo. Tento krok výpočtu zachytíme prodloužením předchozího zápisu: q start = Blok 1 1 1 Blok 1 0 0 Blok 1. Stroj je stále ve stavu Blok 1 a hlava opět čte buňku s nulou. Proto se opět použije instrukce (ii): q start = Blok 1 1 1 Blok 1 0 0 Blok 1 0 0 Blok 1. Takto se hlava pomocí instrukcí (i) a (ii) přesune až na konec slova σ: 1 1 0 0 0 0 0 0 1 1 1 1 q start = Blok 1 Blok 1 Blok 1 Blok 1 Blok 1 Blok 1 Blok 1 1 1 Blok 1 1 1 Blok 1 1 1 Blok 1 1 1 Blok 1 1 1 Blok 1. 9
Nyní je hlava na první prázdné buňce za slovem σ. Proto se použije instrukce (iii) a stroj přejde do stavu Blok 2 (pro přehlednost neuvádíme celý výpočet od začátku): λ λ Blok 2. Pak sedmkrát použije instrukci (iv), která přepíše sedm jedniček na nuly, přičemž zůstane ve stavu Blok 2 : 1 0 1 0 1 0 1 0 1 0 Blok 2 Blok 2 Blok 2 Blok 2 Blok 2 Blok 2 1 0 Blok 2 1 0 Blok 2. V této situaci máme na pásce slovo 10000000000 (tlustě vysázený symbol označuje buňku, na které je hlava). Nyní se použije instrukce (v): 0 1 Konec. Stroj přešel do přijímajícího stavu (q fin = Konec) a výpočet končí, protože přechodová funkce δ(konec, 1) není definována. Výpočet skončil v přijímajícím stavu a na pásce je dvojkový zápis vstupního čísla zvětšeného o jedničku. Pro úplnost dejme ještě jednou přehled celého výpočtu T (10001111111): 1 1 0 0 0 0 0 0 1 1 q start = Blok 1 Blok 1 Blok 1 Blok 1 Blok 1 1 1 1 1 1 1 1 1 1 1 1 1 Blok 1 Blok 1 Blok 1 Blok 1 Blok 1 Blok 1 λ λ 1 0 1 0 1 0 1 0 1 0 Blok 1 Blok 2 Blok 2 Blok 2 Blok 2 Blok 2 Blok 2 1 0 Blok 2 1 0 Blok 2 0 1 Konec = q fin. 2.9. Příklad 2.8. jsme probrali velmi detailně. Samozřejmě nebudeme při všech aplikacích popisovat Turingovy stroje vždy tak podrobně; budeme předpokládat, že se čtenář s mechanismem jejich výpočtu již dostatečně seznámil a místo příkladu 2.8. prostě napíšeme stroj, který počítá x + 1. 2.10. Stavový prostor Turingova stroje (a obecně všech automatů) lze popsat jako orientovaný graf, jehož vrcholy jsou stavy a hrany representují přechodovu funkci. Někdy se přidává speciální vrchol, který representuje fiktivní stav Start, aby bylo zřejmé, kde výpočet začíná. 10
Hrany odpovídají instrukcím přechodové funkce. Hrana A s s x B odpovídá instrukci δ(a, s) = (B, s, x). Je-li přidán speciální vrchol Start, vede z něj nepopsaná hrana do stavu q start. Turingův stroj z příkladu 2.8. bychom mohli zachytit na následujícím obrázku. 3. Výpočet Turingova stroje, akceptory, transducery 3.1. Každý okamžik výpočtu Turingova stroje můžeme popsat pomocí tzv. konfigurace. Konfigurací Turingova stroje rozumíme trojici K = (τ, q, i), kde τ je slovo zapsané na pásce 3, q je stav Turingova stroje a i je celé číslo, které udává posici hlavy Turingova stroje relativně vůči začátku vstupního slova (definujme např. i = 0, je-li hlava na poli s prvním symbolem slova τ, i = 1, je-li hlava na poli s druhým symbolem slova τ atd.) Jsou-li všechna pole pásky prázdná, definujme i = 0. 3.2. Nechť σ je vstupní slovo Turingova stroje. Iniciální konfigurací rozumíme konfiguraci (σ, q start, 0). 3 Přesněji: je to slovo zapsané na pásce bez nekonečného úseku prázdných symbolů před slovem a za slovem. Je-li tedy na pásce... λλτλλ..., kde první ani poslední symbol slova τ není λ, uvádí se v konfiguraci Turingova stroje pouze slovo τ, symboly λ nalevo a napravo od slova τ se neuvádějí. Slovo τ je slovo nad abecedou Σ {λ}; může mít např. tvar 101λλ01λ0. 11
Iniciální konfigurace popisuje, v jaké situaci se nachází Turingův stroj na samém začátku výpočtu nad slovem σ. 3.3. Jeden krok výpočtu znamená provedení jedné instrukce, tedy jedno použití přechodové funkce δ. Řekneme, že stroj přešel z konfigurace K do konfigurace K, jestliže před provedením instrukce byl v konfiguraci K a po použití instrukce ve stavu K. Řekneme, že konfigurace K je koncová, jestliže neexistuje žádná instrukce, kterou by Turingův stroj mohl použít a pokračovat ve výpočtu. To znamená: je-li s symbol, který je na poli, které čte hlava, a stroj je ve stavu q, není δ(q, s) definováno. 3.3. Konečným výpočtem Turingova stroje T nad slovem σ rozumíme konečnou posloupnost konfigurací K 0, K 1,... K m takovou, že K 0 = (σ, q start, 0) je iniciální konfigurace, pro každé i, 0 i m 1, platí, že stroj přejde (použitím jedné instrukce) z konfigurace K i do K i+1 a K m je koncová konfigurace. Číslu m se říká délka délka výpočtu nebo také časová složitost výpočtu Turingova stroje T nad slovem σ. Poznámka. Někdy se časová složitost výpočtu stroje T nad slovem σ definuje jako max(m, σ ). (Symbol značí délku (počet symbolů) slova σ.) Je to z toho důvodu, že se předpokládá, že rozumný výpočet nad slovem σ je takový, který přinejmenším vstupní slovo σ přečte. Jen přečtení slova si vyžádá výpočet délky σ. Touto definicí se tak eliminují příliš krátké výpočty. Jestliže neexistuje koncová konfigurace, řekneme, že výpočet stroje je nekonečný. To odpovídá situaci, kdy se stroj zacyklí. Tak například výpočet Turingova stroje T = (Q = {S, K}; Σ = {a, b}; q start = S; q fin = K; λ; δ(a, S) = (a, S, ), δ(λ, S) = (a, S, ), δ(b, S) = (b, K, )) nad slovem aaa bude nekonečný, zatímco výpočet nad slovem bbb bude mít délku 1. Při výpočtu nad slovem bbb bude výpočet stroje vypadat (bbb, S, 0), (bbb, K, 0), 12
zatímco při výpočtu nad slovem aaa bude stroj počítat do nekonečna: (aaa, S, 0), (aaaa, S, 0), (aaaaa, S, 0), (aaaaaa, S, 0) atd. 3.4. Příklad. Vraťme se znovu k příkladu 2.8. Výpočet stroje z tohoto příkladu nad slovem 10001111111 je posloupnost konfigurací 3.5. Nechť (10001111111, Blok 1, 0), (10001111111, Blok 1, 1), (10001111111, Blok 1, 2), (10001111111, Blok 1, 3), (10001111111, Blok 1, 10), (10001111111, Blok 1, 11), (10001111111, Blok 2, 10), (10001111110, Blok 2, 9), (10001111100, Blok 2, 8), (10001111000, Blok 2, 7), (10001110000, Blok 2, 6), (10001100000, Blok 2, 5), (10001000000, Blok 2, 4), (10000000000, Blok 2, 3), (10010000000, Konec, 3). K 0 = (τ (0), q (0), i (0) ), K 1 = (τ (1), q (1), i (1) ),..., K m = (τ (m), q (m), i (m) ) je konečný výpočet stroje T nad slovem σ. Prostorovou složitostí výpočtu T nad slovem σ je číslo max 0 j m ( τ (j) ). Je to tedy nejdelší slovo, které se během výpočtu vyskytne. Je to velikost paměti potřebná k tomu, aby výpočet mohl proběhnout. V našich příkladech byla vždy prostorová složitost rovna velikosti vstupního slova. V případě řady problémů je však třeba udělat velké mezivýpočty, které vyžadují velkou paměť. 13
3.6. V některých případech pracujeme pouze s rozhodovacími problémy. Jsou to problémy typu rozhodnout, zdali jistý objekt má jistou vlastnost, např. je-li formule tautologií, je-li graf 3-obarvitelný apod. V takových případech budeme potřebovat Turingův stroj, jehož odpovědí je pouze ano nebo ne. Takový stroj nazveme akceptorem. Řekneme, že Turingův stroj (akceptor) T přijímá slovo σ, jestliže výpočet nad slovem σ je konečný a platí, že stroj skončí v přijímajícím stavu q fin (to znamená, že jeho koncová konfigurace je (, q fin, ). Pokud je výpočet nad slovem σ konečný, avšak stroj neskončí v přijímajícím stavu, řekneme, že slovo σ odmítá. 3.7. Nechť T je Turingův stroj (akceptor). Množina L(T ) = {σ : stroj T přijímá slovo σ} se nazývá množina přijímaná strojem T nebo též jazyk přijímaný strojem T. 3.8. Příklad. Uvažme abecedu σ = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}. Popišme Turingův stroj, který dostane na vstup číslo zapsané v desítkovém zápisu a rozhodne, zdali je sudé. Platí T = (Q = {S, L, K}; Σ = {0, 1,..., 9}; q start = S; q fin = K; λ; δ(0, S) = (0, S, ), δ(1, S) = (1, S, ),..., δ(9, S) = (9, S, ), δ(λ, S) = (λ, L, ), δ(0, L) = (0, K, ), δ(2, L) = (2, K, ), δ(4, L) = (4, K, ), δ(6, L) = (6, K, ), δ(8, L) = (8, K, )). L(T ) = {σ : σ je desítkový zápis sudého čísla}. Nechť si čtenář rozebere, jak stroj T počítá. Explicitní konstrukce Turingových strojů jsou zbytečně složité a nepřehledné; obvykle však stačí jen popsat základní princip, jak stroj pracuje, a je pak jen věcí techniky jej skutečně sestrojit. Výpočet stroje T můžeme popsat slovy stroj nejprve dojede na konec slova a zkontroluje, zdali poslední symbol je 0, 2, 4, 6 nebo 8. V tom případě přijme. Jinak odmítne. Nebudeme se dále pouštět do konstrukcí Turingových strojů; bude-li však třeba, princip jejich práce popíšeme podobným způsobem, jako jsme zde popsali výpočet stroje T. 14
3.9. Akceptory jsou Turingovy stroje použitelné pouze na rozhodovací problémy, kde možnou odpovědí je jen ano nebo ne. Často však potřebujeme, aby algoritmus něco spočítal. Turingovým strojům, které něco počítají, se říká transducery v jejich případě nás nezajímá jen to, zdali skončí či neskončí v přijímajícím stavu, ale také to, co je zapsáno na pásce. Nechť Σ je abeceda. Označme Σ množinu všech slov nad abecedou Σ. Řekneme, že transducer počítá funkci f : Σ Σ, jestliže platí: f(σ) = τ, právě když výpočet stroje T nad slovem σ skončí v přijímajícím stavu a na pásce je zapsáno slovo τ. (To znamená, že koncová konfigurace je (τ, q fin, ). Pracujeme zde s funkcemi typu f : Σ Σ To jsou funkce, které jednomu slovu přiřazují jiné slovo. Mějme na paměti, že stroje pracují se slovy. Mohou samozřejmě počítat i funkce typu f : N N; jen místo abstraktního pojmu přirozené číslo je třeba pracovat s nějakou jeho representací, například s jeho binárním zápisem, což je slovo nad abecedou {0, 1}. Stroj, který pracuje s binárními zápisy čísel, počítá funkce typu f : {0, 1} {0, 1}. V mnoha případech je vhodné abecedu rozšířit. Uvažme příklad stroje, který má sečíst dvě přirozená čísla (obě zapsaná v binárním zápise). Pak je účelné pracovat s abecedou {0, 1, } a zadat stroji na pásku dvě čísla (jejichž binární zápisy jsou σ 1 a σ 2 ), která má stroj sečíst, ve formě σ 1 σ 2. Symbol zde slouží jen jako oddělovač. Formálně se sice jedná o funkci f : {0, 1, } {0, 1, }, avšak v souladu s klasickým značením budeme o této funkci (a i o stroji, který ji počítá) hovořit nemůže-li dojít k nedorozumění jako o funkci typu f : N 2 N a budeme prostě a jednoduše psát f(x 1, x 2 ) = x 1 + x 2. Transducer, který funkci f počítá, prostě nazveme jako program na sčítání přirozených čísel. Podobně budeme nakládat s dalšími obvyklými funkcemi. 15
3.10. Všimněme si, že nyní již máme přesný význam některých pojmů, které se jinak užívají ve více či méně vágním smyslu. Řekneme-li algoritmus či program, máme na mysli Turingův stroj. Řekneme-li, že program něco počítá, tak to znamená, že se jedná o transducer počítající nějakou funkci f : Σ Σ. Výpočetní čas je délka výpočtu Turingova stroje nad daným slovem; nejsou to tedy mikrosekundy či sekundy, nýbrž délka posloupnosti konfigurací, která je konečným výpočtem. Výraz program se zacyklí nebo program počítá do nekončena znamená, že výpočet stroje je nekonečný (tedy neexistuje koncová konfigurace). Hovoříme-li o náročnosti výpočtu na paměť, máme na mysli prostorovou složitost výpočtu. Řekneme-li krok výpočtu, máme na mysli jedno použití přechodové funkce δ, tedy jeden přechod z konfigurace do konfigurace následující. 4. Vícepáskové Turingovy stroje 4.1. V minulé kapitole jsme zavedli Turingův stroj jako teoretické zařízení, které má organisovánu paměť jako pásku, po které může jezdit hlava, která čte a zapisuje do buněk pásky symboly. Někdy je jednodušší pracovat s tzv. vícepáskovým Turingovým strojem. Jedná se o Turingův stroj, který má k pásek, a po každé z nich jezdí samostatná hlava. Krok výpočtu vícepáskového stroje je provedení jedné instrukce. Instrukce (přechodová funkce) takového stroje jsou typu δ(q, s 1, s 2,..., s k ) = (q, s 1, s 2,..., s k, x 1, x 2,..., x k ), kde všechna x i {,, }. Interpretace je přirozená: je-li stroj ve stavu q, hlava na první pásce čte symbol s 1, hlava na druhé pásce čte symbol s 2 atd., pak stroj přejde do stavu q, hlava na první pásce zapíše symbol s 1 a provede pohyb x 1, hrava na druhé pásce zapíše symbol s 2 a provede pohyb x 2 atd. Na začátku výpočtu je na první pásce zapsáno vstupní slovo, ostatní pásky jsou prázdné. 4.2. Konfigurace vícepáskového stroje je přímým zobecněním pojmu konfigurace jednopáskového stroje. Je to (2k + 1)-tice K = (τ 1, τ 2,..., τ k, q, i 1, i 2,..., i k ), 16
kde τ j jsou slova na páskách, q je stav stroje a i j jsou celá čísla, která udávají pozice hlav na jednotlivých páskách relativně k začátku slova τ j. 4.3. Při výpočtu k-páskového stroje nad slovem σ je iniciální konfigurace 4 K 0 = (σ, Λ, Λ,..., Λ, q start, 0, 0,..., 0). Pojmy výpočet a časová složitost výpočtu k-páskového stroje jsou přímou analogií termínů zavedených pro jednopáskové stroje. 4.4. Prostorová složitost výpočtu k-páskového stroje nad slovem σ je číslo (j) max ( τ 1 + τ (j) 2 + + τ (j) k ), 0 j m kde τ (j) i je slovo zapsané na i-té pásce v j-tém kroku výpočtu a m je délka výpočtu. Prostorová složitost je tedy největší počet popsaných buněk na páskách během výpočtu. 4.5. Je-li vícepáskový stroj transducerem, tj. počítá nějakou funkci f : Σ Σ, je třeba jednu pásku označit jako tzv. výstupní pásku. Fakt, že stroj T počítá funkci f, znamená, že f(σ) = τ, právě když stroj T skončí v přijímajícím stavu a na výstupní pásce je zapsáno slovo τ. 4.6. Příklad. Vícepáskové Turingovy stroje nám v mnohém usnadní práci. Ukažme příklad stroje, který převádí čísla z binární soustavy do unární. To je transducer, který počítá funkci f, pro kterou platí f(0) = Λ, f(1) = 1, f(10) = 11, f(11) = 111, f(100) = 1111 atd. (Všimněme si, že platí i např. f(01) = f(0000 01) = 1.) Použijeme čtyřpáskový Turingův stroj. Na první pásce bude vstupní slovo σ, čtvrtá páska bude výstupní. Abeceda bude Σ = {0, 1}. Výpočet začne v bloku Blok 1, přijímající stav označme q fin = Konec. Stroj bude pracovat takto: (Blok 1 ) Hlava na první pásce jede na konec slova σ; na ostatních páskách se nic neděje. Jakmile je hlava na posledním symbolu slova σ, na druhou pásku se napíše jedna jednička a stroj přejde do stavu Blok 2. (Blok 2 ) Jestliže hlava na první pásce čte symbol 1, zkopíruje se obsah druhé pásky na čtvrtou pásku. (To znamená: hlava na druhé pásce jede vpravo a 4 Symbol Λ označuje prázdné slovo, tedy slovo délky 0. 17
při každém kroku se na čtvrtou pásku zapíše jednička, dokud hlava na druhé pásce nenarazí na konec slova. Pak se tato hlava vrátí na začátek slova. Na ostatních páskách se nic neděje.) Jestliže hlava na první pásce čte symbol 0, nestane se nic. Přechody do dalších stavů jsou tyto: jestliže hlava na první pásce čte symbol λ, stroj přejde do stavu Konec; jinak přejde do stavu Blok 3. (Blok 3 ) V tomto a následujícím bloku se počet jedniček na druhé pásce zdvojnásobí; s první a čtvrtou páskou se neděje nic. Zdvojnásobení počtu jedniček se provede takto: hlava na druhé pásce stojí na začátku slova. Začne přecházet doprava a při každém kroku vpravo zapíše na třetí pásku jedničku. Jakmile dorazí na druhé pásce na konec slova, přejde do stavu Blok 4. (Blok 4 ) Zdvojnásobování počtu jedniček na druhé pásce pokračuje takto: na třetí pásce jede hlava vlevo, při každém kroku vlevo smaže ze třetí pásky jednu jedničku; přitom hlava na druhé pásce jede vpravo a jedničky na druhou pásku zapisuje. Tento proces skončí ve chvíli, kdy je třetí páska prázdná. Pak se přejde do stavu Blok 5. (Blok 5 ) Hlava na druhé pásce jede na začátek slova na druhé pásce. Když dojede na začátek, posune se hlava na první pásce o jeden symbol vlevo a stroj přejde do stavu Blok 2. Ukažme příklad výpočtu nad slovem s = 110101; označme jednotlivé symboly s 0 = 1, s 1 = 1, s 2 = 0, s 3 = 1, s 4 = 0 s 5 = 1. Na začátku výpočtu ve stavu Blok 1 stroj přejede slovo na první na konec. Nechť jsme v situaci, kdy stroj opouští stav Blok 1 a hlava je na poli s s 5. Obsahy pásek budou během výpočtu, vždy v okamžiku těsně předtím, než stroj ve stavu Blok 5 posune hlavu na první pásce o jeden symbol vlevo, vypadat podle následující tabulky: Hlava na první pásce je na symbolu Obsah druhé pásky Obsah výstupní pásky s 5 = 1 1 1 s 4 = 0 11 1 s 3 = 1 1111 11111 s 2 = 0 11111111 11111 s 1 = 1 1111111111111111 111111111111111111111 s 0 = 1 1111111111111 1 }{{} 32-krát 18 11111111111111111111 1 }{{} 53-krát
Skutečně, binární 110101 je 1 + 4 + 16 + 32 = 53. 4.7. Vícepáskové stroje jsou jen jistým zjednodušením vyjadřování a popisu práce Turingových strojů. Tvrzení o simulacích. Ke každému k-páskovému akceptoru existuje jednopáskový akceptor, který přijímá stejný jazyk. Ke každému k-páskovému transduceru existuje jednopáskový transducer, který počítá stejnou funkci. Naznačme, jak k danému dvoupáskovému Turingově stroji T sestavit stroj T, který počítá stejně jako T. Označme Σ abecedu stroje T. Abecedou stroje T bude Σ = Σ {s : s Σ} {λ }. Například: má-li stroj T abecedu Σ = {a, b, c}, bude stroj T mít abecedu Σ = {a, b, c, a, b, c, λ }. Stroj T pracuje takto: vstupní slovo s 1 s 2... s n stroj na pásce nejprve roztáhne tak, aby bylo s 1 λs 2 λs 3 λ... λs n 1 λs n. Na lichých buňkách jsou přesně symboly s 1 s 2... s n, sudé buňky jsou prázdné. V nich bude stroj T simulovat druhou pásku stroje T. Aby si pamatoval, kde jsou na obou páskách hlavy, označí příslušné buňky symbolem s hvězdičkou: s 1λ s 2 λs 3 λ... λs n 1 λs n Poté bude stroj T simulovat jeden krok stroje T tak, že nejprve hlava pojede po lichých buňkách (první, třetí, pátá atd.) a simuluje akci na první pásce; poté pojede po sudých buňkách a simuluje akci na druhé pásce. Hvězdičkovaným symbolem bude označovat umístění hlavy na pásce. Tuto úvahu je možné zobecnit z dvoupáskových strojů na k-páskové, anebo tuto simulaci použít opakovaně: z k-páskového stroje udělat (k 1)-páskový, (k 2)-páskový atd. 5. Složitost množin 5.1. Řekneme, že množina slov A (nad nějakou abecedou) Σ je rekursivní, jestliže existuje akceptor T takový, že 19
σ A, právě když stroj T přijímá slovo σ, a σ A, právě když stroj T odmítá slovo σ. Jinými slovy to znamená: existuje stroj T takový, který nebude nikdy počítat do nekonečna (tedy: nad každým vstupním slovem se zastaví), a přijímá právě slova z množiny A. Řekneme, že stroj T rozhoduje o náležení do množiny A. 5.2. Poznámka. Uvidíme, že mnoho množin není rekursivních; tedy, neexistují programy (Turingovy stroje), u nichž by bylo zaručeno, že se při rozhodování o náležení prvku do takové množiny vždy zastaví. V případě takových množin máme problém: spustíme-li program, který má rozhodnout, zdali σ A, nemusíme se nikdy dočkat výsledku. V tomto textu však budeme pracovat pouze s rekursivními množinami; o nerekursivních množinách pojednáme v textu Neřešitelnost a nerozhodnutelnost. 5.3. Je-li množina A rekursivní, je i její doplněk 5 co-a rekursivní. Je-li T stroj, který rozhoduje o náležení do množiny A, pak snadno sestavíme stroj T, který bude rozhodovat o náležení do množiny co-a. Stačí stroj T změnit tak, že q fin nebude přijímající stav, přidáme nový přijímající stav q fin a přidáme instrukce, které zajistí, že kdykoliv by stroj měl skončit v některém ze stavů Q \ {q fin }, přejde stroj do stavu q fin a skončí. Tím se zajistí, že každý odmítající výpočet stroje T bude přijímajícím výpočtem stroje T a naopak, každý přijímající výpočet stroje T bude odmítajícím výpočtem stroje T. 5.4. Řekneme, že stroj T, který rozhoduje o náležení do množiny A, počítá s časovou složitostí O(f(n)), jestliže pro každé slovo σ Σ platí, že délka výpočtu stroje nad slovem σ je nanejvýš f( σ ). To znamená: ať dáme na vstup stroji T libovolné slovo σ, pak délka výpočtu závisí na délce slova σ; máme však zaručeno, že tento výpočet nebude delší než f( σ ). 5.5. Řekneme, že časová složitost (rekursivní) množiny A je nanejvýš O(f(n)), jestliže existuje Turingův stroj, který rozhoduje o náležení do množiny A a počítá s časovou složitostí O(f(n)). 5 Doplněk množiny A je množina co-a = Σ \A. Je to množina všech slov nad abecedou Σ, která nepatří do A. 20
5.6. Příklad. Časová složitost množiny B = {σ {0, 1} : σ je dvojkový zápis sudého čísla} je (nanejvýš) O(n) (říkáme též, že je (nanejvýš) lineární), neboť k rozpoznání sudého čísla stačí Turingův stroj, který přejede na konec vstupního slova a ověří, že jeho poslední bit je nula. 5.7. Příklad. Časová složitost množiny C = {σ {0, 1} : σ je dvojkový zápis prvočísla} je nanejvýš O(n 2 2 n ). Mějme zadáno číslo x, o kterém máme rozhodnout, je-li prvočíslem. To můžeme prokázat tak, že sestavíme třípáskový stroj, který na druhé pásce bude postupně zapisovat všechna čísla 2, 3,..., x 1 (ve dvojkovém zápisu) a na třetí pásce vždy ověří, zdali lze zadané číslo x (zapsané na první pásce) číslem na druhé pásce beze zbytku dělit. Použijeme-li na dělení čísel algoritmus, který jsme se učili v první třídě, má tento složitost O(n 2 ). Pokud dělitele najde, odmítne; jinak přijme. Všimněme si, že je-li na vstupu zapsáno číslo x, je délka vstupního slova n log x a tedy x 2 n. Všech čísel v posloupnosti 2, 3,..., x 1 je O(x) = O(2 n ); na zápis každého z těchto čísel potřebujeme nanejvýš n bitů, avšak na dělení potřebujeme čas O(n 2 ). Dohromady tedy dostáváme O(n 2 2 n ). Nahlédněme však, že složitost množiny C je menší. Hledáme-li dělitele čísla x, stačí procházet všechna čísla 6 2, 3,..., x. Kdyby totiž existoval dělitel y čísla x takový, že y > x, musel by existovat i druhý dělitel z splňující z x (jinak by bylo yz > x). Dostáváme tak, že složitost množiny C je (nanejvýš) O(n 2 2 n ). V roce 2002 Agrawal, Kayal a Saxena publikovali algoritmus 7, který rozpoznává prvočísla v polynomiálním čase. Ukázali, že složitost množiny C je nanejvýš O(n 22 ). Dali tak odpověď na dlouho otevřený a slavný problém, který bývá formulován jako P RIMES? P. Tomuto výsledku byl dokonce věnován palcový titulek New York Times ze dne 8. srpna 2002: New Method Said to Solve Key Problem in Math. (Po- 6 značí horní celou část čísla. 7 http://www.cse.iitk.ac.in/users/manindra/primality v6.pdf 21
znamenejme, že Agrawal je jméno slavné a známé, zatímco jeho dva spoluautoři, Kayal a Saxena, byli v době publikace čerstvými držiteli bakalářského titulu informatiky.) Jejich algoritmus je poměrně jednoduchý a elegantní; vyžaduje však jisté základy z algebry, a tak jej zde uvádět nebudeme. Zajímavé na něm je to, že dokáže správně rozhodovat, zdali je dané číslo prvočíslem, avšak nedokáže nalézt žádného (netriviálního) dělitele. Proto je možné, že dva na první pohled podobné problémy, (i) rozhodnout, zdali dané číslo je prvočíslem či číslem složeným, a (ii) nalézt některého (netriviálního) dělitele, mají různou výpočetní složitost. Na prvočíselnosti je založena řada kombinatorických a kryptografických algoritmů. Například známý šifrovací algoritmus RSA je založen na předpokladu, že je-li dáno číslo x, které vzniklo vynásobením dvou prvočísel, není v rozumném čase možné z čísla x tato dvě prvočísla najít. 5.8. Prostorová složitost množiny A je přímou analogií pojmu časová složitost. Řekneme, že prostorová složitost množiny A je nanejvýš O(f(n)), jestliže existuje Turingův stroj T rozhodující o náležení do množiny A takový, že nad každým vstupním slovem použije při svém výpočtu nanejvýš f( σ ) buněk na svých páskách. * Teď už je možné navázat textem o složitosti, lze zavést obvyklé třídy P, NP a PSPACE, DEXT, EXPSPACE; s jistým úsilím i třeba PP, BPP, R, ZPP atd. Například: atd. A P právě když existuje číslo k tak, že časová složitost množiny A je nejvýše O(n k ) A NP právě když existuje množina B P taková, že platí: existuje n tak, že σ A, právě když existuje τ takové, že τ σ n a [σ, τ] B. A PSPACE právě když existuje číslo k tak, že prostorová složitost množiny A je nejvýše O(n k ) 22