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__ | ||
W module tym zajmiemy się przedw wszystkim dwoma niezależnymi problemami: ‘’string-matching’’ w czasie liniowym i pamięci stałej, oraz kodowaniem Huffmana. Zwiazane sa z nimi róne inne ciekawe problemy tekstowe. | |||
Linia 13: | Linia 9: | ||
== String-matching w pamięci stałej dla specjalnych wzorów == | == String-matching w pamięci stałej dla specjalnych wzorów == | ||
Załóżmy, ż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> | |||
Oznaczmy przez <math>MaxSuf(w)</math> maksymalny | Oznaczmy przez <math>MaxSuf(w)</math> leksykograficznie maksymalny sufiks słowa <math>w</math>. Słowo <math>x</math> nazwiemy ‘’specjalnym’’, gdy <math>MaxSuf(x)=x</math>. | ||
{{przyklad||| | {{przyklad||| | ||
Linia 86: | Linia 86: | ||
}} | }} | ||
Zatem ''Naiwne-Liczenie-Okresu''<math>(19)\ =\ 19</math>, dla <math>x\ = \ (aba)^6a</math> | Zatem ''Naiwne-Liczenie-Okresu''<math>(19)\ =\ 19</math>, dla <math>x\ = \ (aba)^6a</math> daje wynik całkowicie niepoprawny. Poprawność algorytmu jest wyjaśniona na rysunku. Korzystamy z następującej prostej własności: | ||
<br> | |||
<center> prefiks specjalnego słowa jest też specjalny. <center> | |||
<center>[[Grafika:Naiwneliczenieokresu.jpg]]<br><br> | <center>[[Grafika:Naiwneliczenieokresu.jpg]]<br><br> | ||
Linia 127: | Linia 130: | ||
Program jest wstępem do programu szukającego dowolne podsłowo, niekoniecznie | Program jest wstępem do programu szukającego dowolne podsłowo, niekoniecznie specjalne. 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>, | ||
Linia 177: | Linia 179: | ||
W algorytmie szukania wzorca w pamięci stałej potrzebna jest pozycja <math>r</math>, od której zaczyna się maksymalny | 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: dla każdego prefiksu liczymy maksymalny sufiks, jak również dodatkowo jego okres. To właśnie | czegoś więcej: <br> | ||
dla każdego prefiksu liczymy jego maksymalny sufiks, jak również dodatkowo jego okres. | |||
<br> | |||
To właśnie | |||
liczenie okresu daje efektywność, chociaż na końcu ten okres nie jest nam potrzebny. | 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 | Przekształcimy najpierw algorytm ''Naiwne-Liczenie-Okresu'' na algorytm liczący długość najdłuższego | ||
Linia 254: | Linia 261: | ||
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>. Algorytm ten był przedsatwiony na wykładach z ASD. 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, | 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. | <math>a</math> staje się lewym synem <math>c</math>, natomiast <math>b</math> staje się prawym synem. | ||
Linia 284: | Linia 291: | ||
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 <math>c</math> istnieje algorytm wielomianowy, którego stopień zależy od <math>c</math>. Natomiast pozostawiamy jako | ustalonego <math>c</math> istnieje algorytm wielomianowy, którego stopień zależy od <math>c</math>. | ||
ćwiczenie przypadek, gdy <math>c</math> jest dowolne, | |||
Natomiast pozostawiamy jako | |||
ćwiczenie przypadek, gdy <math>c</math> jest dowolne, a wszystkie wagi <math>w[i]</math> są równe. Istniej wtedy algorytm wielomianowy. | |||
=== 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 | ||
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ą | zadaną liczbę <math>L</math>. Inaczej mówiąc, ograniczamy z góry wysokość drzewa Huffmana. Zakładamy teraz, że wagi krawędzi są takie same. Istnieją algorytmy | ||
wielomianowe dla tego problemu, stopień wielomianu jest niezależny od <math>L</math>. | wielomianowe dla tego problemu, w których stopień wielomianu jest niezależny od <math>L</math>. |
Wersja z 19:30, 1 paź 2006
Zaawansowane algorytmy tekstowe II
W module tym zajmiemy się przedw wszystkim dwoma niezależnymi problemami: ‘’string-matching’’ w czasie liniowym i pamięci stałej, oraz kodowaniem Huffmana. Zwiazane sa z nimi róne inne ciekawe problemy tekstowe.
String-matching w pamięci stałej dla specjalnych wzorów
Załóżmy, ż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:
Oznaczmy przez leksykograficznie maksymalny 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 daje wynik całkowicie niepoprawny. Poprawność algorytmu jest wyjaśniona na rysunku. Korzystamy z następującej prostej własności:

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 specjalne. 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 jego 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 . Algorytm ten był przedsatwiony na wykładach z ASD. 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, a wszystkie wagi są równe. Istniej wtedy algorytm wielomianowy.
Kodowanie prefiksowe z kodami o ograniczonej długości
Innym ciekawym problemem jest 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. Zakładamy teraz, że wagi krawędzi są takie same. Istnieją algorytmy wielomianowe dla tego problemu, w których stopień wielomianu jest niezależny od .