Zaawansowane algorytmy i struktury danych/Wykład 2: Różnice pomiędzy wersjami
Nie podano opisu zmian |
Nie podano opisu zmian |
||
Linia 3: | Linia 3: | ||
__TOC__ | __TOC__ | ||
Poprzednie algorytmy dokonywały | Poprzednie algorytmy dokonywały na tekstach wejściowych jedynie operacji sprawdzania symboli na równość. | ||
Załóżmy teraz, że alfabet jest liniowo uporządkowany. Pokażemy, że | 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 | 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: | implikuje {\em porządek leksykograficzny} na słowach, na przykład: | ||
Linia 14: | Linia 14: | ||
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>. | 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||| | ||
Linia 20: | Linia 20: | ||
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 | 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. | ||
Jeśli | Jeśli <math>x</math> jest specjalny, to okres każdego prefiksu słowa <math>x</math> można policzyć następującym naiwnym | ||
algorytmem | algorytmem: | ||
{{ algorytm| Funkcja Naiwne-Liczenie-Okresu (j)|funkcja_naiwne_liczenie_okresu| | {{ algorytm| Funkcja Naiwne-Liczenie-Okresu (j)|funkcja_naiwne_liczenie_okresu| | ||
Linia 90: | Linia 90: | ||
<center>[[Grafika:Naiwneliczenieokresu.jpg]]<br><br> | <center>[[Grafika:Naiwneliczenieokresu.jpg]]<br><br> | ||
Rysunek 1: Załóżmy, że w algorytmie ''Naiwne-Liczenie-Okresu'' <math>x[i-period(i-1)] \ne x[i]</math>. Niech | 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>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>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>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>. | Zaprzecza to założeniu, że <math>x</math> jest specjalne. Zatem <math>period(i)=i</math>. | ||
</center> | </center> | ||
Opiszemy teraz program szukania wzorca <math>x</math> | Opiszemy teraz program szukania wzorca <math>x</math> w slowie <math>y</math>i, zakładając że <math>x</math> jest specjalne. | ||
Program wczytuje dwa teksty, pierwszy z nich jest | Program wczytuje dwa teksty, pierwszy z nich jest specjalny: <math>x</math> pamiętamy w tablicy <math>x[0..m-1]</math>, <math>y</math> w | ||
tablicy <math>y[0..n-1]</math>. | tablicy <math>y[0..n-1]</math>. | ||
Program wypisuje wszystkie wystapienia <math>x</math> w <math>y</math>, tzn. wszystkie takie pozycje | Program wypisuje wszystkie wystapienia <math>x</math> w <math>y</math>, tzn. wszystkie takie pozycje | ||
<math>i</math>, | <math>i</math>, że <math>y[i\ldots i+m-1]\ =\ x</math>. Zapisujemy program w języku C++. | ||
{{algorytm|Specjalny-String-Matching|algorytm_specjalny_string_matching| | {{algorytm|Specjalny-String-Matching|algorytm_specjalny_string_matching| | ||
#include <iostream.h><br> | #include <iostream.h><br> | ||
Linia 128: | Linia 128: | ||
Program jest wstępem do programu szukającego dowolne podsłowo, niekoniecznie o własności bycia | Program jest wstępem do programu szukającego dowolne podsłowo, niekoniecznie o własności bycia | ||
specjalnym. Podstawowym niezmiennikiem w programie przed | specjalnym. Podstawowym niezmiennikiem w programie przed każdym wykonaniem i po każdym zakończeniu pętli ''while'' jest: | ||
'''(A)\ ''' <math>x[0 \ldots j-1]\ =\ y[i \ldots i+j-1]</math>, | '''(A)\ ''' <math>x[0 \ldots j-1]\ =\ y[i \ldots i+j-1]</math>, | ||
'''(B)\ ''' Program wypisał wszystkie wcześniejsze wystąpienia <math>i' < i</math>, | '''(B)\ ''' Program wypisał wszystkie wcześniejsze wystąpienia <math>i' < i</math>, | ||
'''(C)\ ''' <math>p</math> jest okresem slowa <math>x[0 \ldots j-1]</math> | '''(C)\ ''' <math>p</math> jest okresem slowa <math>x[0 \ldots j-1]</math> | ||
Algorytm | 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>. | ||
== String-matching w pamięci stałej dla dowolnych wzorców == | == String-matching w pamięci stałej dla dowolnych wzorców == | ||
Algorytym Specjalny-String-Matching | Algorytym Specjalny-String-Matching można łatwo | ||
zmodyfikować tak, aby znajdował on wystąpienia dowolnego słowa (niekoniecznie specjalnego) w czasie liniowym i | zmodyfikować tak, aby znajdował on wystąpienia dowolnego słowa (niekoniecznie specjalnego) w czasie liniowym i | ||
stałej pamięci. | stałej pamięci. | ||
Linia 145: | Linia 145: | ||
'''Własność rozkładu. '''Niech <math>x=uv</math> będzie rozkładem jak wyżej opisany. Wtedy | '''Własność rozkładu. '''Niech <math>x=uv</math> będzie rozkładem jak wyżej opisany. Wtedy | ||
słowo <math>v</math> występuje tylko raz w słowie <math>uv</math>. | |||
Jeśli <math>i'<i</math> są początkami wystąpień <math>v</math> | 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>. | 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 | Z powyższego faktu wynika stosunkowo prosty algorytm szukania <math>x</math> w czasie liniowym i pamięci | ||
stałej. Algorytm ten jest | stałej. Algorytm ten jest modyfikacją algorytmu Specjalny-String-Matching, w którym rolę <math>x</math> pełni <math>v</math>. | ||
{{algorytm |String-matching w pamięci stałej|algorytm_string_matching_pam_st| | {{algorytm |String-matching w pamięci stałej|algorytm_string_matching_pam_st| | ||
Niech <math>v</math> będzie | Niech <math>v</math> będzie leksykograficznie maksymalnym sufiksem <math>x</math>; | ||
Liczymy | Liczymy algorytmem Specjalny-String-Matching kolejne wystąpienia <math>v</math> w <math>y</math>; | ||
Dla każdego wystąpienia <math>i</math> niech <math>i'</math> będzie wystąpieniem poprzednim; | Dla każdego wystąpienia <math>i</math> niech <math>i'</math> będzie wystąpieniem poprzednim; | ||
jeśli <math>i-i' \ge |v|</math> | jeśli <math>i-i' \ge |v|</math>, sprawdź czy <math>u</math> występuje na lewo od pozycji <math>i</math>; | ||
(sprawdzanie to wykonujemy w sposób naiwny) | (sprawdzanie to wykonujemy w sposób naiwny) | ||
jeśli występuje | jeśli występuje, wypisz kolejne wystąpienie całego wzorca <math>x</math>. | ||
}} | }} | ||
Linia 175: | Linia 175: | ||
== Liczenie maksymalnego sufiksu w pamięci stałej == | == Liczenie maksymalnego sufiksu w pamięci stałej == | ||
W algorytmie | W algorytmie szukania 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 | sufiks. Pokażemy teraz, jak ją znajdować w czasie liniowym i w pamięci stałej. Kluczem do tego jest liczenie | ||
czegoś więcej | czegoś więcej: dla każdego prefiksu liczymy maksymalny sufiks, jak również dodatkowo jego okres. To właśnie | ||
liczenie okresu daje efektywność, chociaż na końcu | liczenie okresu daje efektywność, chociaż na końcu ten okres nie jest nam potrzebny. | ||
Przekształcimy najpierw algorytm | Przekształcimy najpierw algorytm ''Naiwne-Liczenie-Okresu'' na algorytm liczący długość najdłuższego | ||
specjalnego prefiksu włącznie z jego okresem. | specjalnego prefiksu włącznie z jego okresem. | ||
Linia 202: | Linia 202: | ||
}} | }} | ||
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. | 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 Maksymalny-Sufiks wykonuje co najwyżej <math>2.|x|</math> porównań symboli. Uzasadnienie pozostawiamy jako ćwiczenie. | ||
Linia 219: | Linia 219: | ||
== Kodowanie prefiksowe: drzewa i kody Huffmana == | == 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, | Zbiór słów jest prefiksowy, gdy żadne słowo nie jest prefiksem drugiego. Taki zbiór słów odpowiada drzewu, | ||
którego ścieżki etykietowane są symbolami | którego ścieżki etykietowane są symbolami. W przypadku binarnym możemy przyjąć, że krawędź w lewo jest | ||
etykietowana zerem, a w prawo jedynką. | etykietowana zerem, a w prawo jedynką. | ||
Przez kodowanie rozumiemy funkcję <math>h</math> która każdemu symbolowi <math>s</math> przyporządkowuje niepusty ciąg binarny | 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> | <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 | ||
są ''skonkatenowane''. Kod jest prefiksowy gdy zbiór kodów symboli jest prefiksowy. Rozważamy | są ''skonkatenowane''). Kod jest prefiksowy gdy zbiór kodów symboli jest prefiksowy. Rozważamy | ||
następujący problem | następujący problem: | ||
Linia 249: | Linia 249: | ||
</center> | </center> | ||
Długość tekstu <math>h(x)</math> jest równa ważonej sumie długości ścieżek, | Długość tekstu <math>h(x)</math> jest równa ważonej sumie długości ścieżek, ważonej w tym sensie, że | ||
długość ścieżki do danego liścia jest przemnożona przez wagę tego liścia. W przykładzie jest to suma: | długość ścieżki do danego liścia jest przemnożona przez wagę tego liścia. W przykładzie jest to suma: | ||
<math>5 *1+2*2+1*4+1*4+2*3\ =\ 23</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 | 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 | ||
w[n])</math>. Musimy algorytm zmodyfikować tak, aby nie tylko sklejał pary ale również tworzył lokalnie drzewo. | 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 | 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>a</math> staje się lewym synem <math>c</math>, natomiast <math>b</math> staje się prawym synem. | ||
{{algorytm| Huffmana (nieformalny opis)|algorytm_huffman_nieformalny| | {{algorytm| Huffmana (nieformalny opis)|algorytm_huffman_nieformalny| | ||
Linia 268: | Linia 268: | ||
}} | }} | ||
Drzewo które algorytm generuje nazywamy drzewem Huffmana. | Drzewo, które algorytm generuje, nazywamy drzewem Huffmana. | ||
Pozostawiamy jako ćwiczenie przerobienie algorytmu Optymalne-Sklejanie-Par na algorytm liczenia kodów i | Pozostawiamy jako ćwiczenie przerobienie algorytmu Optymalne-Sklejanie-Par na algorytm liczenia kodów i | ||
drzew Huffmana. | 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. | 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. | ||
===Kodowanie Huffmana słowami <math>k</math>-arnymi.=== | ===Kodowanie Huffmana słowami <math>k</math>-arnymi.=== | ||
Linia 280: | Linia 280: | ||
===Kodowanie prefiksowe z symbolami kodowymi nierównej długości=== | ===Kodowanie prefiksowe z symbolami kodowymi nierównej długości=== | ||
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ą | 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łą (jest to problem tzw. lopsided trees). Inaczej mówiąc, szukamy takiego optymalnego drzewa, w którym 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>. | |||
ś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 | Pozostawiamy jako ćwiczenie znalezienie efektywnego algorytmu dla małych <math>c</math> (<math>c=2</math> lub <math>c=3</math>). Dla dowolnego | ||
<math>c</math> (będącego częścią wejścia) i dowolnych wag jest to zbyt trudne, nie znamy algorytmu wielomianowego. Dla | <math>c</math> (będącego częścią 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 | ustalonego <math>c</math> istnieje algorytm wielomianowy, którego stopień zależy od <math>c</math>. Natomiast pozostawiamy jako | ||
ćwiczenie przypadek gdy <math>c</math> jest dowolne ale wszystkie wagi <math>w[i]</math> są równe. | ćwiczenie przypadek, gdy <math>c</math> jest dowolne, ale wszystkie wagi <math>w[i]</math> są równe. | ||
=== Kodowanie prefiksowe z kodami o ograniczonej długości=== | === Kodowanie prefiksowe z kodami o ograniczonej długości=== | ||
Innym ciekawym problemem jest | Innym ciekawym problemem jest | ||
też skonstruowanie optymalnego kodu prefiksowego, w którym wszystkie słowa kodowe są ograniczone przez pewną | 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. | zadaną liczbę <math>L</math>. Inaczej mówiąc, ograniczamy z góry wysokość drzewa Huffmana. Istnieją wtedy algorytmy | ||
wielomianowe dla tego problemu, stopień wielomianu jest niezależny od <math>L</math>. | wielomianowe dla tego problemu, stopień wielomianu jest niezależny od <math>L</math>. |
Wersja z 12:53, 25 wrz 2006
Zaawansowane algorytmy tekstowe II
Poprzednie algorytmy dokonywały na tekstach wejściowych jedynie 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:
String-matching w pamięci stałej dla specjalnych wzorów
Oznaczmy przez maksymalny leksykograficznie sufiks słowa . Słowo nazwiemy specjalnym, gdy .
Przykład
Dlaczego słowa o tej własności są interesujące? Większość szybkich algorytmów szukania podsłów korzysta z okresów 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.
Jeśli jest specjalny, to okres każdego prefiksu słowa można policzyć następującym naiwnym algorytmem:
Algorytm Funkcja Naiwne-Liczenie-Okresu (j)
;
for to do
if then ;
return ;
Przykład
są:
a | b | a | a | b | a | a | b | a | a | b | a | a | b | a | a | b | a | a |
1 | 2 | 2 | 4 | 5 | 5 | 7 | 8 | 8 | 10 | 11 | 11 | 13 | 14 | 14 | 16 | 17 | 17 | 19 |
Zatem Naiwne-Liczenie-Okresu, dla , 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.

Rysunek 1: Załóżmy, że w algorytmie Naiwne-Liczenie-Okresu . Niech , . Ponieważ jest prefiksem słowa specjalnego , zatem . Gdyby to wtedy, ze względu na dwie okresowości, jest właściwym podsłowem słowa oraz . Zaprzecza to założeniu, że jest specjalne. Zatem .
Opiszemy teraz program szukania wzorca w slowie i, zakładając że jest specjalne. Program wczytuje dwa teksty, pierwszy z nich jest specjalny: pamiętamy w tablicy , w tablicy . Program wypisuje wszystkie wystapienia w , tzn. wszystkie takie pozycje , że . Zapisujemy program w języku C++.
Algorytm Specjalny-String-Matching
#include <iostream.h>
#include string.h
int , , ;
void przesun();
main() {
char[] x,y; cin>>x>>y; mstrlen(x); nstrlen(y);
while (i n-m-1)
{ if (jm) {cout<<i<<endl; przesun();};
else if (x[j]y[i+j]) {jj+1; if (j1) (x[j-1]x[j-1-p]) pj;};
else przesun(); } }
void przesun()
{ if (j-1<2p) {ii+p; j0;} else {jj-p; ii+p;}
}
Program jest wstępem do programu szukającego dowolne podsłowo, niekoniecznie o własności bycia specjalnym. Podstawowym niezmiennikiem w programie przed każdym wykonaniem i po każdym zakończeniu pętli while jest: (A)\ , (B)\ Program wypisał wszystkie wcześniejsze wystąpienia , (C)\ jest okresem slowa
Algorytm działa w czasie liniowym. Można to udowodnić obserwując zmiany wartości . Zauważmy, że wartość ta nie zmniejsza się, a w wypadku pozytywnego testu zwiększa się co najmniej o 1. Jednocześnie .
String-matching w pamięci stałej dla dowolnych wzorców
Algorytym Specjalny-String-Matching można łatwo zmodyfikować tak, aby znajdował on wystąpienia dowolnego słowa (niekoniecznie specjalnego) w czasie liniowym i stałej pamięci. Niech , gdzie jest leksykograficzne maksymalnym sufiksem . Oznaczmy . Technicznie informacja o rozkładzie sprowadza się do pamiętania .
Własność rozkładu. Niech będzie rozkładem jak wyżej opisany. Wtedy
słowo występuje tylko raz w słowie .
Jeśli są początkami wystąpień oraz , to
na pozycji nie kończy się wystąpienie .
Z powyższego faktu wynika stosunkowo prosty algorytm szukania w czasie liniowym i pamięci
stałej. Algorytm ten jest modyfikacją algorytmu Specjalny-String-Matching, w którym rolę pełni .
Algorytm String-matching w pamięci stałej
Niech będzie leksykograficznie maksymalnym sufiksem ;
Liczymy algorytmem Specjalny-String-Matching kolejne wystąpienia w ;
Dla każdego wystąpienia niech będzie wystąpieniem poprzednim;
jeśli , sprawdź czy występuje na lewo od pozycji ;
(sprawdzanie to wykonujemy w sposób naiwny)
jeśli występuje, wypisz kolejne wystąpienie całego wzorca .
Pozostawiamy bardziej precyzyjny zapis algorytmu jako ćwiczenie.
W ten sposób pokazaliśmy, że problem szukania słowa w słowie można rozwiązać w czasie liniowym i pamięci (dodatkowej) stałej, jeśli znamy początkową pozycję leksykograficznie maksymalnego sufiksu słowa .
Liczenie maksymalnego sufiksu w pamięci stałej
W algorytmie szukania wzorca w pamięci stałej potrzebna jest pozycja , 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łaśnie liczenie okresu daje efektywność, chociaż na końcu ten okres nie jest nam potrzebny. 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|
;
for to do
if \textbf{then}
'else if then
return ;
return ;
}}
Skorzystamy z algorytmu Najdłuższy-Specjalny-Prefiks. Funkcja Maksymalny-Sufiks liczy początkową pozycję i okres maksymalnego sufiksu.
Algorytm funkcja Maksymalny-Sufiks(x)
;
repeat
Najdłuższy-Specjalny-Prefiks;
if then return
else ;
forever
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 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 porównań symboli. Uzasadnienie pozostawiamy jako ćwiczenie.
Algorytm funkcja} Maksymalny-Sufiks(x)
; ; ;
while () do
;
if ( then
else if ( then begin
; ;
else
; ; ;
return ;
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, którego ścieżki etykietowane są symbolami. W przypadku binarnym możemy przyjąć, że krawędź w lewo jest etykietowana zerem, a w prawo jedynką. Przez kodowanie rozumiemy funkcję , która każdemu symbolowi przyporządkowuje niepusty ciąg binarny . Całe słowo zostanie zakodowane na słowo (każda litera jest zakodowana niezależnie i kody są skonkatenowane). Kod jest prefiksowy gdy zbiór kodów symboli jest prefiksowy. Rozważamy następujący problem:
Optymalne kodowanie prefiksowe
Dla danego słowa znaleźć binarne kodowanie prefiksowe takie, że ma minimalną długość.
Przykład
Niech . Liczby wystąpień symboli w słowie są:
Optymalnym kodowaniem jest zostaje zakodowane na , ciąg binarny długości . Optymalne drzewo binarne odpowiadające optymalnemu kodowi prefiksowemu jest pokazane na rysunku.

Rysunek 2:Drzewo Huffmana kodujące optymalnie symbole z wagami odpowiednio . 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ównież sumą wartości w węzłach wewnętrznych:
Długość tekstu jest równa ważonej sumie długości ścieżek, ważonej w tym sensie, że długość ścieżki do danego liścia jest przemnożona przez wagę tego liścia. W przykładzie jest to suma: .
Niech będzie liczbą różnych symboli w , będzie liczbą wystąpień -tego symbolu. Problem możemy rozwiązać, stosując algorytm dla problemu Optymalne Sklejanie Par dla ciągu . 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 , w element tworzy również dowiązania, staje się lewym synem , natomiast staje się prawym synem.
Algorytm Huffmana (nieformalny opis)
Konfiguracje pośrednie algorytmu to zbiory drzew,
początkowo każdy pojedyńczy element z wagą jest pojedyńczym drzewem.
Korzeń każdego drzewa reprezentuje sklejenie jego wszystkich liści.
Za każdym razem sklejamy dwa korzenie drzew o minimalnej wadze.
Drzewo, które algorytm generuje, nazywamy drzewem Huffmana.
Pozostawiamy jako ćwiczenie przerobienie algorytmu Optymalne-Sklejanie-Par na algorytm liczenia kodów i drzew Huffmana.
Z analizy algorytmu Optymalne Sklejanie Par wynika, że problem optymalnych binarnych kodów prefiksowych można rozwiązać w czasie , a jeśli wagi są posortowane, to w czasie liniowym.
Kodowanie Huffmana słowami -arnymi.
Pozostawiamy jako ćwiczenie podobny problem, ale gdy kodujemy w alfabecie -arnym, mamy teraz symbole . W algorytmie jednorazowo możemy sklejać więcej niż dwa elementy.
Kodowanie prefiksowe z symbolami kodowymi nierównej długości
Problem robi się skomplikowany, gdy długość symbolu 0 jest 1, a długość symbolu 1 jest , gdzie jest pewną stałą (jest to problem tzw. lopsided trees). Inaczej mówiąc, szukamy takiego optymalnego drzewa, w którym ważona suma ścieżek jest minimalna, ale długość krawędzi na lewo wynosi 1, a długość krawędzi na prawo wynosi . Pozostawiamy jako ćwiczenie znalezienie efektywnego algorytmu dla małych ( lub ). Dla dowolnego (będącego częścią wejścia) i dowolnych wag jest to zbyt trudne, nie znamy algorytmu wielomianowego. Dla ustalonego istnieje algorytm wielomianowy, którego stopień zależy od . Natomiast pozostawiamy jako ćwiczenie przypadek, gdy jest dowolne, ale wszystkie wagi są równe.
Kodowanie prefiksowe z kodami o ograniczonej długości
Innym ciekawym problemem jest też skonstruowanie optymalnego kodu prefiksowego, w którym wszystkie słowa kodowe są ograniczone przez pewną zadaną liczbę . Inaczej mówiąc, ograniczamy z góry wysokość drzewa Huffmana. Istnieją wtedy algorytmy wielomianowe dla tego problemu, stopień wielomianu jest niezależny od .