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

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Amal (dyskusja | edycje)
mNie podano opisu zmian
Amal (dyskusja | edycje)
mNie podano opisu zmian
Linia 81: Linia 81:
}}
}}


Algorytm bazuje na następującej relacji między P i P':
 
Algorytm bazuje na następującej relacji między P a P':
<center><math>(t=P[j]\ \textrm{oraz}\ x[t+1]\neq x[j+1])\ \Rightarrow\ P'[j]=t</math></center><br>
<center><math>(t=P[j]\ \textrm{oraz}\ x[t+1]\neq x[j+1])\ \Rightarrow\ P'[j]=t</math></center><br>


<center><math>(t=P[j],\ t\ge 0,\ \textrm{oraz}\ x[t+1]= x[j+1])\ \Rightarrow\ P'[j]=P'[t]</math></center><br>
<center><math>(t=P[j],\ t\ge 0,\ \textrm{oraz}\ x[t+1]= x[j+1])\ \Rightarrow\ P'[j]=P'[t]</math></center><br>


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




Linia 99: Linia 100:
}}
}}


Gdyweżmiemy <math>x\ =\ aba^{m-2}</math> to <math>P'[0]=-1</math>, <math>P'[1]=0</math>, <math>P'[2]=-1</math>,oraz <math>P'[j]=1</math>, dla <math>3\leq j\leq m</math>. To jest pesymistyczny przypadek dla algorytmu Silne-Prefikso-Sufiksy, algorytm wykonuje<math>3m-5</math> porównań symboli.
Gdy weźmiemy <math>x\ =\ aba^{m-2}</math> to <math>P'[0]=-1</math>, <math>P'[1]=0</math>, <math>P'[2]=-1</math>, oraz <math>P'[j]=1</math> dla <math>3\leq j\leq m</math>. To jest pesymistyczny przypadek dla algorytmu Silne-Prefikso-Sufiksy, algorytm wykonuje <math>3m-5</math> porównań symboli.


==String-matching: algorytm Knutha-Morrisa-Pratta==
==String-matching: algorytm Knutha-Morrisa-Pratta==
Linia 110: Linia 111:
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'' wartość ''false'' gdy 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 130: Linia 131:
<flash>file=KMP.swf|width=450|height=220</flash></center>
<flash>file=KMP.swf|width=450|height=220</flash></center>


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


'''Obserwacja'''. Tablicę P' możemy w algorytmie KMP zamienić na P bez zmiany złożoności pesymistycznej.
'''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 motywem wprowadzenia silnych prefikso-sufiksów.
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.


<center>[[Grafika:Kmp.gif]]
<center>[[Grafika:Kmp.gif]]
Linia 143: Linia 144:


==Wersja on-line algorytmu KMP==
==Wersja on-line algorytmu KMP==
Przedstawimy teraz wersję on-line algorytmu KMP. Wczytujemy kolejne symbole <math>y</math> i wypisujemy on-line (nabieżąco) odpowiedż:
Przedstawimy teraz wersję on-line algorytmu KMP. Wczytujemy kolejne symbole <math>y</math> i wypisujemy on-line (na bieżąco) odpowiedź:
* 0 - gdy dotychczas wczytany tekst nie zawiera x jako sufiks,
* 0 - gdy dotychczas wczytany tekst nie zawiera x jako sufiks,
* 1 - jeśli zawiera
* 1 - jeśli zawiera
Linia 160: Linia 161:


{{przyklad|||
{{przyklad|||
Jeśli <math>x=aaaa\dots a</math> oraz <math>y=a^{m-1b</math>, to <math>delay(m)=O(1)</math>, <math>delay'(m)=\Theta(m) </math>.
Jeśli <math>x=aaaa\dots a</math> oraz <math>y=a^{m-1}b</math>, to <math>delay(m)=O(1)</math>, <math>delay'(m)=\Theta(m) </math>.


Z lematu o okresowości wynika, że zachodzi następujący fakt:
Z lematu o okresowości wynika, że zachodzi następujący fakt:
Linia 170: Linia 171:
==Wersja real-time algorytmu KMP==
==Wersja real-time algorytmu KMP==


Pokażemy teraz wersje algorytmu on-line która działa real-time, tzn. czas reakcji między wczytaniem symbolu i 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.  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.
<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 187: Linia 188:
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). 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.


Oczywistym jest że opóźnienie (czas reakcji) tego algorytmu jest O(1).
Oczywiste jest, że opóźnienie (czas reakcji) tego algorytmu jest O(1).


Pozostawiamy jako ćwiczenie uzasadnienie tego, że algorytm Real-Time-KMP jest poprawny.  
Pozostawiamy jako ćwiczenie uzasadnienie tego, że algorytm Real-Time-KMP jest poprawny.  
Linia 206: Linia 207:
}}
}}


==Wersja algorytmu KMP z 3/2.n porównaniami==
==Wersja algorytmu KMP z 3n/2  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 222: Linia 223:
&nbsp;&nbsp;&nbsp; algorytm Szukanie-ab wykonuje co najwyżej n porównań w tym przypadku.  
&nbsp;&nbsp;&nbsp; algorytm Szukanie-ab wykonuje co najwyżej n porównań w tym przypadku.  


Uzasadnienie pozostawimay jako ćwiczenie.
Uzasadnienie pozostawiamy jako ćwiczenie.


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 235: Linia 236:




Niech <math>KMP'</math> będzie taką wersją algorytmu KMP w której jedynie szukamy wzorca <math>x'</math>, ale tablica <math>P'</math> jest policzona względem wzorca <math>x</math>.Jeśli <math>j>0</math> i <math>shift\le k</math> to wykonujemy przesunięcie potencjalnego początku i wzorca w y o k+1, gdzie <math>shift=j-P'[j]</math>. Inaczej mówiąc, nie szukamy wszystkich wystąpień x', ale jedynie takich, które mają sens pod względem potencjalnego znalezienia na lewo ciągu <math>a^k</math>.
Niech <math>KMP'</math> będzie taką wersją algorytmu KMP, w której szukamy jedynie wzorca <math>x'</math>, ale tablica <math>P'</math> jest obliczona dla wzorca <math>x</math>. Jeśli <math>j>0</math> i <math>shift\le k</math>, to wykonujemy przesunięcie potencjalnego początku i wzorca w y o k+1, gdzie <math>shift=j-P'[j]</math>. 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 <math>a^k</math>.


Tak zmodyfikowany algorytm KMP zastosujemy jako część algorytmu Oszczędny-KMP.
Tak zmodyfikowany algorytm KMP zastosujemy jako część algorytmu Oszczędny-KMP.
Linia 242: Linia 243:
{{algorytm|Oszczędny-KMP|algorytm_okmp|
{{algorytm|Oszczędny-KMP|algorytm_okmp|
Znajdujemy wystąpienia x' w tekście <math>y[k+1..m]</math> algorytmem KMP';<br>
Znajdujemy wystąpienia x' w tekście <math>y[k+1..m]</math> algorytmem KMP';<br>
dla każdego wystąpienia x' sprawdzamy czy na lewo jest wystąpienie <math>a^k</math>;<br>
dla każdego wystąpienia x' sprawdzamy, czy na lewo jest wystąpienie <math>a^k</math>;<br>
nie sprawdzamy tych pozycji w  y, których zgodność z pewną pozycją w x jest znana; <br>
nie sprawdzamy tych pozycji w  y, których zgodność z pewną pozycją w x jest znana; <br>
<center>
<center>
Linia 259: Linia 260:
Niech zasadniczymi operacjami będą operacje sprawdzania pierwszego b na danej pozycji tekstu y, oraz te sprawdzania symboli które sa z wynikiem pozytywnym. Takich operacji jest co najwyżej n. Pozostałe operacje to  
Niech zasadniczymi operacjami będą operacje sprawdzania pierwszego b na danej pozycji tekstu y, oraz te sprawdzania symboli które sa z wynikiem pozytywnym. Takich operacji jest co najwyżej n. Pozostałe operacje to  


(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> liczba 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, tak więc sumaryczna liczba  dodatkowych  operacji jest co najwyżej <math>\frac{1}{2}n</math>, a liczb wszstkich nie przekracza <math>\frac{3}{2}n</math>.
Suma przesunięć wzorca na tekście <math>y</math> wynosi co najwyżej n, tak więc sumaryczna liczba  dodatkowych  operacji jest co najwyżej <math>\frac{1}{2}n</math>, a liczba wszystkich operacji nie przekracza <math>\frac{3}{2}n</math>.


== Równoważność cykliczna słów ==
== Równoważność cykliczna słów ==
Linia 269: Linia 270:
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.
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 <math>u=u[1..n]</math> jest kaz'rde słowo postaci <math>u^{(k)}\ =\ u[k+1.. n]u[1.. k]</math>. (w szczególności
Rotacją słowa <math>u=u[1..n]</math> jest każde słowo postaci <math>u^{(k)}\ =\ u[k+1.. n]u[1.. k]</math>. (W szczególności
<math>u^{(0)}=u^{(n)}=u)</math>. Niech <math>u,w</math> będą słowami długości <math>n</math>, mówimy, że są one cyklicznie równoważne
<math>u^{(0)}=u^{(n)}=u)</math>. Niech <math>u,w</math> będą słowami długości <math>n</math>; mówimy, że są one cyklicznie równoważne,
gdy  <math>u^{(i)}=w^{(j)}</math> dla pewnych  <math>i,j</math>.
gdy  <math>u^{(i)}=w^{(j)}</math> dla pewnych  <math>i,j</math>.


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


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


Linia 286: Linia 287:
'''while''' <math>(i<n)</math> '''and''' <math>(j<n)</math> '''do'''<br>
'''while''' <math>(i<n)</math> '''and''' <math>(j<n)</math> '''do'''<br>
&nbsp;&nbsp;&nbsp;<math>k:=1</math>;<br>
&nbsp;&nbsp;&nbsp;<math>k:=1</math>;<br>
&nbsp;&nbsp;&nbsp;'''while'''<math>x[i+k]=y[j+k]</math> '''do''' <math>k:=k+1</math>;<br>
&nbsp;&nbsp;&nbsp;'''while''' <math>x[i+k]=y[j+k]</math> '''do''' <math>k:=k+1</math>;<br>
&nbsp;&nbsp;&nbsp;'''if''' <math>k>n</math> '''then return''' true;<br>
&nbsp;&nbsp;&nbsp;'''if''' <math>k>n</math> '''then return''' true;<br>
&nbsp;&nbsp;&nbsp;'''if''' <math>x[i+k]>y[i+k]</math> '''then'''<math>i:=i+k</math> '''else''' <math>j:=j+k</math>;<br>
&nbsp;&nbsp;&nbsp;'''if''' <math>x[i+k]>y[i+k]</math> '''then''' <math>i:=i+k</math> '''else''' <math>j:=j+k</math>;<br>
'''return''' false;<br>
'''return''' false;<br>
}}
}}
Linia 294: Linia 295:
Problem poprawności pozostawiamy jako ćwiczenie.
Problem poprawności pozostawiamy jako ćwiczenie.


Liczba porównań jest oczywiście liniowa. Pozostawiamy jako ćwiczenie policzenie 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 15:04, 26 wrz 2006

Algorytmy tekstowe I

Tekst jest ciągiem symboli; przyjmujemy że jest on zadany tablicą x[1..n], elementami której są symbole ze 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 liczba naturalna niezerowa 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 x) prefiks tekstu x będący jednocześnie sufiksem x. 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 potem 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:

x[j]=x[t+1] oraz t=P[j1]  P[j]=t+1

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

Algorytm Prefikso-Sufiksy


P[0]:=1; t:=1;
for j:=1 to m do
   while t0 and x[t+1]x[j] do t:=P[t]
   t:=t+1; P[j]:=t;

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 tablicy 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 dla prefiksu x[1..i]. Następujący algorytm oblicza długość minimalnego słowa pokrywającego tekstu x. Obliczamy wartości S[i] najmniejszej długości minimalnego słowa pokrywającego x[1i] dla każdego 1in. W i-tej iteracji algorytm pamięta jaki jest ``znany zakres każdego minimalnego słowa pokrywającego.


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

mamy S[15]=3,Zakres[3]=15, ponieważ iZakres[3]q.

Algorytm Rozmiar-Minimalnego-Pokrycia


for i:=2 to n do
   Zakres[i]=i,S[i]=i

for i:=2 to n do
   if P[i]>0 oraz iZakres[S[P[i]]S[P[i]] then
      S[i]:=S[P[i]]; Zakres[S[P[i]]:=i;

return S[n];

Poprawność jest pozostawiona jako ćwiczenie.

Tablica Silnych Prefikso-Sufiksów

Wprowadzimy silną tablicę 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


P[0]:=1; t:=1;
for j:= 1 to m do // t=P[j1]
   while t0 and x[t+1]x[j]do
      t:=P[t];
   t:=t+1;
   if j=m or x[t+1]x[j+1]
      then P[j]:=t else P[j]:=P[t];

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 (ang. pattern).

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


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

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

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 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


repeat forever
   read(symbol);
   while j>1 and x[j+1]symbol do j:=P[j];
   j:=j+1;
   if j=m then
      write(1); j:=P[m];
   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. 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.


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


Algorytm Real-Time-KMP


inicjalizacja: j:=0; Kolejka :=;
repeat forever
   read(symbol);
   insert(symbol,Kolejka);
   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). 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.

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)


output := 0;
repeat 2 times
   if Kolejka niepusta then
      if j=1 then
         j := 0; delete(Kolejka);
      else if x[j+1]first(Kolejka) then j:=P[j];
      else
         j:=j+1; delete(Kolejka);
         if j=m
            output :=1; j :=P[m];
return(output);


Wersja algorytmu KMP z 3n/2 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


wzorcem jest ab
i:=0;
while inm do
   while y[i+2]b doi=i+1;
   if y[i+1]=a then
      wypisz-wystąpienie;i:=i+2

Algorytm KMP dla wzorca ab wykonywał 2n-2 porównań symboli, nowy algorytm jest lepszy. Zachodzi fakt:     algorytm Szukanie-ab wykonuje co najwyżej n porównań w tym przypadku.

Uzasadnienie pozostawiamy jako ćwiczenie.

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 w pseudokodzie oraz 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 sa 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 liczba 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, tak więc sumaryczna liczba dodatkowych operacji jest 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


x:=uu; y:=ww;
i:=0; j:=0;
while (i<n) and (j<n) do
   k:=1;
   while x[i+k]=y[j+k] do k:=k+1;
   if k>n then return true;
   if x[i+k]>y[i+k] then i:=i+k else j:=j+k;
return false;

Problem poprawności pozostawiamy jako ćwiczenie.

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 n.