|
|
(Nie pokazano 6 wersji utworzonych przez 2 użytkowników) |
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. | | W module tym zajmiemy się kodowaniem Huffmana. Zwiazane sa z nimi różne 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:
| |
| <center><math> ab < ababab < abb < abbaa < abbaaaaaaaaaaa < abbaaaaaab</math></center>
| |
|
| |
| 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|||
| |
| 'bajtocja' nie jest słowem specjalnym, ale rotacja tego słowa 'tocjabaj' jest.}}
| |
|
| |
|
| |
| 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 <math>x</math> jest specjalny, to okres każdego prefiksu słowa <math>x</math> można policzyć następującym naiwnym
| |
| algorytmem:
| |
|
| |
| {{ algorytm| Funkcja Naiwne-Liczenie-Okresu (j)|funkcja_naiwne_liczenie_okresu|
| |
| <math>period:=1</math>;<br>
| |
| '''for''' <math>i:=2</math> to <math>j</math> '''do'''<br>
| |
| '''if''' <math>x[i] \ne x[i - period]</math> '''then''' <math>period := i</math>;<br>
| |
| '''return''' <math>period</math>;<br>
| |
| }}
| |
|
| |
| {{przyklad|||
| |
| 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> 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>
| |
| 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 <math>x</math> jest specjalne.
| |
| 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>.
| |
| Program wypisuje wszystkie wystapienia <math>x</math> w <math>y</math>, tzn. wszystkie takie pozycje
| |
| <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|
| |
| #include <iostream.h><br>
| |
| #include string.h<br>
| |
| int <math>i=0</math>, <math>j = 0</math>, <math>p = 1</math>;<br>
| |
| void przesun();<br>
| |
| main() {<br>
| |
| char[] x,y; cin>>x>>y; m<math>=</math>strlen(x); n<math>=</math>strlen(y);<br>
| |
| while (i <math><=</math> n-m-1)<br>
| |
| { if (j<math>==</math>m) {cout<<i<<endl; przesun();};<br>
| |
| else if (x[j]<math>==</math>y[i+j]) {j<math>=</math>j+1; if (j<math>==</math>1) <math>||</math> (x[j-1]<math>!=</math>x[j-1-p]) p<math>=</math>j;};<br>
| |
| else przesun(); } }<br>
| |
|
| |
|
| |
| void przesun()<br>
| |
| { if (j-1<2p) {i<math>=</math>i+p; j<math>=</math>0;} else {j<math>=</math>j-p; i<math>=</math>i+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)\ ''' <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>,
| |
| '''(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>.
| |
|
| |
| == 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 <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ł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> 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 liniowym i pamięci
| |
| 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|
| |
| 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>;
| |
|
| |
| 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>, sprawdź czy <math>u</math> występuje na lewo od pozycji <math>i</math>;
| |
|
| |
| (sprawdzanie to wykonujemy w sposób naiwny)
| |
|
| |
| jeśli występuje, wypisz kolejne wystąpienie całego wzorca <math>x</math>.
| |
| }}
| |
|
| |
| Pozostawiamy bardziej precyzyjny zapis algorytmu jako ćwiczenie.
| |
|
| |
| W ten sposób pokazaliśmy, że problem szukania słowa <math>x</math> w słowie <math>y</math> można rozwiązać
| |
| 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 ==
| |
|
| |
| 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
| |
| 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.
| |
| 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|
| |
| <math>period := 1</math>;<br>
| |
| '''for''' <math>i:=2</math> to <math>|x|</math> '''do''' <br>
| |
| '''if''' <math>x[i] < x[i - period]</math> \textbf{then} <math>period := i</math><br>
| |
| '''else if''' <math>x[i] > x[i - period]</math> ''then'''<br>
| |
| '''return''' <math>(i-1, period)</math>;<br>
| |
| '''return''' <math>(|x|, period)</math>;
| |
| }}
| |
|
| |
| Skorzystamy z algorytmu ''Najdłuższy-Specjalny-Prefiks''. Funkcja ''Maksymalny-Sufiks'' liczy początkową pozycję i okres maksymalnego sufiksu.
| |
|
| |
| {{algorytm|funkcja Maksymalny-Sufiks(x)|fun_max_suf|
| |
| <math>j := 1</math>;<br>
| |
| '''repeat'''<br>
| |
| <math>(i,period) :=</math> ''Najdłuższy-Specjalny-Prefiks''<math>(x[j.. n])</math>;<br>
| |
| '''if''' <math>i=n</math> '''then return''' <math>(j, period)</math><br>
| |
| '''else''' <math>j := j+i - (i \mod period)</math>;<br>
| |
| '''forever''' <br>
| |
| }}
| |
|
| |
| 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>
| |
| <math>r := (i-s) \mod p</math>;<br>
| |
| '''if''' (<math>x[i] = x[s+r])</math> '''then''' <math>i := i+1</math><br>
| |
| '''else if''' (<math>x[i] < x[s+r])</math> '''then begin'''
| |
| <math>i := i+1</math>; <math>p := i-s</math>;
| |
| '''else'''
| |
| <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 == | | == Kodowanie prefiksowe: drzewa i kody Huffmana == |
|
| |
|
Linia 242: |
Linia 25: |
| {{przyklad||| | | {{przyklad||| |
| Niech <math>x=abracadabra</math>. Liczby wystąpień symboli w słowie <math>x</math> są: | | Niech <math>x=abracadabra</math>. Liczby wystąpień symboli w słowie <math>x</math> są: |
| <center><math>w_{a}=5, w_{b}=2, w_{c}=1, w_{d}=1, w_{r}=2.</math></center> | | <center><math>w_{a}=5, w_{b}=2, w_{c}=1, w_{d}=1, w_{r}=2</math></center> |
| }} | | }} |
|
| |
|
|
| |
|
| 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. | | 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. |
|
| |
|
| <center>[[Grafika:Huffnal.jpg]]<br><br> | | <center>[[Grafika:Huffnal.jpg]]<br><br> |
| Rysunek 2:Drzewo Huffmana kodujące optymalnie symbole <math>a,b,c,d,r</math> z wagami odpowiednio | | 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. | | <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ównież sumą wartości w | | 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: <math> 2+4+6+11\ =\ 23.</math> | | węzłach wewnętrznych: <math>2+4+6+11= 23</math> |
|
| |
|
| </center> | | </center> |
Linia 258: |
Linia 41: |
| 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ść 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 |
Zaawansowane algorytmy tekstowe II
W module tym zajmiemy się kodowaniem Huffmana. Zwiazane sa z nimi różne ciekawe problemy tekstowe.
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 .