]> mj.ucw.cz Git - ads2.git/commitdiff
KMP: Prepis casti o KMP, zatim nekompletni
authorMartin Mares <mj@ucw.cz>
Mon, 9 Jan 2012 23:19:06 +0000 (00:19 +0100)
committerMartin Mares <mj@ucw.cz>
Mon, 9 Jan 2012 23:19:06 +0000 (00:19 +0100)
1-kmp/1-kmp.tex

index 3aea2ea2960b1f1e1e3acdddb5c203c90368c05e..a90d6437bc12a05ccb624fcf5ba549d19bce1e51 100644 (file)
@@ -38,9 +38,9 @@ v~terminologii okolo 
 
 \s{Pøíklady:}
 Abeceda mù¾e být tvoøena tøeba písmeny |a| a¾~|z| nebo bity |0| a~|1|.
-Potkáme ale i rozlehlej¹í abecedy, napøíklad dnes bì¾ná znaková sada UniCode
-má $2^{16}$ znakù, v~novìj¹ích verzích dokonce $2^{31}$ znakù. Je¹tì extrémnìj¹ím
-zpùsobem pou¾ívají øetìzce lingvisté: na èeský text se nìkdy dívají jako na~slovo
+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.
 
 Pro na¹e úèely budeme pøedpokládat, ¾e abeceda je \uv{rozumnì malá}, èím¾ myslíme, ¾e
@@ -69,63 +69,95 @@ pr
 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.
 
+\h{Inkrementální algoritmus}
 
-\h{XXX}
+Vra»me se tedy 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ì
+kompaktní výstup (nejvý¹e lineární s~délkou sena), a~pøitom obsahuje informace
+o~v¹ech výskytech.
 
+Na hledání podøetìzce pou¾ijeme {\I inkrementální pøístup.} Tím se obecnì myslí,
+¾e chceme umìt roz¹íøit vstup o~dal¹í znak a pøepoèítat výstup. V~na¹em pøípadì
+budeme pøidávat znak na konec sena a zapoèítáme pøípadný nový výskyt jehly, který
+konèí tímto znakem.
 
-\s{Pøíklad:} Vezmìme si napøíklad staré italské pøízvisko |barbarossa|, které znamená Rudovous. Pøedstavme si, ¾e takovéto slovo hledáme v~nìjakém textu, který zaèíná |barbar|. Víme, ¾e a¾ sem se nám hledaný øetìzec shodoval. Øeknìme, ¾e dal¹í písmenko textu se shodovat pøestane -- místo |o| naèteme napøíklad opìt |b|. {\I Hloupý algoritmus} by velil vrátit se k~|a| a~od~nìj hledat dál. Uvìdomme si ale, ¾e kdy¾ se vracíme z~|barbar| do~|arbar| (tedy øetìzce, který ji¾ známe), mù¾eme si pøedpoèítat, jak dopadne hledání, kdy¾ ho pustíme na~nìj. V~pøedpoèítaném bychom tedy chtìli ukládat, ¾e kdy¾ máme øetìzec |arbar|, tak |ar| a~|r| nám do~hledaného nepasuje a~a¾~|bar| se bude shodovat. Tedy místo toho, abychom spustili nové hledání od~|a|, mù¾eme ho spustit a¾~od~|b|. Co víc, my dokonce víme, jak dopadne to -- pokud toti¾ nastane neshoda po~pøeètení |barbar|, je to stejné, jako kdybychom pøeèetli pouze |bar|, na~které se (pùvodne neshodující se) |b| u¾ navázat dá. Kdyby se nedalo navázat ani tam, tak bychom opìt zkracovali... Nejen, ¾e tedy víme, kam se máme vrátit, ale víme dokonce i~to, co tam najdeme. 
+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.
 
-My¹lenka, ke které míøíme, je pøedpoèítat si nìjakou tabulku, která nám bude øíkat, jak se máme pøi hledání vracet a~jak to dopadne, a~pak u¾ jenom prohlédávat s~pou¾itím této tabulky. 
+Pøedstavme si tedy, ¾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$.
 
-Aby se nám o~tìchto algoritmech lépe mluvilo a~pøedev¹ím psalo, pojïme si povìdìt nìkolik definic.
-\h{Vyhledávací automat (Knuth, Morris, Pratt)}
-{\I Vyhledávací automat} bude graf, jeho¾ vrcholùm budeme øíkat {\I stavy}. Jejich jména budou prefixy hledaného slova a~hrany budou odpovídat tomu, jak jeden prefix mù¾eme získat z~pøedchozího prefixu pøidáním jednoho písmene. Poèáteèní stav je prázdné slovo $\varepsilon$ a~koncový je celá $\iota$. Dopøedné hrany grafu budou popisovat pøechod mezi stavy ve~smyslu zvìt¹ení délky jména stavu (dopøedná funkce $h(\alpha)$, urèující znak na~dopøedné hranì z~$\alpha$). Zpìtné hrany grafu budou popisovat pøechod (zpìtná funkce $z(\alpha)$) mezi stavem $\alpha$ a~nejdel¹ím vlastním suffixem $\alpha$, který je prefixem $\iota$, kdy¾ nastane neshoda.
-
-\figure{barb.eps}{Vyhledávací automat.}{4.1in}
-
-\s{Hledej($\sigma$):}
-\algo
-\:$\alpha \leftarrow \varepsilon$.
-\:Pro $x\in\sigma$ postupnì:
-\:$\indent$Dokud $h(\alpha) \neq x~\&~\alpha \neq \varepsilon : \alpha \leftarrow z(\alpha)$. 
-\:$\indent$Pokud $h(\alpha) = x: \alpha \leftarrow \alpha x$.
-\:$\indent$Pokud $\alpha = \iota$, ohlásíme výskyt.
-\endalgo
+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$,
+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$.
 
-\>Vstupem je $\iota$, hledané slovo (jehla) délky $J=\vert \iota \vert$ a~$\sigma$, text (seno) délky $S=\vert \sigma \vert$.
-\>Výstupem jsou v¹echny výskyty hledaného slova $\iota$ v~textu $\sigma$, tedy mno¾ina $\left\{ k \mid \sigma[k:k+J]=\iota \right\}$
+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.
 
-Pojïme nyní dokázat, ¾e tento algoritmus správnì ohlásí v¹echny výskyty.
+Abychom ale nemuseli suffixy procházet v¹echny, pøedpoèítáme {\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í,
+který lze roz¹íøit o~znak~$x$. Pokud nepùjde roz¹íøit ani jeden z~tìchto kandidátù,
+novým stavem bude prázdný øetìzec.
 
-\s{Definice}: $\alpha(\tau) := $ stav automatu po~pøeètení $\tau$
+Na~této my¹lence je zalo¾en následující algoritmus.
 
-\s{Invariant:} Pokud algoritmus pøeète nìjaký vstup, nachází se ve~stavu, který je nejdel¹ím suffixem pøeèteného vstupu, který je nìjakým stavem.
-$\alpha(\tau) =$ nejdel¹í stav (nejdel¹í prefix jehly), který je suffixem $\tau$ (pøeèteného vstupu).
+\h{Knuthùv-Morrisùv-Prattùv algoritmus (KMP)}
 
-Pojïme si rozmyslet, ¾e z~tohoto invariantu ihned plyne, ¾e algoritmus najde to, co má. Kdykoli toti¾ ohlásí nìjaký výskyt, tak tam tento výskyt opravdu je. Kdykoli pak má nìjaký výskyt ohlásit, tak se v~této situaci jako suffix toho právì pøeèteného textu vyskytuje hledané slovo, pøièem¾ hledané slovo je urèitì stav a~zároveò nejdel¹í ze v¹ech existujících stavù. Tak¾e invariant opravdu øíká, ¾e jsme právì v~koncovém stavu a~algoritmus nám tedy ohlásí výskyt.
-
-\proof {\I (invariantu)}
-Indukcí podle kroku algoritmu. Na~zaèátku pro prázdný naètený vstup invariant triviálnì platí, tedy prázdný suffix $\tau$ je prefixem $\iota$. V~kroku $n$ máme naètený vstup $\tau$ a~k~nìmu pøipojíme znak $x$. Invariant nám øíká, ¾e nejdel¹í stav, který je suffixem, je nejdel¹í suffix, který je stavem. Nyní se ptáme, jaký je nejdel¹í stav, který se dá \uv{napasovat} na~konec øetìzce $\tau x$. Kdykoli v¹ak takovýto suffix máme, tak z~nìj mù¾eme $x$ na~konci odebrat, èím¾ dostaneme suffix slova $\tau$.
+Algoritmus se opírá o~{\I vyhledávací automat.} To je orientovaný graf, jeho¾
+vrcholy ({\I stavy} automatu) odpovídají prefixùm jehly. Vrcholy jsou spojeny
+hranami dvou druhù: {\I dopøedné} popisují roz¹íøení prefixu pøidáním jednoho písmene,
+{\I zpìtné} vedou podle zpìtné funkce, tedy z~ka¾dého stavu do jeho nejdel¹ího
+vlastního suffixu, který je opìt stavem.
 
-\>Tedy: pokud $\beta$ je neprázdným suffixem slova $\tau x$, pak $\beta = \gamma x$, kde $\gamma$ je suffix $\tau$.
+\figure{barb.eps}{Vyhledávací automat pro slovo |barbarossa|}{4.1in}
 
-Suffix, který máme sestrojit, tedy vznikne z~nìjakého suffixu slova $\tau$ pøipsáním~$x$. Chceme najít nejdel¹í suffix slova $\tau x$, který je stavem, tak¾e chceme najít i~nejdel¹í suffix pùvodního slova $\tau$, za který se dá pøidat $x$ tak, aby vy¹lo jméno stavu. Staèí tedy u¾ jen \uv{probírat} suffixy slova $\tau$ od~nejdel¹ího po~nejkrat¹í, zkou¹et k~nim pøidávat $x$ a~a¾ to pùjde, tak jsme na¹li nejdel¹í suffix $\tau x$. Pøesnì toto ov¹em algoritmus dìlá, nebo» zpìtná funkce mu v¾dy øekne nejbli¾¹í krat¹í suffix, který je stavem. Pokud pak nemù¾eme $x$ pøidat ani do~$\varepsilon$, pak je øe¹ením prázdný suffix. Algoritmus tedy funguje. \qed
+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ì
+bude nedefinované, pokud taková hrana neexistuje.
 
-Nyní pojïmì zkoumat to, jak je ve~skuteènosti ná¹ algoritmus rychlý. K tomu bychom si ale nejdøív mìli øíct, jak pøesnì budeme automat reprezentovat. V~algoritmu vystupují nìjaká porovnávání stavù, pøièem¾ není úplnì jasné, jak zaøídit, aby v¹e trvalo konstantnì dlouho. Vyjde nám to ale docela snadno. K reprezentaci automatu nám toti¾ budou staèit pouze dvì pole.
+Kdybychom takový automat mìli, mohli bychom pomocí nìj inkrementální algoritmus
+z~pøedchozí sekce popsat následovnì:
 
-\s{Reprezentace automatu:}
-Oèíslujeme si stavy délkami pøíslu¹ných prefixù, tedy èísly $0 \dots J$. Poté je¹tì potøebujeme nìjakým zpùsobem zakódovat dopøedné a~zpìtné hrany. Vzhledem k~tomu, ¾e z~ka¾dého vrcholu vede v¾dy nejvý¹e jedna dopøedná a~nejvý¹e jedna zpìtná, tak nám evidentnì staèí pamatovat si pro ka¾dý typ hran pouze jedno èíslo na~vrchol. Budeme mít tedy nìjaké pole dopøedných hran, které nám pro ka¾dý stav øekne, jakým písmenkem je nadepsaná dopøedná hrana ze stavu $I$ do~$I+1$. To jsou ale pøesnì písmenka jehly, tak¾e si staèí pamatovat jehlu samotnou. Èili z~$I$ do~$I+1$ vede hrana nadepsaná $\iota [I]$. Pro zpìtné hrany pak budeme potøebovat pole $Z$, které nám pro stav $I$ øekne èíslo stavu, do~kterého vede zpìtná hrana. Tedy $Z[I]$ je cíl zpìtné hrany ze stavu $I$.
-S~touto reprezentací ji¾ doká¾eme na¹i hledací proceduru pøímoèaøe pøepsat tak, aby sahala pouze do~tìchto dvou polí:
+\s{Hledej($\sigma$):}
 \algo
-\:$I \leftarrow 0$.
-\:Pro znaky $x$ z~textu:
-\:$\indent$Dokud $\iota[I] \neq x~\&~I \neq 0: I \leftarrow Z[I]$.
-\:$\indent$Pokud $\iota[I] = x$, pak $I \leftarrow I + 1$.
-\:$\indent$Pokud $I = J$, ohlásíme výskyt.
+\:$S \leftarrow 0$.
+\:Pro znaky $x\in\sigma$ postupnì provádíme:
+\:$\indent$Dokud $\iota[S] \neq x~\&~S \neq 0: S \leftarrow Z[S]$.
+\:$\indent$Pokud $\iota[S] = x$, pak $S \leftarrow S + 1$.
+\:$\indent$Pokud $S = J$, ohlásíme výskyt.
 \endalgo
 
-Zatím se v~algoritmu je¹tì skrývá drobná chyba -- toti¾ algoritmus se obèas zeptá na~dopøednou hranu z~posledního stavu. Pokud jsme právì ohlásili výskyt (jsme tedy v~posledním stavu) a~pøijde nìjaký dal¹í znak, algoritmus se ptá, zda je roven tomu, co je na~dopøedné hranì z~posledního stavu. Ta ale ov¹em neexistuje. Jednodu¹e to ale napravíme tak, ¾e si pøidáme fiktivní hranu, na~které se vyskytuje nìjaké \uv{nepísmenko} -- nìco, co se nerovná ¾ádnému jinému písmenku. Zajistíme tak, ¾e se po~této hranì nikdy nevydáme. Dodefinujeme tedy $\iota[J]$ odli¹nì od~v¹ech znakù.\foot{V jazyce C se toto dodefinování provede vlastnì zadarmo, nebo» ka¾dý øetìzec je v~nìm ukonèen znakem s~kódem nula, který se ve~vstupu nevyskytne\dots Algoritmus bude tedy fungovat i~bez tohoto dodefinování. V jiných jazycích je ale tøeba na~nìj nezapomenout!}
+\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.)
+
+Z~invariantu ihned plyne, ¾e algoritmus správnì ohlásí v¹echny výskyty.
+Jen musíme opravit drobnou chybu -- algoritmus se toti¾ obèas
+zeptá na~dopøednou hranu z~posledního stavu. Pokud jsme právì ohlásili výskyt
+(jsme tedy v~posledním stavu) a~pøijde nìjaký dal¹í znak, algoritmus se ptá,
+zda je roven tomu, co je na~dopøedné hranì z~posledního stavu. Ta ale
+neexistuje. Jednodu¹e to napravíme tak, ¾e pøidáme fiktivní hranu,
+na~které se vyskytuje nìjaké \uv{nepísmenko} -- nìco, co se nerovná ¾ádnému
+jinému písmenku. Zajistíme tak, ¾e se po~této hranì nikdy nevydáme.
+Dodefinujeme tedy $\iota[J]$ odli¹nì od~v¹ech znakù.\foot{V jazyce C se toto
+dodefinování provede vlastnì zadarmo, nebo» ka¾dý øetìzec je v~nìm ukonèen
+znakem s~kódem nula, který se ve~vstupu nevyskytne\dots Algoritmus bude tedy
+fungovat i~bez tohoto dodefinování. V jiných jazycích je ale tøeba na~nìj
+nezapomenout!}
+
+\h{XXX dál následuje pùvodní, je¹tì neupravený text XXX}
+
+Pojïme si rozmyslet, ¾e z~tohoto invariantu ihned plyne, ¾e algoritmus najde to, co má. Kdykoli toti¾ ohlásí nìjaký výskyt, tak tam tento výskyt opravdu je. Kdykoli pak má nìjaký výskyt ohlásit, tak se v~této situaci jako suffix toho právì pøeèteného textu vyskytuje hledané slovo, pøièem¾ hledané slovo je urèitì stav a~zároveò nejdel¹í ze v¹ech existujících stavù. Tak¾e invariant opravdu øíká, ¾e jsme právì v~koncovém stavu a~algoritmus nám tedy ohlásí výskyt.
 
 \s{Lemma:} Funkce {\I Hledej} bì¾í v~èase $\O(S)$.