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
Walen (dyskusja | edycje)
poprawki od KDX
Linia 3: Linia 3:
__TOC__
__TOC__


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




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 <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 22: Linia 22:
== 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><math>x[j]=x[t+1]\ \textrm{oraz}\ t=P[j-1] \ \Rightarrow\ 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|
<math>P[0]:=-1</math>; <math>t:=-1</math>;<br>
<math>P[0]:=-1</math>; <math>t:=0</math>;
'''for''' <math>j:=1</math> '''to''' <math>m</math> '''do'''<br>
'''for''' <math>j:=1</math> '''to''' <math>m</math> '''do'''
&nbsp;&nbsp;&nbsp;'''while''' <math>t\geq 0</math> '''and''' <math>x[t+1]\neq x[j]</math> '''do''' <math>t:=P[t]</math><br>
3  '''begin'''
&nbsp;&nbsp;&nbsp;<math>t:=t+1</math>; <math>P[j]:=t</math>;
4    '''while''' <math>t\geq 0</math> '''and''' <math>x[t+1]\neq x[j]</math> '''do''' <math>t:=P[t];</math>
5    <math>t:=t+1</math>; <math>P[j]:=t</math>;
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 <math>t:=P[t]</math> zmniejsza wartość t co najmniej o jeden. Proste zastosowanie zasady magazynu (lub potencjału) implikuje, że operacji <math>t:=P[t]</math> wykonujemy co najwyżej n. Dowód poprawności pozostawiamy jako ćwiczenie.
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 <math>t:=P[t]</math> zmniejsza wartość ''t'' co najmniej o jeden. Proste zastosowanie zasady magazynu (lub potencjału) implikuje, że operacji <math>t:=P[t]</math> wykonujemy co najwyżej ''n''. Dowód poprawności pozostawiamy jako ćwiczenie.


== Minimalne słowo pokrywające ==
== Minimalne słowo pokrywające ==
Pokażemy pewne proste zastosowanie tablicy 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''.
Zajmiemy się problemem: obliczyć w czasie liniowym długość najkrótszego słowa pokrywającego dany tekst x.
Zajmiemy się problemem: obliczyć w czasie liniowym długość najkrótszego słowa pokrywającego dany tekst ''x''.


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 dla prefiksu <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> najmniejszej długości minimalnego słowa pokrywającego <math>x[1
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>.
\ldots i]</math> dla każdego <math>1 \le i \le n</math>.
W <math>i</math>-tej
W <math>i</math>-tej
iteracji algorytm pamięta jaki jest ``znany zakres'' każdego minimalnego słowa pokrywającego.
iteracji algorytmu pamiętany jest ''znany 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>
Linia 57: Linia 61:


{{algorytm | Rozmiar-Minimalnego-Pokrycia|algorym_rozm_min_pokr|
{{algorytm | Rozmiar-Minimalnego-Pokrycia|algorym_rozm_min_pokr|
'''for ''' <math>i:=2</math> '''to''' <math>n</math> '''do'''<br>
'''for ''' <math>i:=2</math> '''to''' <math>n</math> '''do''' '''begin'''
&nbsp;&nbsp;&nbsp;<math>Zakres[i]=i, S[i]=i</math><br>
2    <math>Zakres[i]=i; S[i]=i;</math>
 
3  '''end;'''
'''for ''' <math>i:=2</math> '''to''' <math>n</math> '''do'''<br>
'''for ''' <math>i:=2</math> '''to''' <math>n</math> '''do'''
&nbsp;&nbsp;&nbsp;'''if''' <math>P[i]>0</math> oraz <math>i-Zakres[S[P[i]] \le S[P[i]]</math> '''then'''<br>
5    '''if''' <math>P[i]>0</math> '''and''' <math>i-Zakres[S[P[i]] \le S[P[i]]</math> '''then''' '''begin'''
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<math>S[i] := S[P[i]]</math>;  <math>Zakres[S[P[i]] := i</math>;<br>
6      <math>S[i] := S[P[i]]</math>;  <math>Zakres[S[P[i]] := i</math>;<br>
 
7    '''end;'''
'''return''' <math>S[n]</math>;
'''return''' <math>S[n]</math>;
}}
}}


Linia 70: Linia 74:


==Tablica Silnych Prefikso-Sufiksów==
==Tablica Silnych Prefikso-Sufiksów==
<font color="red">TODO - KDX - ???</font>
Wprowadzimy silną tablicę prefikso-sufiksów dla wzorca <math>x[1..m]</math>:  
Wprowadzimy silną tablicę prefikso-sufiksów dla wzorca <math>x[1..m]</math>:  
&nbsp;&nbsp;&nbsp;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 91: Linia 98:


{{algorytm|Silne-Prefikso-Sufiksy|algorytm_silne_prefikso_sufiksy|
{{algorytm|Silne-Prefikso-Sufiksy|algorytm_silne_prefikso_sufiksy|
<math>P'[0]:=-1</math>; <math>t:=-</math>1;<br>
<math>P'[0]:=-1</math>; <math>t:=-</math>1;
'''for''' <math>j:=</math> 1 '''to''' <math>m</math> '''do''' // <math>t=P[j-1]</math> <br>
'''for''' <math>j:=</math> 1 '''to''' <math>m</math> '''do''' '''begin''' // <math>t=P[j-1]</math>  
&nbsp;&nbsp;&nbsp;'''while''' <math>t\geq 0</math> '''and''' <math>x[t+1]\neq x[j]</math>'''do'''<br>
3    '''while''' <math>t\geq 0</math> '''and''' <math>x[t+1]\neq x[j]</math>'''do'''
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<math>t:=P'[t]</math>;<br>
4      <math>t:=P'[t]</math>;
&nbsp;&nbsp;&nbsp;<math>t:=t+1</math>;<br>
5    <math>t:=t+1</math>;
&nbsp;&nbsp;&nbsp;'''if''' <math>j=m</math> '''or''' <math>x[t+1]\neq x[j+1]</math><br>
6    '''if''' <math>j=m</math> '''or''' <math>x[t+1]\neq x[j+1]</math>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''then''' <math>P'[j]:=t</math> '''else''' <math>P'[j]:=P'[t]</math>;<br>
7      '''then''' <math>P'[j]:=t</math> '''else''' <math>P'[j]:=P'[t]</math>;
8  '''end;'''
}}
}}


Linia 105: Linia 113:


Przedstawimy klasyczny algorytm Knutha-Morrisa-Pratta (w skrócie KMP) dla problemu ''string-matching''u:
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 (ang. pattern).
&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>.
Linia 115: Linia 123:


{{algorytm|Algorytm KMP|algorytm_kmp|
{{algorytm|Algorytm KMP|algorytm_kmp|
<math>i:=0</math>; <math>j:=0</math>;<br>
<math>i:=0</math>; <math>j:=0</math>;
'''while''' <math>i\leq n-m</math> '''do'''<br>
'''while''' <math>i\leq n-m</math> '''do''' '''begin'''
&nbsp;&nbsp;&nbsp;'''while''' <math>j<m</math> '''and''' <math>x[j+1]=y[i+j+1]</math> '''do''' <math>j=j+1</math>;<br>
3    '''while''' <math>j<m</math> '''and''' <math>x[j+1]=y[i+j+1]</math> '''do''' <math>j=j+1</math>;
&nbsp;&nbsp;&nbsp;'''if''' <math>j=m</math> '''then return'''(true);<br>
4    '''if''' <math>j=m</math> '''then return'''(true);
&nbsp;&nbsp;&nbsp;<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;'''
}}
}}


Linia 149: Linia 158:


{{algorytm|On-Line-KMP|algorytm_online_KMP|
{{algorytm|On-Line-KMP|algorytm_online_KMP|
'''repeat forever'''<br>
'''repeat forever'''
&nbsp;&nbsp;&nbsp;read(<math>symbol</math>);<br>
2    read(<math>symbol</math>);
&nbsp;&nbsp;&nbsp;'''while''' <math>j>-1</math> and <math>x[j+1]\ne symbol</math> '''do''' <math>j:=P'[j]</math>;<br>
3    '''while''' <math>j>-1</math> and <math>x[j+1]\ne symbol</math> '''do''' <math>j:=P'[j]</math>;
&nbsp;&nbsp;&nbsp;<math>j:=j+1</math>;<br>
4    <math>j:=j+1</math>;
&nbsp;&nbsp;&nbsp;'''if''' <math>j=m</math> '''then''' <br>
5    '''if''' <math>j=m</math> '''then'''  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;write(1); <math>j := P'[m]</math>;<br>
6      write(1); <math>j := P'[m]</math>;
&nbsp;&nbsp;&nbsp;'''else''' write(0);
7    '''else''' write(0);
}}
}}


Linia 179: Linia 188:


{{algorytm|Real-Time-KMP|algorytm_rtkmp|
{{algorytm|Real-Time-KMP|algorytm_rtkmp|
inicjalizacja:  <math>j:=0</math>; Kolejka <math>:= \emptyset</math>;<br>
inicjalizacja:  <math>j:=0</math>; Kolejka <math>:= \emptyset</math>;
'''repeat forever'''<br>
'''repeat forever'''
&nbsp;&nbsp;&nbsp;read(symbol);<br>
3    read(symbol);
&nbsp;&nbsp;&nbsp;insert(symbol,Kolejka); <br>
4    insert(symbol,Kolejka);  
&nbsp;&nbsp;&nbsp;'''write'''(OUTPUT(Kolejka, j));
5    '''write'''(OUTPUT(Kolejka, j));
}}
}}


Linia 193: Linia 202:


{{algorytm|Real-Time-KMP: funkcja OUTPUT(Kolejka, j)|algorytm_rtkmp_fun|
{{algorytm|Real-Time-KMP: funkcja OUTPUT(Kolejka, j)|algorytm_rtkmp_fun|
output <math>:=</math> 0;<br>
output <math>:=</math> 0;
'''repeat 2 times'''<br>
'''repeat 2 times'''
&nbsp;&nbsp;&nbsp;'''if ''' Kolejka niepusta '''then'''<br>
3    '''if ''' Kolejka niepusta '''then'''
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''if''' <math>j=-1</math> '''then''' <br>
4      '''if''' <math>j=-1</math> '''then'''  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;j <math>:=</math> 0; delete(Kolejka);<br>
5        j <math>:=</math> 0; delete(Kolejka);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''else if'''  <math>x[j+1]\ne first(Kolejka)</math> '''then'''  <math>j:=P'[j]</math>;<br>
6      '''else if'''  <math>x[j+1]\ne first(Kolejka)</math> '''then'''  <math>j:=P'[j]</math>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''else''' <br>
7      '''else'''  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<math>j:=j+1</math>; delete(Kolejka); <br>
8        <math>j:=j+1</math>; delete(Kolejka);  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''if''' <math>j=m</math><br>
9      '''if''' <math>j=m</math>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;output <math>:= </math>1; j <math>:= P'[m]</math>; <br>
10      output <math>:= </math>1; j <math>:= P'[m]</math>;  
'''return'''(output);
11 '''return'''(output);


}}
}}


==Wersja algorytmu KMP z 3n/2 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|
wzorcem jest <math>ab</math> <br>
wzorcem jest <math>ab</math>  
<math>i:=0</math>; <br>
<math>i:=0</math>;  
'''while''' <math>i\leq n-m</math> '''do'''<br>
'''while''' <math>i\leq n-m</math> '''do''' '''begin'''
&nbsp;&nbsp;&nbsp;'''while''' <math>y[i+2]\ne b</math> '''do'''<math>i=i+1</math>;<br>
4    '''while''' <math>y[i+2]\ne b</math> '''do'''<math>i=i+1</math>;
&nbsp;&nbsp;&nbsp;'''if''' <math>y[i+1]= a</math> '''then''' <br>
5    '''if''' <math>y[i+1]= a</math> '''then'''  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;wypisz-wystąpienie;i<math>:=</math>i+2
6      wypisz-wystąpienie;i<math>:=</math>i+2
7  '''end;'''
}}
}}


Linia 283: Linia 293:


{{algorytm|Równoważność-Cykliczna|algorytm_równoważność_cykliczna|
{{algorytm|Równoważność-Cykliczna|algorytm_równoważność_cykliczna|
<math>x:=uu</math>; <math>y:=ww</math>;<br>
<math>x:=uu</math>; <math>y:=ww</math>;
<math>i:=0</math>; <math>j:=0</math>;<br>
<math>i:=0</math>; <math>j:=0</math>;
'''while''' <math>(i<n)</math> '''and''' <math>(j<n)</math> '''do'''<br>
'''while''' <math>(i<n)</math> '''and''' <math>(j<n)</math> '''do''' '''begin'''
&nbsp;&nbsp;&nbsp;<math>k:=1</math>;<br>
4    <math>k:=1</math>;
&nbsp;&nbsp;&nbsp;'''while''' <math>x[i+k]=y[j+k]</math> '''do''' <math>k:=k+1</math>;<br>
5    '''while''' <math>x[i+k]=y[j+k]</math> '''do''' <math>k:=k+1</math>;
&nbsp;&nbsp;&nbsp;'''if''' <math>k>n</math> '''then return''' true;<br>
6    '''if''' <math>k>n</math> '''then return''' true;
&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>
7    '''if''' <math>x[i+k]>y[i+k]</math> '''then''' <math>i:=i+k</math> '''else''' <math>j:=j+k</math>;
'''return''' false;<br>
8  '''end;'''
'''return''' false;
}}
}}



Wersja z 15:10, 27 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 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 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 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 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:

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

TODO - problem z rysunkiem, napis nie pasuje do podpisu, co to jest q?


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


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

TODO - KDX - ???

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


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

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


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


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


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


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 jako ćwiczenie wyprowadzenie dokładnego wzoru na maksymalną liczbę porównań symboli dla tekstów długości n.