Algorytmy i struktury danych/Algorytmy tekstowe I: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Dorota (dyskusja | edycje)
Nie podano opisu zmian
Rytter (dyskusja | edycje)
Nie podano opisu zmian
Linia 5: Linia 5:
Tekst jest ciągiem symboli. Przyjmujemy, że jest on zadany tablicą <math>x[1,\ldots,n]</math>, elementami której są symbole ze skończonego zbioru ''A'' (zwanego alfabetem). Liczba <math>n=|x|</math> jest długością (rozmiarem) tekstu. W większości naszych algorytmów jedyne operacje dopuszczalne na symbolach wejściowych to porównania dwóch symboli.  
Tekst jest ciągiem symboli. Przyjmujemy, że jest on zadany tablicą <math>x[1,\ldots,n]</math>, elementami której są symbole ze skończonego zbioru ''A'' (zwanego alfabetem). Liczba <math>n=|x|</math> jest długością (rozmiarem) tekstu. W większości naszych algorytmów jedyne operacje dopuszczalne na symbolach wejściowych to porównania dwóch symboli.  


Algorytmy na tekstach wyróżniają się tym, że wykorzystują specyficzne, kombinatoryczne własności tekstów. '''Okresem''' tekstu <math>x</math> jest każda liczba naturalna niezerowa <math>p</math> taka, że <math>x[i]=x[i+p]</math>, dla każdego <math>i</math>, dla którego obie strony są zdefiniowane. Przez ''per(x)'' oznaczmy minimalny okres x.  
Algorytmy na tekstach wyróżniają się tym, że wykorzystują specyficzne, kombinatoryczne własności tekstów. '''Okresem''' tekstu <math>x</math> jest każda niezerowa  liczba naturalna <math>p</math> taka, że <math>x[i]=x[i+p]</math>, dla każdego <math>i</math>, dla którego obie strony są zdefiniowane. Przez ''per(x)'' oznaczmy minimalny okres x.  




Pojęciem dualnym do okresu jest '''prefikso-sufiks''' tekstu. Jest to najdłuższy właściwy (nie będący całym tekstem) prefiks tekstu ''x''  będący jednocześnie jego sufiksem.  Oczywiste jest, że <math>|x|-per(x)</math> jest długością prefikso-sufiksu ''x''. Jeśli <math>per(x)=|x|</math> to prefikso-sufiksem ''x'' jest słowo puste o długości zerowej.
Pojęciem dualnym do okresu jest '''prefikso-sufiks''' tekstu. Jest to najdłuższy właściwy (nie będący całym tekstem) prefiks tekstu ''x''  będący jednocześnie jego sufiksem.  Oczywiste jest, że <math>|x|-per(x)</math> jest długością prefikso-sufiksu ''x''. Jeśli <math>per(x)=|x|</math> to prefikso-sufiksem ''x'' jest słowo puste o długości zerowej.


Oznaczmy przez <math>P[k]</math> rozmiar prefikso-sufiksu <math>x[1..k]</math>; zatem <math>per(x)=n-P[n]</math>, gdzie <math>n=|x|</math>.
Oznaczmy przez <math>P[k]</math> rozmiar prefikso-sufiksu <math>x[1..k]</math>. Zatem <math>per(x)=n-P[n]</math>, gdzie <math>n=|x|</math>.




Linia 18: Linia 18:
<center><math>P[1..11]\ =\ [0,\ 0,\ 1,\ 2,\ 3,\ 4,\ 5,\ 6,\ 7,\ 8,\ 0].</math></center>
<center><math>P[1..11]\ =\ [0,\ 0,\ 1,\ 2,\ 3,\ 4,\ 5,\ 6,\ 7,\ 8,\ 0].</math></center>


Wartość <math>P[0]</math> jest wartością sztuczną  (przyjmiemy potem <math>P[0]=-1</math>).
Wartość <math>P[0]</math> jest wartością sztuczną  (przyjmiemy, że <math>P[0]=-1</math>).
}}
}}
== Obliczanie tablicy  Prefikso-Sufiksów==
== Obliczanie tablicy  Prefikso-Sufiksów==
Linia 40: Linia 40:


== Minimalne słowo pokrywające ==
== Minimalne słowo pokrywające ==
Pokażemy pewne, proste zastosowanie tablic prefikso-sufiksów.
Pokażemy pewne proste zastosowanie tablic prefikso-sufiksów.
Słowem pokrywającym tekst ''x'' jest każdy taki tekst ''y'', którego wystąpienia w ''x''
Słowem pokrywającym tekst ''x'' jest każdy taki tekst ''y'', którego wystąpienia w ''x''
pokrywają cały tekst ''x''. Na przykład słowo ''y=aba'' pokrywa tekst ''x=ababaaba'', natomiast nie pokrywa tekstu ''abaaababa''.
pokrywają cały tekst ''x''. Na przykład słowo ''y=aba'' pokrywa tekst ''x=ababaaba'', natomiast nie pokrywa tekstu ''abaaababa''.
Linia 46: Linia 46:


Niech <math>S[i]</math>
Niech <math>S[i]</math>
będzie rozmiarem minimalnego słowa pokrywającego dla prefiksu <math>x[1..i]</math>. Następujący algorytm oblicza długość minimalnego słowa
będzie rozmiarem minimalnego słowa pokrywającego d <math>x[1..i]</math>. Następujący algorytm oblicza długość minimalnego słowa
pokrywającego tekstu ''x''. Obliczamy wartości <math>S[i]</math> długości najkrótszego słowa pokrywającego <math>x[1
pokrywającego tekstu ''x''.  Algorytm jest efektywny ponieważ liczy ‘’za dużo’’: dwie dodatkowe tablice.
\ldots i]</math> dla każdego <math>1 \le i \le n</math>.
W <math>i</math>-tej
W <math>i</math>-tej
iteracji algorytmu pamiętany jest ''znany zakres'' każdego minimalnego słowa pokrywającego.
iteracji algorytmu pamiętany jest ''znany dotychczas zakres'' każdego minimalnego słowa pokrywającego.
 
<font color="red">TODO - problem z rysunkiem, napis nie pasuje do podpisu, co to jest q?</font>


<center> [[Grafika:Minpokslowo.jpg]]<br>
<center> [[Grafika:Minpokslowo.jpg]]<br>


Rysunek 3: <math>i</math>-ta iteracja algorytmu dla <math>i=15</math> oraz słowa  <math>x\ =\ abaabababaababa\ldots</math>. Tuż przed
Rysunek 1: <math>i</math>-ta iteracja algorytmu dla <math>i=15</math> oraz słowa  <math>x\ =\ abaabababaababa\ldots</math>. Tuż przed
rozpoczęciem tej iteracji mamy <math>P[i]=8</math>, <math>q=S[8]=3</math>, <math>Zakres[3]=13</math>. Po zakończeniu <math>i</math>-tej iteracji
rozpoczęciem tej iteracji mamy <math>P[i]=8</math>, <math>Zakres[3]=13</math>. Po zakończeniu <math>i</math>-tej iteracji
mamy  <math>S[15]=3,Zakres[3]=15</math>, ponieważ <math>i-Zakres[3] \le q</math>. </center>
mamy  <math>S[15]=3,Zakres[3]=15</math>.


{{algorytm | Rozmiar-Minimalnego-Pokrycia|algorym_rozm_min_pokr|
{{algorytm | Rozmiar-Minimalnego-Pokrycia|algorym_rozm_min_pokr|
Linia 75: Linia 72:
==Tablica Silnych Prefikso-Sufiksów==
==Tablica Silnych Prefikso-Sufiksów==


<font color="red">TODO - KDX - ???</font>
Wprowadzimy teraz  tablicę ‘’silnych’’ prefikso-sufiksów dla wzorca <math>x[1..m]</math>:  
 
Wprowadzimy silną tablicę prefikso-sufiksów dla wzorca <math>x[1..m]</math>:  
jeśli <math>j<|x|</math>, to <math>P'[j]=k</math>, gdzie <math>k</math> jest maksymalnym rozmiarem słowa będącego właściwym prefiksem i sufiksem <math>x[1..j]</math> i spełniającego dodatkowy warunek <math>x[k+1]\ne x[j+1]</math> dla <math>j<n</math>.  
jeśli <math>j<|x|</math>, to <math>P'[j]=k</math>, gdzie <math>k</math> jest maksymalnym rozmiarem słowa będącego właściwym prefiksem i sufiksem <math>x[1..j]</math> i spełniającego dodatkowy warunek <math>x[k+1]\ne x[j+1]</math> dla <math>j<n</math>.  
<br> Jeśli takiego k nie ma, to przyjmujemy <math>P'[j]=-1</math>. Przyjmujemy ponadto, że <math>P'[m]=P[m]</math>.
<br> Jeśli takiego k nie ma, to przyjmujemy <math>P'[j]=-1</math>. Przyjmujemy ponadto, że <math>P'[m]=P[m]</math>.
Linia 119: Linia 114:
Operacją ''dominującą'' w algorytmie jest porównanie dwóch symboli.
Operacją ''dominującą'' w algorytmie jest porównanie dwóch symboli.


Zaczniemy od obliczania jedynie pierwszego wystąpienia. Algorytm KMP przegląda tekst y od lewej do prawej, sprawdzając, czy jest zgodność na pozycji <math>j+1</math> we wzorcu x oraz na pozycji <math>i+j+1</math> w tekście y. Jeśli jest niezgodność, to przesuwamy potencjalny początek (pozycja i) wystąpienia x w y. Zakładamy, że algorytm zwraca na końcu wartość ''false'', jeśli nie zwróci wcześniej ''true''. Pozostawiamy dowód poprawności (określenie niezmienników) jako ćwiczenie.
Zaczniemy od obliczania jedynie pierwszego wystąpienia. Algorytm KMP przegląda tekst y od lewej do prawej, sprawdzając, czy jest zgodność na pozycji <math>j+1</math> we wzorcu x oraz na pozycji <math>i+j+1</math> w tekście y. Jeśli jest niezgodność, to przesuwamy potencjalny początek (pozycja i) wystąpienia x w y. Zakładamy, że algorytm zwraca na końcu wartość ''false'', jeśli nie zwróci wcześniej ''true''. Pozostawiamy dowód poprawności (określenie niezmienników) jako ćwiczenie.




Linia 133: Linia 128:
Operacją '''dominującą''' w algorytmie jest  operacja: <math>x[j+1]=y[i+j+1]</math>.
Operacją '''dominującą''' w algorytmie jest  operacja: <math>x[j+1]=y[i+j+1]</math>.


Udowodnimy, że algorytm KMP wykonuje co najwyżej 2n-m porównań symboli. Zauważmy, że dla danej pozycji w tekście y jest ona co najwyżej raz porównana z pewną pozycją we wzorcu w porównaniu pozytywnym (gdy symbole są równe). Jednocześnie każde negatywne porównanie powoduje przesunięcie pozycji <math>i</math> co najmniej o jeden, maksymalna wartość i wynosi n-m, zatem mamy takich porównań co najwyżej n-m, w sumie co najwyżej 2n-m porównań w algorytmie KMP.
Algorytm KMP wykonuje co najwyżej 2n-m porównań symboli. Zauważmy, że dla danej pozycji w tekście y jest ona co najwyżej raz porównana z pewną pozycją we wzorcu w porównaniu pozytywnym (gdy symbole są równe). Jednocześnie każde negatywne porównanie powoduje przesunięcie pozycji <math>i</math> co najmniej o jeden, maksymalna wartość i wynosi n-m, zatem mamy takich porównań co najwyżej n-m, w sumie co najwyżej 2n-m porównań w algorytmie KMP.




Linia 178: Linia 173:
}}
}}


==Wersja real-time algorytmu KMP==
==Wersja ‘’real-time’’ algorytmu KMP==


Pokażemy teraz wersję algorytmu on-line, która działa w czasie rzeczywistym, tzn. czas reakcji między wczytaniem symbolu a daniem odpowiedzi jest O(1), niezależnie od rozmiaru alfabetu.  
Pokażemy teraz wersję algorytmu on-line, która działa w czasie rzeczywistym, tzn. czas reakcji między wczytaniem symbolu a daniem odpowiedzi jest O(1), niezależnie od rozmiaru alfabetu.  


Algorytm zachowuje się podobnie jak algorytm On-Line-KMP; podstawowa różnica polega na tym, że algorytm wkłada do kolejki wczytane symbole, które jeszcze nie są przetworzone w sensie algorytmu KMP.  Algorytm zachowuje się podobnie jak algorytm on-line,  ale wczytuje kolejne symbole z kolejki, a nie bezpośrednio z wejścia. Rysunek pokazuje relacje tego algorytmu do algorytmu KMP. Symbole z wejścia najpierw wędrują do kolejki.
Algorytm zachowuje się podobnie jak algorytm On-Line-KMP; podstawowa różnica polega na tym, że algorytm wkłada do kolejki wczytane symbole, które jeszcze nie są przetworzone w sensie algorytmu KMP.  Rysunek pokazuje relacje tego algorytmu do algorytmu KMP. Symbole z wejścia najpierw wędrują do kolejki.
<center>[[Grafika:Rtkmp.png]]<br>
<center>[[Grafika:Rtkmp.png]]<br>
Rysunek 2: Typowa konfiguracja w algorytmie real-time-KMP.</center>
Rysunek 2: Typowa konfiguracja w algorytmie real-time-KMP.</center>
Linia 195: Linia 190:
}}
}}


W celu skrócenia zapisów pojedynczych algorytmów rozbijamy algorytm na dwie części. Zasadnicza część jest zapisana jako osobna funkcja OUTPUT(Kolejka,  j). Funkcja ta liczy 0 lub 1, w zależności od tego czy ostatnio wczytany symbol kończy wystąpienie wzorca x. Zmienne Kolejka, j  są globalne.
W celu skrócenia zapisów pojedynczych algorytmów rozbijamy algorytm na dwie części. Zasadnicza część jest zapisana jako osobna funkcja OUTPUT(Kolejka,  j). Wynikiem funkcji jest  0 lub 1, w zależności od tego czy ostatnio wczytany symbol kończy wystąpienie wzorca x. Zmienne Kolejka, j  są globalne.
 
Oczywiste jest, że opóźnienie (czas reakcji) tego algorytmu jest O(1).
Oczywiste jest, że opóźnienie (czas reakcji) tego algorytmu jest O(1).


Linia 218: Linia 212:
==Wersja algorytmu KMP z <math>\frac{3n}{2}</math>  porównaniami==
==Wersja algorytmu KMP z <math>\frac{3n}{2}</math>  porównaniami==


Algorytm KMP wykonuje co najmniej 2n-m porównań symboli. Załóżmy, że są to operacje dominujące i spróbujmy zmniejszyć stały współczynnik 2 do <math>\frac{3}{2}</math>. Na początku załóżmy, że <math>x=ab</math>. Następujący algorytm znajduje wszystkie wystąpienia wzorca ab w tekście y.
Algorytm KMP wykonuje co najmniej 2n-m porównań symboli. Załóżmy, że są to operacje dominujące i spróbujmy zmniejszyć stały współczynnik 2 do <math>\frac{3}{2}</math>. Na początku załóżmy, że <math>x=ab</math>. Następujący algorytm znajduje wszystkie wystąpienia wzorca ‘’ab’’ w tekście y.


{{algorytm|Szukanie-ab|algorytm_szukanie_ab|
{{algorytm|Szukanie-ab|algorytm_szukanie_ab|
Linia 230: Linia 224:
}}
}}


Algorytm KMP dla wzorca ab wykonywał 2n-2 porównań symboli, nowy algorytm jest lepszy. Zachodzi fakt:
Algorytm KMP dla wzorca ‘’ab’’ i tekstu ‘’aaa...aa’’ wykonywał 2n-2 porównań symboli, nowy algorytm jest lepszy. Algorytm Szukanie-ab wykonuje co najwyżej n porównań w tym przypadku. Dla tekstu ‘’abab’’ algorytm wykinuje n+1 porównań.
&nbsp;&nbsp;&nbsp; algorytm Szukanie-ab wykonuje co najwyżej n porównań w tym przypadku.  


Uzasadnienie pozostawiamy jako ćwiczenie.
Pozostawiamy jako ćwiczenie policzenie maksymalnej liczby porównań dla tego algorytmu (wzorzec ‘’ab’’).
Widać, że podstawowa idea to sprawdzanie najpierw pierwszego symbolu wzorca  różnego od poprzednich.


Uogólnimy algorytm na dowolne wzorce. Niech x zawiera co najmniej dwa różne symbole,  <math>x=a^kb\alpha</math>, gdzie <math>a\ne b</math>.Oznaczmy <math>x'=b\alpha</math> ''skrócony wzorzec''  
Uogólnimy algorytm na dowolne wzorce. Niech x zawiera co najmniej dwa różne symbole,  <math>x=a^kb\alpha</math>, gdzie <math>a\ne b</math>.Oznaczmy <math>x'=b\alpha</math> ''skrócony wzorzec''  
Linia 261: Linia 255:
</center>
</center>


Pozostawiamy jako ćwiczenie dokładny zapis algorytmu w pseudokodzie oraz dowód tego, że algorytm Oszczędny-KMP wykonuje co najwyżej <math>\frac{3}{2}n</math> porównan.  
Pozostawiamy jako ćwiczenie dokładny zapis algorytmu oraz dokładniejszy dowód tego, że algorytm Oszczędny-KMP wykonuje co najwyżej <math>\frac{3}{2}n</math> porównan.  


Ogólna idea jest przedstawiona na rysunku.  
Ogólna idea jest przedstawiona na rysunku.  
Linia 272: Linia 266:
(1) sprawdzanie w części <math>\alpha</math> z wynikiem negatywnym; wtedy przesuwamy wzorzec co najmniej o k,
(1) sprawdzanie w części <math>\alpha</math> z wynikiem negatywnym; wtedy przesuwamy wzorzec co najmniej o k,


(2) sprawdzanie części <math>a^k</math> na lewo od ''pozytywnego'' <math>b</math> (w kwadraciku na rysunku), na pozycjach, gdzie wcześniej było sprawdzanie ''negatywnego'' b. Wtedy odległość między pozytywnymi kolejnymi b jest co najmniej 2w, gdzie <math>w\le k</math> liczba sprawdzanych na lewo symboli a. Zatem lokalnie przesunięcie jest co najmniej dwukrotnie większe niż liczba dodatkowych operacji.
(2) sprawdzanie części <math>a^k</math> na lewo od ''pozytywnego'' <math>b</math> (w kwadraciku na rysunku), na pozycjach, gdzie wcześniej było sprawdzanie ''negatywnego'' b. Wtedy odległość między pozytywnymi kolejnymi b jest co najmniej 2w, gdzie <math>w\le k</math> jest liczbą sprawdzanych na lewo symboli a. Zatem ‘’lokalnie’’ przesunięcie jest co najmniej dwukrotnie większe niż liczba dodatkowych operacji.


Suma przesunięć wzorca na tekście <math>y</math> wynosi co najwyżej n, sumaryczna liczba  dodatkowych operacji jest więc co najwyżej <math>\frac{1}{2}n</math>, a liczba wszystkich operacji nie przekracza <math>\frac{3}{2}n</math>.
Suma przesunięć wzorca na tekście <math>y</math> wynosi co najwyżej n, sumaryczna liczba  dodatkowych operacji jest więc co najwyżej <math>\frac{1}{2}n</math>, a liczba wszystkich operacji nie przekracza <math>\frac{3}{2}n</math>.
Linia 305: Linia 299:


Problem poprawności pozostawiamy jako ćwiczenie.
Problem poprawności pozostawiamy jako ćwiczenie.
 
Liczba porównań jest oczywiście liniowa. Pozostawiamy również jako ćwiczenie wyprowadzenie dokładnego wzoru na maksymalną liczbę porównań symboli dla tekstów długości <math>n</math>.
Liczba porównań jest oczywiście liniowa. Pozostawiamy jako ćwiczenie wyprowadzenie dokładnego wzoru na maksymalną liczbę porównań symboli dla tekstów długości <math>n</math>.

Wersja z 09:50, 1 paź 2006

Algorytmy tekstowe I

Tekst jest ciągiem symboli. Przyjmujemy, że jest on zadany tablicą x[1,,n], elementami której są symbole ze skończonego zbioru A (zwanego alfabetem). Liczba n=|x| jest długością (rozmiarem) tekstu. W większości naszych algorytmów jedyne operacje dopuszczalne na symbolach wejściowych to porównania dwóch symboli.

Algorytmy na tekstach wyróżniają się tym, że wykorzystują specyficzne, kombinatoryczne własności tekstów. Okresem tekstu x jest każda niezerowa liczba naturalna p taka, że x[i]=x[i+p], dla każdego i, dla którego obie strony są zdefiniowane. Przez per(x) oznaczmy minimalny okres x.


Pojęciem dualnym do okresu jest prefikso-sufiks tekstu. Jest to najdłuższy właściwy (nie będący całym tekstem) prefiks tekstu x będący jednocześnie jego sufiksem. Oczywiste jest, że |x|per(x) jest długością prefikso-sufiksu x. Jeśli per(x)=|x| to prefikso-sufiksem x jest słowo puste o długości zerowej.

Oznaczmy przez P[k] rozmiar prefikso-sufiksu x[1..k]. Zatem per(x)=nP[n], gdzie n=|x|.


Przykład

Dla x = abababababb mamy:

P[1..11] = [0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 0].

Wartość P[0] jest wartością sztuczną (przyjmiemy, że P[0]=1).

Obliczanie tablicy Prefikso-Sufiksów

Przedstawimy jeden z możliwych algorytmów liniowych obliczania tablicy P. Jest to iteracyjna wersja algorytmu rekurencyjnego, który moglibyśmy otrzymać korzystając z faktu:

Jeśli x[j]=x[t+1] oraz t=P[j1]<math>,to<math>P[j]=t+1

W algorytmie obliczania P[j] korzystamy z wartości P[k], dla k<j.

Algorytm Prefikso-Sufiksy


1  P[0]:=1; t:=0;
2  for j:=1 to m do
3   begin
4    while t0 and x[t+1]x[j] do t:=P[t];
5    t:=t+1; P[j]:=t;
6   end;

Złożoność liniowa wynika stąd, że w każdej iteracji zwiększamy wartość t co najwyżej o jeden, a wykonanie każdej operacji t:=P[t] zmniejsza wartość t co najmniej o jeden. Proste zastosowanie zasady magazynu (lub potencjału) implikuje, że operacji t:=P[t] wykonujemy co najwyżej n. Dowód poprawności pozostawiamy jako ćwiczenie.

Minimalne słowo pokrywające

Pokażemy pewne proste zastosowanie tablic prefikso-sufiksów. Słowem pokrywającym tekst x jest każdy taki tekst y, którego wystąpienia w x pokrywają cały tekst x. Na przykład słowo y=aba pokrywa tekst x=ababaaba, natomiast nie pokrywa tekstu abaaababa. Zajmiemy się problemem: obliczyć w czasie liniowym długość najkrótszego słowa pokrywającego dany tekst x.

Niech S[i] będzie rozmiarem minimalnego słowa pokrywającego d x[1..i]. Następujący algorytm oblicza długość minimalnego słowa pokrywającego tekstu x. Algorytm jest efektywny ponieważ liczy ‘’za dużo’’: dwie dodatkowe tablice. W i-tej iteracji algorytmu pamiętany jest znany dotychczas zakres każdego minimalnego słowa pokrywającego.


Rysunek 1: i-ta iteracja algorytmu dla i=15 oraz słowa x = abaabababaababa. Tuż przed rozpoczęciem tej iteracji mamy P[i]=8, Zakres[3]=13. Po zakończeniu i-tej iteracji mamy S[15]=3,Zakres[3]=15.

Algorytm Rozmiar-Minimalnego-Pokrycia


1  for  i:=2 to n do begin
2    Zakres[i]=i;S[i]=i;
3  end;
4  for  i:=2 to n do
5    if P[i]>0 and iZakres[S[P[i]]S[P[i]] then begin
6      S[i]:=S[P[i]];  Zakres[S[P[i]]:=i;
7 end; 8 return S[n];

Poprawność jest pozostawiona jako ćwiczenie.

Tablica Silnych Prefikso-Sufiksów

Wprowadzimy teraz tablicę ‘’silnych’’ prefikso-sufiksów dla wzorca x[1..m]: jeśli j<|x|, to P[j]=k, gdzie k jest maksymalnym rozmiarem słowa będącego właściwym prefiksem i sufiksem x[1..j] i spełniającego dodatkowy warunek x[k+1]x[j+1] dla j<n.
Jeśli takiego k nie ma, to przyjmujemy P[j]=1. Przyjmujemy ponadto, że P[m]=P[m].

Wartości tablicy P' mogą być znacznie mniejsze niż wartości tablicy P.

Przykład

Dla x = abaab mamy:

P[0..5] = [1, 0, 0, 1, 1, 2 ];  P[0..5] = [1, 0, 1, 1, 0, 2 ].


Algorytm bazuje na następującej relacji między P a P':

(t=P[j] oraz x[t+1]x[j+1])  P[j]=t

(t=P[j], t0, oraz x[t+1]=x[j+1])  P[j]=P[t]

Nie musimy obliczać tablicy P; potrzebna jest jedynie ostatnia wartość t=P[j], którą obliczamy on-line.


Algorytm Silne-Prefikso-Sufiksy


1  P[0]:=1; t:=1;
2  for j:= 1 to m do begin // t=P[j1] 
3    while t0 and x[t+1]x[j]do
4       t:=P[t];
5    t:=t+1;
6    if j=m or x[t+1]x[j+1]
7      then P[j]:=t else P[j]:=P[t];
8  end;

Gdy weźmiemy x = abam2 to P[0]=1, P[1]=0, P[2]=1, oraz P[j]=1 dla 3jm. To jest pesymistyczny przypadek dla algorytmu Silne-Prefikso-Sufiksy, algorytm wykonuje 3m5 porównań symboli.

String-matching: algorytm Knutha-Morrisa-Pratta

Przedstawimy klasyczny algorytm Knutha-Morrisa-Pratta (w skrócie KMP) dla problemu string-matchingu:   obliczyć w w tekście y wszystkie (lub pierwsze) wystąpienia danego tekstu x, zwanego wzorcem.

Oznaczmy m=|x|,n=|y|, gdzie mn.

Operacją dominującą w algorytmie jest porównanie dwóch symboli.

Zaczniemy od obliczania jedynie pierwszego wystąpienia. Algorytm KMP przegląda tekst y od lewej do prawej, sprawdzając, czy jest zgodność na pozycji j+1 we wzorcu x oraz na pozycji i+j+1 w tekście y. Jeśli jest niezgodność, to przesuwamy potencjalny początek (pozycja i) wystąpienia x w y. Zakładamy, że algorytm zwraca na końcu wartość false, jeśli nie zwróci wcześniej true. Pozostawiamy dowód poprawności (określenie niezmienników) jako ćwiczenie.


Algorytm Algorytm KMP


1  i:=0; j:=0;
2  while inm do begin
3    while j<m and x[j+1]=y[i+j+1] do j=j+1;
4    if j=m then return(true);
5    i:=i+jP[j]; j:=max(0,P[j])
6  end;

Operacją dominującą w algorytmie jest operacja: x[j+1]=y[i+j+1].

Algorytm KMP wykonuje co najwyżej 2n-m porównań symboli. Zauważmy, że dla danej pozycji w tekście y jest ona co najwyżej raz porównana z pewną pozycją we wzorcu w porównaniu pozytywnym (gdy symbole są równe). Jednocześnie każde negatywne porównanie powoduje przesunięcie pozycji i co najmniej o jeden, maksymalna wartość i wynosi n-m, zatem mamy takich porównań co najwyżej n-m, w sumie co najwyżej 2n-m porównań w algorytmie KMP.


<flash>file=KMP.swf|width=450|height=220</flash>

Algorytm dla x=ab, y=aa..a wykonuje 2n-2porównania, zatem 2n-m jest dolną i jednocześnie górną granicą na liczbę porównań w algorytmie.

Obserwacja. Tablicę P' możemy w algorytmie KMP zamienić na P bez zmiany złożoności pesymistycznej.

W wersji on-line algorytmu okaże się, że jest zdecydowana różnica między użyciem P' i P; to właśnie jest motywacją dla wprowadzenia silnych prefikso-sufiksów.



Rysunek 1: Jedna iteracja algorytmu KMP. Przesunięcie shift=jP[j] potencjalnego początku wystąpienia wzorca gdy x[j+1]y[i+j+1].

Wersja on-line algorytmu KMP

Przedstawimy teraz wersję on-line algorytmu KMP. Wczytujemy kolejne symbole y i wypisujemy on-line (na bieżąco) odpowiedź:

  • 0 - gdy dotychczas wczytany tekst nie zawiera x jako sufiks,
  • 1 - jeśli zawiera

Algorytm On-Line-KMP


1  repeat forever
2    read(symbol);
3    while j>1 and x[j+1]symbol do j:=P[j];
4    j:=j+1;
5    if j=m then 
6      write(1); j:=P[m];
7    else write(0);

Oznaczmy przez delay(m) maksymalną liczbę kroków algorytmu On-Line-KMP między wczytaniem symbolu i daniem odpowiedzi. Przez delay'(m) oznaczmy podobną wielkość, w sytuacji gdy zamiast tablicy P' użyjemy P.

Przykład

Jeśli x=aaaaa oraz y=am1b, to delay(m)=O(1), delay(m)=Θ(m).

Z lematu o okresowości wynika, że zachodzi następujący fakt:

delay(m) = Θ(logm)

Uzasadnienie pozostawiamy jako ćwiczenie.

Wersja ‘’real-time’’ algorytmu KMP

Pokażemy teraz wersję algorytmu on-line, która działa w czasie rzeczywistym, tzn. czas reakcji między wczytaniem symbolu a daniem odpowiedzi jest O(1), niezależnie od rozmiaru alfabetu.

Algorytm zachowuje się podobnie jak algorytm On-Line-KMP; podstawowa różnica polega na tym, że algorytm wkłada do kolejki wczytane symbole, które jeszcze nie są przetworzone w sensie algorytmu KMP. Rysunek pokazuje relacje tego algorytmu do algorytmu KMP. Symbole z wejścia najpierw wędrują do kolejki.


Rysunek 2: Typowa konfiguracja w algorytmie real-time-KMP.


Algorytm Real-Time-KMP


1  inicjalizacja:  j:=0; Kolejka :=;
2  repeat forever
3    read(symbol);
4    insert(symbol,Kolejka); 
5    write(OUTPUT(Kolejka, j));

W celu skrócenia zapisów pojedynczych algorytmów rozbijamy algorytm na dwie części. Zasadnicza część jest zapisana jako osobna funkcja OUTPUT(Kolejka, j). Wynikiem funkcji jest 0 lub 1, w zależności od tego czy ostatnio wczytany symbol kończy wystąpienie wzorca x. Zmienne Kolejka, j są globalne. Oczywiste jest, że opóźnienie (czas reakcji) tego algorytmu jest O(1).

Pozostawiamy jako ćwiczenie uzasadnienie tego, że algorytm Real-Time-KMP jest poprawny.

Algorytm Real-Time-KMP: funkcja OUTPUT(Kolejka, j)


1  output := 0;
2  repeat 2 times
3    if  Kolejka niepusta then
4      if j=1 then 
5        j := 0; delete(Kolejka);
6      else if  x[j+1]first(Kolejka) then   j:=P[j];
7      else 
8        j:=j+1; delete(Kolejka); 
9      if j=m
10       output :=1; j :=P[m]; 
11 return(output);


Wersja algorytmu KMP z 3n2 porównaniami

Algorytm KMP wykonuje co najmniej 2n-m porównań symboli. Załóżmy, że są to operacje dominujące i spróbujmy zmniejszyć stały współczynnik 2 do 32. Na początku załóżmy, że x=ab. Następujący algorytm znajduje wszystkie wystąpienia wzorca ‘’ab’’ w tekście y.

Algorytm Szukanie-ab


1  wzorcem jest ab 
2  i:=0; 
3  while inm do begin
4    while y[i+2]b doi=i+1;
5    if y[i+1]=a then 
6      wypisz-wystąpienie;i:=i+2
7  end;

Algorytm KMP dla wzorca ‘’ab’’ i tekstu ‘’aaa...aa’’ wykonywał 2n-2 porównań symboli, nowy algorytm jest lepszy. Algorytm Szukanie-ab wykonuje co najwyżej n porównań w tym przypadku. Dla tekstu ‘’abab’’ algorytm wykinuje n+1 porównań.

Pozostawiamy jako ćwiczenie policzenie maksymalnej liczby porównań dla tego algorytmu (wzorzec ‘’ab’’). Widać, że podstawowa idea to sprawdzanie najpierw pierwszego symbolu wzorca różnego od poprzednich.

Uogólnimy algorytm na dowolne wzorce. Niech x zawiera co najmniej dwa różne symbole, x=akbα, gdzie ab.Oznaczmy x=bα skrócony wzorzec

Przykład

x = aaaabaaaababa, wtedy x = baaaababa, α = aaaababa.


Podamy nieformalny zarys działania oszczędniejszej wersji algorytmu KMP, w której osobno szukamy x' i osobno części ak.


Niech KMP będzie taką wersją algorytmu KMP, w której szukamy jedynie wzorca x, ale tablica P jest obliczona dla wzorca x. Jeśli j>0 i shiftk, to wykonujemy przesunięcie potencjalnego początku i wzorca w y o k+1, gdzie shift=jP[j]. Inaczej mówiąc, nie szukamy wszystkich wystąpień x', ale jedynie takich, które mają sens z punktu widzenia potencjalnego znalezienia na lewo ciągu ak.

Tak zmodyfikowany algorytm KMP zastosujemy jako część algorytmu Oszczędny-KMP. Graficzna ilustracja działania algorytmu Oszczędny-KMP jest pokazana na rysunku.

Algorytm Oszczędny-KMP


Znajdujemy wystąpienia x' w tekście y[k+1..m] algorytmem KMP';
dla każdego wystąpienia x' sprawdzamy, czy na lewo jest wystąpienie ak;
nie sprawdzamy tych pozycji w y, których zgodność z pewną pozycją w x jest znana;



Rysunek 3:Typowa konfiguracja w algorytmie Oszczędny-KMP.

Pozostawiamy jako ćwiczenie dokładny zapis algorytmu oraz dokładniejszy dowód tego, że algorytm Oszczędny-KMP wykonuje co najwyżej 32n porównan.

Ogólna idea jest przedstawiona na rysunku.



Rysunek 4: Ilustracja tego, że liczba operacji dodatkowych jest ograniczona przez 12n.

Niech zasadniczymi operacjami będą operacje sprawdzania pierwszego b na danej pozycji tekstu y oraz te sprawdzania symboli, które są z wynikiem pozytywnym. Takich operacji jest co najwyżej n. Pozostałe operacje to

(1) sprawdzanie w części α z wynikiem negatywnym; wtedy przesuwamy wzorzec co najmniej o k,

(2) sprawdzanie części ak na lewo od pozytywnego b (w kwadraciku na rysunku), na pozycjach, gdzie wcześniej było sprawdzanie negatywnego b. Wtedy odległość między pozytywnymi kolejnymi b jest co najmniej 2w, gdzie wk jest liczbą sprawdzanych na lewo symboli a. Zatem ‘’lokalnie’’ przesunięcie jest co najmniej dwukrotnie większe niż liczba dodatkowych operacji.

Suma przesunięć wzorca na tekście y wynosi co najwyżej n, sumaryczna liczba dodatkowych operacji jest więc co najwyżej 12n, a liczba wszystkich operacji nie przekracza 32n.

Równoważność cykliczna słów

W poprzednich algorytmach porównywaliśmy symbole jedynie w sensie ich równości. Pokażemy teraz problem, który pokazuje użyteczność porządku liniowego na alfabecie.

Rotacją słowa u=u[1..n] jest każde słowo postaci u(k) = u[k+1..n]u[1..k]. (W szczególności u(0)=u(n)=u). Niech u,w będą słowami długości n; mówimy, że są one cyklicznie równoważne, gdy u(i)=w(j) dla pewnych i,j.

Naturalnym algorytmem sprawdzania cyklicznej równoważności jest szukanie słowa u w słowie ww, ale podamy algorytm znacznie prostszy bazujący na porządku leksykograficznym, który będzie działał w czasie liniowym i w miejscu (dodatkowa pamięć jest stała).

W algorytmie rozszerzamy tablice u,w na uu, ww, ale robimy to jedynie dla uproszczenia, w rzeczywistości możemy poruszać się cyklicznie po u i po w; modyfikację pozostawiamy jako ćwiczenie.

Algorytm Równoważność-Cykliczna


1  x:=uu; y:=ww;
2  i:=0; j:=0;
3  while (i<n) and (j<n) do begin
4    k:=1;
5    while x[i+k]=y[j+k] do k:=k+1;
6    if k>n then return true;
7    if x[i+k]>y[i+k] then i:=i+k else j:=j+k;
8  end;
9  return false;

Problem poprawności pozostawiamy jako ćwiczenie. Liczba porównań jest oczywiście liniowa. Pozostawiamy również jako ćwiczenie wyprowadzenie dokładnego wzoru na maksymalną liczbę porównań symboli dla tekstów długości n.