From: Martin Mares Date: Mon, 30 Jan 2012 21:26:19 +0000 (+0100) Subject: KMP: Korektury, rusime konvenci o velkych pismenech, byla obludna X-Git-Url: http://mj.ucw.cz/gitweb/?a=commitdiff_plain;h=2c9fce84a8fc72bdca1ca511ff808ec406d4c803;p=ads2.git KMP: Korektury, rusime konvenci o velkych pismenech, byla obludna --- diff --git a/1-kmp/1-kmp.tex b/1-kmp/1-kmp.tex index 6aec1bb..ae58d31 100644 --- a/1-kmp/1-kmp.tex +++ b/1-kmp/1-kmp.tex @@ -4,14 +4,18 @@ \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? @@ -30,8 +34,8 @@ v~terminologii okolo \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 @@ -41,7 +45,7 @@ Abeceda m 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 @@ -50,9 +54,8 @@ ukl \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$. @@ -65,15 +68,15 @@ pr \:$\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. @@ -82,25 +85,28 @@ Na hled 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í, @@ -120,30 +126,30 @@ vlastn \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.) @@ -155,7 +161,7 @@ Jen mus 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.} @@ -170,9 +176,11 @@ doleva. Proto je v 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é, @@ -194,32 +202,32 @@ jsme pro 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 @@ -232,25 +240,23 @@ Line 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í @@ -259,10 +265,14 @@ v \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} @@ -270,13 +280,13 @@ D 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 @@ -286,29 +296,28 @@ suffixu zat 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¾ @@ -316,55 +325,55 @@ nen \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), -\:$\(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), +\:$\(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), -\:$\(I)$ -- kam z~nìj vede zkratková hrana (takté¾), -\:$\(I)$ -- zda tu konèí nìjaké slovo (a~pokud ano, tak které), -\:$\(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). +\:$\(s)$ -- kam z~nìj vede zkratková hrana (takté¾), +\:$\(s)$ -- zda tu konèí nìjaké slovo (a~pokud ano, tak které), +\:$\(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 $\(I, x) = \emptyset~\&~I \ne \$: $I \= \(I)$. -\:Pokud $\(I, x) \ne \emptyset$: $I \= \(I,x)$. -\algout Nový stav~$I$. +\algin Jsme ve~stavu~$s$, pøeèetli jsme znak~$x$. +\:Dokud $\(s, x) = \emptyset~\&~s \ne \$: $s \= \(s)$. +\:Pokud $\(s, x) \ne \emptyset$: $s \= \(s,x)$. +\algout Nový stav~$s$. \endalgo \algo{AcHledej} \cmt{Spu¹tìní automatu na daný øetìzec} \algin Seno~$\sigma$, zkonstruovaný automat. -\:$I \= \$. +\:$s \= \$. \:Pro znaky $x\in\sigma$ postupnì provádíme: -\::$I \= \alg{AcKrok}(I, x)$. -\::$K \= I$. -\::Dokud $K \neq \emptyset$: -\:::Je-li $\(K) \neq \emptyset$: -\::::Ohlásíme $\(K)$. -\:::$K \= \(K)$. +\::$s \= \alg{AcKrok}(s, x)$. +\::$j \= s$. +\::Dokud $j \neq \emptyset$: +\:::Je-li $\(j) \neq \emptyset$: +\::::Ohlásíme $\(j)$. +\:::$j \= \(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(\)$. 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 @@ -372,34 +381,36 @@ paraleln 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 $\(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 $\(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 $\$ ve~v¹ech stavech. -\:$\(R) \= \emptyset$, $\(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 $\$ ve~v¹ech stavech. +\:$\(r) \= \emptyset$, $\(r) \= \emptyset$. \:Zalo¾íme frontu $F$ a~vlo¾íme do~ní syny koøene. -\:Pro v¹echny syny~$K$ koøene: $\(K) \= R$, $\(K) \= \emptyset$. +\:Pro v¹echny syny~$s$ koøene: $\(s) \= r$, $\(s) \= \emptyset$. \:Dokud $F \neq \emptyset$: -\::Vybereme $I$ z~fronty $F$. -\::Pro v¹echny syny $K$ vrcholu $I$: -\:::$Q \= \alg{AcKrok}(\(I), \hbox{písmeno na~hranì $IK$})$. -\:::$\(K) \= Q$. -\:::Pokud $\(Q) \neq \emptyset$: $\(K) \= Q$. -\:::Jinak $\(K) \= \(Q)$. -\:::Vlo¾íme $K$ do~fronty $F$. +\::Vybereme $i$ z~fronty $F$. +\::Pro v¹echny syny $s$ vrcholu $i$: +\:::$z \= \alg{AcKrok}(\(s), \hbox{písmeno na~hranì $is$})$. +\:::$\(s) \= z$. +\:::Pokud $\(z) \neq \emptyset$: $\(s) \= z$. +\:::Jinak $\(s) \= \(z)$. +\:::Vlo¾íme $s$ do~fronty $F$. \algout Strom, pole \, \ a \. \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: @@ -418,16 +429,16 @@ P 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, $$ @@ -444,14 +455,14 @@ Cel \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