\h{Jehla v~kupce sena}
-Uva¾ujme následující problém: máme nìjaký text~$\sigma$ délky~$S$ (seno), chceme v~nìm najít
-v¹echny výskyty nìjakého podøetìzce~$\iota$ délky~$J$ (jehly). Seno pøitom bude øádovì del¹í
+Uva¾ujme následující úlohu: máme nìjaký text~$\sigma$ délky~$S$ (budeme mu øíkat {\I seno}) a
+chceme v~nìm najít v¹echny výskyty nìjakého podøetìzce~$\iota$ délky~$J$ ({\I jehly}). Seno pøitom bude øádovì del¹í
ne¾ jehla.
+Kupøíkladu v~senì |bananas| se jehla |ana| vyskytuje hned dvakrát, pøièem¾
+výskyty se pøekrývají.
+
Triviální øe¹ení pøesnì podle definice by vypadalo následovnì: Zkusíme v¹echny mo¾né pozice,
kde by se v~senì mohla jehla nacházet, a pro ka¾dou z~nich otestujeme, zda tam opravdu je.
Pozic je øádovì~$S$, ka¾dé porovnání stojí a¾~$J$, celkovì tedy algoritmus bì¾í v~èase
-$\O(SJ)$. (Rozmyslete si, jak by vypadaly vstupy, pro které skuteènì spotøebujeme tolik èasu.)
+$\O(SJ)$. (Rozmyslete si, jak by vypadaly vstupy, pro které skuteènì spotøebujeme tolik èasu
+-- viz cvièení \exref{naivejs}.)
Zkusme jiný pøístup: nalezneme v~senì první znak jehly a od tohoto místa budeme porovnávat
dal¹í znaky. Pokud se pøestanou shodovat, pøepneme opìt na hledání prvního znaku. Jen¾e odkud?
\s{Definice:}
\itemize\ibull
-\:{\I Abeceda $\Sigma$} je nìjaká koneèná mno¾ina {\I znakù,} z~nich¾ se
- skládají na¹e øetìzce.
+\:{\I Abeceda $\Sigma$} je nìjaká koneèná mno¾ina, jejím prvkùm budeme øíkat {\I znaky}
+ (nìkdy té¾ {\I písmena}).
\:{\I $\Sigma^*$} je mno¾ina v¹ech {\I slov} neboli {\I øetìzcù} nad abecedou~$\Sigma$,
co¾ jsou koneèné posloupnosti znakù ze~$\Sigma$.
\endlist
Potkáme ov¹em i rozlehlej¹í abecedy: napøíklad dnes bì¾ná znaková sada UniCode
má $2^{16}=65\,536$ znakù, v~novìj¹ích verzích dokonce $2^{31}\approx 2\cdot 10^9$ znakù. Je¹tì extrémnìj¹ím
zpùsobem pou¾ívají øetìzce lingvisté: na èeský text se nìkdy dívají jako na~øetìzec
-nad abecedou, její¾ prvky jsou èeská slova.
+nad abecedou, její¾ znaky jsou èeská slova.
Pro na¹e úèely budeme pøedpokládat, ¾e abeceda je \uv{rozumnì malá}, èím¾ myslíme, ¾e
její velikost je konstantní a navíc dostateènì malá na to, abychom si mohli dovolit
\s{Znaèení:}
\itemize\ibull
\:{\I Slova} budeme znaèit malými písmenky øecké abecedy $\alpha$, $\beta$, \dots
-\:{\I Znaky} oznaèíme malými písmeny latinky $a$, $b$, \dots{} \hfil\break
+\:{\I Znaky} abecedy oznaèíme malými písmeny latinky |a|, |b|, \dots{} \hfil\break
Znak budeme pou¾ívat i ve~smyslu jednoznakového øetìzce.
-\:{\I Èísla} budeme znaèit velkými písmeny $A$, $B$, \dots
\:{\I Délka slova} $\vert \alpha \vert$ udává, kolika znaky je slovo tvoøeno.
\:{\I Prázdné slovo} znaèíme písmenem $\varepsilon$, je to jediné slovo délky~0.
\:{\I Zøetìzení} $\alpha\beta$ vznikne zapsáním slov $\alpha$ a~$\beta$ za sebe. Platí $\vert \alpha\beta \vert=\vert \alpha \vert+\vert \beta \vert$, $\alpha\varepsilon=\varepsilon\alpha=\alpha$.
\:$\alpha[:] = \alpha$.
\endlist
-\>Dodejme je¹tì, ¾e prázdné slovo je prefixem, suffixem i~podslovem jakéhokoliv slova vèetnì sebe sama.
-Ka¾dé slovo je také prefixem, suffixem i~podslovem sebe sama. To se ne v¾dy hodí, pak budeme hovoøit
-o~{\I vlastním} prefixu, suffixu èi podslovì, èím¾ myslíme, ¾e alespoò jeden znak nebude obsahovat.
+\>Dodejme je¹tì, ¾e ka¾dé slovo je prefixem sebe sama a prázdné slovo je
+prefixem ka¾dého slova. Pokud budeme hovoøit o~{\I vlastním} suffixu, budeme
+tím myslet suffix rùzný od~celého slova. Analogicky pro prefixy a podslova.
\h{Inkrementální algoritmus}
-Vra»me se tedy zpìt k~pùvodnímu problému hledání podøetìzcù. Nejprve si
+Vra»me se nyní zpìt k~pùvodnímu problému hledání podøetìzcù. Nejprve si
ujasnìme, co má být výstupem algoritmu. Budeme chtít nalézt mno¾inu v¹ech
-indexù~$K$ takových, ¾e $\sigma[K:K+\vert\iota\vert] = \iota$. To je dostateènì
+indexù~$k$ takových, ¾e $\sigma[k:k+\vert\iota\vert] = \iota$. To je dostateènì
kompaktní výstup (nejvý¹e lineární s~délkou sena), a~pøitom obsahuje informace
o~v¹ech výskytech.
budeme pøidávat znak na konec sena a zapoèítáme pøípadný nový výskyt jehly, který
konèí tímto znakem.
-Abychom toho dosáhli, budeme si prùbì¾nì udr¾ovat informaci o~tom, jaký nejdel¹í
-prefix jehly konèí právì pøidaným znakem. Tomu budeme øíkat {\I stav algoritmu.}
-A~jakmile bude tento prefix roven celé jehle, ohlásíme výskyt.
+Abychom toho dosáhli, budeme si prùbì¾nì udr¾ovat informaci o~tom, jakým nejdel¹ím
+prefixem jehly konèí zatím pøeètená èást sena. Tomu budeme øíkat {\I stav
+algoritmu.} A~jakmile bude tento prefix roven celé jehle, ohlásíme výskyt.
+
+V~na¹em \uv{kokosovém} pøíkladì se tedy po~pøeètení sena |clanekoko| nacházíme
+ve~stavu |koko|, následují stavy |kok|, |koko| a |kokos|.
-Pøedstavme si tedy, ¾e jsme pøeèetli øetìzec~$\sigma$, který konèil stavem~$\alpha$.
+Pøedstavme si nyní obecnì, ¾e jsme pøeèetli øetìzec~$\sigma$, který konèil stavem~$\alpha$.
Teï vstup roz¹íøíme o~znak~$x$ na~$\sigma x$. V~jakém stavu se nyní máme
nacházet? Pokud to nebude prázdný øetìzec, musí konèit na~$x$, tedy ho mù¾eme
napsat ve~tvaru $\alpha'x$.
V¹imneme si, ¾e $\alpha'$ musí být suffixem slova~$\alpha$: Jeliko¾ $\alpha' x$
je prefix jehly, je $\alpha'$ také prefix jehly. A~proto¾e $\alpha'x$ je suffixem~$\sigma x$,
-musí $\alpha'$ být suffixem~$\sigma$. Tedy jak $\alpha$, tak $\alpha'$ jsou suffixy slova~$\alpha$,
+musí $\alpha'$ být suffixem~$\sigma$. Tedy jak~$\alpha$, tak $\alpha'$ jsou suffixy slova~$\alpha$,
které jsou souèasnì prefixy jehly. Ov¹em stav~$\alpha$ jsme vybrali jako nejdel¹í slovo
s~touto vlastností, tak¾e $\alpha'$~musí být nejvý¹e tak dlouhé, a~tudí¾ je prefixem~$\alpha$.
Staèilo by tedy probrat v¹echny suffixy slova~$\alpha$, které jsou prefixem jehly,
-a~vybrat z~nich nejdel¹í, který po roz¹íøení o~znak~$x$ je stále prefixem jehly.
+a~vybrat z~nich nejdel¹í, který po roz¹íøení o~znak~$x$ stále je prefixem jehly.
-Abychom ale nemuseli suffixy procházet v¹echny, pøedpoèítáme {\I zpìtnou funkci~$z$.}
+Abychom ale nemuseli suffixy procházet v¹echny, pøedpoèítáme si {\I zpìtnou funkci~$z$.}
Ta nám pro ka¾dý prefix jehly øekne, jaký je jeho nejdel¹í vlastní suffix,
který je opìt prefixem jehly. To nám umo¾ní procházet rovnou kandidáty na nový stav:
staèí probrat øetìzce $\alpha$, $z(\alpha)$, $z(z(\alpha))$, \dots{} a pou¾ít první,
\figure{barb.eps}{Vyhledávací automat pro slovo |barbarossa|}{4.1in}
Reprezentace automatu bude pøímoèará: stavy oèíslujeme od~0 do~$J$, dopøedná hrana
-povede v¾dy ze stavu~$S$ do~$S+1$ a bude odpovídat roz¹íøení prefixu o~pøíslu¹ný
-znak jehly, tedy $\iota[S]$. Zpìtné hrany si budeme pamatovat v~poli~$Z$, tedy
-$Z[S]$ bude øíkat èíslo stavu, do~nìj¾ vede zpìtná hrana ze~stavu~$S$, pøípadnì
+povede v¾dy ze stavu~$s$ do~$s+1$ a bude odpovídat roz¹íøení prefixu o~pøíslu¹ný
+znak jehly, tedy o~$\iota[s]$. Zpìtné hrany si budeme pamatovat v~poli~$Z$, tedy
+$Z[s]$ bude øíkat èíslo stavu, do~nìj¾ vede zpìtná hrana ze~stavu~$s$, pøípadnì
bude nedefinované, pokud taková hrana neexistuje.
Kdybychom takový automat mìli, mohli bychom pomocí nìj inkrementální algoritmus
z~pøedchozí sekce popsat následovnì:
\proc{KmpKrok} \cmt{Jeden krok automatu}
-\algin Jsme ve~stavu~$I$, pøeèetli jsme znak~$x$.
-\:Dokud $\iota[I] \neq x~\&~I \neq 0: I \= Z[I]$.
-\:Pokud $\iota[I] = x$, pak $I \= I + 1$.
-\algout Nový stav~$I$.
+\algin Jsme ve~stavu~$s$, pøeèetli jsme znak~$x$.
+\:Dokud $\iota[s] \neq x~\&~s \neq 0: s \= Z[s]$.
+\:Pokud $\iota[s] = x$, pak $s \= s + 1$.
+\algout Nový stav~$s$.
\endalgo
-\algo{Kmp} \cmt{Spu¹tìní automatu na øetìzec~$\sigma$.}
+\algo{KmpHledej} \cmt{Spu¹tìní automatu na øetìzec~$\sigma$.}
\algin Seno~$\sigma$, zkonstruovaný automat.
-\:$I \= 0$.
+\:$s \= 0$.
\:Pro znaky $x\in\sigma$ postupnì provádíme:
-\::$I \= \alg{KmpKrok}(I,x)$.
-\::Pokud $I = J$, ohlásíme výskyt.
+\::$s \= \alg{KmpKrok}(s,x)$.
+\::Pokud $s = J$, ohlásíme výskyt.
\endalgo
-\s{Invariant:} Stav algoritmu~$I$ v~ka¾dém okam¾iku øíká, jaký nejdel¹í
+\s{Invariant:} Stav algoritmu~$s$ v~ka¾dém okam¾iku øíká, jaký nejdel¹í
prefix jehly je suffixem zatím pøeètené èásti sena. (To u¾ víme z~úvah
o~inkrementálním algoritmu.)
algoritmus zeptá na~dopøednou hranu z~posledního stavu. Ta ale
neexistuje. Napravíme to jednodu¹e: pøidáme fiktivní dopøednou hranu,
na~ní¾ je napsán znak odli¹ný od~v¹ech skuteèných znakù. Tím zajistíme,
-¾e se po této hranì nikdy nevydáme. Staèí tedy vhodnì dodefinovat $\iota[i]$.%
+¾e se po této hranì nikdy nevydáme. Staèí tedy vhodnì dodefinovat $\iota[J]$.%
\foot{V jazyce C mù¾eme zneu¾ít toho, ¾e ka¾dý øetìzec je ukonèen znakem
s~nulovým kódem.}
jsme pro¹li dopøedných hran, tak¾e také nejvý¹e~$S$.
\qed
-\s{Konstrukce automatu:}
-Algoritmus je tedy lineární, ale potøebuje, aby mu nìkdo zkonstruoval
-automat. Dopøedné hrany vytvoøíme snadno, ale jak si poøídit ty zpìtné?
+\ss{Konstrukce automatu}
+
+Hledání tedy pracuje v~lineárním èase, zbývá domyslet, jak v~lineárním èase
+sestrojit automat. Stavy a dopøedné hrany získáme triviálnì, se zpìtnými budeme
+mít trochu práce.
Podnikneme my¹lenkový pokus: Pøedstavme si, ¾e automat u¾ máme hotový, ale nevidíme,
jak vypadá uvnitø. Chtìli bychom zjistit, jak v~nìm vedou zpìtné hrany, ov¹em jediné,
To je zajímavé pozorování, øeknete si, ale jak nám pomù¾e ke~konstrukci automatu,
kdy¾ samo u¾ hotový automat potøebuje? Pomù¾e pìkný trik: pokud hledáme zpìtnou
-hranu z~$I$-tého stavu, spou¹tíme automat na slovo délky~$I-1$, tak¾e se mù¾eme
-dostat pouze do prvních~$I-1$ stavù a vùbec nám nevadí, ¾e v~tom $I$-tém je¹tì
+hranu z~$i$-tého stavu, spou¹tíme automat na slovo délky~$i-1$, tak¾e se mù¾eme
+dostat pouze do prvních~$i-1$ stavù a vùbec nám nevadí, ¾e v~tom $i$-tém je¹tì
není zpìtná hrana hotova.%
\foot{Konstruovat nìjaký objekt pomocí tého¾ objektu je osvìdèený postup, který
si u¾ vyslou¾il i svùj vlastní název. V~angliètinì se mu øíká {\I bootstrapping}
-a z~tohoto názvu vzniklo i bootování poèítaèù, proto¾e pøi nìm operaèní systém
-vlastnì do pamìti zavádí sám sebe. Kde se toto slovo vzalo? Bootstrap znamená èesky
+a z~tohoto názvu vzniklo bootování poèítaèù, proto¾e pøi nìm operaèní systém
+zavádí do pamìti sám sebe. Kde se toto slovo vzalo? Bootstrap znamená èesky
{\I ¹truple} -- to je takové to oèko na patì boty, které usnadòuje nazouvání.
A~v~jednom z~pøíbìhù o~baronu Prá¹ilovi sly¹íme barona vyprávìt, jak se uvíznuv
v~ba¾inì zachránil tím, ¾e se vytáhl za ¹truple. Krásný popis bootování, není-li¾
pravda?}
-Konstrukce automatu tedy bude vypadat tak, ¾e nejdøíve sestrojíme pouze dopøedné
-hrany, pak rozpracovaný automat spustíme na øetìzec $\iota[1:{}]$ a podle toho,
-jakými stavy prochází, doplòujeme zpìtné hrany. A~jeliko¾ vyhledávání je lineární,
-celá konstrukce trvá $\O(J)$.
+Pøi konstrukci automatu tedy nejdøíve sestrojíme dopøedné hrany, naèe¾
+rozpracovaný automat spustíme na øetìzec $\iota[1:{}]$ a podle toho,
+jakými stavy bude procházet, doplníme zpìtné hrany. Jak u¾ víme, vyhledávání má
+lineární slo¾itost, tak¾e celá konstrukce potrvá $\O(J)$.
-Hotový algoritmus mù¾eme zapsat následovnì:
+Hotový algoritmus pro konstrukci automatu mù¾eme zapsat následovnì:
\algo{KmpKonstrukce}
\algin Jehla~$\iota$ délky~$J$.
\:$Z[0] \= ?$, $Z[1] \= 0$.
-\:$I \= 0$.
-\:Pro $K = 2, \ldots, J-1$:
-\::$I \= \alg{KmpKrok}(I, \iota[K])$.
-\::$Z[K] \= I$.
+\:$s \= 0$.
+\:Pro $i = 2, \ldots, J-1$:
+\::$s \= \alg{KmpKrok}(s, \iota[i])$.
+\::$Z[i] \= s$.
\algout Pole zpìtných hran~$Z$.
\endalgo
s~délkou sena pak potøebujeme na~samotné vyhledání.
\qed
-\h{Hledání více øetìzcù najednou: algoritmus Aho-Corasicková}
+\h{Hledání více øetìzcù najednou: algoritmus Aho-Corasicková (AC)}
-Nyní si zahrajeme tuté¾ hru, ov¹em v~trochu slo¾itìj¹ích kulisách. Tentokrát
-bude jehel vícero: $\iota_1, \ldots, \iota_N$, jejich délky oznaèíme $J_I = \vert \iota_I \vert $.
-Opìt dostaneme nìjaké seno~$\sigma$ délky $S=\vert \sigma \vert$ a chceme nalézt
+Nyní si zahrajeme tuté¾ hru v~trochu slo¾itìj¹ích kulisách. Tentokrát
+bude jehel vícero: $\iota_1, \ldots, \iota_N$, jejich délky oznaèíme $J_i = \vert \iota_i \vert $.
+Opìt dostaneme nìjaké seno~$\sigma$ délky~$S$ a chceme nalézt
v¹echny výskyty jehel v~senì.
Pøedtím, ne¾ se pustíme do~vlastního vyhledávacího algoritmu, mìli bychom si
-ujasnit, co bude jeho výstupem. Dokud byla jehla jedna jediná, bylo to jasné --
+opìt ujasnit, co bude jeho výstupem. Dokud byla jehla jedna jediná, bylo to zøejmé --
chtìli jsme nalézt mno¾inu v¹ech pozic v~senì, na~kterých zaèínaly výskyty
-jehly. Jak tomu bude zde? Budeme chtít vìdìt, která jehla se vyskytuje na které
-pozici. Jinými slovy budeme chtít vypsat v¹echny dvojice $(K,I)$ takové,
-¾e $\sigma[K:K+J_I]= \iota_I$.
-
-Zde se v¹ak skrývá jedna drobná zrada. Budeme se asi muset vzdát nadìje
-na algoritmus, jeho¾ slo¾itost je lineární v~celkové délce v¹ech jehel
-a~sena. Výstup toti¾ mù¾e být del¹í ne¾ lineární. Pokud je toti¾ jedna
-jehla suffixem druhé, na jedné pozici v~senì mohou konèit výskyty obou.
-Proto budeme hledat algoritmus, který bude lineární v~délce vstupu plus
+jehly. Jak tomu bude zde? Chceme se dozvìdìt, která jehla se vyskytuje na které
+pozici. Jinými slovy vypsat v¹echny dvojice $(k,i)$ takové, ¾e $\sigma[k:k+J_i]= \iota_i$.
+
+Tìchto dvojic mù¾e být pomìrnì hodnì. Pokud je toti¾ jedna jehla suffixem druhé,
+na jedné pozici v~senì mohou konèit výskyty obou. Celková velikost výstupu
+tak mù¾e být vìt¹í ne¾ lineární v~délce vstupu (viz cvièení~\exref{acslout}).
+Budeme proto hledat algoritmus, který bude lineární v~délce vstupu plus
délce výstupu, co¾ je evidentnì to nejlep¹í, èeho mù¾eme dosáhnout.
Algoritmus, který si nyní uká¾eme, objevili v~roce 1975 pan Aho a~paní
\s{Vyhledávací automat:}
Opìt se budeme sna¾it sestrojit vyhledávací automat, jeho¾ stavy budou
-odpovídat prefixùm jehel a dopøedné hrany roz¹iøování prefixù o~jeden znak.
-Tentokrát navíc nebude jasnì definovaný koncový stav, oznaèíme si proto
-v¹echny stavy, které odpovídají nìkteré z~jehel (na pøíkladu na obrázku
-je vidìt, ¾e to nemusí být jen listy).
+odpovídat prefixùm jehel a dopøedné hrany budou popisovat roz¹iøování
+prefixù o~jeden znak. Hrany tedy budou tvoøit strom orientovaný smìrem
+od~koøene (písmenkový strom neboli trii pro daný slovník).
+
+Ka¾dý list stromu bude odpovídat nìkteré z~jehel, ale jak je vidìt
+na obrázku, nìkteré jehly se mohou vyskytovat i ve~vnitøních vrcholech
+(pokud je jedna jehla prefixem jiné). Výskyty jehel ve~stromu si tedy
+nìjak oznaèíme, pøíslu¹ným stavùm budeme øíkat {\I koncové.}
\figure{ara_strom_zkr.eps}{Vyhledávací automat pro slova |ara|, |bar|, |arab|, |baraba| a |barbara|.}{1.25in}
Jejich definice bude úplnì stejná jako u~automatu KMP.
Z~ka¾dého stavu pùjde zpìtná hrana do~jeho nejdel¹ího vlastního suffixu, který je také
stavem. Èili se budeme sna¾it jméno stavu zkracovat zleva tak dlouho, ne¾ dostaneme
-opìt jméno stavu. Z~koøene -- prázdného stavu -- pak evidentnì ¾ádná zpìtná hrana nepovede.
+jméno dal¹ího stavu. Z~koøene -- prázdného stavu -- pak evidentnì ¾ádná zpìtná hrana nepovede.
Funkce pro hledání v~senì bude vypadat stejnì jako u~KMP: zaène v~poèáteèním
-stavu (to je koøen stromu), postupnì bude roz¹iøovat seno o~dal¹í písmenka,
-poka¾dé zkusí jít dopøednou hranou a pokud to nejde, bude se vracet po~zpìtných
-hranách tak dlouho, a¾ buïto bude existovat vhodná dopøedná hrana, nebo se vrátí
-do koøene stromu a tehdy nový znak zahodí.
+stavu (to je koøen stromu) a postupnì bude roz¹iøovat seno o~dal¹í písmenka.
+Poka¾dé zkusí jít dopøednou hranou a pokud to nepùjde, bude se vracet po~zpìtných
+hranách. Pøitom se buïto dostane do vrcholu, kde vhodná dopøedná hrana existuje,
+nebo se vrátí a¾ do koøene stromu a tehdy nový znak zahodí.
Stejnì jako u~KMP nahlédneme, ¾e procházení sena trvá $\O(S)$ a ¾e platí analogický
invariant, toti¾ ¾e se v~ka¾dém okam¾iku nacházíme ve~stavu, který odpovídá nejdel¹ímu
Jediné, co se bude od KMP li¹it, je, kdy ohlásit výskyt. U~KMP to bylo snadné: kdykoliv
jsme dospìli do posledního stavu, znamenalo to nalezení jehly. Nabízí se hlásit
výskyt, kdykoliv dojdeme do stavu oznaèeného jako koncový. To ale nefunguje:
-pokud ná¹ ukázkový automat pøeète seno |bara|, skonèí ve stavu~|bara|, a~pøitom
-by mìl ohlásit výskyt jehly |ara|. Stejnì tak pøeèteme-li |barbara|, nev¹imneme
-si, ¾e na tém¾e místì konèí i |ara|.
+pokud ná¹ ukázkový automat pøeète seno |bara|, skonèí ve stavu~|bara|, který není
+koncový, a~pøitom by zde mìl ohlásit výskyt jehly |ara|. Stejnì tak pøeèteme-li |barbara|,
+nev¹imneme si, ¾e na tém¾e místì konèí i |ara|.
Platí ale, ¾e v¹echna slova, která bychom mìli v~daném stavu ohlásit, jsou suffixy
-jména tohoto stavu. Mohli bychom je tedy najít tak, ¾e se vydáme po zpìtných hranách
-a¾ do koøene a kdykoliv projdeme pøes koncový vrchol, ohlásíme výskyt. To ov¹em
+jména tohoto stavu. Mohli bychom se tedy vydat po zpìtných hranách
+a¾ do koøene a kdykoliv projdeme pøes koncový vrchol, ohlásit výskyt. To ov¹em
trvá pøíli¹ dlouho -- jistì by se stávalo, ¾e bychom podnikli dlouhou cestu do koøene
-a nena¹li na ní ani jeden výskyt.
+a nena¹li na ní nic.
-Dal¹í, co se nabízí, je pøedpoèítat si pro ka¾dý stav~$\beta$ mno¾inu slov ${\cal S}(\beta)$,
+Dal¹í, co se nabízí, je pøedpoèítat si pro ka¾dý stav~$\beta$ mno¾inu slov $M(\beta)$,
jejich¾ výskyty máme v~tomto stavu hlásit. To by fungovalo, ale existují mno¾iny jehel,
-pro které bude celková velikost mno¾in ${\cal S}(\beta)$ superlineární. Museli bychom se
-tedy vzdát lákavé mo¾nosti stavby automatu v~lineárním èase. (Rozmyslete si, jak by
-takové jehly vypadaly.)
+pro které bude celková velikost mno¾in $M(\beta)$ superlineární (viz cvièení~\exref{acslm}).
+Museli bychom se tedy vzdát lákavé mo¾nosti stavby automatu v~lineárním èase.
-Jak to tedy vyøe¹íme? Zavedeme zkratky (na obrázku zelenì):
+Jak to tedy vyøe¹íme? Zavedeme zkratky (na obrázku vyznaèeny zelenì):
\s{Definice:}
-{\I Zkratková hrana} ze~stavu~$\alpha$ vede do nejbli¾¹ího stavu $\zeta(\alpha)$ dosa¾itelného
-z~$\alpha$ po zpìtných hranách, který je koncový.
+{\I Zkratková hrana} ze~stavu~$\alpha$ vede do nejbli¾¹ího koncového stavu $\zeta(\alpha)$
+dosa¾itelného z~$\alpha$ po zpìtných hranách.
-Jinými slovy, $\zeta(\alpha)$ nám øekne, jaký je nejdel¹í vlastní suffix slova~$\alpha$, který
+Jinými slovy, zkratka $\zeta(\alpha)$ nám øekne, jaký je nejdel¹í vlastní suffix slova~$\alpha$, který
je jehlou. Pokud takový suffix neexistuje, ¾ádná zkratková hrana ze~stavu~$\alpha$ nepovede.
Pomocí zkratkových hran mù¾eme snadno vyjmenovat v¹echny výskyty. Budeme postupovat stejnì,
jako bychom procházeli po v¹ech zpìtných hranách, jen budeme dlouhé úseky zpìtných hran, na~nich¾
\s{Reprezentace automatu:}
Vyhledávací automat se tedy sestává ze stromu dopøedných hran, ze zpìtných
-hran a ze~zkratkových hran. Ne¾ vyslovíme samotný algoritmus AC, rozmysleme si, jak automat
-ulo¾it do pamìti. Pro ka¾dý stav si budeme pamatovat:
+hran a ze~zkratkových hran. Rozmysleme si, jak v¹e ulo¾it do pamìti.
+Pro ka¾dý stav si budeme pamatovat:
\itemize\ibull
-\:$I$ -- poøadové èíslo stavu (tøeba v~poøadí, jak vrcholy vznikaly),
-\:$\<Zpìt>(I)$ -- kam z~nìj vede zpìtná hrana (vyu¾íváme toho, ze mù¾e být nejvý¹e jedna, tak¾e si zapamatujeme
+\:$s$ -- poøadové èíslo stavu (tøeba podle toho, jak vrcholy vznikaly),
+\:$\<Zpìt>(s)$ -- kam z~nìj vede zpìtná hrana (vyu¾íváme toho, ze mù¾e být nejvý¹e jedna, tak¾e si zapamatujeme
èíslo stavu, do nìj¾ vede),
-\:$\<Zkratka>(I)$ -- kam z~nìj vede zkratková hrana (takté¾),
-\:$\<Slovo>(I)$ -- zda tu konèí nìjaké slovo (a~pokud ano, tak které),
-\:$\<Dopøedu>(I,x)$ -- kam vede dopøedná hrana oznaèená písmenem~$x$ (pro malé abecedy si to
- mù¾eme pamatovat v~poli, pro velké tøeba v~he¹ovací tabulce nebo stromu).
+\:$\<Zkratka>(s)$ -- kam z~nìj vede zkratková hrana (takté¾),
+\:$\<Slovo>(s)$ -- zda tu konèí nìjaké slovo (a~pokud ano, tak které),
+\:$\<Dopøedu>(s,x)$ -- kam vede dopøedná hrana oznaèená písmenem~$x$ (pro malé abecedy si to
+ mù¾eme pamatovat v~poli, pro velké viz cvièení~\exref{bigsigma}).
\endlist
\>Celý algoritmus pro zpracování sena automatem pak bude vypadat takto:
\proc{AcKrok} \cmt{Jeden krok automatu}
-\algin Jsme ve~stavu~$I$, pøeèetli jsme znak~$x$.
-\:Dokud $\<Dopøedu>(I, x) = \emptyset~\&~I \ne \<koøen>$: $I \= \<Zpìt>(I)$.
-\:Pokud $\<Dopøedu>(I, x) \ne \emptyset$: $I \= \<Dopøedu>(I,x)$.
-\algout Nový stav~$I$.
+\algin Jsme ve~stavu~$s$, pøeèetli jsme znak~$x$.
+\:Dokud $\<Dopøedu>(s, x) = \emptyset~\&~s \ne \<koøen>$: $s \= \<Zpìt>(s)$.
+\:Pokud $\<Dopøedu>(s, x) \ne \emptyset$: $s \= \<Dopøedu>(s,x)$.
+\algout Nový stav~$s$.
\endalgo
\algo{AcHledej} \cmt{Spu¹tìní automatu na daný øetìzec}
\algin Seno~$\sigma$, zkonstruovaný automat.
-\:$I \= \<koøen>$.
+\:$s \= \<koøen>$.
\:Pro znaky $x\in\sigma$ postupnì provádíme:
-\::$I \= \alg{AcKrok}(I, x)$.
-\::$K \= I$.
-\::Dokud $K \neq \emptyset$:
-\:::Je-li $\<Slovo>(K) \neq \emptyset$:
-\::::Ohlásíme $\<Slovo>(K)$.
-\:::$K \= \<Zkratka>(K)$.
+\::$s \= \alg{AcKrok}(s, x)$.
+\::$j \= s$.
+\::Dokud $j \neq \emptyset$:
+\:::Je-li $\<Slovo>(j) \neq \emptyset$:
+\::::Ohlásíme $\<Slovo>(j)$.
+\:::$j \= \<Zkratka>(j)$.
\endalgo
-\>Jak u¾ jsme nahlédli, v¹echny kroky automatu dohromady trvají $\O(S)$.
+\>Stejným argumentem jako u~KMP zdùvodníme, ¾e v¹echny kroky automatu dohromady trvají $\O(S)$.
Mimo to je¹tì hlásíme výskyty, co¾ trvá $\O(\<poèet výskytù>)$. Zbývá
ukázat, jak automat sestrojit.
\s{Konstrukce automatu:}
-Stejnì jako u~KMP, i zde nahlédneme, ¾e zpìtná hrana ze stavu $\beta$ vede tam,
-kam by se dostal automat pøi hledání slova $\beta$ bez prvního znaku. Opìt bychom
-tedy chtìli zaèít sestrojením dopøedných hran a pak spu¹tìním je¹tì nehotového
-automatu doplòovat hrany zpìtné, doufajíce, ¾e si vystaèíme s~u¾ sestrojenou
+Opìt se inspirujeme algoritmem KMP a nahlédneme, ¾e zpìtná hrana ze~stavu~$\beta$ vede tam,
+kam by se automat dostal pøi hledání slova~$\beta$ bez prvního znaku. Chtìli bychom
+tedy zaèít sestrojením dopøedných hran a pak spou¹tìním je¹tì nehotového automatu
+na jednotlivé jehly doplòovat zpìtné hrany, doufajíce, ¾e si vystaèíme s~u¾ sestrojenou
èástí automatu.
-Pøi konstrukci zpìtných hran tedy chceme automatem vyhledávat v¹echny jehly
-bez prvních znakù. To v¹ak nemù¾eme dìlat jednu jehlu po druhé, proto¾e zpìtné
-hrany mohou vést køí¾em mezi jednotlivými vìtvemi stromu. Mohlo by se nám tedy
-stát, ¾e pøi hledání nìjakého slova potøebujeme zpìtnou hranu, která vede
-do~jiného slova, které jsme je¹tì nezkonstruovali. Proto tento postup sel¾e.
+Kdybychom v¹ak automat spou¹tìli na jednu jehlu po~druhé (poka¾dé bez prvního
+znaku), dostali bychom se do úzkých, proto¾e zpìtné hrany mohou vést køí¾em mezi
+jednotlivými vìtvemi stromu. Mohlo by se nám tedy stát, ¾e pøi hledání nìjakého
+slova potøebujeme zpìtnou hranu, která vede do~jiného slova, které jsme je¹tì
+nezkonstruovali. Proto tento postup sel¾e.
Mù¾eme v¹ak vyu¾ít toho, ¾e ka¾dá zpìtná hrana vede ve~stromu alespoò o~jednu
hladinu vý¹, a strom konstruovat po~hladinách. To si lze pøedstavit tak, ¾e
jeden krok ka¾dého z~tìchto hledání, co¾ nám dá zpìtné hrany v~dal¹ím patøe stromu.
Kdykoliv vytvoøíme zpìtnou hranu, sestrojíme také zkratkovou hranu z~tého¾
-vrcholu. Pokud vede zpìtná hrana z~$K$ do~$Q$ a $\<Slovo>(Q) \ne \emptyset$,
-musí vést zkratka z~$K$ také do~$Q$. Pokud v~$Q$ ¾ádné slovo neskonèí, musí
-zkratka z~$K$ vést do tého¾ vrcholu, jako zkratka z~$Q$.
+vrcholu. Pokud vede zpìtná hrana ze stavu~$s$ do stavu~$z$ a $\<Slovo>(z)$ je definováno,
+musí vést zkratka z~$s$ také do~$z$. Pokud v~$z$ ¾ádné slovo nekonèí, musí
+zkratka z~$s$ vést do tého¾ vrcholu, kam vede zkratka ze~$z$.
\algo{AcKonstrukce}
-\algin Slova $\iota_1,\ldots,\iota_N$.
-\:Zalo¾íme prázdný strom, $R \=$ jeho koøen.
-\:Vlo¾íme do~stromu slova $\iota_1 \dots \iota_N$, nastavíme $\<Slovo>$ ve~v¹ech stavech.
-\:$\<Zpìt>(R) \= \emptyset$, $\<Zkratka>(R) \= \emptyset$.
+\algin Slova $\iota_1,\ldots,\iota_n$.
+\:Zalo¾íme strom, který obsahuje pouze koøen~$r$.
+\:Vlo¾íme do~stromu slova $\iota_1 \dots \iota_n$, nastavíme $\<Slovo>$ ve~v¹ech stavech.
+\:$\<Zpìt>(r) \= \emptyset$, $\<Zkratka>(r) \= \emptyset$.
\:Zalo¾íme frontu $F$ a~vlo¾íme do~ní syny koøene.
-\:Pro v¹echny syny~$K$ koøene: $\<Zpìt>(K) \= R$, $\<Zkratka>(K) \= \emptyset$.
+\:Pro v¹echny syny~$s$ koøene: $\<Zpìt>(s) \= r$, $\<Zkratka>(s) \= \emptyset$.
\:Dokud $F \neq \emptyset$:
-\::Vybereme $I$ z~fronty $F$.
-\::Pro v¹echny syny $K$ vrcholu $I$:
-\:::$Q \= \alg{AcKrok}(\<Zpìt>(I), \hbox{písmeno na~hranì $IK$})$.
-\:::$\<Zpìt>(K) \= Q$.
-\:::Pokud $\<Slovo>(Q) \neq \emptyset$: $\<Zkratka>(K) \= Q$.
-\:::Jinak $\<Zkratka>(K) \= \<Zkratka>(Q)$.
-\:::Vlo¾íme $K$ do~fronty $F$.
+\::Vybereme $i$ z~fronty $F$.
+\::Pro v¹echny syny $s$ vrcholu $i$:
+\:::$z \= \alg{AcKrok}(\<Zpìt>(s), \hbox{písmeno na~hranì $is$})$.
+\:::$\<Zpìt>(s) \= z$.
+\:::Pokud $\<Slovo>(z) \neq \emptyset$: $\<Zkratka>(s) \= z$.
+\:::Jinak $\<Zkratka>(s) \= \<Zkratka>(z)$.
+\:::Vlo¾íme $s$ do~fronty $F$.
\algout Strom, pole \<Slovo>, \<Zpìt> a \<Zkratka>.
\endalgo
-Jeliko¾ tento algoritmus pouze napøeskáèku hledá v¹echny jehly, musí být jeho
-èasová slo¾itost shora omezena souètem slo¾itostí hledání jehel, co¾, jak u¾
-víme, je pro ka¾dou jehlu lineární s~její délkou. Dokonce mù¾e dobìhnout i
-rychleji, jeliko¾ jsou-li dva ze simulovaných výpoètù v~tém¾e stavu, poèítáme
-krok automatu pouze jednou (to je vidìt tøeba na spoleèném zaèátku slov |araba|
-a |arbara|).
+Pro rozbor èasové slo¾itosti si uvìdomíme, ¾e konstrukce zpìtných hran
+hledá v¹echny jehly, jen kroky jednotlivých hledání vhodným zpùsobem støídá
+(jakoby je provádìla paralelnì).
+Její èasovou slo¾itost tedy mù¾eme shora omezit souètem slo¾itostí hledání
+jehel, co¾, jak u¾ víme, je lineární v~délce jehel.
+Konstrukce dokonce mù¾e dobìhnout i rychleji, jsou-li toti¾ dva z~provádìných
+výpoètù v~tém¾e stavu, poèítáme krok automatu pouze jednou. To je vidìt tøeba
+na spoleèném zaèátku slov |araba| a |arbara|.
Chování celého algoritmu shrneme do následující vìty:
he¹ovací funkci~$H$, které $J$-ticím znakù pøiøazuje èísla z~mno¾iny $\{0,\ldots,N-1\}$
pro nìjaké dost velké~$N$. Budeme posouvat okénko délky~$J$ po~senì, pro ka¾dou
jeho polohu si spoèteme he¹ znakù uvnitø okénka, porovnáme s~he¹em jehly a pokud
-se rovnají, porovnáme je znak po~znaku.
+se rovnají, porovnáme okénko s~jehlou znak po~znaku.
Pokud je he¹ovací funkce \uv{kvalitní}, málokdy se stane, ¾e by se he¹e rovnaly,
tak¾e místo èasu $\Theta(J)$ na porovnáváním øetìzcù si vystaèíme s~porovnáním
he¹ù v~konstantním èase. Jen¾e ouha, èas $\Theta(J)$ potøebujeme i na vypoètení
-he¹e pro ka¾dou polohu okénka, tak¾e jsme si nepomohli.
+he¹e pro ka¾dou polohu okénka. Jak z~toho ven?
-Poøídíme si proto he¹ovací funkci, kterou lze pøi posunutí okénka o~jednu
+Poøídíme si he¹ovací funkci, kterou lze pøi posunutí okénka o~jednu
pozici doprava v~konstantním èase pøepoèítat. Tyto po¾adavky splòuje tøeba
-polynom
+funkce
$$
H(x_1,\ldots,x_J) = (x_1P^{J-1} + x_2P^{J-2} + \ldots + x_{J-1}P^1 + x_JP^0) \bmod N,
$$
\algo{RabinKarp}
\algin Jehla $\iota$ délky~$J$, seno~$\sigma$ délky~$S$.
-\:$X \= H(\iota)$. \cmt{he¹ jehly}
-\:$Y \= H(\sigma[{}:J])$. \cmt{he¹ první pozice okénka}
+\:$j \= H(\iota)$. \cmt{he¹ jehly}
+\:$h \= H(\sigma[{}:J])$. \cmt{he¹ první pozice okénka}
\:Zvolíme~$P$ a~$N$ a pøedpoèítáme $P^J \bmod N$.
-\:Pro $I$ od~$0$ do~$S-J$: \cmt{mo¾né pozice okénka}
-\::Je-li $X=Y$:
-\:::Pokud $\sigma[I:I+J] = \iota$, ohlásíme výskyt na pozici~$I$.
-\::Pokud $I<S-J$: \cmt{pøepoèítáme he¹}
-\:::$Y \= (P\cdot Y - \sigma[I]\cdot P^J + \sigma[I+J]) \bmod N$.
+\:Pro $i$ od~$0$ do~$S-J$: \cmt{mo¾né pozice okénka}
+\::Je-li $h=j$:
+\:::Pokud $\sigma[i:i+J] = \iota$, ohlásíme výskyt na pozici~$i$.
+\::Pokud $i<S-J$: \cmt{pøepoèítáme he¹}
+\:::$h \= (P\cdot h - \sigma[i]\cdot P^J + \sigma[i+J]) \bmod N$.
\endalgo
\s{Analýza:}
porovnává øetìzce, má èasovou slo¾itost $\O(JS)$. Mù¾e být opravdu tak pomalý,
uvá¾íme-li, ¾e porovnávání øetìzcù skonèí, jakmile najde první neshodu? Sestrojte
vstup, na kterém algoritmus pobì¾í $\Theta(JS)$ krokù, pøesto¾e nic nenajde.}
+\exid{naivejs}
\ex{Naleznìte pøíklad jehel a sena, v~nìm¾ se nachází superlineární poèet
výskytù vzhledem k~celkové velikosti vstupu (souèet velikostí jehel a délky
sena).}
+\exid{acslout}
\ex{Uva¾ujme zjednodu¹ený algoritmus AC, který nepou¾ívá zkratkové hrany
a v¾dy projde po zpìtných hranách a¾ do koøene. Uka¾te pøíklad vstupu,
na~kterém je tento algoritmus asymptoticky pomalej¹í.}
-\ex{Oznaème $M(S)$ mno¾inu øetìzcù, jejich¾ výskyty algoritmus AC ohlásí
-ve~stavu~$S$. Uka¾te, ¾e kdybychom si mìli v¹echny mno¾iny $M(S)$ pamatovat
-explicitnì, budeme pro nìkteré vstupy potøebovat superlineární mno¾ství
+\ex{Oznaème $M(s)$ mno¾inu øetìzcù, jejich¾ výskyty algoritmus AC ohlásí
+ve~stavu~$s$. Uka¾te, ¾e kdybychom si mìli v¹echny mno¾iny $M(s)$ pamatovat
+explicitnì, budeme pro nìkteré sady jehel potøebovat superlineární mno¾ství
prostoru vzhledem k~velikosti vstupu, tak¾e automat nepùjde sestrojit
v~lineárním èase.}
+\exid{acslm}
\ex{Rozmyslete si, ¾e mno¾iny $M(S)$ z~pøedchozího pøíkladu by bylo mo¾né
reprezentovat jako srùstající spojové seznamy -- tedy takové, kde si ka¾dý
prvek pamatuje ukazatel na svého následníka, který ov¹em mù¾e le¾et v~jiném
seznamu. Uka¾te, ¾e námi zavedené zkratkové hrany lze interpretovat jako
-v~takových seznamech.}
+ukazatele v~takových seznamech.}
\ex{Rozmyslete si, jak algoritmy z~této kapitoly upravit, aby si poradily
s~velkými abecedami.}
+\exid{bigsigma}
\ex{Upravte algoritmus AC, aby pro ka¾dou pozici v~senì vypsal nejdel¹í
tam konèící jehlu, a~to efektivnìji ne¾ vyjmenováním v¹ech jehel. Jak se
Jak o~dvou øetìzcích zjistit, zda je jeden rotací druhého?}
\ex{Jak v~lineárním èase zrotovat øetìzec, dostaèuje-li pamì» poèítaèe jen
-na ulo¾ení jedhoho øetìzce a $\O(1)$ pomocných promìnných?}
+na ulo¾ení jednoho øetìzce a $\O(1)$ pomocných promìnných?}
-\ex{Navrhnìte algoritmus, který v~lineárním èase nalezne tu z~rotací zadaného
+\exx{Navrhnìte algoritmus, který v~lineárním èase nalezne tu z~rotací zadaného
øetìzce, je¾ je lexikograficky minimální.}
\ex{Navrhnìte datovou strukturu pro básníky, která si bude pamatovat slovník
a bude umìt hledat rýmy. Tedy pro libovolné zadané slovo najde takové slovo
ve~slovníku, které má se zadaným nejdel¹í spoleèný suffix.}
-\exx{Upravte datovou strukturu z~pøedchozího cvièení, aby je-li nejlep¹ích rýmù
-více, vypsala lexikograficky nejmen¹í z~nich.}
+\exx{Upravte datovou strukturu z~pøedchozího cvièení, aby v~pøípadì, ¾e nejlep¹ích rýmù
+je více, vypsala lexikograficky nejmen¹í z~nich.}
\ex{Jak reprezentovat slovník, abyste umìli rychle vyhledávat v¹echny
pøesmyèky zadaného slova?}
podøetìzec.}
\ex{Definujme Fibonacciho slova takto: $F_0=|a|$, $F_1=|b|$, $F_{n+2}=F_nF_{n+1}$.
-Jak pro zadaný øetìzec nad abecedou $\{|a|,|b|\}$ najít nejdel¹í Fibonacciho podslovo?
+Jak v~zadaném øetìzci nad abecedou $\{|a|,|b|\}$ najít nejdel¹í Fibonacciho podslovo?
}
-\exx{Pokraèujme v~pøedchozím cvièení. Dostaneme-li øetìzec nad nìjakou obecnou abecedou,
-jak nalézt jeho nejdel¹í podøetìzec, který je isomorfní s~nìjakým Fibonacciho slovem
-(li¹í se pouze substitucí jiných znakù za |a| a~|b|)?}
+\exx{Pokraèujme v~pøedchozím cvièení. Dostaneme øetìzec nad nìjakou obecnou abecedou,
+chceme nalézt jeho nejdel¹í podøetìzec, který je isomorfní s~nìjakým Fibonacciho slovem
+(li¹í se pouze substitucí jiných znakù za |a| a~|b|).}
\ex{Je dáno slovo. Chceme nalézt jeho nejdel¹í prefix, který je souèasnì suffixem.}