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

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Rytter (dyskusja | edycje)
m Zastępowanie tekstu – „<math> ” na „<math>”
 
(Nie pokazano 73 wersji utworzonych przez 7 użytkowników)
Linia 1: Linia 1:
<font size="6"> Algorytmy tekstowe II </font>
__TOC__


__TOC__


Poprzednie algorytmy dokonywały jedynie na tekstach wejściowych operacji sprawdzania symboli na równość.
Załóżmy teraz, że alfabet jest liniowo uporządkowany. Pokażemy, że  porównywanie symboli w sensie
porządku liniowego można istotnie wykorzystać w algorytmach tekstowych. Porządek liniowy na symbolach
implikuje {\em porządek  leksykograficzny} na słowach, na przykład:
<center><math> ab < ababab < abb < abbaa < abbaaaaaaaaaaa < abbaaaaaab</math></center>


== Równoważność cykliczna słów ==
Najważniejszymi strukturami danych związanymi z tekstami są te, które dotyczą efektywnej reprezentacji zbioru  wszystkich podsłów  tekstu. Przed wszystkim interesuje nas to, żeby taka reprezentacja  przyspieszała wyszukiwanie słów, a jednocześnie żeby była konstruowalna  w czasie liniowym albo prawie liniowym (z dokładnością do logarytmów).


Pokażemy problem, który prawdopodobie najlepiej pokazuje użyteczność porządku liniowego na alfabecie.
Oznaczmy przez <math>Subwords(x)</math>  
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
wszystkie podsłowa tekstu <math>x</math>,  a wszystkie wystąpienia (początkowe pozycje) słowa <math>z</math> w słowie <math>x</math> oznaczmy przez <math>Occ(z,x)</math>. (Oznaczenie <math>Occ</math> jest skrótem od ang. ''occurrences'').
<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
Chcemy znaleźć taką reprezentację zbioru <math>Subwords(x)</math>, by można było łatwo odpowiedzieć na pytanie, czy <math>z\in Subwords(x)</math>,  
algorytm znacznie prostszy bazujący , który  będzie działal  w czasie liniowym i {\em w miejscu} (dodatkowa
co jest równoważne <math>Occ(z,x)\ne \emptyset</math>, jak również rozwiązywać inne problemy tekstowe. Poza tym chcemy, by rozmiar tej reprezentacji był liniowy, podczas gdy rozmiar <math>Subwords(x)</math> może być kwadratowy.
pamięć jest stała).    W algorytmie roszerzamy tablicę <math>u,w</math> na <math>uu,\ ww</math> ale robimy to jedynie dla
Spośród wielu dobrych reprezentacji najbardziej znanymi są ''tablice sufiksowe'' (oznaczane przez <math>SUF</math>), ''drzewa sufiksowe''
uproszczenia, w rzeczywistości możemy poruszać się cyklicznie po <math>u</math> i po <math>w</math>, pozostawiamy modyfikację jako
i ''grafy podsłów'' (nie rozważane w tym module).
ćwiczenie.  


{{algorytm|Równoważność-Cykliczna|algorytm_rownowaznosc_cykliczna|
===Tablice i drzewa sufiksowe===
<math>x:=uu</math>; <math>y:=ww</math>;<br>
 
<math>i:=0</math>; <math>j:=0</math>;<br>
Niech <math>x=a_{1}a_{2}\dots a_{n}</math> i niech <math>x_{n+1}=\#</math> będzie specjalnym znakiem leksykograficznie większym od każdego innego symbolu 9w przyszłości będziemy również używać <math>x_{n+1}=\#</math> jako najmniejszego
'''while''' <math>(i<n)</math> '''and''' <math>(j<n)</math> '''do'''<br>
symbolu).
&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>
Oznaczmy przez <math>sufiks_{i}=a_{i}a_{i+1}\dots a_{n}</math> sufiks tekstu ''x'' zaczynający się na pozycji ''i''-tej.
&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>
Niech <math>SUF[k]</math> będzie pozycją, od której zaczyna się ''k''-ty leksykograficznie sufiks ''x''.
'''return''' false;<br>
Sufiks zaczynający się na pozycji <math>(n+1)</math>-szej nie jest brany pod uwagę.
}}
 
Ciąg sufiksów posortowany leksykograficznie wygląda następująco:
 
<center><math>sufix_{SUF[1]}< sufix_{SUF[2]}<sufix_{SUF[3]}<\ldots sufix_{SUF[n]}</math></center>
 
 
<center> [[Grafika:Zasd_2_1.jpg]] </center>
<br>
 
Rysunek 1: Tablicą sufiksową tekstu <math>x= babaabababba\#</math> jest ciąg  <math>SUF= [4,\ 2,\ 5,\ 7,\ 9,\ 12,\ 3,\ 1,\ 6,\ 8,\ 11,\ 10]</math>
<br>
<br>
Oznaczmy przez <math>lcp[k]</math> długość wspólnego prefiksu <math>k</math>-tego i następnego sufiksu w kolejności leksykograficznej. Na rysunku wartości najdłuższego wspólnego prefiksu między kolejnymi słowami są przedstawione jako zacienione segmenty. Odpowiadają one tablicy  <math>lcp= [1,\ 3,\ 4,\ 2,\ 1,\ 0,\ 2,\ 4,\ 3,\ 2,\ 1]</math>.
<br>
Tablica sufiksowa ma następująca ‘’sympatyczną’’ własność: Niech
 
<math>min_{z}=\min\{k : z  \mbox{ jest prefiksem }  sufiks_{SUF[k]}\}</math>,
<math>max_{z}=\max\{k : z  \mbox{ jest prefiksem }  sufiks_{SUF[k]}\}</math>.
 
Wtedy <math>Occ(z,x)</math> jest przedziałem w tablicy sufiksowej od <math>min_{z}</math> do <math>max_{z}</math>.
 
'''Drzewo sufiksowe''' jest drzewem, w którym każda ścieżka jest etykietowana kolejnymi symbolami pewnego sufiksu, oraz każdy sufiks <math>x</math> jest obecny w drzewie. Gdy dwie ścieżki się ''rozjeżdżają'', tworzy się wierzchołek. Mówiąc bardziej formalnie, każdej krawędzi jest przypisane jako etykieta pewne podsłowo <math>x</math>. Krawędzie wychodzące z tego samego węzła różnią się pierwszymi symbolami swoich etykiet (patrz rysunek).
 
 
Etykiety są kodowane przedziałami w tekście <math>x</math>:  para liczb  <math>[i,j]</math> reprezentuje podsłowo <math>a_ia_{i+1}\ldots_j</math> (zobacz prawe drzewo na rysunku). Dzięki temu reprezentacja ma rozmiar <math>O(n)</math>. Wagą krawędzi jest długość odpowiadającego jej słowa.
<br> <br>
 
<center> [[Grafika: Zasd_2_2.jpg]]  
</center>
<br> Rysunek 2: Drzewo sufiksowe dla tekstu <math>x= babaabababba</math>. Na końcu jest dodany znak <math>\#</math>. Końcowe węzły zawierają informację, gdzie zaczyna się sufiks, którym dochodzimy do danego węzła.
 
 
Obie reprezentacje pozwalają szybko rozwiązywać problem string-matchingu oraz mają rozmiar liniowy.  Niech z będzie wzorcem o długości m, a <math>x</math> słowem  długości n. Z reguły <math>m << n</math>.
 
=== Szukanie podsłów===
 
Pokażemy, jak sprawdzać, czy <math>z</math> występuje w <math>x</math>.
 
'''Używając drzewa sufiksowego''' (czas <math>O(m)</math>)
 
''Idziemy'' od korzenia w dół czytając kolejne symbole <math>z</math>, czasami posuwamy się po wewnętrznej etykiecie pewnej krawędzi. Zbiór wystąpień odpowiada zbiorowi liści w poddrzewie węzła, do którego doszliśmy. Jeśli po drodze ''utknęliśmy'' i nie dało się dalej schodzić po drzewie, oznacza to, że <math>z \notin Subwords(x)</math>
 
'''Używając tablicy sufiksowej''' (czas <math>O(m \log n)</math>)
 
Możemy sprawdzić, czy <math>z</math> jest prefiksem <math>i</math>-tego sufiksu w czasie <math>O(m)</math>. Korzystając z tego, wykonujemy rodzaj  binarnego szukania. W ten sposób znajdujemy pierwszy sufiks, którego prefiksem jest z. Jeśli jest taki sufiks, to <math>z \in Subwords(x)</math>. W przeciwnym wypadku z nie jest podsłowem x.
 
Podobnie znajdujemy ostatni sufiks. Zbiór wystąpień odpowiada przedziałowi w tablicy <math>SUF</math> między obliczonymi pierwszym i ostatnim sufiksem zaczynającym się od z.
 
=== Wyznaczanie liczby podsłów===  
 
Pokażemy, jak znaleźć liczbę podsłów słowa <math>x</math> przy pomocy tablicy sufiksowej lub drzewa sufiksowego. Końcowego markera <math>\#</math> nie traktujemy jako części słowa <math>x</math>. Liczba podsłów jest równa <math>|Subwords(x)|</math>. Jeśli wszystkie symbole słowa są różne to <math>|Subwords(x)|={n \choose{2}}</math>.
 
'''Używając drzewa sufiksowego''', czas <math>O(n)</math>
 
<br> Sumujemy wagi krawędzi drzewa.


Zdefiniujmy:
'''Używając tablicy sufiksowej''', czas <math>O( n)</math>
<center>
<math>D(u)=\{k:1\leq k\leq n</math> oraz <math>u^{(k)}>w^{(j)}</math> dla pewnego <math>j\}</math>,<br>
<math>D(w)=\{k:1\leq k\leq n</math> oraz <math>w^{(k)}>u^{(j)}</math> dla pewnego <math>j\}</math>.
</center>


Skorzystamy z prostego faktu:
Niech <math>SUMA(lcp)</math> będzie sumą elementów tablicy <math>lcp</math>. Liczbę podsłów obliczamy jako
Jeśli <math>D(u)=[1.. n]</math> lub <math>D(w)=[1.. n]</math>, to <math>u,w</math> nie są równoważne.  


Uzasadnienie pozostawiamy jako ćwiczenie. Poprawność algorytmu wynika teraz z tego, że po każdej głównej iteracji zachodzi niezmiennik:
<center>
<math>D(w)\supseteq [1.. i]</math>\ oraz \ <math>D(u)\supseteq [1.. j]</math>.</center>


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>.
<center><math>{n+1\choose{2}}-SUMA(lcp)</math></center>


== String-matching w pamięci stałej dla specjalnych wzorów ==


Pozostawiamy jako ćwiczenie uzasadnienie tego, że liczba podsłów jest poprawnie obliczona (korzystając z drzewa sufisowego lub z tablicy sufiksowej).


Oznaczmy przez <math>MaxSuf(w)</math> maksymalny leksykograficznie sufiks słowa <math>w</math>. Słowo <math>x</math> nazwiemy specjalnym gdy <math>MaxSuf(x)=x</math>.


{{przyklad|||
{{przyklad|||
'bajtocja' nie jest słowem specjalnym, ale rotacja tego słowa 'tocjabaj' jest.}}
Dla przykładowego  tekstu <math>x = babaabababba</math> mamy <math>|Subwords(x)|=55</math>. Proponujemy to wyliczyć z tablicy sufiksowej i drzewa sufiksowego dla<math>x</math>, danego na rysunku. Suma elementów tablicy  <math>lcp</math> wynosi 23. Liczba podsłów to:  <math>78-23= 55</math>}}
 
Podobnie jak tablicę sufiksową możemy zdefiniować tablicę <math>ROT</math> odpowiadającą posortowanemu ciągowi wszystkich cyklicznych przesunięć słowa <math>x</math> (rotacji <math>x</math>).
 
Pozostawiamy jako ćwiczenie znalezienie liniowego algorytmu obliczania tablicy <math>ROT</math>, przy założeniu, że mamy liniowy algorytm obliczania tablicy sufiksowej.
 
 
'''Dygresja.''' Ciekawą klasę słów, dla których tablice <math>SUF,\ ROT</math> są szczególnie interesujące, stanowią słowa Fibonacciego <math>F_n</math>. W tym szczególnym przypadku załóżmy, że pozycje numerujemy od zera. Dla każdego <math>n</math> tablica <math>ROT</math> jest postępem arytmetycznym (modulo długość słowa). Natomiast tablica <math>SUF</math> jest postępem arytmetycznym, gdy  <math>n</math> jest parzyste.
 
 
Słowa Fibonacciego definiujemy następująco:<math>F_0=a,\ F_1=ab,\ F_{n+1}= F_n\cdot F_{n-1}</math> Na przykład: <math>F_3= abaab,\ F_4= abaababa,\ F_5= abaababaabaab</math>. Oznaczmy przez <math>SUF_n</math> tablicę <math>SUF</math> dla słowa Fibonacciego <math>F_n</math>; wtedy:
<center><math>SUF_4 = [7\;2\;5\;0\;3\;6\;1\;4], SUF_5 = [10\;7\;2\;11\;8\;5\;0\;3\;12\;9\;6\;1\;4]</math>.</center>




Dlaczego słowa o tej własności  są interesujące ? Większość szybkich algorytmów szukania podsłów korzysta z okresów <math>p</math> prefiksów słowa. Liczenie tych okresów w ogólnym przypadku jest ''wąskim gardłem'' w projekcie algorytmu. Natomiast dla słow specjalnych  liczenie okresów jest trywialne.
Pozostawiamy jako ćwiczenie znalezienie wzoru na <math>|Subwords(F_n)|</math>.


Jeśli  <math>x</math> jest specjalny to okres każdego prefiksu słowa <math>x</math> można policzyć następującym naiwnym
== Drzewa sufiksowe =>tablice sufiksowe ==
algorytmem;


{{ algorytm| Funkcja Naiwne-Liczenie-Okresu (j)|funkcja_naiwne_liczenie_okresu|
W celu znalezienia początkowych pozycji sufiksów w porządku leksykograficznym przechodzimy drzewo sufiksowe metodą DFS, zakładając, że dla każdego węzła lista jego synów jest w kolejności leksykograficznej etykiet krawędzi prowadzących do synów. Wystarczy sprawdzać pierwsze symbole tych etykiet.
<math>period:=1</math>;<br>
 
'''for''' <math>i:=2</math> to <math>j</math> '''do'''<br>
Załóżmy, że w liściach mamy początki sufiksów, które do nich prowadzą.
&nbsp;&nbsp;&nbsp;'''if''' <math>x[i] \ne x[i - period]</math> '''then''' <math>period := i</math>;<br>
'''return''' <math>period</math>;<br>
}}


{{przyklad|||
Kolejność odwiedzania liści w naszym przejściu metodą DFS automatycznie generuje elementy tablicy sufiksowej.
Funkcja ''Naiwne-Liczenie-Okresu'' daje zły wynik dla tekstów które nie są specjalne, na przykład  załóżmy że <center>
<math>x= (aba)^6a = abaabaabaabaabaabaa</math>.} </center> Wtedy kolejne wartości okresów dla pozycji <math>j=1,2,..</math>
są:
<center>
<table>
<tr>
<td>a</td>
<td>b</td>
<td>a</td>
<td>a</td>
<td>b</td>
<td>a</td>
<td>a</td>
<td>b</td>
<td>a</td>
<td>a</td>
<td>b</td>
<td>a</td>
<td>a</td>
<td>b</td>
<td>a</td>
<td>a</td>
<td>b</td>
<td>a</td>
<td>a </td>
</tr>
<tr>
<td>
1</td>
<td>2</td>
<td>2</td>
<td>4</td>
<td>5</td>
<td>5</td>
<td>7</td>
<td>8</td>
<td>8</td>
<td>10</td>
<td>11</td>
<td>11</td>
<td>13</td>
<td>14</td>
<td>14</td>
<td>16</td>
<td>17</td>
<td>17</td>
<td>19
</td>
</tr>
</table>
</center>
}}


Zatem ''Naiwne-Liczenie-Okresu''<math>(19)\ =\ 19</math>, dla  <math>x\ = \ (aba)^6a</math>, wynik całkowicie niepoprawny. Poprawność algorytmu jest wyjaśniona na rysunku. Korzystamy z prostej własności, że prefiks specjalnego słowa jest też specjalny.
== Tablice sufiksowe =>drzewa sufiksowe ==
Pokażemy konstruktywnie następujący istotny fakt:


<center>[[Grafika:Naiwneliczenieokresu.jpg]]<br>
jeśli znamy tablicę sufiksową i tablicę <math>lcp</math>, to drzewo sufiksowe dla danego tekstu możemy ''łatwo'' skonstruować w czasie liniowym.
Rysunek 1: Załóżmy, że w algorytmie  ''Naiwne-Liczenie-Okresu'' <math>x[i-period(i-1)] \ne x[i]</math>.  Niech
<math>a=x[i]</math>, <math>b=x[i-period]</math>. Ponieważ <math>uz</math> jest prefiksem  słowa specjalnego <math>x</math> zatem  <math>a <b</math>. Gdyby
<math>period(i)<i</math> to wtedy, ze względu na dwie okresowości,
<math>zb</math> jest właściwym podsłowem słowa  <math>x[1.. i-1]</math> oraz <math>zb>x</math>.
Zaprzecza to założeniu, że <math>x</math> jest specjalne. Zatem <math>period(i)=i</math>.
</center>


Opiszemy teraz program szukania wzorca <math>x</math>  w slowie <math>y</math>i, zakładając że x jest sepcjalne.
Przypuśćmy, że
Program wczytuje dwa teksty, pierwszy z nich jest specjalne:  <math>x</math> pamiętamy w tablicy <math>x[0..m-1]</math>, <math>y</math> w
<math>SUF= [i_1,i_2,\ldots,i_n]</math>, a więc:
tablicy <math>y[0..n-1]</math>.
<center><math>sufiks_{i_1}< sufiks_{i_2}< sufiks_{i_3}< \ldots sufiks_{i_n}</math>.</center>
Program wypisuje wszystkie wystapienia <math>x</math> w <math>y</math>, tzn. wszystkie takie pozycje
<math>i</math>, ze <math>y[i\ldots i+m-1]\ =\ x</math>. Zapisujemy program w języku C++.
{{algorytm|Specjalny-String-Matching|algorytm_specjalny_string_matching|
#include <iostream.h><br>
#include string.h<br>
int i=0,j=0,p=1;<br>
void przesun();<br>
main() {<br>
char[] x,y; cin>>x>>y; m=strlen(x); n=strlen(y);<br>
while (i <= n-m-1)<br>
&nbsp;&nbsp;&nbsp;{ if (j==m) {cout<<i<<endl;  przesun();};<br>
&nbsp;&nbsp;&nbsp;&nbsp;else if (x[j)==y[i+j] {j=j+1; if (j==1)||(x[j-1]!=x[j-1-p]) p=j;};<br>
&nbsp;&nbsp;&nbsp;&nbsp;else  przesun();  }}<br>




void przesun()
{ if (j-1<2p) {i=i+p; j=0;} else {j=j-p; i=i+p;}}


{{algorytm|Drzewo-Sufiksowe|algorytm_drzewo_sufiksowe|
1  <math>T :=</math> drzewo reprezentujące <math>sufiks_{i_1}</math> (jedna krawędź);
2  '''for''' <math>k:=2</math> '''to''' <math>n</math> '''do'''
3    wstaw nową ścieżkę o sumarycznej etykiecie <math>sufiks_{i_k}</math> do <math>T</math>;
}}
}}


Program jest wstępem do programu szukajacego dowolne posłowo, niekoniecznie o wlasnosci bycia
specjalnym. Postawowym niezmiennikiem  w programie przed kazdym wykonaniem i po kazdym zakonczeniu pętli ''while' jest:
'''(A)\ ''' <math>x[0 \ldots j-1]\ =\ y[i \ldots i+j-1]</math>, .
'''(B)\ ''' Program wypisal wszsytkie wczesniejsze wystapienia <math>i' < i</math>,
'''(C)\ ''' <math>p</math> jest okresem slowa <math>x[0 \ldots j-1]</math>


Algorytm działa w czasie liniowym, można to udowodnić obserwując zmiany wartości <math>2i+j</math>, zauważmy, że wartość ta nie zmniejsza się, a w wypadku pozytywnego testu  <math> x[j)==y[i+j]</math> zwiększa się co najmniej o 1. Jednocześnie <math>2i+j\le 3n</math>.
<center> [[Grafika:Zasd_2_3.jpg]]<br> Rysunek 3: Wstawianie kolejnego sufiksu <math>sufiks_{i_k}</math> do drzewa sufiksowego, przed włożeniem wszystkie krawędzie ''ścieżki roboczej'' od <math>u</math> do korzenia  są skierowane  w lewo. </center>
 
 
Opiszemy w jaki sposób wstawiamy kolejny sufiks <math>\beta</math> do drzewa. Operacja ta jest zilustrowana na rysunku. Załóżmy, że w każdym węźle drzewa trzymamy długość tekstu, który ten węzeł reprezentuje (jako pełna etykieta od korzenia do węzła).
 
Niech <math>\alpha</math> będzie poprzednio wstawionym sufiksem, a <math>u</math> ostatnio utworzonym liściem.
 
Wtedy wstawienie <math>\beta</math> polega na znalezieniu  maksymalnego wspólnego prefiksu <math>\gamma_1</math> tekstów <math>\alpha</math>, <math>\beta</math>. Niech <math>\beta=\gamma_1\cdot \gamma_2</math>. Znajdujemy węzeł <math>v</math> odpowiadający ścieżce od korzenia etykietowanej <math>\gamma_1</math>.
 
Kluczowym pomyslem algorytmicznym  jest tutaj to, że węzła <math>v</math> szukamy nie od korzenia, ale od ostatnio utworzonego liścia <math>u</math>. Jeśli takiego węzła <math>v</math> nie ma (jest wewnątrz krawędzi) to go  tworzymy. Następnie tworzymy nowy liść <math>w</math> odpowiadający sufiksowi <math>\beta</math>, oraz krawędź <math>(v,w)</math> etykietowaną <math>\gamma_2</math>.


== String-matching w pamięci stałej dla dowolnych wzorców ==


Algorytym Specjalny-String-Matching  można  łatwo
Z tablicy <math>lcp</math> odczytujemy długość <math>\gamma_1</math>.  
zmodyfikować tak, aby znajdował on wystąpinia dowolnego słowa (niekoniecznie specjalnego) w czasie liniowym i
stałej pamięci.
Niech <math>x = uv</math>, gdzie <math>v</math> jest leksykograficzne maksymalnym sufiksem <math>x</math>. Oznaczmy <math>r=|u|</math>. Technicznie
informacja o rozkładzie <math>uv</math> sprowadza się do pamiętania <math>r</math>.


W celu obliczenia <math>v</math> posuwamy się ścieżką od <math>u</math> w górę drzewa, aż znajdziemy węzeł oddalony od korzenia o <math>|\gamma|</math>.


'''Własność rozkładu. '''Niech <math>x=uv</math> będzie rozkładem jak wyżej opisany. Wtedy
Przechodząc drzewo posuwamy się po węzłach drzewa, przeskakując (w czasie stalym)
Słowo <math>v</math> występuje tylko raz w słowie <math>uv</math>.
potencjalnie długie teksty na krawędziach.  
Jeśli <math>i'<i</math> są początkami wystąpień <math>v</math>, oraz <math>i-i'<r</math> to
na pozycji <math>i-1</math> nie kończy się wystąpienie <math>u</math>.




Z powyższego faktu wynika stosunkowo prosty algorytm szukania <math>x</math> w czasie loiniowym i pamięci
Koszt operacji wstawienia jest proporcjonalny do sumy: jeden plus zmniejszenie głębokości nowego liścia w stosunku do starego. Suma tych zmniejszeń jest liniowa. <math>\gamma_1</math> jest najdłuższym wspólnym prefiksem słów <math>sufiks_{i_{k-1}}</math> i <math>sufiks_{i_k}</math>. Kluczowe znaczenie w operacji ma znajomość  wartości <math>|\gamma_1|= lcp[k-1]</math>. Wiemy kiedy się zatrzymać idąc do góry od węzła <math>u</math> w kierunku korzenia.  
stałej. Algorytm ten jest modyfikacja agorytmu Specjalny-String-Matching , w ktorym rolę <math>x</math> pełni <math>v</math>.


{{algorytm |String-matching w pamięci stałej|algorytm_string_matching_pam_st|
Lokalnie wykonana praca w jednej iteracji jest zamortyzowana zmniejszeniem się głębokości aktualnego liścia w stosunku do poprzedniego. W sumie praca jest liniowa.
Niech <math>v</math> będzie  leksykograficznie maksymalnym sufiksem <math>x</math>;


Liczymy  algorytmem Specjalny-String-Matching  kolejne wystąpienia <math>v</math> w <math>y</math>;
Historia algorytmu jest pokazana dla przykładowego tekstu na rysunkach.


Dla każdego wystąpienia <math>i</math> niech <math>i'</math> będzie wystąpieniem poprzednim;
<center> [[Grafika:Zasd_2_4.jpg||500px]] <br><br> <br> Rysunek 4: Pierwsze 6 iteracji algorytmu Drzewo-Sufiksowe dla tekstu
<math>babaabababba\#</math>. </center>


jeśli <math>i-i' \ge |v|</math> to sprawdź czy <math>u</math> występuje na lewo od pozycji <math>i</math>;
<br>
<br>


(sprawdzanie to wykonujemy w sposób naiwny)
<center> [[Grafika:Zasd_2_5.jpg||600px]] <br> Rysunek 5: Ostatnie 6 iteracji algorytmu Drzewo-Sufiksowe dla tekstu <math>babaabababba\#</math>. </center>
<br><br>


jeśli występuje to wypisz kolejne wystąpienie całego wzorca <math>x</math>.
== Obliczanie tablicy ''lcp'' ==
}}


Pozostawiamy bardziej precyzyjny zapis algorytmu jako ćwiczenie.
Niech <math>rank(i)</math> będzie pozycją <math>sufiks_i</math> w porządku leksykograficznym. W naszym przykładowym słowie mamy:
<center><math>rank = [8, 2, 7, 1, 3, 9, 4, 10, 5, 12, 11, 6]</math></center>


W ten sposób pokazaliśmy, że problem szukania słowa <math>x</math> w słowie <math>y</math> można rozwiązać
Niech  <math>lcp'[k] = lcp[rank[k]-1]</math>.
w czasie liniowym i pamięci (dodatkowej) stałej, jeśli znamy początkową pozycję <math>r</math>
leksykograficznie maksymalnego sufiksu <math>v</math> słowa <math>x</math>.


== Liczenie maksymalnego sufiksu w pamięci stałej ==
Załóżmy, dla uproszczenia, że  <math>lcp[0]=0</math> oraz że tekst kończy się specjalnym symbolem
Obliczamy tablice <math>lcp',\ lcp</math> następująco:


W algorytmie szukanie wzorca w pamięci stałej potrzebna jest pozycja <math>r</math>  od której zaczyna się maksymalny
sufiks. Pokażemy teraz jak ją znajdować w czasie liniowym i w pamięci stałej. Kluczem do tego jest liczenie
czegoś więcej, dla każdego prefiksu liczymy maksymalny sufiks jak również dodatkowo jego okres. To własnie
liczenie okresu daje efektywność, chociaż na końcu nam ten okres jest  niepotrzebny.
Przekształcimy najpierw algorytm  ''Naiwne-Liczenie-Okresu'' na algorytm liczący długość najdłuższego
specjalnego prefiksu włącznie z jego okresem.


{algorytm| funkcja Najdłuższy-Specjalny-Prefiks(x)|fun_najdl_spec_pref|
{{algorytm|Oblicz-lcp|algorytm_oblicz_lcp|
<math>period := 1</math>;<br>
1 '''for''' <math>k:=1</math> '''to''' <math>n</math> '''do'''
'''for''' <math>i:=2</math> to <math>|x|</math> '''do''' <br>
2    oblicz <math>lcp'[k]</math> korzystając z faktu, że <math>lcp'[k]\ge lcp'[k-1]-1</math>;
&nbsp;&nbsp;&nbsp;'''if''' <math>x[i] < x[i - period]</math> \textbf{then} <math>period := i</math><br>
3    // koszt iteracji <math>O(lcp'[k]-lcp'[k-1]+const)</math>
&nbsp;&nbsp;&nbsp;'''else if''' <math>x[i] > x[i - period]</math> ''then'''<br>
4  '''for''' <math>k:=1</math> '''to''' <math>n</math> '''do'''
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''return''' <math>(i-1, period)</math>;<br>
  5    <math>lcp[rank[k]-1] := lcp'[k]</math>  
'''return''' <math>(|x|, period)</math>;
}}
}}


Skorzystamy z algorytmu ''Najdłuższy-Specjalny-Prefiks''. Funkcja ''Maksymalny-Sufiks'' liczy początkową pozycję i okres maksymalnego sufiksu.
Można to zapisać w języku C++ następująco


{{algorytm|funkcja Maksymalny-Sufiks(x)|fun_max_suf|
{{algorytm|Oblicz-lcp1|algorytm_oblicz_lcp1|
<math>j := 1</math>;<br>
for <math>(int\ \ i=1; i<=n; i++)\ \  R[SUF[i]] = i;</math>
'''repeat'''<br>
<math>l = 0;</math>
&nbsp;&nbsp;&nbsp;<math>(i,period) :=</math> ''Najdłuższy-Specjalny-Prefiks''<math>(x[j.. n])</math>;<br>
for <math>(int\ \ i=1; i<=n; i++</math>) <math>\{</math>
&nbsp;&nbsp;&nbsp;'''if''' <math>i=n</math> '''then return''' <math>(j, period)</math><br>
  if <math>(R[i] > 1)\ \{</math>
&nbsp;&nbsp;&nbsp;'''else''' <math>j := j+i - (i \mod period)</math>;<br>
      while <math>(x[l+i] == x[l+SUF[R[i]-1]])\ \ l++</math>;
'''forever''' <br>
      <math>lcp[R[i]-1] = l;\ \}</math>
  <math>l = max(0, l-1);\}</math>
}}
}}


Możemy przepisać algorytm Maksymalny-Sufiks tak aby nie wywoływał on funkcji Najdłuższy-Specjalny-Prefiks, wpisując tę funkcję do algorytmu. Arytmetyczna funkcja  <math>\mod</math> może być usunięta i zastąpiona przez operacje dodawania i odejmowania bez zmiany asymptotycznej złożoności.


Algorytm Maksymalny-Sufiks wykonuje co najwyżej <math>2.|x|</math> porównań symboli. Uzasadnienie pozostawiamy jako ćwiczenie.


{{algorytm| funkcja} Maksymalny-Sufiks(x)|fun_max_suf2|
<math>s := 1</math>;  <math>i := 2</math>;  <math>p := 1</math>;<br>
'''while''' (<math>i \le n</math>) '''do'''<br>
&nbsp;&nbsp;&nbsp;<math>r := (i-s) \mod p</math>;<br>
&nbsp;&nbsp;&nbsp;'''if''' (<math>x[i] = x[s+r])</math> '''then''' <math>i := i+1</math><br>
&nbsp;&nbsp;&nbsp;'''else if''' (<math>x[i] < x[s+r])</math> '''then begin'''
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<math>i := i+1</math>; <math>p := i-s</math>;
&nbsp;&nbsp;&nbsp;'''else'''
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<math>s := i-r</math>; <math>i := s+1</math>; <math>p := 1</math>;<br>
'''return''' <math>s</math>;<br>
}}
== Kodowanie prefiksowe: drzewa i kody Huffmana ==


Zbiór słów jest prefiksowy gdy żadne słowo nie jest prefiksem drugiego. Taki zbiór słów odpowiada drzewu,
Pozostawiamy jako ćwiczenie dowód tego, że
którego ścieżki etykietowane symbolami, w przypadku binarnym możemy przyjąć, że krawędź w lewo jest
 
etykietowana zerem, a w prawo jedynką.
<center> <math>lcp'[k]\ge lcp'[k-1]-1</math>. </center>
Przez kodowanie rozumiemy funkcję <math>h</math> która każdemu symbolowi <math>s</math> przyporządkowuje niepusty ciąg binarny
 
<math>h(s)</math>, całe słowo <math>x</math> zostanie zakodowane na słowo <math>h(x)</math> (każda litera jest zakodowana niezależnie i kody
Jeśli <math>lcp'[k-1]-1=t</math>, to <math>lcp'[k]</math> obliczamy sprawdzając symbol po symbolu (od lewej do prawej) zgodność prefiksów odpowiednich słów startując od pozycji <math>t</math>. W ten sposób sumaryczny koszt jest liniowy. W każdej iteracji cofamy się o jeden, a potem idziemy  ''do przodu'' (sprawdzając kolejne symbole). Jest to analiza typu ''jeden krok do tyłu i kilka do przodu''. Liczba iteracji jest liniowa, więc liczba kroków do tyłu też.  Ponieważ odległość ''do celu'' jest liniowa, to suma kroków też jest liniowa.
''skonkatenowane''. Kod jest prefiksowy gdy zbiór kodów symboli jest prefiksowy. Rozważamy
 
następujący problem.
==Słownik podsłów bazowych i konstrukcja tablicy sufiksowej w czasie ''O(n log n)''==
 
Opiszemy uproszczoną wersję algorytmu  Karpa-Millera-Rosenberga (w skrócie algorytmu ''KMR'') rozwiązywania problemów tekstowych metodą ''słownika podsłów bazowych''. Ustalmy pewnie tekst <math>x</math> długości <math>n</math>.
 
Zakładamy w tej sekcji, że dodatkowym symbolem jest <math>x_{n+1}=\#</math>, leksykograficznie najmniejszy symbol. Przez segment <math>k</math>-bazowy  rozumiemy segment tekstu <math>x[i..i+2^k-1]</math>  długości <math>2^k</math> lub kończący się na <math>x_{n+1}</math>.  
 
Teoretycznie możemy założyć, że po symbolu <math>\#</math> mamy bardzo dużo takich symboli na prawo i każdy segment startujący w<math>x[1..n]</math> ma ''dokładnie '' długość <math>2^k</math>.
 
 
'''Słownik podsłów bazowych''' (w skrócie DBF(x), od ang. ''dictionary of basic factors'')  składa  się z  <math>\log n</math> tablic  <br>
 
<math>NAZWA_0</math>, <math>NAZWA_1</math>, <math>NAZWA_2</math>,<math>\ldots NAZWA_{\log n}</math>.
 
Zakładamy, że <math>NAZWA_k[i]</math> jest pozycją słowa <math>x[i..i+2^k-1]</math> na posortowanej liście (bez powtórzeń) wszystkich podsłów długości <math>2^k</math> słowa <math>x</math>. Jeśli długość ''wystaje'' poza koniec <math>x</math> to przyjmujemy że tam (wirtualnie) same symbole <math>\#</math>. Poniżej przedstawiamy przykład słownika podsłów bazowych  <math>DBF(abaabbaa)</math>.
 
<center> [[Grafika:Zasd_2_4a.jpg]]</center>
 
Algorytm liczenia tablic ''Nazwa'' jest bardzo prosty. Załóżmy od razu, że symbole są ponumerowane leksykograficznie. Wtedy <math>NAZWA_0</math> jest zasadniczo równa tekstowi <math>x</math>.
 
<center> [[Grafika:Zasd_2_6.jpg]] <br> Rysunek6: Słowo rozmiaru <math>2^{k+1}</math> otrzymuje najpierw nazwę-kompozycję:  kombinacją nazw (będących liczbami naturalnymi z przedziału <math>[1..n]</math>) dwóch podsłów długości <math>2^k</math> . </center>
 
 
'''Opis jednej iteracji <math>(transformacja: NAZWA_k\ =>\ NAZWA_{k+1}</math>)'''
 
 
Dla każdego <math>i</math> tworzymy nazwę-kompozycję slowa <math>x[i..i+2^{k+1}-1]</math> jako <center><math>NAZWA_k[i], NAZWA_k[i+2^k]</math></center>
 
Każda taka kompozycja jest parą liczb naturalnych. Sortujemy te pary za pomocą algorytmu ''radix-sort'' i  w ten sposób otrzymujemy tablicę, która koduje (w porządku leksykograficznym) każdą parę liczbą naturalną (pozycją w porządku leksykograficznym). Wartością <math>NAZWA_{k+1}[i]</math> jest kod pary <math>(NAZWA_k[i],NAZWA_k[i+2^k])</math>.
 
 
Zauważmy, że tablica sufiksowa odpowiada tablicy <math>NAZWA_{\lceil \log n \rceil}</math>. Możemy to podsumować następująco:
<br>
1. słownik ''DBF(x)'' możemy skonstruować w czasie <math>O(n \log n)</math>i pamięci <math>O(n \log n)</math> (jest to również rozmiar słownika). <br>
2. Tablicę sufiksową możemy otrzymać,stosując algorytm KMR, w czasie <math>O(n \log n)</math> i pamięci <math>O(n)</math>. (Potrzebujemy pamiętać jedynie ostatnie dwie tablice w każdej iteracji.)
 
== Konstrukcja tablicy ''SUF'' w czasie ''O(n)'': algorytm KS ==
 
Opiszemy teraz błyskotliwy algorytm Karkkainena-Sandersa ( w skrócie ''KS'') będący zoptymalizowaną wersją algorytmu KMR liczenia tablicy sufiksowej. Zauważmy, że algorytm KMR oblicza znacznie więcej niż tablica sufiksowa, ponieważ konstruuje słownik podsłów bazowych wielkości <math>n \log n</math> (mający liczne inne zastosowania, ale jako całość być może niepotrzebny przy
liczeniu tablicy sufiksowej)
 
Główną częścią algorytmu KS jest obliczanie częściowej tablicy sufiksowej w sposób rekurencyjny. Rozbijmy zbiór  pozycji [1..n] tekstu <math>x</math>  na dwa zbiory N, M  :
 
Zbiór  N składa się z co trzeciej pozycji, a M jest zbiorem pozostałych pozycji.
 
<center>  N = {3,6,9,12,15,....},  M = {1,2,4,5,7,8,10,11,...} </center>
 
Przez <math>SUF[M]</math>, oznaczmy tablicę sufiksową dla pozycji ze zbioru <math>M</math>, podobnie zdefiniujmy <math>SUF[N]</math>.
 
<math>SUF[M]</math> daje posortowany ciąg sufiksów zaczynających się na pozycjach ze zbioru <math>M</math>.
 
Dla początkowego przykładowego tekstu <math>x= babaabababba\#</math> mamy <br>
 
 
<center> <math>M= \{1,2,4,5,7,8,10,11\}\ \ \ \ \ N= \{3,6,9,12\}</math>  </center>  <br>
 
<center> <math>SUF[M]=  [4,\ 2,\ 5,\ 7, \ 1,  \ 8,\ 11,\ 10]\ \ \ \ SUF[N]=  [ 9,\ 12,\ 3,\ 6,]</math></center>
<br>
 
''' Sprowadzenie obliczania <math>SUF[M]</math> do obliczania tablicy sufiksowej rozmiaru <math>\frac{2}{3}n</math>'''
 
 
Posortujmy leksykograficznie wszystkie podsłowa długości 3 w słowie <math>x</math> korzystając z radix-sort. Każdemu takiemu słowu przyporządkujmy nazwę będącą jego pozycją w posortowanym leksykograficznie ciągu, oznaczmy <math>kod(z)</math> otrzymaną nazwę  podsłowa długości 3. Zakładamy, że <math>x</math> kończy się dodatkowo dwoma symbolami <math>\#</math>, ale rozważamy tylko podsłowa zaczynające się w <math>x</math>. Dla uproszczenia załóżmy, że 3 jest dzielnikiem n.
 
Tworzymy nowe słowo <math>compress(x)</math> w następujący sposób:
 
<center><math>y1= \ kod(a_1a_2a_3)\cdot kod(a_4a_5a_6) \ldots kod(a_{n-2}a_{n-1}a_n)</math>
 
<math>y2= kod(a_2a_3a_4)\cdot kod(a_5a_6a_7) \ldots kod(a_{n-1}a_{n}a_{n+1})</math>
 
<math>compress(x)= y1\ \& \ y2\ ;</math> gdzie <math>\&</math> jest nowym maksymalnym symbolem  </center>
 
<br><br> '''Przykład'''. Weźmy początkowy przykład <math>x\ = \ babaababbba\#</math>, gdzie <math>\#</math>jest większe niże a,b. Mamy
 
<center> <math>aab \prec aba\prec bab \prec ba\# \prec bba</math>,</center>


Zatem kody tych trójek są kolejno
<math>1,\ 2,\ 3,\ 4, \ 5</math>.


===Optymalne kodowanie prefiksowe===
Oznaczmy<math>kod(z)=<z></math>. Wtedy
Dla danego słowa <math>x</math> znaleźć binarne kodowanie prefiksowe takie,
że <math>h(x)</math> ma minimalną długość.


<center> <math>y1= <bab><aab><aba><bba>= 3\ 1\ 2\ 5  ;</math></center>


{{przyklad|||
<center> <math>y2= <aba><aba><bab><ba\#>= 2\ 2\ 3\ 4</math></center>  
Niech  <math>x=abracadabra</math>. Liczby wystąpień symboli w słowie <math>x</math> są:
<br>
<center><math>w_{a}=5, w_{b}=2, w_{c}=1, w_{d}=1, w_{r}=2.</math></center>
}}


<center> <math>compress(x) = = 3\ 1\ 2\ 5 \ \&\ 2\ 2\ 3\ 4</math>,</center>
<br>


Optymalnym  kodowaniem jest <math>h(a)=0,h(b)=10,h(c)=1100,h(d)=1101,h(r)=111.</math>  <math>abracadabra</math> zostaje zakodowane na  <math>01011101100011010101110</math>, ciąg binarny długości <math>23</math>. Optymalne drzewo binarne odpowiadające optymalnemu kodowi prefiksowemu jest pokazane na rysunku.
<br>
Jeśli mamy tablicę sufiksową dla słowa <math>compress(x)</math>, można łatwo obliczyć <math>SUF[M]</math> w czasie liniowym.
Pozostawiamy to jako ćwiczenie.


<center>[[Grafika:Huffnal.jpg]]<br>
Rysunek 2:Drzewo Huffmana kodujące optymalnie symbole  <math>a,b,c,d,r</math> z wagami odpowiednio
<math>S\ =\ (5, 2, 1, 1, 2)</math>. Liczby w wewnętrznych węzłach są sumą wag w liściach odpowiadającego poddrzewa.
Koszt całkowity kodowania jest ważoną sumą długości ścieżek do liści, jest równeż sumą wartości w
węzłach wewnętrznych:  <math> 2+4+6+11\ =\ 23.</math>


</center>


Długość tekstu <math>h(x)</math> jest równa ważonej sumie długości ścieżek, ważoenj w tym sensie, że
{{algorytm|<math>KS</math> (Karkkainen-Sanders)|algorytm_KS|
długość ścieżki do danego liścia jest przemnożona przez wagę tego liścia. W przykładzie jest to suma:
1. <math>x' := compress(x)</math>;
<math>5 *1+2*2+1*4+1*4+2*3\ =\ 23</math>.


Niech <math>n</math> będzie liczbą różnych symboli w <math>x</math>, <math>w[i]</math> będzie liczbą wystąpień <math>i</math>-tego symbolu. Problem możemy rozwiązać stosując algorytm dla problemu ''Optymalne Sklejanie Par'' dla ciągu <math>w[1],w[2],\ldots
2. obliczamy tablicę sufiksową dla x' rekurencyjnie;
w[n])</math>. Musimy algorytm zmodyfikować tak, aby nie tylko sklejał pary ale również tworzył lokalnie drzewo.
Inaczej mówiąc algorytm w momencie sklejania elementów <math>a</math>, <math>b</math> w element <math>c</math> tworzy równieź dowiązania,
<math>a</math> staje się lewym synem <math>c</math>, natomiast  <math>b</math> staje się prawym synem.


{{algorytm| Huffmana (nieformalny opis)|algorytm_huffman_nieformalny|
3. obliczamy <math>SUF[M]</math> w czasie liniowym, znając tablicę sufiksową dla x';
Konfiguracje pośrednie algorytmu to zbiory drzew,


początkowo każdy pojedyńczy element <math>i</math> z wagą <math>w[i]</math> jest pojedyńczym drzewem.
4. obliczamy <math>SUF[N]</math> w czasie liniowym (bez rekursji), znając <math>SUF[M]</math>;


Korzeń każdego drzewa reprezentuje sklejenie jego wszystkich liści.  
5. scalamy posortowane ciągi  <math>SUF[M],\ SUF[N]</math> w tablicę sufiksową dla całego słowa <math>x</math>


Za każdym razem sklejamy dwa korzenie drzew o minimalnej wadze.
}}
}}


Drzewo które algorytm generuje nazywamy drzewem Huffmana.
Krok 1 algorytmu sprowadza się do ''radix-sort''u, podobnie jak w algorytmie KMR. Kroki 3,4 są proste i ich implementację pozostawiamy czytelnikowi jako ćwiczenie.
 
Najważniejszy jest krok scalania. Mamy dwie posortowane listy sufiksów i trzeba je scalić w jedną posortowaną listę. Zasadniczym problemem jest implementacja operacji porównania leksykograficznego dwóch (długich) sufiksów w czasie stałym. Jeśli  oba sufiksy są typu <math>M</math> lub oba są  typu <math>N</math>, to porównanie jest w czasie stałym, bo mamy posortowane listy takich sufiksów.


Pozostawiamy jako ćwiczenie przerobienie algorytmu Optymalne-Sklejanie-Par na algorytm liczenia kodów i
Pokażemy na przykładzie kluczową operację porównania sufiksu typu M z sufiksem typu N w czasie stałym.
drzew Huffmana.  


Z analizy algorytmu ''Optymalne Sklejanie Par'' wynika, że problem optymalnych binarnych kodów prefiksowych można rozwiązać w czasie <math>O(n \log n)</math>, a jeśli wagi <math>w[i]</math> są posortowane to w czasie liniowym.
{{przyklad|||
Nierówność <math>sufiks_2< sufiks_{12}</math> jest równoważna temu, że zachodzi co najmniej jeden z warunków:


===Kodowanie Huffmana słowami <math>k</math>-arnymi.===
1. <math>(a_2 < a_{12})</math> <br>
2. <math>\ (a_2=a_{12}, a_3 < a_{13})</math> <br>
3. <math>(a_2=a_{12}, a_3=a_{13}, sufiks_4<sufiks_{14})</math>


Pozostawiamy jako ćwiczenie podobny problem, ale gdy kodujemy w alfabecie <math>k</math>-arnym, mamy teraz symbole <math>0,1,\ldots, k-1</math>. W algorytmie jednorazowo możemy sklejać więcej niż dwa elementy.


===Kodowanie prefiskowe z symbolami kodowymi nierównej długości===
Jednakże <math>4,14\in M</math>, zatem <math>sufiks_4</math> i <math>sufiks_{14}</math>, są typu M i można je porównać w czasie stałym.
Problem robi się skomplikowany,gdy długość symbolu 0 jest 1 a długość symbolu 1 jest <math>c</math>, gdzie <math>c</math> jest pewną stała (jest to po
 
angielsku problem tzw. lopsided trees). Inaczej mówiąc szukamy takiego optymalnego drzewa, że ważona suma
 
ścieżek jest minimalna, ale długość krawędzi na lewo wynosi 1 a długość krawędzi na prawo wynosi <math>c</math>.
 
Pozostawiamy jako ćwiczenie znalezienie efektywnego algorytmu dla małych <math>c</math> (<math>c=2</math> lub <math>c=3</math>). Dla dowolnego
Niech <math>T(n)</math> będzie czasem działania algorytmu KS. Zachodzi <center><math>T(n) = T(\lceil \frac{2}{3}\cdot n  \rceil) + O(n)</math></center>
<math>c</math> (będącego częścia wejścia) i dowolnych wag jest to zbyt trudne, nie znamy algorytmu wielomianowego. Dla
 
ustalonego c istnieje algorytm wielomianowy którego stopień zależy od c. Natomiast pozostawiamy jako
Rozwiązaniem jest <math>T(n)=O(n)</math>. Mamy więc liniowy algorytm liczenia tablicy sufiksowej. Daje to również liniowy algorytm konstrukcji drzewa sufiksowego.
ćwiczenie przypadek gdy <math>c</math> jest dowolne ale wszystkie wagi <math>w[i]</math> są równe.  
 
}}


=== Kodowanie prefiskowe z kodami o ograniczonej długości===
Istnieje kilka interesujących algorytmów, które konstruują drzewo sufiksowe w czasie liniowym, bez korzystania z tablicy sufiksowej (algorytmy Weinera, McCreighta, Ukkonena). W algorytmach tych współczynnik przy złożoności liniowej wynosi \log |A| , gdzie A jest alfabetem.
Innym ciekawym problemem jest
też skonstruowanie optymalnego kodu prefiksowego, w którym wszystkie słowa kodowe są ograniczone przez pewną
zadaną liczbę <math>L</math>. Inaczej mówiąc ograniczamy z góry wysokość drzewa Huffmana. Istnieją algorytmy
wielomianowe dla tego problemu, stopień wielomianu niezależny od <math>L</math>.

Aktualna wersja na dzień 22:12, 11 wrz 2023


Najważniejszymi strukturami danych związanymi z tekstami są te, które dotyczą efektywnej reprezentacji zbioru wszystkich podsłów tekstu. Przed wszystkim interesuje nas to, żeby taka reprezentacja przyspieszała wyszukiwanie słów, a jednocześnie żeby była konstruowalna w czasie liniowym albo prawie liniowym (z dokładnością do logarytmów).

Oznaczmy przez Subwords(x) wszystkie podsłowa tekstu x, a wszystkie wystąpienia (początkowe pozycje) słowa z w słowie x oznaczmy przez Occ(z,x). (Oznaczenie Occ jest skrótem od ang. occurrences).

Chcemy znaleźć taką reprezentację zbioru Subwords(x), by można było łatwo odpowiedzieć na pytanie, czy zSubwords(x), co jest równoważne Occ(z,x), jak również rozwiązywać inne problemy tekstowe. Poza tym chcemy, by rozmiar tej reprezentacji był liniowy, podczas gdy rozmiar Subwords(x) może być kwadratowy. Spośród wielu dobrych reprezentacji najbardziej znanymi są tablice sufiksowe (oznaczane przez SUF), drzewa sufiksowe i grafy podsłów (nie rozważane w tym module).

Tablice i drzewa sufiksowe

Niech x=a1a2an i niech xn+1=# będzie specjalnym znakiem leksykograficznie większym od każdego innego symbolu 9w przyszłości będziemy również używać xn+1=# jako najmniejszego symbolu).

Oznaczmy przez sufiksi=aiai+1an sufiks tekstu x zaczynający się na pozycji i-tej.

Niech SUF[k] będzie pozycją, od której zaczyna się k-ty leksykograficznie sufiks x. Sufiks zaczynający się na pozycji (n+1)-szej nie jest brany pod uwagę.

Ciąg sufiksów posortowany leksykograficznie wygląda następująco:

sufixSUF[1]<sufixSUF[2]<sufixSUF[3]<sufixSUF[n]



Rysunek 1: Tablicą sufiksową tekstu x=babaabababba# jest ciąg SUF=[4, 2, 5, 7, 9, 12, 3, 1, 6, 8, 11, 10]

Oznaczmy przez lcp[k] długość wspólnego prefiksu k-tego i następnego sufiksu w kolejności leksykograficznej. Na rysunku wartości najdłuższego wspólnego prefiksu między kolejnymi słowami są przedstawione jako zacienione segmenty. Odpowiadają one tablicy lcp=[1, 3, 4, 2, 1, 0, 2, 4, 3, 2, 1].
Tablica sufiksowa ma następująca ‘’sympatyczną’’ własność: Niech

minz=min{k:z jest prefiksem sufiksSUF[k]}, maxz=max{k:z jest prefiksem sufiksSUF[k]}.

Wtedy Occ(z,x) jest przedziałem w tablicy sufiksowej od minz do maxz.

Drzewo sufiksowe jest drzewem, w którym każda ścieżka jest etykietowana kolejnymi symbolami pewnego sufiksu, oraz każdy sufiks x jest obecny w drzewie. Gdy dwie ścieżki się rozjeżdżają, tworzy się wierzchołek. Mówiąc bardziej formalnie, każdej krawędzi jest przypisane jako etykieta pewne podsłowo x. Krawędzie wychodzące z tego samego węzła różnią się pierwszymi symbolami swoich etykiet (patrz rysunek).


Etykiety są kodowane przedziałami w tekście x: para liczb [i,j] reprezentuje podsłowo aiai+1j (zobacz prawe drzewo na rysunku). Dzięki temu reprezentacja ma rozmiar O(n). Wagą krawędzi jest długość odpowiadającego jej słowa.


Rysunek 2: Drzewo sufiksowe dla tekstu x=babaabababba. Na końcu jest dodany znak #. Końcowe węzły zawierają informację, gdzie zaczyna się sufiks, którym dochodzimy do danego węzła.


Obie reprezentacje pozwalają szybko rozwiązywać problem string-matchingu oraz mają rozmiar liniowy. Niech z będzie wzorcem o długości m, a x słowem długości n. Z reguły m<<n.

Szukanie podsłów

Pokażemy, jak sprawdzać, czy z występuje w x.

Używając drzewa sufiksowego (czas O(m))

Idziemy od korzenia w dół czytając kolejne symbole z, czasami posuwamy się po wewnętrznej etykiecie pewnej krawędzi. Zbiór wystąpień odpowiada zbiorowi liści w poddrzewie węzła, do którego doszliśmy. Jeśli po drodze utknęliśmy i nie dało się dalej schodzić po drzewie, oznacza to, że zSubwords(x)

Używając tablicy sufiksowej (czas O(mlogn))

Możemy sprawdzić, czy z jest prefiksem i-tego sufiksu w czasie O(m). Korzystając z tego, wykonujemy rodzaj binarnego szukania. W ten sposób znajdujemy pierwszy sufiks, którego prefiksem jest z. Jeśli jest taki sufiks, to zSubwords(x). W przeciwnym wypadku z nie jest podsłowem x.

Podobnie znajdujemy ostatni sufiks. Zbiór wystąpień odpowiada przedziałowi w tablicy SUF między obliczonymi pierwszym i ostatnim sufiksem zaczynającym się od z.

Wyznaczanie liczby podsłów

Pokażemy, jak znaleźć liczbę podsłów słowa x przy pomocy tablicy sufiksowej lub drzewa sufiksowego. Końcowego markera # nie traktujemy jako części słowa x. Liczba podsłów jest równa |Subwords(x)|. Jeśli wszystkie symbole słowa są różne to |Subwords(x)|=(n2).

Używając drzewa sufiksowego, czas O(n)


Sumujemy wagi krawędzi drzewa.

Używając tablicy sufiksowej, czas O(n)

Niech SUMA(lcp) będzie sumą elementów tablicy lcp. Liczbę podsłów obliczamy jako


(n+12)SUMA(lcp)


Pozostawiamy jako ćwiczenie uzasadnienie tego, że liczba podsłów jest poprawnie obliczona (korzystając z drzewa sufisowego lub z tablicy sufiksowej).


Przykład

Dla przykładowego tekstu x=babaabababba mamy |Subwords(x)|=55. Proponujemy to wyliczyć z tablicy sufiksowej i drzewa sufiksowego dlax, danego na rysunku. Suma elementów tablicy lcp wynosi 23. Liczba podsłów to: 7823=55

Podobnie jak tablicę sufiksową możemy zdefiniować tablicę ROT odpowiadającą posortowanemu ciągowi wszystkich cyklicznych przesunięć słowa x (rotacji x).

Pozostawiamy jako ćwiczenie znalezienie liniowego algorytmu obliczania tablicy ROT, przy założeniu, że mamy liniowy algorytm obliczania tablicy sufiksowej.


Dygresja. Ciekawą klasę słów, dla których tablice SUF, ROT są szczególnie interesujące, stanowią słowa Fibonacciego Fn. W tym szczególnym przypadku załóżmy, że pozycje numerujemy od zera. Dla każdego n tablica ROT jest postępem arytmetycznym (modulo długość słowa). Natomiast tablica SUF jest postępem arytmetycznym, gdy n jest parzyste.


Słowa Fibonacciego definiujemy następująco:F0=a, F1=ab, Fn+1=FnFn1 Na przykład: F3=abaab, F4=abaababa, F5=abaababaabaab. Oznaczmy przez SUFn tablicę SUF dla słowa Fibonacciego Fn; wtedy:

SUF4=[72503614],SUF5=[1072118503129614].


Pozostawiamy jako ćwiczenie znalezienie wzoru na |Subwords(Fn)|.

Drzewa sufiksowe =>tablice sufiksowe

W celu znalezienia początkowych pozycji sufiksów w porządku leksykograficznym przechodzimy drzewo sufiksowe metodą DFS, zakładając, że dla każdego węzła lista jego synów jest w kolejności leksykograficznej etykiet krawędzi prowadzących do synów. Wystarczy sprawdzać pierwsze symbole tych etykiet.

Załóżmy, że w liściach mamy początki sufiksów, które do nich prowadzą.

Kolejność odwiedzania liści w naszym przejściu metodą DFS automatycznie generuje elementy tablicy sufiksowej.

Tablice sufiksowe =>drzewa sufiksowe

Pokażemy konstruktywnie następujący istotny fakt:

jeśli znamy tablicę sufiksową i tablicę lcp, to drzewo sufiksowe dla danego tekstu możemy łatwo skonstruować w czasie liniowym.

Przypuśćmy, że SUF=[i1,i2,,in], a więc:

sufiksi1<sufiksi2<sufiksi3<sufiksin.


Algorytm Drzewo-Sufiksowe


1  T:= drzewo reprezentujące sufiksi1 (jedna krawędź);
2  for k:=2 to n do
3    wstaw nową ścieżkę o sumarycznej etykiecie sufiksik do T; 



Rysunek 3: Wstawianie kolejnego sufiksu sufiksik do drzewa sufiksowego, przed włożeniem wszystkie krawędzie ścieżki roboczej od u do korzenia są skierowane w lewo.


Opiszemy w jaki sposób wstawiamy kolejny sufiks β do drzewa. Operacja ta jest zilustrowana na rysunku. Załóżmy, że w każdym węźle drzewa trzymamy długość tekstu, który ten węzeł reprezentuje (jako pełna etykieta od korzenia do węzła).

Niech α będzie poprzednio wstawionym sufiksem, a u ostatnio utworzonym liściem.

Wtedy wstawienie β polega na znalezieniu maksymalnego wspólnego prefiksu γ1 tekstów α, β. Niech β=γ1γ2. Znajdujemy węzeł v odpowiadający ścieżce od korzenia etykietowanej γ1.

Kluczowym pomyslem algorytmicznym jest tutaj to, że węzła v szukamy nie od korzenia, ale od ostatnio utworzonego liścia u. Jeśli takiego węzła v nie ma (jest wewnątrz krawędzi) to go tworzymy. Następnie tworzymy nowy liść w odpowiadający sufiksowi β, oraz krawędź (v,w) etykietowaną γ2.


Z tablicy lcp odczytujemy długość γ1.

W celu obliczenia v posuwamy się ścieżką od u w górę drzewa, aż znajdziemy węzeł oddalony od korzenia o |γ|.

Przechodząc drzewo posuwamy się po węzłach drzewa, przeskakując (w czasie stalym) potencjalnie długie teksty na krawędziach.


Koszt operacji wstawienia jest proporcjonalny do sumy: jeden plus zmniejszenie głębokości nowego liścia w stosunku do starego. Suma tych zmniejszeń jest liniowa. γ1 jest najdłuższym wspólnym prefiksem słów sufiksik1 i sufiksik. Kluczowe znaczenie w operacji ma znajomość wartości |γ1|=lcp[k1]. Wiemy kiedy się zatrzymać idąc do góry od węzła u w kierunku korzenia.

Lokalnie wykonana praca w jednej iteracji jest zamortyzowana zmniejszeniem się głębokości aktualnego liścia w stosunku do poprzedniego. W sumie praca jest liniowa.

Historia algorytmu jest pokazana dla przykładowego tekstu na rysunkach.




Rysunek 4: Pierwsze 6 iteracji algorytmu Drzewo-Sufiksowe dla tekstu babaabababba#.




Rysunek 5: Ostatnie 6 iteracji algorytmu Drzewo-Sufiksowe dla tekstu babaabababba#.



Obliczanie tablicy lcp

Niech rank(i) będzie pozycją sufiksi w porządku leksykograficznym. W naszym przykładowym słowie mamy:

rank=[8,2,7,1,3,9,4,10,5,12,11,6]

Niech lcp[k]=lcp[rank[k]1].

Załóżmy, dla uproszczenia, że lcp[0]=0 oraz że tekst kończy się specjalnym symbolem Obliczamy tablice lcp, lcp następująco:


Algorytm Oblicz-lcp


1  for k:=1 to n do
2    oblicz lcp[k] korzystając z faktu, że lcp[k]lcp[k1]1; 
3    // koszt iteracji O(lcp[k]lcp[k1]+const)
4  for k:=1 to n do
 5    lcp[rank[k]1]:=lcp[k] 

Można to zapisać w języku C++ następująco

Algorytm Oblicz-lcp1


for (int  i=1;i<=n;i++)  R[SUF[i]]=i;
l=0;
for (int  i=1;i<=n;i++) {
  if (R[i]>1) {
      while (x[l+i]==x[l+SUF[R[i]1]])  l++;
      lcp[R[i]1]=l; }
  l=max(0,l1);}



Pozostawiamy jako ćwiczenie dowód tego, że

lcp[k]lcp[k1]1.

Jeśli lcp[k1]1=t, to lcp[k] obliczamy sprawdzając symbol po symbolu (od lewej do prawej) zgodność prefiksów odpowiednich słów startując od pozycji t. W ten sposób sumaryczny koszt jest liniowy. W każdej iteracji cofamy się o jeden, a potem idziemy do przodu (sprawdzając kolejne symbole). Jest to analiza typu jeden krok do tyłu i kilka do przodu. Liczba iteracji jest liniowa, więc liczba kroków do tyłu też. Ponieważ odległość do celu jest liniowa, to suma kroków też jest liniowa.

Słownik podsłów bazowych i konstrukcja tablicy sufiksowej w czasie O(n log n)

Opiszemy uproszczoną wersję algorytmu Karpa-Millera-Rosenberga (w skrócie algorytmu KMR) rozwiązywania problemów tekstowych metodą słownika podsłów bazowych. Ustalmy pewnie tekst x długości n.

Zakładamy w tej sekcji, że dodatkowym symbolem jest xn+1=#, leksykograficznie najmniejszy symbol. Przez segment k-bazowy rozumiemy segment tekstu x[i..i+2k1] długości 2k lub kończący się na xn+1.

Teoretycznie możemy założyć, że po symbolu # mamy bardzo dużo takich symboli na prawo i każdy segment startujący wx[1..n] ma dokładnie długość 2k.


Słownik podsłów bazowych (w skrócie DBF(x), od ang. dictionary of basic factors) składa się z logn tablic

NAZWA0, NAZWA1, NAZWA2,NAZWAlogn.

Zakładamy, że NAZWAk[i] jest pozycją słowa x[i..i+2k1] na posortowanej liście (bez powtórzeń) wszystkich podsłów długości 2k słowa x. Jeśli długość wystaje poza koniec x to przyjmujemy że są tam (wirtualnie) same symbole #. Poniżej przedstawiamy przykład słownika podsłów bazowych DBF(abaabbaa).

Algorytm liczenia tablic Nazwa jest bardzo prosty. Załóżmy od razu, że symbole są ponumerowane leksykograficznie. Wtedy NAZWA0 jest zasadniczo równa tekstowi x.


Rysunek6: Słowo rozmiaru 2k+1 otrzymuje najpierw nazwę-kompozycję: kombinacją nazw (będących liczbami naturalnymi z przedziału [1..n]) dwóch podsłów długości 2k .


Opis jednej iteracji (transformacja:NAZWAk => NAZWAk+1)


Dla każdego

i

tworzymy nazwę-kompozycję slowa

x[i..i+2k+11]

jako

NAZWAk[i],NAZWAk[i+2k]

Każda taka kompozycja jest parą liczb naturalnych. Sortujemy te pary za pomocą algorytmu radix-sort i w ten sposób otrzymujemy tablicę, która koduje (w porządku leksykograficznym) każdą parę liczbą naturalną (pozycją w porządku leksykograficznym). Wartością NAZWAk+1[i] jest kod pary (NAZWAk[i],NAZWAk[i+2k]).


Zauważmy, że tablica sufiksowa odpowiada tablicy NAZWAlogn. Możemy to podsumować następująco:
1. słownik DBF(x) możemy skonstruować w czasie O(nlogn)i pamięci O(nlogn) (jest to również rozmiar słownika).
2. Tablicę sufiksową możemy otrzymać,stosując algorytm KMR, w czasie O(nlogn) i pamięci O(n). (Potrzebujemy pamiętać jedynie ostatnie dwie tablice w każdej iteracji.)

Konstrukcja tablicy SUF w czasie O(n): algorytm KS

Opiszemy teraz błyskotliwy algorytm Karkkainena-Sandersa ( w skrócie KS) będący zoptymalizowaną wersją algorytmu KMR liczenia tablicy sufiksowej. Zauważmy, że algorytm KMR oblicza znacznie więcej niż tablica sufiksowa, ponieważ konstruuje słownik podsłów bazowych wielkości nlogn (mający liczne inne zastosowania, ale jako całość być może niepotrzebny przy liczeniu tablicy sufiksowej)

Główną częścią algorytmu KS jest obliczanie częściowej tablicy sufiksowej w sposób rekurencyjny. Rozbijmy zbiór pozycji [1..n] tekstu x na dwa zbiory N, M  :

Zbiór N składa się z co trzeciej pozycji, a M jest zbiorem pozostałych pozycji.

N = {3,6,9,12,15,....}, M = {1,2,4,5,7,8,10,11,...}

Przez SUF[M], oznaczmy tablicę sufiksową dla pozycji ze zbioru M, podobnie zdefiniujmy SUF[N].

SUF[M] daje posortowany ciąg sufiksów zaczynających się na pozycjach ze zbioru M.

Dla początkowego przykładowego tekstu x=babaabababba# mamy


M={1,2,4,5,7,8,10,11}     N={3,6,9,12}


SUF[M]=[4, 2, 5, 7, 1, 8, 11, 10]    SUF[N]=[9, 12, 3, 6,]


Sprowadzenie obliczania SUF[M] do obliczania tablicy sufiksowej rozmiaru 23n


Posortujmy leksykograficznie wszystkie podsłowa długości 3 w słowie x korzystając z radix-sort. Każdemu takiemu słowu przyporządkujmy nazwę będącą jego pozycją w posortowanym leksykograficznie ciągu, oznaczmy kod(z) otrzymaną nazwę podsłowa długości 3. Zakładamy, że x kończy się dodatkowo dwoma symbolami #, ale rozważamy tylko podsłowa zaczynające się w x. Dla uproszczenia załóżmy, że 3 jest dzielnikiem n.

Tworzymy nowe słowo compress(x) w następujący sposób:

y1= kod(a1a2a3)kod(a4a5a6)kod(an2an1an)

y2=kod(a2a3a4)kod(a5a6a7)kod(an1anan+1)

compress(x)=y1 & y2 ; gdzie & jest nowym maksymalnym symbolem



Przykład. Weźmy początkowy przykład x = babaababbba#, gdzie #jest większe niże a,b. Mamy

aababababba#bba,

Zatem kody tych trójek są kolejno 1, 2, 3, 4, 5.

Oznaczmykod(z)=<z>. Wtedy

y1=<bab><aab><aba><bba>=3 1 2 5;
y2=<aba><aba><bab><ba#>=2 2 3 4


compress(x)==3 1 2 5 & 2 2 3 4,



Jeśli mamy tablicę sufiksową dla słowa compress(x), można łatwo obliczyć SUF[M] w czasie liniowym. Pozostawiamy to jako ćwiczenie.


Algorytm KS (Karkkainen-Sanders)


1. x:=compress(x);

2. obliczamy tablicę sufiksową dla x' rekurencyjnie;

3. obliczamy SUF[M] w czasie liniowym, znając tablicę sufiksową dla x';

4. obliczamy SUF[N] w czasie liniowym (bez rekursji), znając SUF[M];

5. scalamy posortowane ciągi SUF[M], SUF[N] w tablicę sufiksową dla całego słowa x


Krok 1 algorytmu sprowadza się do radix-sortu, podobnie jak w algorytmie KMR. Kroki 3,4 są proste i ich implementację pozostawiamy czytelnikowi jako ćwiczenie.

Najważniejszy jest krok scalania. Mamy dwie posortowane listy sufiksów i trzeba je scalić w jedną posortowaną listę. Zasadniczym problemem jest implementacja operacji porównania leksykograficznego dwóch (długich) sufiksów w czasie stałym. Jeśli oba sufiksy są typu M lub oba są typu N, to porównanie jest w czasie stałym, bo mamy posortowane listy takich sufiksów.

Pokażemy na przykładzie kluczową operację porównania sufiksu typu M z sufiksem typu N w czasie stałym.

Przykład

Nierówność sufiks2<sufiks12 jest równoważna temu, że zachodzi co najmniej jeden z warunków:

1. (a2<a12)
2.  (a2=a12,a3<a13)
3. (a2=a12,a3=a13,sufiks4<sufiks14)


Jednakże 4,14M, zatem sufiks4 i sufiks14, są typu M i można je porównać w czasie stałym.


Niech T(n) będzie czasem działania algorytmu KS. Zachodzi
T(n)=T(23n)+O(n)

Rozwiązaniem jest T(n)=O(n). Mamy więc liniowy algorytm liczenia tablicy sufiksowej. Daje to również liniowy algorytm konstrukcji drzewa sufiksowego.

Istnieje kilka interesujących algorytmów, które konstruują drzewo sufiksowe w czasie liniowym, bez korzystania z tablicy sufiksowej (algorytmy Weinera, McCreighta, Ukkonena). W algorytmach tych współczynnik przy złożoności liniowej wynosi \log |A| , gdzie A jest alfabetem.