Temat: Algorytmy wyszukiwania wzorca w tekście 1. Sformułowanie
Transkrypt
Temat: Algorytmy wyszukiwania wzorca w tekście 1. Sformułowanie
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 Xk k - znakowy prefiks tekstu X. Jeżeli X = X[0... t], to Xk=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 słowo ab jest prefiksem słowa abcca cca ⊃ abcca słowa cca jest sufiksem słowa abcca Jeżeli X = abcabcd, to X[2 ... 5] = cabc oraz X3= 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: an, P: am, to wykonujemy maksymalną liczbę porównań równą : (n - m+1)⋅ m. Zatem: Tmax (n, m ) = Θ((n − m + 1)m ) 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 Pq" Π[q]= max {k : k < q, Pk ⊃ Pq } 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: ababaca Π[1] = 0 P: ababaca ababaca Π[2] = 0 P: ababaca ababaca ababaca Π[3] = 1 P: ababaca ababaca ababaca ababaca Π[4] = 2 P: ababaca ababaca ababaca ababaca ababaca Π[5] = 3 6 P: ababaca ababaca ababaca ababaca ababaca ababaca Π[6] = 0 P: ababaca ababaca ababaca ababaca ababaca ababaca ababaca Π[7] = 1 T : abacbababaabcbab ... P : ababaca q 1 2 3 0 0 1 Π[q] ... przesunięcie 4 2 5 3 6 0 7 1 a b a c b a b a b a a b c b a b ... a b a b a c a 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=am oraz tekst T=an. 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 ts oznacza liczbę odpowiadającą ciągowi znaków T[s ... s+m-1]. Prawdziwa jest następująca własność: p = ts 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 p (x ) = P[m − 1] + P[m − 2]x + P[m − 3]x 2 + ... + P[0]x m −1 dla x=d. Liczba ts jest wartością wielomianu: Wt s ( x ) = T [s + m − 1] + T [s + m − 2]x + T [s + m − 3]x 2 + ... + T [s ]x m −1 dla x=d. 10 Wartości p i t0 możemy policzyć kosztem liniowym stosując schemat Hornera : W ( x ) = a 0 + x(a1 + x(a 2 + ... + x(a n −1 + xa n ))...) Łatwo zauważyć, że wartości ts dla s=1, 2, ..., n-m można obliczyć kosztem stałym, ze wzoru: ts =d ⋅ (ts-1 -dm-1⋅T[s])+T[s+m] Algorytm Rabina-Karpa s = 0; p = Wp(d); // d = card(Σ) t0 = Wt0(d); if ( p = = t0) „wzorzec P występuje w tekście T z przesunięciem s=0”; while (s<=n-m) { ts =d ⋅ (ts-1 -dm-1⋅T[s])+T[s+m]; s++; if ( p = = ts) „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: t0 = 34587, t1 =10⋅ (34 587-104⋅3)+6=45876 t2 =58765, t3 = 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 t0 również można obliczyć takim samym kosztem. Odciski t1,...,tn-m można policzyć stałym kosztem, gdy mamy wcześniej policzoną jednorazowo wartość dm-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 t0, t1,..., tn-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 = Wp(d) % q; t0 = Wt0(d) % q if ( p = = t0) „wzorzec P występuje w tekście T z przesunięciem s=0”; while (s<=n-m) { ts =(d⋅ (ts-1 -d m-1⋅T[s])+T[s+m]) % q; s++; if ( p = = ts) „wzorzec P występuje w tekście T z przesunięciem s”; } Problem 2 Pojawia się jednak problem niejednoznaczności, ponieważ prawdziwość warunku p = = ts 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 = = ts 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 = an i P = am. 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== ts 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 Wp(d) oraz Wt0(d) to nadal duże liczby, dla dużego d, pomimo tego, że wartości t0 oraz p są mniejsze równe q. Jak zatem obliczyć t0 oraz p, aby nie używać arytmetyki dużych liczb? 14 Rozwiązanie problemu 3 Wartości t0 oraz p obliczamy stosując algorytm potęgowania modularnego: h=1; for (i=0; i<m; i++) // obliczamy h=dm-1 % q h=(d*h) % q; p = 0; ts = 0; // obliczamy wartości t0 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