]> mj.ucw.cz Git - ads2.git/commitdiff
KMP: Rabin-Karp
authorMartin Mares <mj@ucw.cz>
Sun, 15 Jan 2012 22:07:57 +0000 (23:07 +0100)
committerMartin Mares <mj@ucw.cz>
Sun, 15 Jan 2012 22:07:57 +0000 (23:07 +0100)
1-kmp/1-kmp.tex

index 2fd259bf6069c1da08b073739aeeda709ae984f9..a0d36fca0cf438f1d0020402bd44616dde5aa0b3 100644 (file)
@@ -371,9 +371,9 @@ 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~$S$ do~$Q$ a $\<Slovo>(Q) \ne \emptyset$,
-musí vést zkratka z~$S$ také do~$Q$. Pokud v~$Q$ ¾ádné slovo neskonèí, musí
-zkratka z~$S$ vést do tého¾ vrcholu, jako zkratka z~$Q$.
+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$.
 
 \s{Algoritmus: Konstrukce automatu}
 \algo
@@ -381,15 +381,15 @@ zkratka z~$S$ v
 \: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~$S$ koøene: $\<Zpìt>(S) \= R$, $\<Zkratka>(S) \= \emptyset$.
+\:Pro v¹echny syny~$K$ koøene: $\<Zpìt>(K) \= R$, $\<Zkratka>(K) \= \emptyset$.
 \:Dokud $F \neq \emptyset$:
 \::Vybereme $I$ z~fronty $F$.
-\::Pro v¹echny syny $S$ vrcholu $I$:
-\:::$Q \= \<Krok>(\<Zpìt>(I), \hbox{písmeno na~hranì $IS$})$.
-\:::$\<Zpìt>(S) \= Q$.
-\:::Pokud $\<Slovo>(Q) \neq \emptyset$: $\<Zkratka>(S) \= Q$.
-\:::Jinak $\<Zkratka>(S) \= \<Zkratka>(Q)$.
-\:::Vlo¾íme $S$ do~fronty $F$.
+\::Pro v¹echny syny $K$ vrcholu $I$:
+\:::$Q \= \<Krok>(\<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$.
 \endalgo
 
 Jeliko¾ tento algoritmus pouze napøeskáèku hledá v¹echny jehly, musí být jeho
@@ -407,27 +407,64 @@ jednotliv
 
 \h{Rabinùv-Karpùv algoritmus}
 
-Nyní si uká¾eme je¹tì jeden algoritmus na~hledání jedné jehly, který nebude mít v~nejhor¹ím pøípadì lineární slo¾itost, ale bude ji mít prùmìrnì. Bude daleko jednodu¹¹í a~uká¾e se, ¾e je v~praxi daleko rychlej¹í. Bude to algoritmus zalo¾ený na~hashování.
+Nyní si uká¾eme je¹tì jeden algoritmus na~hledání jedné jehly, tentokrát
+zalo¾ený na he¹ování. Aèkoliv jeho èasová slo¾itost v~nejhor¹ím pøípadì
+bude srovnatelná s~hledáním hrubou silou, v~prùmìru bude lineární a v~praxi
+èasto pobì¾í rychleji ne¾ KMP.
+
+Pøedstavme si, ¾e máme seno délky $S$ a~jehlu délky $J$. Poøídíme si nìjakou
+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.
+
+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.
+
+Poøídíme si proto 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
+$$
+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,
+$$
+pøièem¾ písmena pova¾ujeme za pøirozená èísla a $P$ je nìjaká vhodná konstanta -- potøebujeme,
+aby byla nesoudìlná s~$N$ a aby $P^J$ bylo øádovì vìt¹í ne¾~$N$.
+Posuneme-li nyní okénko z~$x_1,\ldots,x_J$ na $x_2,\ldots,x_{J+1}$, he¹ se zmìní takto:
+$$\eqalign{
+H(x_2,\ldots,x_{J+1}) &= (x_2P^{J-1} + x_3P^{J-2} + \ldots + x_JP^1 + x_{J+1}P^0) \bmod N \cr
+&= (P\cdot H(x_1,\ldots,x_J) - x_1P^J + x_{J+1}) \bmod N. \cr
+}$$
+Pokud si mocninu $P^J$ pøepoèítáme, probìhne aktualizace he¹e v~konstantním èase.
+
+Celý algoritmus pak bude vypadat následovnì:
 
+\algo
+\:$X \= H(\iota)$. \cmt{he¹ jehly}
+\:$Y \= 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¹}
+\:::$X \= (P\cdot X - \sigma[I]\cdot P^J + \sigma[I+J]) \bmod N$.
+\endalgo
 
-Pøedstavme si, ¾e máme seno délky $S$ a~jehlu délky $J$, a~vezmìme si nìjakou hashovací funkci, které dáme na~vstup $J$-tici znakù (tedy podslova dlouhá jako jehla). Tato hashovací funkce nám je pak zobrazí do~mno¾iny $\{0,\ldots,N-1\}$ pro nìjaké dost velké~$N$. Jak nám toto pomù¾e pøi hledání jehly? Vezmeme si libovolné \uv{okénko} délky $J$ a~ne¾ budeme zji¹»ovat, zda se v~nìm jehla vyskytuje, tak si spoèítáme hashovací funkci a~porovnáme ji s~hashem jehly. Èili ptáme se, jestli je hash ze sena od~nìjaké pozice $I$ do~pozice $I+J$ roven hashi jehly -- formálnì: $h(\sigma [I: I+J ]) = h(\iota)$. Teprve tehdy, kdy¾ zjistíme, ¾e se hodnota hashovací fce shoduje, zaèneme doopravdy porovnávat øetìzce.
-
-Není to ale nìjaká hloupost? Mù¾e nám vùbec takováto konstrukce pomoci? Není to tak, ¾e na~spoèítání hashovací funkce z~$J$ znakù, potøebujeme tìch $J$ znakù pøeèíst, co¾ je stejnì rychlé, jako rovnou øetìzce porovnávat? Pou¾ijeme trik, který bude spoèívat v~tom, ¾e si zvolíme ¹ikovnou hashovací funkci. Udìláme to tak, abychom ji mohli pøi posunutí \uv {okénka} o~jeden znak doprava v~konstantním èase pøepoèítat. Chceme umìt z~$h(x_1 \dots x_j)$ spoèítat $h(x_2 \dots x_{j+1})$.
-Na~zaèátku si tedy spoèítáme hash jehly a~první $J$-tice znakù sena. Pak ji¾ jenom posouváme \uv {okénko} o~jedna, pøepoèítáme hashovací funkci a~kdy¾ se shoduje s~hashem jehly, tak porovnáme. Budeme pøitom vìøit tomu, ¾e pokud se tam jehla nevyskytuje, pak máme hashovací funkci natolik rovnomìrnou, ¾e pravdìpodobnost toho, ¾e se pøesto strefíme do~hashe jehly, je $1/N$. Jinými slovy jenom v~jednom z~øádovì $N$ pøípadù budeme porovnávat fale¹nì -- tedy provedeme porovnání a~vyjde nám, ¾e výsledek je neshoda. V~prùmìrném pøípadì tedy mù¾eme stlaèit slo¾itost a¾ témìø k~lineární.
+\s{Analýza:}
+V~nejhor¹ím pøípadì pro ka¾dou pozici okénka porovnáváme øetìzce a celý
+algoritmus spotøebuje èas $\Theta(JS)$. Abychom zjistili, jak tomu bude
+v~prùmìrném pøípadì, odhadnìme pravdìpodobnost porovnání.
 
-Podívejme se teï na~prùmìrnou èasovou slo¾itost. Budeme urèitì potøebovat èas na~projití jehly a~sena. Navíc strávíme nìjaký èas nad fale¹nými porovnáními, kterých bude v~prùmìru na~ka¾dý $N$-tý znak sena jedno porovnání s~jehlou -- tedy $SJ / N$, pøièem¾ $N$ mù¾eme zvolit dost velké na~to, abychom tento èlen dostali pod nìjakou rozumnou konstantu... Nakonec budeme potøebovat jedno porovnání na~ka¾dý opravdový výskyt, èemu¾ se nevyhneme. Pøipoèteme tedy je¹tì $J \cdot$ {\I $\sharp$výskytù}. Dostáváme tedy: $ \O(J+S+SJ/N+J \cdot$ {\I $\sharp$výskytù}).
+Pøedev¹ím pokud nastane výskyt, urèitì porovnáváme. Nenastane-li, he¹ jehly
+se shoduje s~he¹em okénka s~pravdìpodobností $1/N$ (za~pøedpokladu dokonale
+náhodného chování he¹ovací funkce, co¾ té na¹í budeme vìøit, aèkoliv jsme
+to poøádnì nedokázali).
 
-Zbývá malièkost -- toti¾ kde vzít hashovací funkci, která toto v¹e splòuje. Jednu si uká¾eme. Bude to vlastnì takový hezký polynom:
-$$h(x_1 \dots x_j) := \left(\sum_{I=1}^{J} x_I \cdot p^{J-I}\right) \bmod N.$$
-Jinak zapsáno se tedy jedná o:
-$$(x_1 \cdot p^{J-1} + x_2 \cdot p^{J-2} + \dots + x_J \cdot p^0 ) \bmod N.$$
-Po posunutí okénka o~jedna chceme dostat:
-$$(x_2 \cdot p^{J-1} + x_3 \cdot p^{J-2} + \dots + x_J \cdot p^1 + x_{J+1} \cdot p^0 ) \bmod N.$$
-Kdy¾ se ale podíváme na~èleny tìchto dvou polynomù, zjistíme, ¾e se li¹í jen o~málo. Pùvodní polynom staèí pøenásobit~$p$, odeèíst první èlen s~$x_1$ a~naopak pøièíst chybìjící èlen $x_{J+1}$. Dostáváme tedy:
-$$h(x_2 \dots x_{J+1}) = (p \cdot h(x_1 \dots x_J) - x_1 \cdot p^J + x_{J+1}) \bmod N.$$
-Pøepoèítání hashovací funkce tedy není nic jiného, ne¾ pøenásobení té minulé~$p$, odeètení nìjakého násobku toho znaku, který vypadl z~okénka, a~pøiètení toho znaku, o~který se okénko posunulo. Pokud tedy máme k~dispozici aritmetické operace v~konstantním èase, zvládneme konstantnì pøepoèítávat i~hashovací funkci.
+V~prùmìru tedy spotøebujeme èas $\O(S + VJ + S/N\cdot J)$, kde $V$ je poèet
+nalezených výskytù. Pokud nám bude staèit najít první výskyt a zvolíme $N > SJ$,
+algoritmus pobì¾í v~prùmìrném èase $\O(S)$.
 
-Tato hashovací funkce se dokonce nejen hezky poèítá, ale dokonce se i~opravdu \uv{hezky} chová (tedy \uv{rozumnì} náhodnì), pokud zvolíme vhodné~$p$. To bychom mìli zvolit tak, aby bylo rozhodnì nesoudìlné s~$N$ -- tedy $\<NSD>(p, N) = 1$. Aby se nám navíc dobøe projevilo modulo obsa¾ené v~hashovací funkci, mìlo by být~$p$ relativnì velké (lze dopoèítat, ¾e optimum je mezi $2/3 \cdot N$ a~$3/4 \cdot N$). S~takto zvoleným~$p$ se tato hashovací funkce chová velmi pøíznivì a~v~praxi má celý algoritmus takøka lineární èasovou slo¾itost (prùmìrnou).
 
 %% Cvièení: velké abecedy