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

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Walen (dyskusja | edycje)
poprawki od KDX
m Zastępowanie tekstu – „<math> ” na „<math>”
 
(Nie pokazano 75 wersji utworzonych przez 4 użytkowników)
Linia 3: Linia 3:
__TOC__
__TOC__


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 tekstowe mają decydujące znaczenie przy wyszukiwaniu informacji typu tekstowego, ten typ informacji jest szczególnie popularny w informatyce, np. w edytorach tekstowych i wyszukiwarkach internetowych. 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>.






{{przyklad|||
{{przyklad|||
Dla <math>x\ =\ abababababb</math> mamy:
Dla <math>x= abababababb</math> mamy:
<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>).
}}
}}
<br>
Wprowadzimy również  tablicę ‘’'silnych 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>.
<br> Jeśli takiego k nie ma, to przyjmujemy <math>P'[j]=-1</math>.
<br> Przyjmujemy ponadto, że <math>P'[m]=P[m]</math>.
<br>
Wartości tablicy P' mogą być znacznie mniejsze niż wartości tablicy P.
{{przyklad|||
Dla <math>x= abaab</math> mamy:
<center><math>P[0..5]= [-1,\ 0,\ 0,\ 1,\ 1,\ 2\ ];\ \ P'[0..5]= [-1,\ 0,\ -1,\ 1,\ 0,\ 2\ ]</math>.</center>
}}
== Obliczanie tablicy  Prefikso-Sufiksów==
== 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:
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:


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


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


{{algorytm|Prefikso-Sufiksy|algorytm_prefikso_sufiksy|
{{algorytm|Prefikso-Sufiksy|algorytm_prefikso_sufiksy|
  1  <math>P[0]:=-1</math>; <math>t:=0</math>;
  1  <math>P[0]:=-1</math>; <math>t:=-1</math>;
  2  '''for''' <math>j:=1</math> '''to''' <math>m</math> '''do'''
  2  '''for''' <math>j:=1</math> '''to''' <math>m</math> '''do'''
  3  '''begin'''
  3  '''begin'''
Linia 40: Linia 53:


== 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 59:


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 tekst <math>x[1..i]</math>.  
pokrywającego tekstu ''x''. Obliczamy wartości <math>S[i]</math> długości najkrótszego słowa pokrywającego <math>x[1
 
\ldots i]</math> dla każdego <math>1 \le i \le n</math>.
Następujący algorytm oblicza długość minimalnego słowa
pokrywającego tekstu ''x''.  Algorytm jest efektywny ponieważ liczy dodatkową tablicę ''Zakres''.
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>


<center> [[Grafika:Minpokslowo.jpg]]<br>
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>S[8]=2,\ Zakres[3]=13</math>.
Zatem spełniony jest warunek  <math>i-Zakres[S[P[i]] \le S[P[i]]</math>. Po zakończeniu <math>i</math>-tej iteracji
mamy  <math>S[15]=3,Zakres[3]=15</math>.


Rysunek 3: <math>i</math>-ta iteracja algorytmu dla <math>i=15</math> oraz słowa  <math>x\ =\ abaabababaababa\ldots</math>. Tuż przed
{{algorytm | Rozmiar-Minimalnego-Pokrycia|algorym_rozm_min_pokr|
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
<Source>
mamy  <math>S[15]=3,Zakres[3]=15</math>, ponieważ <math>i-Zakres[3] \le q</math>. </center>
[[pascal,1]]
for i:=2 to n do begin
  Zakres[i]=i; S[i]=i;
end;
for i:=2 to n do
  if P[i]>0 and i-Zakres[S[P[i]]] <= S[P[i]] then begin
    S[i] := S[P[i]];  Zakres[S[P[i]] := i
  end;
return S[n];
</Source>
}}


<!--
{{algorytm | Rozmiar-Minimalnego-Pokrycia|algorym_rozm_min_pokr|
{{algorytm | Rozmiar-Minimalnego-Pokrycia|algorym_rozm_min_pokr|
  1  '''for ''' <math>i:=2</math> '''to''' <math>n</math> '''do''' '''begin'''
  1  '''for ''' <math>i:=2</math> '''to''' <math>n</math> '''do''' '''begin'''
Linia 69: Linia 97:
  7    '''end;'''
  7    '''end;'''
  8  '''return''' <math>S[n]</math>;
  8  '''return''' <math>S[n]</math>;
}}
}}-->


Poprawność jest pozostawiona jako ćwiczenie.
==Algorytmy Knutha-Morrisa-Pratta i Morrisa-Pratta==


==Tablica Silnych Prefikso-Sufiksów==
Przedstawimy klasyczne algorytmy Knutha-Morrisa-Pratta (w skrócie KMP) oraz Morrisa-Pratta (w skrócie MP)
 
dla problemu ''string-matching''u:
<font color="red">TODO - KDX - ???</font>
&nbsp; obliczyć w w tekście <math>y</math> wszystkie (lub pierwsze) wystąpienia danego tekstu <math>x</math>, zwanego wzorcem.


Wprowadzimy silną tablicę prefikso-sufiksów dla wzorca <math>x[1..m]</math>:
Algorytmy MP i KMP różnią się jedynie tym że jeden używa tablicy P a drugi P'. Tablica P' jest bardziej skomplikowana, będziemy się zatem głównie koncentrować na algorytmie MP, poza wersją on-line (gdzie waśnie P' ma przewagę).
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>.
 
Wartości tablicy P' mogą być znacznie mniejsze niż wartości tablicy P.  
 
{{przyklad|||
Dla <math>x\ =\ abaab</math> mamy:
<center><math>P[0..5]\ =\ [-1,\ 0,\ 0,\ 1,\ 1,\ 2\ ];\ \ P'[0..5]\ =\ [-1,\ 0,\ -1,\ 1,\ 0,\ 2\ ].</math></center>
}}
 
 
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],\ t\ge 0,\ \textrm{oraz}\ x[t+1]= x[j+1])\ \Rightarrow\ P'[j]=P'[t]</math></center><br>
 
Nie musimy obliczać tablicy P; potrzebna jest jedynie ostatnia wartość <math>t=P[j]</math>, którą obliczamy on-line.
 
 
{{algorytm|Silne-Prefikso-Sufiksy|algorytm_silne_prefikso_sufiksy|
1  <math>P'[0]:=-1</math>; <math>t:=-</math>1;
2  '''for''' <math>j:=</math> 1 '''to''' <math>m</math> '''do''' '''begin''' // <math>t=P[j-1]</math>
3    '''while''' <math>t\geq 0</math> '''and''' <math>x[t+1]\neq x[j]</math>'''do'''
4      <math>t:=P'[t]</math>;
5    <math>t:=t+1</math>;
6    '''if''' <math>j=m</math> '''or''' <math>x[t+1]\neq x[j+1]</math>
7      '''then''' <math>P'[j]:=t</math> '''else''' <math>P'[j]:=P'[t]</math>;
8  '''end;'''
}}
 
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==
 
Przedstawimy klasyczny algorytm Knutha-Morrisa-Pratta (w skrócie KMP) dla problemu ''string-matching''u:
&nbsp; obliczyć w w tekście <math>y</math> wszystkie (lub pierwsze) wystąpienia danego tekstu <math>x</math>, zwanego wzorcem.


Oznaczmy <math>m=|x|, n=|y|</math>, gdzie <math>m\le n</math>.
Oznaczmy <math>m=|x|, n=|y|</math>, gdzie <math>m\le n</math>.


Operacją ''dominującą'' w algorytmie jest porównanie dwóch symboli.
Zaczniemy od obliczania jedynie pierwszego wystąpienia. Algorytm MP 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''.  
 
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.




{{algorytm|Algorytm KMP|algorytm_kmp|
{{algorytm|Algorytm MP|algorytm_kmp|
  1  <math>i:=0</math>; <math>j:=0</math>;
  1  <math>i:=0</math>; <math>j:=0</math>;
  2  '''while''' <math>i\leq n-m</math> '''do''' '''begin'''
  2  '''while''' <math>i\leq n-m</math> '''do''' '''begin'''
  3    '''while''' <math>j<m</math> '''and''' <math>x[j+1]=y[i+j+1]</math> '''do''' <math>j=j+1</math>;
  3    '''while''' <math>j<m</math> '''and''' <math>x[j+1]=y[i+j+1]</math> '''do''' <math>j=j+1</math>;
  4    '''if''' <math>j=m</math> '''then return'''(true);
  4    '''if''' <math>j=m</math> '''then return'''(true);
  5    <math>i:=i+j-P'[j]</math>; <math>j:=\max(0,P'[j])</math>
  5    <math>i:=i+j-P[j]</math>; <math>j:=\max(0,P[j])</math>
  6  '''end;'''
  6  '''end;'''
}}
}}


Operacją '''dominującą''' w algorytmie jest  operacja: <math>x[j+1]=y[i+j+1]</math>.
<br>
'''Uwaga:''' Algorytm działa podobnie gdy zamiast  prefikso-sufiksów użyjemy tablicy P' silnych prefisko-sufksów. Algorytm w całości jest wtedy bardziej skomplikowany ze względu na trudniejszy ''preprocessing''
(liczenie P' jest trudniejsze od P).


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 MP z tablicą P' zamiast P nazywamy '''algorytmem Knutha-Morrisa-Pratta''' i oznaczamy przez KMP.
<br>


Operacją '''dominującą''' w algorytmach KMP i MP jest  operacja porównania symboli: <math>x[j+1]=y[i+j+1]</math>.
Algorytmy KMP i MP wykonują 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ń.
<br> Poniższa animacja pokazuje przykładowe działanie algorytmu KMP.




Linia 142: Linia 138:
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>
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'''.  
 
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.
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.


Linia 158: Linia 153:


{{algorytm|On-Line-KMP|algorytm_online_KMP|
{{algorytm|On-Line-KMP|algorytm_online_KMP|
0  <math>j:=0;</math>
  1  '''repeat forever'''
  1  '''repeat forever'''
  2    read(<math>symbol</math>);
  2    read(<math>symbol</math>);
Linia 167: Linia 163:
}}
}}


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


{{przyklad|||
{{przyklad|||
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>.
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:
<center><math> delay(m)\ =\ \Theta(\log m)</math></center>
<center><math>delay(m)= \Theta(\log m)</math></center>


Uzasadnienie pozostawiamy jako ćwiczenie.
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.
<center>[[Grafika:Rtkmp.png]]<br>
Rysunek 2: Typowa konfiguracja w algorytmie real-time-KMP.</center>




{{algorytm|Real-Time-KMP|algorytm_rtkmp|
==Obliczanie Tablicy Silnych Prefikso-Sufiksów==
1  inicjalizacja:  <math>j:=0</math>; Kolejka <math>:= \emptyset</math>;
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). 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).
Algorytm liczenia silnych prefikso-sufiksów bazuje na następującej relacji między P a P':
<center><math>(t=P[j]\ \text{oraz}\ x[t+1]\neq x[j+1])\ \Rightarrow\ P'[j]=t</math></center><br>


Pozostawiamy jako ćwiczenie uzasadnienie tego, że algorytm Real-Time-KMP jest poprawny.
<center><math>(t=P[j],\ t\ge 0,\ \text{oraz}\ x[t+1]= x[j+1])\ \Rightarrow\ P'[j]=P'[t]</math></center><br>


{{algorytm|Real-Time-KMP: funkcja OUTPUT(Kolejka, j)|algorytm_rtkmp_fun|
Nie musimy obliczać tablicy P; potrzebna jest jedynie ostatnia wartość <math>t=P[j]</math>, którą obliczamy on-line.
1  output <math>:=</math> 0;
2  '''repeat 2 times'''
3    '''if ''' Kolejka niepusta '''then'''
4      '''if''' <math>j=-1</math> '''then'''
5        j <math>:=</math> 0; delete(Kolejka);
6      '''else if'''  <math>x[j+1]\ne first(Kolejka)</math> '''then'''  <math>j:=P'[j]</math>;
7      '''else'''
8        <math>j:=j+1</math>; delete(Kolejka);
9      '''if''' <math>j=m</math>
10      output <math>:= </math>1; j <math>:= P'[m]</math>;
11 '''return'''(output);


}}


==Wersja algorytmu KMP z <math>\frac{3n}{2}</math> porównaniami==
{{algorytm|Silne-Prefikso-Sufiksy|algorytm_silne_prefikso_sufiksy|
 
<math>P'[0]:=-1</math>; <math>t:=-1</math>;<br>
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.
  2  '''for''' <math>j:=</math> 1 '''to''' <math>m</math> '''do''' <br>
 
3    '''while''' <math>t\geq 0</math> '''and''' <math>x[t+1]\neq x[j]</math>'''do'''
{{algorytm|Szukanie-ab|algorytm_szukanie_ab|
4          <math>t:=P'[t]</math>;
1 wzorcem jest <math>ab</math>  
5    <math>t:=t+1</math>;
  2  <math>i:=0</math>;
  6   '''if''' <math>j=m</math> '''or''' <math>x[t+1]\neq x[j+1]</math>
'''while''' <math>i\leq n-m</math> '''do''' '''begin'''
7      '''then''' <math>P'[j]:=t</math> '''else''' <math>P'[j]:=P'[t]</math>;
4    '''while''' <math>y[i+2]\ne b</math> '''do'''<math>i=i+1</math>;
  5   '''if''' <math>y[i+1]= a</math> '''then'''  
6      wypisz-wystąpienie;i<math>:=</math>i+2
'''end;'''
}}
}}
 
<br>
Algorytm KMP dla wzorca ab wykonywał 2n-2 porównań symboli, nowy algorytm jest lepszy. Zachodzi fakt:
Gdy weźmiemy <math>x= aba^{m-2}</math> to
&nbsp;&nbsp;&nbsp; algorytm Szukanie-ab wykonuje co najwyżej n porównań w tym przypadku.
<br>
 
<math>P'[0]=-1</math>, <math>P'[1]=0</math>, <math>P'[2]=-1</math>, oraz dla <math>3\leq j\leq m</math> <math>P'[j]=1</math>. <br>
Uzasadnienie pozostawiamy jako ćwiczenie.
Jest to jest pesymistyczny przypadek dla algorytmu Silne-Prefikso-Sufiksy, algorytm wykonuje wtedy <math>3m-5</math> porównań symboli.
 
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''
 
{{przyklad|||
<math>x\ =\ aaaabaaaababa</math>, wtedy <math>x'\ =\ baaaababa</math>, <math>\alpha\ =\ aaaababa</math>.
 
}}
 
 
Podamy nieformalny zarys działania oszczędniejszej wersji algorytmu KMP, w której osobno szukamy x' i osobno części <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.
Graficzna ilustracja działania algorytmu Oszczędny-KMP jest pokazana na rysunku.
 
{{algorytm|Oszczędny-KMP|algorytm_okmp|
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>
nie sprawdzamy tych pozycji w  y, których zgodność z pewną pozycją w x jest znana; <br>
<center>
[[Grafika:Okmp.png]]<br><br>
Rysunek 3:Typowa konfiguracja w algorytmie Oszczędny-KMP.
}}
</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.
 
Ogólna idea jest przedstawiona na rysunku.
 
<center>[[Grafika:Okmp2.png]]<br><br>
Rysunek 4: Ilustracja tego, że liczba operacji dodatkowych jest ograniczona przez <math>\frac{1}{2}n</math>.</center>
 
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,
 
(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 liczba wszystkich operacji nie przekracza <math>\frac{3}{2}n</math>.
 
== 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 <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,
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
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  <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>; modyfikację  pozostawiamy jako
ćwiczenie.
 
{{algorytm|Równoważność-Cykliczna|algorytm_równoważność_cykliczna|
1  <math>x:=uu</math>; <math>y:=ww</math>;
2  <math>i:=0</math>; <math>j:=0</math>;
3  '''while''' <math>(i<n)</math> '''and''' <math>(j<n)</math> '''do''' '''begin'''
4    <math>k:=1</math>;
5   '''while''' <math>x[i+k]=y[j+k]</math> '''do''' <math>k:=k+1</math>;
6    '''if''' <math>k>n</math> '''then return''' true;
7    '''if''' <math>x[i+k]>y[i+k]</math> '''then''' <math>i:=i+k</math> '''else''' <math>j:=j+k</math>;
8  '''end;'''
9  '''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 <math>n</math>.

Aktualna wersja na dzień 10:28, 5 wrz 2023

Algorytmy tekstowe I

Algorytmy tekstowe mają decydujące znaczenie przy wyszukiwaniu informacji typu tekstowego, ten typ informacji jest szczególnie popularny w informatyce, np. w edytorach tekstowych i wyszukiwarkach internetowych. 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).


Wprowadzimy również 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 ].

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], to 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:=1;
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 tekst x[1..i].

Następujący algorytm oblicza długość minimalnego słowa pokrywającego tekstu x. Algorytm jest efektywny ponieważ liczy dodatkową tablicę Zakres. 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, S[8]=2, Zakres[3]=13. Zatem spełniony jest warunek iZakres[S[P[i]]S[P[i]]. Po zakończeniu i-tej iteracji mamy S[15]=3,Zakres[3]=15.

Algorytm Rozmiar-Minimalnego-Pokrycia


[[pascal,1]]
for i:=2 to n do begin
   Zakres[i]=i; S[i]=i;
end;
for i:=2 to n do
  if P[i]>0 and i-Zakres[S[P[i]]] <= S[P[i]] then begin
    S[i] := S[P[i]];  Zakres[S[P[i]] := i
  end;
return S[n];


Algorytmy Knutha-Morrisa-Pratta i Morrisa-Pratta

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

Algorytmy MP i KMP różnią się jedynie tym że jeden używa tablicy P a drugi P'. Tablica P' jest bardziej skomplikowana, będziemy się zatem głównie koncentrować na algorytmie MP, poza wersją on-line (gdzie waśnie P' ma przewagę).

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

Zaczniemy od obliczania jedynie pierwszego wystąpienia. Algorytm MP 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.


Algorytm Algorytm MP


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;


Uwaga: Algorytm działa podobnie gdy zamiast prefikso-sufiksów użyjemy tablicy P' silnych prefisko-sufksów. Algorytm w całości jest wtedy bardziej skomplikowany ze względu na trudniejszy preprocessing (liczenie P' jest trudniejsze od P).

Algorytm MP z tablicą P' zamiast P nazywamy algorytmem Knutha-Morrisa-Pratta i oznaczamy przez KMP.

Operacją dominującą w algorytmach KMP i MP jest operacja porównania symboli: x[j+1]=y[i+j+1]. Algorytmy KMP i MP wykonują 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ń.
Poniższa animacja pokazuje przykładowe działanie algorytmu 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. 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


0  j:=0;
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.



Obliczanie Tablicy Silnych Prefikso-Sufiksów

Algorytm liczenia silnych prefikso-sufiksów 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
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];


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