Temat: Algorytmy wyszukiwania wzorca w tekście 1. Sformułowanie problemu Dany jest tekst T oraz wzorzec P, będące ciągami znaków o długości równej odpowiednio n i m (n m 1), nad pewnym ustalonym i skończonym alfabetem. Należy znaleźć wszystkie wystąpienia wzorca P w tekście T. Będziemy zakładać, że zarówno tekst jak i wzorzec są ciągami znaków zapamiętanymi w strukturze danych o dostępie indeksowym (np. w tablicach: char T[n], P[m]) Przykład T = aabbcadbbbacadbdcbbacadba P = cad Wystąpienia wzorca P w tekście T podamy wskazując pozycje s w teście, dla których: T[s] = P[0] i T[s+1] = P[1] i... i T[s+m-1]=P[m-1]. W przykładzie s = 4, s = 11, s = 20. Gdy T = alalalala P = ala to wystąpienia wzorca P w tekście T są następujące: s = 0, s = 2, s = 4, s = 6. 1
2. Notacja i terminologia * - zbiór wszystkich tekstów (słów) utworzonych z symboli alfabetu ε - słowo o długości zero nazywane słowem pustym x - długość słowa x xy - konkatenacja (złożenie) słów x i y w x w jest prefiksem (przedrostkiem ) słowa x, gdy x = wy dla pewnego słowa y * w x w jest sufiksem (przyrostkiem ) słowa x, gdy x = yw dla pewnego słowa y * X[s... k] fragment tekstu X od znaku o indeksie s do znaku o indeksie k X k k - znakowy prefiks tekstu X. Jeżeli X = X[0... t], to X k =X[0.. k-1] Gdy T[s... s+m-1] = P[0... m-1] dla 0 s n m, to mówimy, że wzorzec P występuje z przesunięciem s w tekście T albo równoważnie, że wzorzec P występuje w tekście T od pozycji s. Jeżeli P występuje w tekście T z przesunięciem s, to s nazywamy poprawnym przesunięciem, w przeciwnym razie s nazywamy niepoprawnym przesunięciem. 2
Przykład ab abcca cca abcca słowo ab jest prefiksem słowa abcca słowa cca jest sufiksem słowa abcca Jeżeli X = abcabcd, to X[2... 5] = cabc oraz X 3 = abc T = ababcdabc P = bcd Przesunięcie s=0 jest niepoprawnym przesunięciem wzorca P względem tekstu T, natomiast przesunięcie s = 3 jest poprawnym przesunięciem wzorca P względem tekstu T. 3. Algorytm naiwny Idea tego algorytmu polega na przeglądaniu tekstu T sekwencyjnie, kolejno przesuwając się po jednym znaku tekstu od lewej do prawej i sprawdzaniu za każdym razem, czy kolejne znaki wzorca P pokrywają się z kolejnymi znakami tekstu. Algorytm naiwny s = 0; while (s <= n-m) j =0; while (j < m) if (P[j]= =T[s+j]) j = j+1; else break; if (j == m) P występuje w T od pozycji s ; s++; 3
Przykład n=18, m=4 T: aaabaababbababaaba P: baba P: baba P: baba P: baba... P: baba P występuje w T od pozycji 9... P: baba P występuje w T od pozycji 11... P: baba Koszt czasowy algorytmu naiwnego Operacją elementarną są porównania między znakami wzorca i tekstu. Jeśli wzorzec nie występuje w tekście i przekonujemy się o tym po pierwszym porównaniu (przy każdym położeniu wzorca względem tekstu), to wykonujemy minimalną liczbę porównań równą n-m+1. Jeżeli wzorzec występuje na każdej pozycji tekstu, na przykład, gdy: T: a n, P: a m, to wykonujemy maksymalną liczbę porównań równą : (n - m+1) m. Zatem: ( n, m) = Θ( ( n m ) m) T 1 max + 4
4. Algorytm Knutha - Morrisa - Pratta (KMP) Algorytm KMP opiera się na wykorzystaniu pomocniczej funkcji Π (zwanej funkcją prefiksową), którą wyznacza się dla wzorca, niezależnie od tekstu. Algorytm wyznaczający funkcję prefiksową ma złożoność liniową względem długości wzorca. Funkcja ta pozwala na zwiększenie (ale tylko w określonych sytuacjach) przesunięcia wzorca względem tekstu. W algorytmie naiwnym przesunięcie zwiększa się zawsze tylko o jeden. Π : 1,..., m 0,1,..., m Π[q] = "maksymalna długość prefiksu wzorca P, który jest równocześnie sufiksem P q " Π[q]= max k : k < q, P k P q Wiedząc, że q znaków wzorca pasuje przy przesunięciu o s, następne potencjalnie poprawne przesunięcie s' można wyliczyć jako: s' = s+ q - Π[q] W najlepszym przypadku s' = s+q (gdy Π[q]=0) i eliminujemy wówczas przesunięcia: s+1, s+2,..., s+q-1. 5
Przykład P: Π[1] = 0 P: Π[2] = 0 P: Π[3] = 1 P: Π[4] = 2 P: Π[5] = 3 6
P: Π[6] = 0 P: Π[7] = 1 T : abacbababaabcbab... P : q 1 2 3 4 5 6 7 Π[q] 0 0 1 2 3 0 1... a b a c b a b a b a a b c b a b... przesu- a b a b a c a nięcie a b a b a c a s s' s' = s+ q - Π[q] s' = 5+ 5-3 = 7 7
Po przesunięciu wzorca o 5 pozycji w prawo, 5 kolejnych znaków wzorca pokrywa się ze znakami tekstu. Znając te 5 znaków wzorca wiemy, że przesunięcie o tylko jedno miejsce w prawo nie jest poprawne, gdyż a wypadnie pod literą b. Natomiast przesunięcie o dwa miejsca w prawo ma szansę powodzenia. Informacje tego typu mogą być wydedukowane na podstawie samego wzorca. Algorytm obliczania funkcji prefiksowej Π Π[1] =0; k=0; for (q = 2; q<=m; q++) while (k>0 && P[k]!=P[q-1]) k = Π [k]; if (P[k] = =P[q-1]) k = k+1; Π [q] = k; Algorytm KMP q= 0; for ( i =0; i < n; i++) while (q>0 && P[q]!=T[i]) q = Π [q]; if (P[q] = = T[i]) q= q+1; if (q = = m) "wzorzec znaleziono na pozycji ; q = Π [q]; 8
Koszt czasowy algorytmu KMP Koszt obliczenia wszystkich wartości funkcji Π wynosi O(m), gdyż : (1) warunek P[k] =P[q-1] (po pętli) może być sprawdzany co najwyżej m razy, a z drugiej strony, (2) warunek P[k]!=P[q] (w pętli) też może być sprawdzany co najwyżej m razy. Stąd, razem mamy co najwyżej 2m porównań. Analogicznie można uzasadnić, że koszt zasadniczego algorytmu KMP jest równy O(n). Można pokazać, że całkowity koszt algorytmu KMP wynosi O(n+m). Jest to właściwie koszt średni, ale przypadek pesymistyczny dla algorytmu KMP zdarza się bardzo rzadko. Tym przypadkiem jest sytuacja, w której wzorzec P=a m oraz tekst T=a n. Wówczas przesunięcie w każdym kroku pętli (*) zwiększa się tylko o jeden. 9
5. Algorytm Rabina-Karpa (RK) Metodę zastosowaną w tym algorytmie można nazwać metodą ''odcisku''. Zamiast porównywać znak po znaku wzorzec z tekstem, używa się specjalnej funkcji (właśnie "odcisku ), która wiąże z każdym ciągiem znaków o długości m jedną liczbę. Liczba ta ma identyfikować blok o m znakach. Zamiast porównywać ciągi znaków, porównuje się reprezentujące je liczby (odciski). Ogólnie można przyjąć, że każdy znak jest cyfrą w systemie pozycyjnym o podstawie d, gdzie d=card(σ). Zatem każdy ciąg m kolejnych znaków można rozumieć jako liczbę m cyfrową w systemie pozycyjnym o podstawie d. Niech p oznacza liczbę odpowiadającą wzorcowi P, a t s oznacza liczbę odpowiadającą ciągowi znaków T[s... s+m-1]. Prawdziwa jest następująca własność: p = t s wtedy i tylko wtedy, gdy T[s... s+m-1] =P[0... m-1], czyli wzorzec P występuje w tekście T od pozycji s. Liczba p jest wartością wielomianu: W x = P m 1 + P m 2 x + P m 3 p dla x=d. 2 m 1 ( ) [ ] [ ] [ ] x +... + P[ 0] x Liczba t s jest wartością wielomianu: x = T s + m 1 + T s + m 2 x + T Wt s 2 m 1 ( ) [ ] [ ] [ s + m 3] x +... + T[ s] x dla x=d. 10
Wartości p i t 0 możemy policzyć kosztem liniowym stosując schemat Hornera : W ( x) = a0 + x( a1 + x( a2 +... + x( a n 1 + xan ))...) Łatwo zauważyć, że wartości t s dla s=1, 2,..., n-m można obliczyć kosztem stałym, ze wzoru: t s =d (t s-1 -d m-1 T[s])+T[s+m] Algorytm Rabina-Karpa s = 0; p = W p (d); // d = card(σ) t 0 = W t0 (d); if ( p = = t 0 ) wzorzec P występuje w tekście T z przesunięciem s=0 ; while (s<=n-m) t s =d (t s-1 -d m-1 T[s])+T[s+m]; s++; if ( p = = t s ) wzorzec P występuje w tekście T z przesunięciem s ; Przykład Σ=0, 1, 2, 3, 4, 5, 6, 7, 8, 9, d=10 T: 34587656743256989 P:56983 Wówczas: t 0 = 34587, t 1 =10 (34 587-10 4 3)+6=45876 t 2 =58765, t 3 = 87656 itd. 11
Koszt czasowy algorytmu RK Operacją elementarną nie są tym razem porównania między znakami wzorca i tekstu, a operacje arytmetyczne realizowane przy obliczaniu odcisku wzorca i tekstu. Odcisk wzorca można policzyć kosztem O(m). Odcisk t 0 również można obliczyć takim samym kosztem. Odciski t 1,...,t n-m można policzyć stałym kosztem, gdy mamy wcześniej policzoną jednorazowo wartość d m-1 (można tę wartość policzyć kosztem O(logm), stosując algorytm oparty na metodzie "dziel i zwyciężaj". Zatem koszt całego algorytmu wyniesie O(n+m). W związku z tym, że alfabet wzorca i tekstu może być duży (np. d = card(σ)=256) pojawiają się dwa problemy związane z implementacją algorytmu RK. Problem 1 Wartości p i t 0, t 1,..., t n-m mogą być bardzo duże, a wtedy nie można zakładać, że każda operacja arytmetyczna ma ten sam koszt co operacja arytmetyczna dla liczb mieszczących się w słowie maszynowym. Rozwiązanie Problemu 1 Problem ten można rozwiązać stosując zamiast zwykłej arytmetyki, arytmetykę modulo, tzn. obliczone wartość odcisku dzieli się modulo pewna wybrana liczba pierwsza q. Zwykle wybiera się q takie, że d q powinno być nie większe niż jedno słowo maszynowe. Przy tym założeniu w trakcie obliczania wartości odcisków będą używane standardowe operatory arytmetyczne (tj. dla małych liczb ). 12
Algorytm Rabina-Karpa w arytmetyce modularnej s = 0; p = W p (d) % q; t 0 = W t0 (d) % q if ( p = = t 0 ) wzorzec P występuje w tekście T z przesunięciem s=0 ; while (s<=n-m) t s =(d (t s-1 -d m-1 T[s])+T[s+m]) % q; s++; if ( p = = t s ) wzorzec P występuje w tekście T z przesunięciem s ; Problem 2 Pojawia się jednak problem niejednoznaczności, ponieważ prawdziwość warunku p = = t s nie oznacza, że na pewno P[0...m-1]=T[s... s+m-1]. Równość reszt z dzielenia dwóch liczb nie oznacza bowiem, że same liczby są na pewno sobie równe. Aby algorytm nie zwracał niepoprawnych wyników należy zatem, w każdym przypadku, gdy p = = t s dodatkowo sprawdzić równość odpowiednich ciągów badając je znak po znaku. Powoduje to jednak, że koszt algorytmu RK, w najgorszym przypadku wynosi Θ((n-m+1) m). Przykładem przypadku pesymistycznego dla algorytmu RK jest przypadek: T = a n i P = a m. Wówczas każdy blok tekstu o długości m daje ten sam odcisk równy odciskowi wzorca i konieczne jest sprawdzenie znak po znaku. 13
W bardzo wielu zastosowaniach, zajście zdarzenia p== t s przy jednoczesnej niezgodności wzorca z tekstem, jest bardzo rzadkie. Liczbę q można tak wybrać, aby prawdopodobieństwo takiego zdarzenia było równe 1/n. Przy dużym n prawdopodobieństwo pomyłki jest zatem bardzo małe i można nie sprawdzać zgodności znak po znaku, co poowduje, że czas oczekiwany (złożoność średnia) wykonania algorytmu RK wynosi O(n+m). Wersja bez sprawdzania symbol po symbolu i z arytmetyką modulo q gwarantuje liniowy czas wykonania, ale z małym prawdopodobieństwem wynik może się okazać niepoprawny. Takie algorytmy, które z dopuszczalnym prawdopodobieństwem zwracają wynik niepoprawny nazywamy algorytmami Monte Carlo (zawsze szybko i prawdopodobnie poprawnie). Wersja algorytmu ze sprawdzaniem w przypadku, gdy odcisk wzorca jest zgodny modulo q z odciskiem bloku tekstu gwarantuje poprawny wynik, ale z małym prawdopodobieństwem algorytm ten będzie działał dłużej niż liniowo. Algorytmy tego typu nazywane są algorytmami Las Vegas (zawsze poprawnie i prawdopodobnie szybko). Problem 3 Wartości W p (d) oraz W t0 (d) to nadal duże liczby, dla dużego d, pomimo tego, że wartości t 0 oraz p są mniejsze równe q. Jak zatem obliczyć t 0 oraz p, aby nie używać arytmetyki dużych liczb? 14
Rozwiązanie problemu 3 Wartości t 0 oraz p obliczamy stosując algorytm potęgowania modularnego: h=1; for (i=0; i<m; i++) // obliczamy h=d m-1 % q h=(d*h) % q; p = 0; ts = 0; // obliczamy wartości t 0 oraz p for (i = 0; i<m ; i++) p = (d*p+p[i]) % q; ts = (d*ts + T[i]) % q; Algorytm Rabina-Karpa w wersji Las Vegas h=1; for (i=0; i<m; i++) h=(d*h) % q; p = 0; ts = 0; for (i = 0; i<m ; i++) p = (d*p+p[i]) % q; ts = (d*ts + T[i]) % q; for (s = 0; s<=n-m; s++) bool=0; if (p = = ts) i=0; bool=1; 15
while (i < m && bool) if (P[i]!=T[s+i]) bool = 0; i++; if (bool) wzorzec P występuje w tekście T z przesunięciem s ; if (s < n-m) ts=(ts+d*q-t[s]*h) % q; ts=(ts*d+t[s+m])% q; 16