Algorytm Karpa-Rabina

Algorytm Karpa-Rabina

Algorytm Karpa-Rabina to technika dopasowywania wzorca, która pozwala na znajdowanie określonego podciągu w danym tekście. Został opracowany w 1987 roku przez Michaela O. Rabina i Richarda Karpa.

Rozważmy wzorzec:

x[1…m]

(składający się z m znaków) oraz ciąg do przeszukania:

y[1…n]

(złożony z n znaków; przy czym n >= m). Problem polega na znalezieniu indeksu i, dla którego zachodzi równość:

y[i…i+m−1] = x[1…m].

W algorytmie Karpa-Rabina wykorzystywana jest funkcja mieszająca h. Algorytm przeszukuje wszystkie podciągi:

yi := y[i…i+m−1] dla i ∈ [1…n−m].

Bezpośrednie porównanie podciągu yi z x (które może być kosztowne) jest przeprowadzane tylko wtedy, gdy:

h(yi) = h(x).

Efektywność algorytmu zależy od konstrukcji funkcji mieszającej h – powinna istnieć funkcja N, która niskim kosztem wyznacza:

h(yi+1) na podstawie znanej już wartości h(yi).

W praktyce traktuje się tekst jako liczbę zapisaną w systemie o określonej podstawie (zwykle wykorzystując kody ASCII znaków).

Oczekiwana złożoność obliczeniowa to:

O(m + n).

Pseudokod (dane x, y, n, m):

Sx ← h(x[1…m])

Sy ← h(y[1…m])

for i ← 1 to n−m+1 do

begin

if Sx = Sy then

begin

porównaj ciąg x[1…m] z podciągiem y[i…i+m−1]

if wynik porównania prawdziwy then zwróć indeks i

end

Sy ← N(Sy, y[i+1], y[i+m])

end

Zastosowanie

Jednym z najprostszych zastosowań algorytmu Rabina-Karpa jest wykrywanie plagiatu. Przykładowo, gdy student pisze wypracowanie na temat „Pana Tadeusza”, profesor może porównać jego pracę z innymi opracowaniami na ten sam temat. Algorytm ten pozwala na identyfikację skopiowanych fragmentów. Aby przeciwdziałać próbą oszustwa przez niewielkie zmiany w tekście, algorytm może być skonfigurowany tak, aby ignorował szczegóły, takie jak znaki przestankowe, poprzez ich usunięcie lub wprowadzenie marginesu błędu przy porównywaniu skrótu tekstu ze wzorcem (ponieważ podobne ciągi znaków mają podobne skróty). W przypadku dużej liczby poszukiwanych ciągów, algorytmy pojedynczego wyszukiwania mogą być niepraktyczne.

Różne algorytmy poszukiwania ruchomych podciągów

Podstawowym celem algorytmu jest zlokalizowanie podciągu o długości m (wzoru) w tekście o długości n, na przykład odnalezienie słowa „zegar” w zdaniu: „Zegarmistrz wziął za naprawę 20 złotych.” Najprostszy algorytm realizuje to przez przeszukiwanie wszystkich możliwych podciągów.

1 function NaiveSearch(string s[1..n], string sub[1..m])

2 for i from 1 to n

3 for j from 1 to m

4 if s[i+j-1] ≠ sub[j]

5 jump to next iteration of outer loop

6 return i

7 return not found

Choć ten sposób działa w wielu przypadkach, w niektórych scenariuszach, np. przy poszukiwaniu tekstu składającego się z „b” oraz 10,000 „a” w ciągu składającym się z 10 milionów „a”, czas wykonania wynosi Θ(mn).

Algorytm Knutha-Pratta-Morrisa redukuje ten czas do Θ(n) dzięki prekomputacji, analizując każdy znak w tekście tylko raz. Algorytm Boyer-Moore’a przesuwa wskaźnik nie o 1, ale tak daleko, jak to możliwe, co zmniejsza liczbę iteracji w najlepszym przypadku do n/m. Algorytm Rabina-Karpa skupia się natomiast na przyspieszeniu działania linii kodu od 3 do 6.

Użycie funkcji skrótu do zmiennego przeszukiwania podciągu

Algorytm Rabina-Karpa wykorzystuje funkcję skrótu do przyspieszenia porównywania wzorca z podciągami w tekście. Funkcja skrótu przekształca każdy ciąg znaków na formę numeryczną – skrót (ang. hash). Algorytm opiera się na fakcie, że jeśli dwa teksty są równe, to ich skróty również muszą być równe. W przypadku odwrotnym, dwa różne teksty mogą mieć ten sam skrót, dlatego konieczne jest również porównanie tekstów.

1 function RabinKarp(string s[1..n], string sub[1..m])

2 hsub := hash(sub[1..m])

3 hs := hash(s[1..m])

4 for i from 1 to n

5 if hs = hsub

6 if s[i..i+m-1] = sub

7 return i

8 hs := hash(s[i+1..i+m])

9 return not found

Linie 2, 3, 6 i 8 wymagają Ω(m) czasu każda. Jednak linie 2 i 3 są realizowane tylko raz, a linia 6 uruchamiana jest wyłącznie w przypadku równości skrótów. Linia 5 wykonuje się n razy, ale zajmuje stały czas. Złożoność algorytmu może być negatywnie wpływana przez linię 8.

Jeśli wyliczymy skrót dla podciągu s[i+1…i+m] w sposób naiwny, zajmie to Ω(m) czasu, co oznacza, że ogólna złożoność algorytmu wyniesie Ω(mn). Kluczem jest wyliczenie aktualnego skrótu na podstawie skrótu z poprzedniej iteracji, co realizuje algorytm postępującego skrótu.

Najprostsza wersja tego algorytmu wyznacza wartość każdego znaku w podciągu, a następnie oblicza skrót według wzoru:

hs[i+1…i+m] = hs[i…i+m−1] − s[i] + s[i+m].

Ta funkcja skrótu nie ma jednak równomiernego rozkładu, co powoduje, że linia 6 jest wykonywana znacznie częściej w porównaniu do bardziej zaawansowanych algorytmów.

Funkcje skrótu będące w użyciu

Wydajność algorytmu Karpa-Rabina zależy od efektywnego obliczania wartości skrótu kolejnych podciągów tekstu. Jednym z efektywnych algorytmów postępującego skrótu traktuje podciągi jako liczby w systemie o określonej podstawie, zazwyczaj będącej dużą liczbą pierwszą. Na przykład dla podciągu „hi” wartość skrótu wynosi 104 * 101^1 + 105 * 101^0 = 10609 (gdzie kody ASCII dla h i i to odpowiednio 104 oraz 105). Główną zaletą tego sposobu hashowania jest możliwość wyznaczenia wartości skrótu kolejnego podciągu na podstawie wartości poprzedniego podciągu, wykonując stałą liczbę operacji niezależnych od długości tego podciągu.

Wielowzorcowe wyszukiwanie Rabina-Karpa

Algorytm Rabina-Karpa jest mniej efektywny w wyszukiwaniu pojedynczego wzorca w porównaniu do algorytmów takich jak Knutha-Morrisa-Pratta czy Boyer’a-Moore’a, ponieważ jest najwolniejszy w przypadku pesymistycznego ułożenia ciągu. Jednak w kontekście wielowzorcowego wyszukiwania nie ma sobie równych. Aby wyszukać w tekście wielokrotnie powtarzający się wzorzec, wystarczy stworzyć uproszczoną wersję algorytmu Rabina-Karpa, wykorzystującą filtr Blooma do sprawdzania, czy skrót analizowanego podciągu jest równy skrótowi poszukiwanego podciągu.

function RabinKarpSet(string s[1..n], set of string subs, m) {

set hsubs := emptySet

for each sub in subs

insert hash(sub[1..m]) into hsubs

hs := hash(s[1..m])

for i from 1 to n

if hs ∈ hsubs

if s[i..i+m-1] = a substring with hash hs

return i

hs := hash(s[i+1..i+m])

return not found

}

W tym przykładzie zakłada się, że wszystkie podciągi mają narzuconą długość m, ale można to z łatwością zmienić. Porównujemy obecną wartość skrótu z wartościami skrótów wszystkich podciągów jednocześnie, a następnie porównujemy znalezione pary ze wszystkimi podciągami o danej wartości skrótu.

Inne algorytmy, przy wyszukiwaniu pojedynczego wzorca, mają złożoność czasową O(n), a dla wielokrotnie (k razy) powtarzającego się wzorca O(n*k). W przeciwieństwie do algorytmu Rabina-Karpa, który wyszukuje k wzorów w czasie O(n+k), ponieważ sprawdzenie, czy skrót podciągu jest równy skrótowi wzorca, zajmuje O(1) czasu.

Linki zewnętrzne

Richard M. Karp, Michael O. Rabin. Efficient randomized pattern-matching algorithms. „IBM Journal of Research and Development”. 31 (2), marzec 1987. brak numeru strony

Karp-Rabin algorithm na stronie EXACT STRING MATCHING ALGORITHMS

Przeczytaj u przyjaciół: