P r z e d m o w a .......................
2.
P o d s ta w o w e z a s a d y a n a liz y a l g o r y t m ó w .......... ...........................
13
1.1. 1.2. 1.3. 1.4. 1.5.
13 20 21
'1(1 4.1 42 42
S o r t o w a n i e ............. ..................................
49
2.1. 2.2. 2.3.
50
Selectionsoi'1. —sortowanie przez selekcję . . Insertionsort - sortowanie przez wstawianie Quicksort —sortowanie szybkie....................
£ 8
Złożoność obliczeniowa.................................................................................... Równania rekurencyjne.................................................................................. Funkcje tw orzące............................................................................................. Poprawność semantyczna . . . . Podstawowe struktury danych 1.5.1. Lista.............................. ' 1.5.2. Zbiór............................... 1.5.3. G rał'............................... 1.5.4. Notacja funkcyjna dla atrybutów obiektów. . . 1.5.5. Drzewo............................•.................................. 1.6. Eliminacja re k u rsji...................................................... 1.7. Koszt zamortyzowany operacji w strukturze danych 1.8. Metody układania algorytmów . . . 1.8.1. Metoda „dziel i zwyciężaj”, 1.8.2. Programowanie dynamiczne................................................................ 1.8.3. Metoda zachłanna................................................................................ 1.8.4. Inne metody........................................................................................ Z a d a n ia ........................................................................................................................
5 o $ l £ § g 3 8
1.
9
53
6
3.
S p is treści
2.4. 2.5. 2.6. 2.7. 2.8. 2.9. 2.10.
Dolne ograniczenie na złożoność problemu sortow ania............................. Sortowanie pozycyjne...................................................................................... Kolejki priorytetowe i algorytm heapsorl,.................................................... Drzewa turniejowe i zadania selekcji........................................................... Szybkie algorytmy wyznaczania /c-tego największego elementu w ciągu . . Scalanie ciągów uporządkowanych................................................................ Sortowanie zewnętrzne!.................................................................................... 2.10.1. Scalanie wielofazowe z4 plikam i..................................................... 2.10.2. Scalanie, wielofazowe z3 plikam i..................................................... Z a d a n ia .........................................................................................................................
62 66 70 78 83 86 89 90 91 94
S ło w n ik i ............................................................................................................
99
3.1. 3.2. 3.3.
Implementacja listowa nieuporządkowana........... ....................................... 1.00 implementacja listowa uporządkowana......................................................... 100 Drzewa poszukiwań binarnych....................................................................... 105 3.3.1. Drzewa A V L ........................................................................................ 113 3.3.2. Samoorganizujące się drzewa BST . ........................................ 117 3.4. M ieszanie........................................................................................................... 120 3.4.1. Wybór funkcji mieszającej.................................................................. 121 3.4.2. Struktury danych stosowane do rozwiązywania problemu kolizji . 122 3.5. Wyszukiwanie pozycyjne...................................................................................... 126 3.5.1. Drzewa R.ST................................................ ........................................ 127 3.5.2. Drzewa T R IK ...................................................................................... 130 3.5.3. Drzewa PATRICIA............................................................................. 131 3.6. Wyszukiwanie zew nętrzne............................................................................. 134 3.6.1. Pliki nieuporządkow ane...........................................................................1.35 3.6.2. Pliki z funkcji} m ieszającą.................................................................. 135 3.6.3. Sekwencyjne pliki indeksow ane....................................................... 135 3.6.4. B-drzewo jako wielopoziomowy indeksrz a d k i.................................. 136 3.6.5. B-drzewo jako wielopoziomowy indeks g ę sty ................................... 138 Z a d a n ia ........................................................................................... : ........................... 138
4.
Z ło ż o n e s t r u k t u r y d a n y c h d la z b io ró w e l e m e n t ó w ....................
143
4.1.
Problem sumowania rozłącznych zbiorów.................................................... 143 4.1.1. Implementacja lis to w a ............................................................................. 144 4.1.2. Implementacja drzewowa.................................................................... 148 4.2. Zlączalne kolejki priorytetow e...................................................................... 155 Z a d a n ia ......................................................................................................................... 162
5.
A lg o ry tm y t e k s t o w e ............................ : ..................................................... 5.1.
Problem wyszukiwania w zorca.................................................................. . 5.1.1. Algorytm N („naiwny”) .....................................................................
164 165 165
Spis treści
7
5.1.2. Algorytm KMP (Knutha-M orrisa-Pratta)......................................... 5.1.3. Algorytm liniowy dla problemu wyszukiwania wzorca dwuwymiarowego, czyli algorytm B a k e ra ....................................... 5.1.4. Algorytm GS' (wersja algorytmu Galila-Scifbrasa dla pewnej klasy wzorców).................................................................................... 5.1.5. Algorytm KMR (Karpa-Millera-Rosenberga).................................. 5.1.6. Algorytm KR (K arpa-Eabina)........................................................... 5.1.7. Algorytm BM (Boyera-Moore’a ) ......................................................... 5.1.8. Algorytm PP (Fishera-Patorsona).................................................... 5.2. Drzewa sufiksowe i grafy pod słó w ................................................................ 5.2.1. Niez,warta reprezentacja drzewa sufiksowego................................ 5.2.2. Tworzenie drzewa sufiksowego......................................................... 5.2.3. Tworzenie grafu podslów.................................................................... 5.3. Inne algorytmy tek sto w e................................................................................ 5.3.1. Obliczanie najdłuższego wspólnego podslowa.................................. 5.3.2. Obliczanie najdłuższego wspólnego podciągu.................................. 5.3.3. Wyszukiwanie słów podwójnych...................................................... 5.3.4. Wyszukiwanie słów sym etrycznych.................................................. 5.3.5. Równoważność cykliczna.................................................................... 5.3.6. Algorytm H u ffm an n ........................................................................... 5.3.7. Obliczanie leksykograficznie maksymalnego sulik.su.................... 5.3.8. Jednoznaczne kodowanie.................................................................... 5.3.9. Liczenie liczby podslów....................................................................... Z a d a n ia .........................................................................................................................
;i6g
A lg o ry tm y r ó w n o le g le ................................................................................
205
6.1. Równolegle obliczanie wyrażeń i prostych programów sekwencyjnych . • . 6.2. Sortowanie rów nolegle.................................................................................... Z a d a n ia ........................................................................................................................
207 221 224
A lg o ry tm y g r a f o w e .....................................................................................
227
7.1. Spójne składow e................................................................. 7.2. Dwuspójne sk ład o w e...................................................................................... 7.3. Silnie spójne składowe i silna orientacja...................................................... 7.4. Cykle E u le r a .................................................................................................... 7.5. 5-kolorowanie grafów p lanarnych.................................................................. 7.6. Najkrótsze ścieżki i minimalne dfzewo rozpinające.................................... Z a d a n ia ........................................................................................................................
229 232 239 245 249 254 256
8. A lg o ry tm y g e o m e t r y c z n e ........................................................................
258
8.1. Elementarne algorytmy geom etryczne..........................................................
259
6.
7.
169
171 172 174 175 178 180 180 182 185 189 189 190 190 193 194 195 196 199 200 200
S pis treści
8.2. 8.3. 8.4.
Problem przynależności.................................................................................. Wypukła o to czk a............................................................................................. Metoda zam iatan ia............................................................... 8.4.1. Najmniej odległapara punktów ............................................................ 8.4.2. Pary przecinających sięodcinków....................................................... Z a d a n ia ........................................................................................................................
260 263 271 272 275 282
B ib lio g r a f ia
284
S k o ro w id z
286
®rsediiiii|va: IlSli
iniejsza książka jest przeznaczona dla.czytelników interesujących się głębiej infor matyka, w tym przede wszystkim dla studentów informatyki. W szczególności może służyć jako podręcznik do wykładów: „Algorytmy i struktury danych” i „Analiza algorytmów” dla studentów studiów informatycznych. Jej fragmenty mogą być także wykorzystane, w nauczaniu przedmiotu „Metody programowania” i „Kombinatoryka i teoria grafów” w ujęciu algorytmicznym. Sadzimy, że książką może też zainteresować szersze kręgi czytelników, gdyż daje elementarne wprowadzenie do nowo czesnych metod tworzenia i analizy algorytmów. Metody te oraz bogactwo różnorodnych struktur danych, przedstawionych w książce, mogą być pomocne, w projektowaniu efek tywnych algorytmów dla problemów pojawiających się w praktyce programistycznej lub pracy badawczej.
N
Zakładamy, że czytelnik ma pewne podstawowe przygotowanie z kombinatoryki i ra chunku prawdopodobieństwa (na poziomie, szkoły średniej) i że. umie układać algorytmy w Pascalu. Znajomość przedmiotów: „Wstęp do informatyki” , „Metody programowa nia” i „Analiza matematyczna J” jest pożądana przy czytaniu tej książki, ale nie konieczna. Książka powstała z not,■tick do wykładów: „Algorytmy i struktury danych” oraz „Analiza algorytmów” , prowadzonych przez nas dla studentów informatyki Uniwersytetu War szawskiego w latach 1986-1994. Pierwsza jej wersja ukazała się w postaci skryptu Uni wersytetu Warszawskiego [BDR], Niniejsza książka składa się. /. 8 rozdziałów. Rozdział I stanowi wprowadzenie do dzie dziny analizy algorytmów. Zdefiniowaliśmy w nim takie pojęcia, juk złożoność oblicze niowa i poprawność semantyczna algorytmu. Omówiliśmy rozwiązywanie, równań rekurcncyjnych i podstawowe struktury danych: listy, zbiory, grafy, drzewa i ich realizacje. Przedstawiliśmy także elementarne metody algorytmicznego rozwiązywania problemów.
10
P rz e d m o w a
Rozdział 2 zawiera omówienie głównych algorytmów sortowania wraz z ich analizą i zastosowaniami wprowadzonych struktur danych do rozwiązywania pokrewnych prob lemów. Rozdział 2 jest poświęcony zadaniu wyszukiwania elementów w zbiorze. Przed stawiliśmy w nim podstawowe struktury danych i częściową ich analizę. W rozdziale 4 omówiliśmy i zanalizowaliśmy dwie złożone struktury danych, umożliwiające szybkie wykonywanie, operacji na zbiorach rozłącznych. Opisaliśmy rozwiązanie problemu su mowania zbiorów rozłącznych i przedstawiliśmy implementację zlączalnych kolejek priorytetowych za pomocą drzew dwumianowych. Rozdziały 5, 6, 7, 8 są poświęcone dziedzinom informatyki teoretycznej, w której badania nad algorytmami rozwijały się w ostatnich lalach najszybciej. Przedstawiliśmy w nich algorytmy tekstowe, a także algorytmy równolegle, grafowe i geometryczne. Każdy rozdział jest zakończony zesta wem zadań umożliwiających pogłębienie zdobywanej wiedzy. Celem naszym nie było dostarczenie programów gotowych do uruchomienia. Pełna implementacja niektórych z zaprezentowanych algorytmów wymaga pewnego wysiłku programistycznego. Zachę camy Cię do podjęcia go, ponieważ dopiero polna, poprawna implementacja świadczy o dobrym zrozumieniu przerabianego materiału. Większość przedstawionych tu algorytmów i struktur danych uważa się już dziś za klasy czne. Można je znaleźć (rozproszone) w licznych książkach poświęconych algorytmom (niestety w większości w języku angielskim). Czytelnikowi zainteresowanemu innym spojrzeniem na omawianą tematykę oraz poszerzaniem wiedzy dotyczącej problemów poruszanych w tym opracowaniu polecamy podane niżej pozycje. A. V. Aho, J. E. Hopcroft, j . D. Ullman: Projektowanie i analiza algorytmów komputerowych [AHUj Jest to książka poświęcona algorytmom i strukturom danych z wielu różnych działów informatyki teoretycznej. Ł. Banachowski, A. Kreczmar: Elementy analizy algorytmów |BK| W książce, tej przedstawiono podstawowe zasady analizy algorytmów, zilustrowane ciek;iwy m i przy kładami. L. Banachowski, A. Kreczmar, W. Ryttcr: Analiza algorytmów i struktur da nych | BKR ] W książce tej omówiono złożone metody projektowania i analizowania algorytmów oraz struktur danych. T. II. Cormcn, C. E. Leiserson, R. L. Rivcst: Wprowadżenic do algorytmów ICJ.RI W książce tej przedstawiono najważniejsze (od elementarnych do złożonych) algorytmy i struktury danych z wielu różnych dziedzin informatyki.
I
P rz e d m o w a
11
M. Crochemorc, W. Ryttcr: Text algorithms [CR] Jest to książka poświęcona algorytmom na tekstach. A. Gibbons, W. Rytter: Efficient parallel algorithms [GR| W książce tej omówiono algorytmy równolegle z wielu różnych działów informatyki teoretycznej. J. Jaja: An introduction to parallel algorithms [J| Jest to wprowadzenie do problematyki algorytmów i obliczeń równoległych. 1). E. Knuth: The art of computer programming. Sorting and searching |K| W książce tej omówiono klasyczne metody sortowania i wyszukiwania. W. Lipski: Kombinatoryka dla programistów [L] Jest to książka poświęcona podstawowym algorytmom grafowym. K. Mchlhorn: IVlulti-dimensional searching and computational geometry |M| Tematem książki są problemy i algorytmy geometryczne. F. P. Preparatu, M. 1. Shamos: Computational geometry (an introduction) [PS] Jest. to doskonale wprowadzenie do problematyki geometrii obliczeniowej. R. Sedgevvick: Algorithms [S] W książce lej bardzo przystępnie omówiono różne algoryimy i struktury danych. Na przedstawionej tu liście nie ma oczywiście wszystkich pozycji poświęconych algoryt mom i strukturom danych. Wymieniliśmy tylko Ic, które wykorzystaliśmy do przygoto wania notatek do wykładów, a następnie tej książki. Opis większości omawianych lii problemów, algorytmów i struktur danych można znaleźć w książkach z tej listy. W razie odstępstwa od powyższej zasady podajemy bezpośrednie źródło omawianego tematu.
Warszawa 199ó r.
fiiU
J ;W
P
b
d
s i i a w
b
- ^
analizy al^bi"yfrai©w
tym rozdziale przedstawiamy podstawowe pojęcia stosowane przy badaniu al gorytmów i struktur danych. Przede wszystkim wyjaśniamy, na czym polega analiza algorytmu w dwóch głównych aspektach: poprawności semantycznej i złożoności obliczeniowej. Omawiamy elementarne struktury danych definiowane abs trakcyjnie (jako listy, zbiory, grafy, drzewa itd.), z możliwymi różnymi konkretnymi implementacjami (reprezentacjami). Na końcu rozdziału przedstawiamy podstawowe metody konstruowania efektywnych algorytmów (metoda „dziel i zwyciężaj” , progra mowanie dynamiczne, metoda zachłanna, metoda kolejnych transformacji).
W
Analiza algorytmów to dział informatyki zajmujący się szukaniem najlepszych algoryt mów dla zadań komputerowych. Analiza algorytmów polega między innymi na znalezie niu odpowiedzi na podane tu pytania. 1. 2. 3. 4.
Czy dany problem może być rozwiązany na komputerze w dostępnym czasie i pamięci? Który ze znanych algorytmów należy zastosować w danych okolicznościach? Czy istnieje lepszy algorytm od rozważanego? A może jest on optymalny? Jak uzasadnić, że. stosując dany algorytm, rozwiąże się zamierzone, zadanie?
Dokonując analizy algorytmu, zwracamy uwagę na jego poprawność semantyczną, pros totę, czas działania, ilość zajmowanej pamięci, optymalne,ść oraz okoliczności, w jakich należy go używać, a w jakich nie.
1. 1.
Złożoność obliczeniowa Złożoność obliczeniową algorytmu definiuje się jako ilość zasobów komputero wych, potrzebnych do jego wykonania. Podstawowymi zasobami rozważanymi w anali zie algorytmów są czas działania i ilość zajmowanej pamięci.
1.4
1. 1’od sta w o w e zasad y an alizy algorytm ów
Zauważmy, że nie jest na ogól możliwe wyznaczenie złożoności obliczeniowej jako funkcji danych wejściowych (takich jak ciągi, tablice, drzewa czy grafy). Zwykle, co naturalne, z zestawem danych wejściowych jest związany jego rozmiar, rozumiany - mówiąc ogólnie - jako liczba pojedynczych danych wchodzących w jego skład. W problemie sortowania na przykład za rozmiar przyjmuje się zazwyczaj liczbę elemen tów w ciągu wejściowym, w problemie przejścia drzewa binarnego - liczbę węzłów w drzewie, a w problemie wyznaczenia wartości wielomianu - stopień wielomianu. Rozmiar zestawu danych d będziemy oznaczać przez |r/[. Aby móc wyznaczać złożoność obliczeniową algorytmu, musimy się jeszcze umówić, w jakich jednostkach będziemy ją liczyć. Na złożoność obliczeniową składa się złożo ność pamięciowa i złożoność czasowa. W wypadku złożoności pamięciowej za jedno stkę przyjmuje się zwykle słowo pamięci maszyny. Sytuacja jest nieco bardziej skom plikowana w wypadku złożoności czasowej. Złożoność czasowa powinna być własnoś cią samego tylko algorytmu jako metody rozwiązania problemu - niezależnie od kom putera, języka programowania czy sposobu jego zakodowania. W tym celu wyróżnia się w algorytmie charakterystyczne dla niego operacje, nazywane operacjami dominujący mi - takie, że łączna icli liczba jest proporcjonalna do liczby wykonali wszystkich opera cji jednostkowych w dowolnej komputerowej realizacji algorytmu. Dla algorytmów sortowania na przykład za operację dominującą przyjmuje się zwykle porównanie dwóch elementów w ciągu wejściowym, a czasem też przestawienie elemen tów w ciągu; dla algorytmów .przeglądania drzewa binarnego przyjmuje się przejście dowiązania między węzłami w drzewie, a dla algorytmów wyznaczania wartości wielo mianu .operacje arytmetyczne -I-, ~, * i /. Za jednostkę złożoności czasowej przyjmu je się wykonanie jedne j operacji dominującej. Złożoność obliczeniową algorytmu traktuje się jako funkcję rozmiaru danych //. Wyróż nia się: złożoność pesymistyczną - definiowaną jako ilość zasobów komputerowych, potrzebnych przy „najgorszych” danych wejściowych rozmiaru //, oraz złożoność ocze kiwaną - definiowaną jako ilość zasobów komputerowych, potrzebnych przy „typo wych” danych wejściowych rozmiaru n. Aby zdefiniować precyzyjnie pojęcia pesymistycznej i oczekiwanej złożoności czasowej, wprowadzi my następujące, oznaczenia: Dn . zbiór zestawów danych wejściowych rozmiaru //; /(//) . liczba operacji dominujących dla zestawu danych wejściowych d\ Xn -. zmienna losowa, której wartością jest dla d e D„; pltl. - rozkład prawdopodobieństwa zmiennej losowej X„, tzn. prawdopodobieństwo, że dla danych rozmiaru n algorytm wykona A operacji dominujących (A. > 0). Rozkład prawdopodobieństwa zmiennej losowej Xn wyznacza się na podstawie infor macji o zastosowaniach rozważanego algorytmu. Gdy na przykład zbiór Dn jest skon-
15
1.1. Z łożoność o b liczen io w a
czony, przyjmuje się często model probabilistyczny, w którym każdy zestaw danych rozmiaru n może się pojawić na wejściu do algorytmu z jednakowym prawdopodo bieństwem. Przez pesymistyczną złożoność czasową algorytmu rozumie się funkcję
ii
W(n) = sup{.!(//): ,/ e
gdzie sup oznacza kres górny zbioru. Przez oczekiwaną złożoność czasową algorytmu rozumie się funkcję Mn) = Z kpnk k>0 tzn. wartość oczekiwaną ave(X„) zmiennej losowej Aj,. Aby stwierdzić, na ile funkcje W(n) i A(n) są reprezentatywne dla wszystkich da nych wejściowych rozmiaru n, rozważa się miary wrażliwości algorytmu: miarę wraż liwości pesymistycznej, czyli A(;i) = sup{/!(
(var(Aj,) jest wariancją zmiennej losowej Aj,). Im większe są wartości funkcji A(n) i 5(h), tym algorytm jest bardziej wrażliwy na dane wejściowe i tym bardziej jego zachowanie w wypadku rzeczywistych danych może odbiegać od zachowania opisanego funkcjami W(n) i A(n). □ P rzykład :
Przeszukiwanie sekwencyjne ciągu
Dane wejściowe: L, N. a, gdzie /V jest liczbą naturalną N > 0, a jest poszukiwanym elementem, L[l..N + 1] jest tablicą, w której na miejscach od I do N znajdują się ele menty ciągu. Wynik:
Zmienna logiczna p taka, że p = true s a znajduje się w 1.ĄI..N}
Algorytm: begin
j ■= i ; . I,[JV+1] := a ; while L [ j ) p := j < N
and;
a do j
: = j -I- 1;
16
X. P o d sta w o w e za sa d y an a lizy algorytm ów
Rozmiar danych wejściowy cli: Operacja dominująca: Pesymistyczna złożoność czasowa: Pesymistyczna wrażliwość czasowa:
n —N porównanie: L[j\ ż- a W(n) — n -I- I A(//) = n
A jaka jesl oczekiwana złożoność czasowa? Załóżmy, że prawdopodobieństwo zna lezienia a na każdym z n możliwych miejsc jesl lakie samo i wiadomo, że a jest w L|'1../V], tzn. że p k = — dla k = 1,2, .... n n Wówczas
Mn)
X M>„t = - £ * = - » t =i " k« l
//(// -I- 1)
n+ I "~2~
Oczekiwana wrażliwość czasowa: var(X(1) = X k *= i
n+ I /i
2
I ( //(//. + i)(2/i + 1) n ( 6 (n -I- I) (2// -l- 1) —
2(/i -I- I) n(n + 1) - 2
(n + i)2 4
-1-
n
n -I- I w 2 ”J )
n+ I (4/i + 2 - 3/7. - 3) = ~~vi"
czyli 8(/j) s
.29n
Zauważmy, że zarówno funkcje wrażliwości powyższego algorytmu, jak i funkcje jego złożoności są liniowe; wynika stąd duża wrażliwość liczby operacji dominujących na dane wejściowe.
Faktyczna złożoność czasowa algorytmu (czas działania) w oliwili jego użycia jako pro gramu różni się od wyliczonej teoretycznie współczynnikiem proporcjonalności, który zależy od konkretnej realizacji tego algorytmu. Istotną zatem częścią informacji, która jest zawarta w funkcjach złożoności W(n) i A(n), jest ich rząd wielkości, czyli ich zachowanie asymptotyczne, gdy n dąży do nieskończoności. Zwykle staramy się podać jak najprostszą funkcję charakteryzującą rząd wielkości W(n) i /!(//), na przykład //, /ilog/i, n2, n \
17
1.1. Z łożoność o b liczen io w a
Używani)' w tym celu następujących oznaczeń dla rzędów wielkości funkcji. Niech J\ t /;: /V —> A’, u {()}, gdzie /V i A’, oznaczają zbiory liczb - odpowiednio - naturalnych i rzeczywistych dodatnich. Mówimy, że f jest co najwyżej rzędu g, co zapisujemy jako /j//) = <>{g(n)), jeśli istnieją stała rzeczywista e > Oi stała ii;it:uralna //,, takie, że nierówno.śe/(/0 -S cg(//) zachodzi dla każdego n > na. Oto przykład: ir -i- 2n — Of/r), bo rr + 2n < 3ir dla każdego naturalnego //. Mówimy, że f jest co najmniej rzędu g, co zapisujemy jako /(;/) = il(,ą(//J), jeśli K(«) = 0(f(n)). ' Mówimy, że f jest dokładnie rzędu g, co zapisujemy jako f(n ) = 0(n), jeśli zarówno /(») = ()(j;(/i)), jak i f(n) = .Q(,t;(//)). Poprawny jest też termin f jest asymptotycznie równoważne g i oznaczenie f(n) s gin). Oto przykład: rr + 2n - /r, bo zarówno n2 + 2n < 3/t", jak i n2 + 2n > ii ' dla każdego ii > 0. Będziemy także używać oznaczenia/(n) = g(n) + 0(li(n)), gdy/(«) - / ,’(//) - 0(li(n)), na przykład (1/2) n2 + 5 n -I- I = (1/2) z/2 -I- (>(//). Zauważmy, że w ten sposób zachowujemy współczynnik proporcjonalności przy najbardziej znaczącym składniku sumy i pomijamy współczynniki przy mniej znaczących składnikach sumy. Rzędy wielkości dwóch funkcji/(/t) i g(ii) mogą być porównane przez obliczenie granicy . ..
,.
/( " )
A = lim -----g00 Jeśli li - +oo, to g(n) — 0(J\n)), ale nie/(//) = 0(g(n)). Jeśli li — c > 0, to /(«) = ,','(»)■ Jeśli fi --- 0, to f(n) = 0(g(n))y ale nie g(n) = 0(f(n)). iStosujiic n;t przykład regułę de l/Hospilala, otrzymujemy Uli
ologrt
czyli n.log/1 =
In n . Mn lim —;— r~ = lim /iln 2 In 2 k
0
ale nie /r = Ofnlog/t).
Większość rozważanych algorytmów ma złożoność czasową proporcjonalną do jednej z podanych tu funkcji. log/t - złożoność logarytmiczna Czas działania logarytmiczny występuje na przykład dla algorytmów typu: zadanie roz miaru n zostaje sprowadzone do zadania rozmiaru n/2 -I- pewna stała liczba działań, na przykład poszukiwanie binarne, w ciągu uporządkowanym a, Sir/., < ... < a„.
.18
l. P o d sta w o w e z a sa d y an a lizy algorytm ów
Aby stwierdzić, czy „v znajduje się w iym ciągu, porównujemy ,v najpierw z «| ,,, Jeśli A- < fi| „n J >10 szukamy dalej x w ciągu u, < u, < ... n | j _ Jeśli natomiast
I...1J
11 Dla liczby rzeczywistej ,v zapis oznacza największa liczbę całkowita., nic większa niż ~ najmniejsza liczbę, całkowita, nie mniejsza niż w.
a, a
zapis | v
1.1. Z łożoność ob liczen io w a
19
Zauważmy, że, algorytm o złożoności wykładniczej może być zrealizowany jedynie dla małych rozmiarów danych. Istnieje próg, od którego funkcja wykładnicza zaczyna rosnąć lak szybko, że realizacja algorytmu na komputerze staje się niemożliwa. Załóżmy na przykład, że dla danych rozmiaru n jest wykonywanych 2" operacji jednostkowych i że każda operacja jednostkowa zajmuje odpowiednio 1()“r> i 10-"9 sekund na dwóch różnych komputerach. Czas działania potrzebny do realizacji algorytmu jest przedsta wiony w tabeli 1.1. Tabela U . Porównanie czasów realizacji algorytmu wykładniczego na dwóch komputerach 20
50
100
Czas działania (2710")
1,04 s
35,7 lat
4 ■ i 0 1" wieków
5 ■ 1(),M wieków
Czas działania (2710'')
0,001 S
13 dni
4 • 10" wieków
5 ■ 10'" wieków
Rozmiar
n
. 200
Widać, że nawet iOOO-krotne przyspieszenie szybkości działania komputera niewiele pomaga algorytmowi wykładniczemu. Nicrealizowalność uważa się za wewnętrzną ce chę algorytmu o złożoności wykładniczej. Aby jednak mieć pełny obraz sytuacji, powin niśmy jeszcze rozważyć wrażliwość algorytmu na dane wejściowe. Może się zdarzyć, że dla danego algorytmu W(n) = 2" + 0(1), ale także A(//) = 2" -l- 0(1). Wówczas nie może my twierdzić, że algorytm jest nicrcalizownlny dla reprezentatywnych danych. Dane wejściowe, dla których czas działania jest wykładniczy, mogą się nigdy nie pojawić w rzeczywistych okolicznościach! Właśnie taka sytuacja zachodzi dla metody simplex programowania liniowego. Choć metoda ta ma złożoność wykładniczą dla „najgor szych” danych, dla pojawiających się w praktyce danych wejściowych działa w czasie wielomianowym, a nawet liniowym. Co więcej, w wypadku takich danych przewyższa metodę elipsoidalną, której pesymistyczna złożoność czasowa jest wielomianowa! Przy korzystaniu z wyników analizy złożoności algorytmu należy zatem brać pod uwagę następujące uwarunkowania: • algorytm i jego realizacja przeznaczona do wykonania są zwykle wyrażone w dwóch całkowicie różnych językach; ® wrażliwość algorytmu na dane wejściowe może spowodować, że faktyczne zachowa nie się algorytmu na używanych danych, będzie odbiegać od zachowania opisanego funkcjami złożoności W(n) i /\(n); • może być trudno przewidzieć rzeczywisty rozkład prawdopodobieństwa zmiennej losowej Aj,; • dla niektórych algorytmów nic są znane matematyczne oszacowania wielkości W(n) i A(n): szczególnie wyznaczenie A(n) dla rzeczywistego rozkładu prawdopodobieństwa może, Sianowie bardzo trudny problem matematyczny; j( • czasami działanie dwóch algorytmów trudno jest jednoznacznie porównać; jeden dzia ła lepiej dla pewnej klasy zestawów danych, a drugi dla innych.
20
1. P o d sta w o w e za sa d y an a lizy algorytm ów
Ważną cechą algorytmu jest jego prostota, z której zwykle wynika mniejszy współczyn nik proporcjonalności przy złożoności obliczeniowej oraz łatwość realizacji (zaprogra mowania). Szczególnie więc w dwóch przypadkach: ® program, w którym jest stosowany nasz algorytm, ma być wykonany raz lub tylko kilka razy; ® algorytm ma być stosowany tylko dla małych rozmiarów danych; należy wybierać algorytm raczej pod kątem jego prostoty niż małej złożoności oblicze niowej (oczywiście najlepiej używać zawsze algorytmów zarówno prostych, jak i szyb kich w sensie asymptotycznym).
1. 2 . Równania rekurencyjne Wyznaczenie złożoności algorytmu sprowadza się często do rozwiązania równania rekurencyjnego. Stosowane są zwykle dwie metody: rozwinięcie równania do sumy (me toda 1) i znalezienie funkcji tworzącej (metoda 2). Metodą 2 zajmiemy się w następnym podrozdziale. Teraz pokażemy zastosowanie meto dy 1 do rozwiązania trzech często pojawiających się równań rekurencyjnych (c oznacza stalą naturalną dodatnią). (I) (T ( 1) = 0 1 T(n) = 7'(L«/2 J) -l- c dla n > I (Równanie to otrzymujemy jako równanie złożoności wtedy, kiedy problem rozmiaru n sprowadza się do podproblemu rozmiaru połowę mniejszego). Metoda rozwiązania takiego równania polega na podstawieniu n = 2* (lj. potęgi dwójki), rozwiązania powstającego równania. Stąd możemy już wnioskować (zob. zad. 1.4), że rząd wielkości rozwiązania oryginalnego równania jest taki sam jak równania dla potęg dwójki. Podstawmy więc u = 2*. Wtedy 7(2*) = 7(2*" ') + c = 7(2* - 2) + <; + c = 7(2") -i- kc = kc = dog n Stąd wynika, że T(n) = ©(log/i) (2)
f 7( I) = 0 I 7\n) - 7’(L///2_|) -I- 7’( f nl2 ] ) -I- c dla n > 1
21
1.3. F unkcje tw orzące
(Równanie lo otrzymujemy jako równanie złożoności wtedy, kiedy problem rozmiaru n .sprowadza się do dwóch podproblemów rozmiaru /t/2 -i- stała liczba działań). Podstaw my więc n - 2*. Wtedy 7(2*) = 27X2* -') + c = 2(27’(2a'~ 2) + c) + c = 2'T(2k ' 2) -I- 2'c -I- 2"c = ,
2* - I
= 2*7X2") + c(2k" ' + 2} " 2 + ... + 2°) - 0 -1- c - - - - - - - = c(n - I) Stąd, jak poprzednio, wnioskujemy, że T(n) = ©(//)
(3) f 7X1) = 0 i T(n) = 7’(L/t/2 J ) -l- 7Xl /t/2 I) -l- en dla n > I (Równanie to otrzymamy jako równanie złożoności wtedy, kiedy problem rozmiaru sprowadza się. do dwóch podproblemów rozmiaru /t/2 + liniowa liczba działań). Pod stawmy n = 2*. Wtedy ii
7X2*) = 27X2*- ') + c2k = 2(27X2*-“) -i- c2* “ ') + c2* = = 2a7X'2* - •’■) + c2* + c:2* = 2*7X2") + fa-2* = 0 + cnlogu
Mamy zatem T{ń) = ©(tilogtt)
Funkcje tworzące Czasami trudno wyznaczyć rozwiązanie równania T(n) bezpośrednio z równania rekurencyjnego (może nie istnieć zwięzły wzór). Można wówczas spróbować zastosować metodę funkcji tworzących, która polega na znalezieniu funkcji F(z)= I,T(n)z" n>(1 nazywanej funkcją tworzącą l\n ), i mi jej podstawie wnioskować o własnościach samej funkcji T(n). Metodę tę stosuje się często w analizie probabilistycznej algorytmów (do wyznaczenia wartości oczekiwanej i wariancji zmiennej losowej Xn). Rozważmy funkcję tworzącą
22
i. P od staw ow e zasad y a n alizy algorytm ów
rozkładu prawdopodobieństwa pnk zmiennej losowej Xn (z równań rekureneyjnych na pnk trudno jest czysto wyznaczyć rozwiązanie):
k>0
Zauważmy, że wówczas X Pnk = 1 k 2: II
Wartość oczekiwaną i wariancję zmiennej losowej X„ można wyrazić za pomocą warto ści pochodnych funkcji Pn(z) dla z = I w następujący sposób: avo(/Y„) = P'„ ( I) var(X„) = P" ( I) -I- P'n ( I) —/ ’j ( I )2 ponieważ, P„(.z)\ — X kPnk z k 7: I
i
K(.Z)= I k{k- I)Pn,Jk-:2 k
Stąd P'n( I) -
2
X kpnk i P " ( I) = X k(k - I)pnk, a zatem k 7: I
* 17 2
var(X„) = X (k - P'„ ( I ))’/>„* = X l<2pnt - 2 A£ 0
A Ł (I
I) Z A-p„t + P' ( I )2 X /V = Jt > 0
k > i)
= XM - 1)/>„*-I- X/
,j(I)3+/',j(1 j2- P"(I)+P'„i\)-P:s\f k :■ii
*a n
Można więc wyznaczyć wielkości ave(X„) i vur{X„) (a co za tym idzie, również złożoność oczekiwaną i oczekiwaną wrażliwość algorytmu), nie znając cxplicite rozkładu pnk, a tyl ko jego funkcję tworzącą.
1.4 . P o p ra w n o ść
semantyczna
Poprawność semantyczna oznacza, że program wykonuje postawione przed nim za danie. Stosowaną metodą dowodu jest indukcja matematyczna względem liczby powtó-
i
23
1.4. P o p ra w n o ś ć s e m a n ty c z n a
rżeń instrukcji iteracyjnej bądź poziomu zagnieżdżenia realizacji procedury rekurencyjnej. Rozważmy na przykład algorytm binarny potęgowania: [ m > oj y := 1; :n : n; wh.il b m /- 0 do begin (y: x v - y»z* a m > 0} i f ocld (m ) then y : — y * z ; m : = ni div 2; z := z* z end;
{y - x "}
Warunek y, nazywany niezmiennikiem instrukcji iteracyjnej, opisuje wartości zmien nych w trakcie realizacji programu. Zamieszczony warunek yjest spełniony na początku, gdy rozpoczyna się realizacja instrukcji iteracyjnej, i każde powtórzenie tej instrukcji zachowuje go. Zachodzi on zatem, gdy kończy się realizacja instrukcji iteracyjnej, z cze go łatwo wyprowadzić warunek końcowy y = x". Niezmiennik jest zwykłe rozszerzeniem warunku końcowego, jak to zwykle bywa przy dowodach indukcyjnych. Chociaż przed stawiony dowód dowodzi warunku |_y = ,t"}, to jednak nie tłumaczy działania algorytmu. Aby zrozumieć, jak działa algorytm, przypatrzmy się postaci binarnej liczby m wewnątrz algorytmu. { n = (aja,_, . . . a„) ,, a ,e {0, 1}, a, = 1}
z := x ; y ;= 1; m := n ; k : = 0 ; while m > 0 do begin {y :
m=
( a , . a k ) ., a z - x** a y =
••"'o’tt a 1 S k a a, - 1}
i f odd(łTi) then y : — y * z ; m : iiidiv 2; z z* z }
k : tr, ./■;■-j- I. end; fy -
Zazwyczaj wymaga się od dowodów poprawności, aby na ich podstawie można było zrozumieć, jak faktycznie działa algorytm i dlaczego jest poprawny. Aby dowód poprawności by 1 kompletny, musimy jeszcze dodatkowo udowodnić dwie własności: ® wykonalność operacji częściowych, jak dzielenie, przechodzenie po dowiązaniu w drzewie lub liście, określenie, zmiennej indeksowanej itp.; ® skończoność działania każdej instrukcji iteracyjnej i każdego wywołania procedury rekurencyjnej.
24
1. P o d sta w o w e zasa d y an a lizy algorytm ów
W wypadku algorytmu potęgowania binarnego jedyna częściowa operacja div jest za wsze wykonalna (gdyż dzielimy przez 2) oraz obliczenie instrukcji iteracyjnej jest koń czone na mocy następującej własności liczb naturalnych: dla każdej liczby naturalnej n, wykonując dzielenie całkowite n wielokrotnie przez 2, po skończonej liczbie kroków otrzymamy 0. Co więcej, liczba wykonali instrukcji iteracyjnej jest równa liczbie dzieleń całkowitych przez 2, czyli długości binarnej //. Dla n > 0 mani) zatem W(u) = A(n) - LlognJ -I- I = log// + 0(1) A(n)
5(//.) = O
(rozmiarem danych jest n, a operacją dominującą - dzielenie całkowite przez 2). Widzi my, że w tym wypadku dowodzenie skończoności działania instrukcji iteracyjnej jest po wiązane ze znajdowaniem pesymistycznej złożoności czasowej. .lako przykład algorytmu rekurencyjnego rozważmy algorytm Euklidesa znajdowania największego wspólnego dzielnika dwóch dodatnich liczb naturalnych. function N W D (x, y : i n t e g e r ) : in te g e r ; var r : i n t e g e r ; b e ^ i n ( a : x > 0 a y > 0} r := x mod y; if r = 0 then NWD := y else NWD := NWD (y, r) {[J:NWD= (x, y) } end;
Przez (x, y) oznaczyliśmy największy wspólny dzielnik dodatnich liczb naturalnych a: i y. Poprawność funkcji NWD względem podanych warunków pokazujemy dowodząc, że dla każdych dodatnich wartości naturalnych x i y obliczenie wywołania funkcji NWI >( v. y) kończy się z wartością NWD = (x, y). Stosujemy indukcję względem wartości y. Za kładając poprawność dla wszystkich 0 < yl < y, otrzymujemy, że x mod y - 0 i wtedy (,v, y) = y. Możemy też zastosować założenie indukcyjne dla pary (y, x mod y) i wewnęt rznego wywołania rekurencyjnego. Wtedy (x, y) = (y, x mod y).
1. 5 . Podstawowe struktury danych Poniżej rozważamy podstawowe struktury danych: listę, graf, zbiór i drzewo, wpro wadzając potrzebne w dalszej części książki oznaczenia i omawiając podstawowe meto dy implementacji tych struktur. Będziemy zakładać, żc elementy wchodzące w skład rozważanych struktur danych pochodzą z pewnego niepustogo uniwersum U. Jak wiado mo z zasad programowania strukturalnego, zagadnienia dotyczące budowy samej struk tury danych i jej użycia w algorytmie wygodnie jest rozważać oddzielnie.
1.5. P o d sta w o w e stru k tu ry dan ych
25
1.5.1.
L is ta U sta1’ to skończony ciąg elementów: <•/ = |..v,, x,, .... ,vj. Skrajne elementy listy X, i ,v„ nazywają się końcami listy (odpowiednio - lewym i prawym), a wielkość \q\=-n długością (lub rozmiarem) listy. Szczególnym przypadkiem listy jest lista pu sta: q = | |. Weźmy dwie. listy: q - |,v,, x„ .... v j i r = |y„ y3, .... y,„|, i niech 0 < i < j < ;/: Podstawowymi abstrakcyjnymi operacjami na listach są: ® dostęp do elementu listy - r/|7| = .a:,;
• podlista - q\i..j\ - |v,, ...............x,|; • złożenie - q&r = l v,, .... .v„, y....... y„,|. Za pomocą tych Irzecli podslitwowych operacji można definiować inne operacje na listach, na przyldad wstawianie elementu x za element x, na liście q: q\ I ../|&|.v|&r/|/ -I- l..|r/|]. Listy używa się zwykle w specjalny sposób, ograniczając się do zmian jej końców: (a) Jront(q) = r/| I] (pobieranie lewego końca listy); (b) push(q, x) = |.v|&ry (wstawienie elementu x na lewy koniec listy); (c) popiq) = ą\2..\q\\ (usunięcie bieżącego lewego końca listy); (d) rear(q) = t/[|r/|] (e) inject{q, x) = r/&|.\;| (I) ejt’ct(q) = f/| l..|r/| -1]
(pobieranie prawego końca listy); (wsttiwicnie elementu x na prawy koniec listy); (usunięcie bieżącego prawego końca listy).
Listę, na której można wykonać wszystkich sześć operacji, nazywa się kolejką podwój ną. W szczególnych przypadkach, łzn. kiedy uwzględnia się tylko operacje front, push i pop, nazywa się ją stosem, a kiedy uwzględnia się tylko operacje from, pop i inject - kolejką. (Operacje, na abstrakcyjnej strukturze danych mogą. być realizowane, za pomo cą funkcji albo procedur). Dwie podstawowe, implementacje (reprezentacje) listy q = , .v2, .... .vj to: • tablicowa - q[i\ = ,v,, gdzie I < i < • dowiązaniowa - różne warianty są przedstawione na rysunku 1.1. W implementacjach pojedynczej liniowej i podwójnej liniowej dowiązanie prowadzące do listy wskazuje na pierwszy element na liście, a w implementacji pojedynczej cyklicz nej i podwójnej cyklicznej - na ostatni. Aby mieć gwarancję, że struktura dowiązaniowa nigdy nie będzie pusta, dodaje, się na początku listy element pusty, nazywany głową lub wartownikiem listy. 1 W tym sensie lisia jest tym samym co w inateinalyce ei;|*>.
26
I. P od staw ow e zasad y an a lizy algorytm ów
Pojedyncza liniowa
jc,--------►x , _____ ►_________ t.x
Pojedyncza cykliczna
,v,_____ „ v.,______^
Podwójna liniowa
„\(
Podwójna cykliczna
.v,
” v, ’
^ ...
— ------- -------------- 1>
x2
„x
xn
----- ----{► ,vf
Rys. 1.1. Różne wnrinnly iniplcincnUicji clowuiznniowej lisi
Następujące upcracje na listach mają stalą złożoność czasową: « w implementacji pojedynczej liniowej: operacje, stosu, wstawianie jednego elementu za drugi, usuwanie następnego elementu; ® w implementacji pojedynczej cyklicznej: te operacje co wyżej plus złożenie oraz ope racje rear i inject; o w implementacji podwójnej cyklicznej: te operacje co wyżej plus eject, wstawianie jednego elementu przed drugim, usuwanie danego elementu, odwracanie listy. Każda zatem operacja dotycząca kolejki podwójnej ma pesymistyczną złożoność czaso wą 6>(l) w implementacji podwójnej cyklicznej. Wadą tej implementacji jest użycie O(n) komórek pomocniczej pamięci na pamiętanie dowiązań (n jest rozmiarem listy). Jeśli jest znana maksymalna długość m kolejki podwójnej, to bardziej oszczędna pa mięciowo jest implementacja listy za pomocą tablicy cyklicznej Q[0..m - I j, w której następnikiem pozycji 0 < i < ni -- I jest pozycja (/+ l)m o d m. Wówczas jeśli (j ~. | Vj, ,v„ ..., .vn|, to g[(Ż + i) mod m] = ,v, dla 1 < i < n \ pewnej pozycji 0 < k < ni ~ I. Przykładowo operacja popici) ma implementację: />.>/>( k , n) : : if n = 0 then e r r o r "
else begin k := ( k + 1 ) m o d in;
n := n - 1. end ; In.'.inikcja e rro r ozniiexa pr/.i'i\v;mic obliczeń.
1.5. P o d sta w o w e stru k tu ry danych
27
a operacja push(ij, .v): p u s h ik , n, x) : i f n = jii then e rro r e ls e begin
C?IXI k := ( k -- 1 )m o d m;
n :- n -I- 1 end;
1.5.2.
Z b ió r W przeciwieństwie do elementów listy elementy w zbiorze S ■ X j» 2, ■-v„j me są podane w żadnym ustalonym porządku. (Zawsze będziemy zakładać, że rozważany zbiór jest skończony). Liczbę n elementów w zbiorze S oznaczamy przez |.Sj i nazywamy rozmiarem zbioru S. Podstawowymi operacjami na zbiorach są: S S U {x} (wstawienie elementu ,v do zbioru S); (a) insert(x, S):\ (b) delcle(x, S):: S := S - {>v} (usunięcie elementu ,v ze zbioru S)\ [ true, jeśli ,v e S (c) inemheiix, S):\ wynikiem jest wartość \ false, jeśli ,v
j
"""■ 1false,
65 '
* jeśli x Z S
. .
Operacje insert, delete i member mają pesymistyczną złożoność czasową (){I). Złożo ność pamięciowa jest proporcjonalna do rozmiaru zbioru indeksów tablicy C (czyli faktycznie do rozmiaru uniwersum U). ® Implementacje listowe Oczywiście ustawiając elementy zbioru S w pewnym porządku, otrzymujemy listę. Wszystkie implementacje listy mogą być użyte do reprezentowania zbioru. Przy
28
1. P o d sta w o w e za sa d y a n a lizy algorytm ów
rozważanych wcześniej implementacjach listy pesymistyczna złożoność czasowa pod stawowych operacji na zbiorach jest proporcjonalna do rozmiaru zbiorów. Opisując algorytmy, będziemy często stosować rozszerzenia języka Pascal. Należy to traktować jako postać roboczą - pośrednią przed ostatecznym, ścisłym zapisem algoryt mu w języku programowania. Postać pośrednia ułatwia zrozumienie istoty działania algorytmu. Trzeba pamiętać, że postać ostateczna, zapisana w języku programowania, zawiera często taką liczbę szczegółów, że trudno wychwycić ideę algorytmu. Z tego właśnie względu przyjęło się stosować w dokumentacji oprogramowania pośrednią po stać zapisu algorytmu. Przykładem użytecznej abstrakcji algorytmicznej jest konstrukcja f o r each x in S do I używana do opisu działań /, wykonywanych dla każdego elementu ,v należącego do listy lub zbioru S. Nie bierzemy tu pod uwagę mechanizmu przeglądania elementów w .S'; koncentrujemy się na opisie przetwarzania każdego x, co zwykle stanowi istotę danego algorytmu. Instrukcje: s unia 0; f o r each x in S do suma : = suma + x przedstawiają sumowanie liczb w zbiorze S. Opis reprezentacji zbioru S i sposobu wybo ru jego elementów odkładamy na później, koncentrując się w danej chwili tylko na jednym problemie (w tym wypadku na sumowaniu elementów zbioru .S). W następnym podrozdziale podajemy więcej przykładów związanych z odkładaniem na później dokładnego opisu pewnych aspektów algorytmu.
1.5.3.
G raf Graf to system, który zapisujemy jako G = (V, E), gdzie V oznacza zbiór skoń czony, którego elementy są nazywane wierzchołkami (gdy będzie nam zależało na pod kreśleniu, że gra! jest strukturą danych, używać też będziemy nazwy węzły|, a E - zbiór krawędzi, czyli par wierzchołków ze zbioru V, przy czym, dokładniej, albo /S jest pod zbiorem zbioru par uporządkowanych {(a-, y): a, y e V a x & y} i wtedy graf nazywa się zorientowany, a krawędzie są oznaczane strzałkami łączącymi wierzchołki (rys. I.2a), albo /Sjest podzbiorem zbioru wszystkich dwuelementowych podzbiorów zbioru V i wte dy grał nazywa się niezorientowany, a krawędzie są oznaczane liniami (rys. I.2b).
(a) (b)
y y
Kys. 1.2. Dwa rodzaje krawędzi w grafach: (a) w grafie zorientowa nym; (b) w grafie niezorientowanym
1.5. P od staw ow e stru k tu ry danych
29
W obu łych wypadkach krawędź łączącą wierzchołki ,v i >’ oznaczamy jako (x, y). Roz m iar grafu G -- (V, E) jest równy sumie dwóch liczb: u = | V| i ni = |£j. D[a ,,r;ipu n(n - I) niezorientowanego (co oczywiste) iii < a dla zorientowanego ni < n(n • ; g Oto podstawowe implementacje grafu G = (V, E) (zakładamy, że zbiór węzłów V może być zbiorem indeksów dla tablic). • Listy sąsiedztwa Dla każdego ,v e V budujemy listę (oznaczaną przez L[-v|) wierzchołków y będących sąsiadami ,v (ii. (,v. v) e El. W lej implementacji jest potrzebna pamięć 0(n -l- w). • Macierz sąsiedztwa fi, jeśli (,:v, y) e E /U-c, ,y| = i . . „ (0, jeśli (,v, y) ci b W tej implementacji jest potrzebna pamięć O(ir). Definicje dotyczące grafów są zamieszczone w rozdziale poświęconym algorytmom na grafach. Teraz rozważymy tylko jeden przykład problemu grafowego.
□
P rzykład:
Algorytm przechodzenia grafu
Niech G = (V, E), |V| = n, |/i'| = in, p e V. Wychodząc od wierzchołka />, należy od wiedzić każdy wierzchołek i każdą krawędź, które są osiągalne z p. Dozwolonym ru chem jest przejście krawędzią grafu wychodzącą z odwiedzonego już wierzchołka. W jednym kroku będziemy odwiedzać jeden z wierzchołków oraz jedną z krawędzi grafu i zaznaczać je jako odwiedzone. Na początku wszystkie wierzchołki i wszystkie krawę dzie są zaznaczone jako nie odwiedzone.1 1. Odwiedź wierzchołek p i zaznacz go jako odwiedzony. 2. Dopóki z jednego z odwiedzonych wierzchołków wychodzi nie odwiedzona jeszcze krawędź, wykonuj podane czynności: a) wybierz odwiedzony wierzchołek, powiedzmy v, z którego wychodzi nie odwie dzona krawędź; b) wybierz nie odwiedzoną krawędź, powiedzmy (u, w), wychodzącą z wierzchołka u; c) zaznacz krawędź (i>, w) jako odwiedzoną; d) jeśli wierzchołek w nie został odwiedzony, odwiedź go i zaznacz juko odwiedzony.
oO
I. P od staw ow e zasad y an a lizy algorytm ów
.Stosując indukcję względem odległości danego wie'rzeholka od wierzchołka początko wego p, możemy udowodnić, że. w każdym algorytmie stosującym się do powyższego schematu musi nastąpić odwiedzenie jeden raz każdego wierzchołka i każdej krawędzi gral'u G osiągalnej z />. Nie biorąc pod uwagę samej procedury odwiedzania wierzchoł ków i krawędzi, złożoność każdego takiego algorytmu jest proporcjonalna do łącznej liczby wierzchołków i krawędzi (a więc jest liniowa względem rozmiaru grafu). Powyższy opis stanowi schemat klasy algorytmów. Aby otrzymać konkretny algorytm, należy wykonać podane tu kroki. !. Przyjąć odpowiednią reprezentację grafu, na przykład V = {], 2, ..., //} i listy sąsiedzt wa A|v| dla w s V. 2. Określić, co to znaczy „zaznacz wierzchołek jako odwiedzony” . Można na przykład dołączyć do każdego wierzchołka wpole visilcil\\>] i przyjąć, że false oznacza „wierz chołek nie odwiedzony” , a lnie „wierzchołek odwiedzony” . 3. Określić sposób wyboru odwiedzanego wierzchołka, z którego wychodzi nie odwiedzona krawędź. Można na przykład przechowywać takie wierzchołki w kolejce lub na stosie. 4. Określić sposób odróżniania (dla danego wierzchołka) krawędzi odwiedzonych od nie odwiedzonych. Można na przykład trzymać na liście sąsiedztwa danego wierzchołka r wskaźnik currenl\v] do pierwszej nie odwiedzonej krawędzi (currcnĄy] = nil ozna cza, że wszystkie krawędzie wychodzące z danego wierzchołka zostały odwiedzone). Przyjmując te przykładowe ustalenia, uzyskujemy bardziej uszczegółowiony schemat przechodzenia grain (visit jest procedurą „odwiedzania” wierzchołka; zrezygnowaliśmy ■/. procedury odwiedzania krawędzi). Oto on: for v bogiń
:t. t o n do
v i s i Cecil vj
false;
ustaw wskaźnik c u r r e u fc[V| na pierwszy wierzchołek na liście L[v| end; v i s i U p ) ; W s i Cedfpl := t r u e ; if cu.vrcivritfpl <> nil then begin S : -jpj ; while S <> 0 clo f.S zawiera wszystkie odwiedzone do tej pory wierzchołki, z których wychodzą nie odwiedzone jeszcze krawędzie} bogiń wybierz wierzchołek v z e zbioru S; niech w będzie wierzchołkiem wskazywanym przez c u r r e n t ] vj ; przesuń wskaźnik c u r r e n t l y ] do następnego wierzchołka na
liście ó|V|; if c u r r e n (:|V| = nil then S := S .j.v); if not v i s i t e ć l \ w ] than
1.5.
31
P o d s ta w o w e s t r u k t u r y d a n y c h
begin v i s i t (w) ; v i s i t e c i \ w ] := true; if currently] <> nil then S := S u {w}
endi
I
end end;
Dwie podstawowe implementacje zbioru S w tym schemacie algorytmów to stos i kolej ka. Najpierw uszczegółowimy schemat przechodzenia grafu, przedstawiając S za pomocą stosu i interpretując operacje na S jako operacje na stosie. (Przyjmujemy, że operacje pop i push będą realizowane jako procedury, a from jako funkcja). Otrzymujemy algorytm przechodzenia grafu w gl;\b (metoda DFS). procedure d . i s ; begin for v 1 to n do begin visitedjv] := f a l s e ; ustaw wskaźnik curre.ni.jvj na pierwszy wierzchołek na liście Ii|v] end ; v i s i t , ( p ) ; visitedfp] := t r u e ; if currentfp] <:> nil then begin S := 0; pu sh (S , p) ; while S <> 0 do {stos S zawiera wszystkie odwiedzone do te j pory wierzchołki , z których wychodzą nie odwiedzone jeszcze krawędzie} begin v := f r o n t (Sj ; niech w będzie wierzchołkiem wskazywanym przez currently] ; przesuń wskaźnik currently] do następnego wierzchołka na liście L]v|; if currenŁ[v] = n i l then p o p (.5) ; if not v i s i t e d { wl then begin visit{w) ; v i s i ted]uj := true; if c u r r e n t \ w\ o nil then p u s h (S , w) end
end end end;
,1. P o d s t a w o w e zasady analizy algorytmów
1I
------------- -------------- ------------- -
Zapiszemy leni/, schemat przechodzenia grafu, przedstawiając S jako kolejką i inter pretując operacje na S jako operacje na kolejce. (Przyjmujemy, że operacje pop i inject będą realizowane jako procedury, a front jako funkcja). Otrzymujemy algorytm prze chodzenia grafu wszerz, (metoda HFS). procedure h f s ; begin for v : = 1 to n do begin visited! v] := f a l s e ; ustaw wskaźnik c u r r e n t[v \ na pierwszy wierzchołek na liście
L[v| end ; visit (p); visited[p| := true; if currenttp] o nil then begin S := []; i n j e c t ( S , p ) ; while S <> 0 do {kolejka S zawiera wszystkie odwiedzone do tej pory wierzchołki, z których wychodzą nie odwiedzone jeszcze krawędzie] begin v := f r o n t ( S ) ; niech w będzie wierzchołkiem wskazywanym przez, currentl vj ; przesuń wskaźnik currently] do następnego wierzchołka na liście L\ y]; if curren Ł[y| = nil then pop(.S) ; .if not visited]w] then begin v i s i t (w); visited! wj := t r u e ; if current[w] o nil then i n j e c t ( S , w) end end end end;
2
4-
3
6
Uys. 1.3. Przykładowy graf'
1.5. P od staw ow e stru k tu ry danych
33
Weźmy grał' z rys mik u 1.3. Jeśli p = I, UW = |.2, 3], /-12| = |4, 5J, /.|3] = [5, 6J, /.[4] = [5], 1Ą5] = | i j, L[6| = |J. u, podczas wykonywania algorytmu dis nastąpi odwiedzenie wierzchołków grafu w na stępującej kolejności: I, 2, 4, 5, 3, 6. Natomiast w wyniku realizacji algorytmu his nastąpi odwiedzenie wierzchołków grafu w następującej kolejności: I, 2, 3, 4, 5, 6.
_____ _.ra Zastosowana powyżej metoda konstrukcji algorytmów nazywa się metodą kolejnych transformacji. Wychodząc od ogólnego schematu algorytmu, dokonujemy kolejnych wyborów transformujących zapis algorytmu na coraz bardziej szczegółowy. W procedu rach clfs i bfs zostawiliśmy jeszcze do „dopracowania” kilka spraw, na przykład kwestię reprezentacji list sąsiedztwa i wskaźników currenl[v\, przesuwających się po liście sąsie dztwa wierzchołka s w trakcie odwiedzania kolejnych krawędzi wychodzących z r. Ostatecznych (przed realizacją algorytmu) transformacji dokonuje kompilator, prze kształcając program w binarny kod maszynowy.
1.5.4.
No.tacja fun kcyjna dla atryb u tów o b ie k tó w Do oznaczenia atrybutów obiektów będziemy stosować notację funkcyjną. Jeśli X jest zbiorem węzłów w strukturze danych, a Y dowolnym zbiorem i jest określona funkcja f: X —> Y, to będziemy to zapisywać jako,f(x), na przykład /(.v) := y. Nic będziemy zatem stosować notacji wskazujących, jak dana funkcja jest realizowana przez konstrukcje języka programowania, to znaczy notacji obiektowej (wskaźnikowej): x* -f (f - nazwa pola rekordu), na przykład x A. f :=.y, albo notacji, tablicowej: J[x\ (/'■- nazwa tablicy), na przykład ./[.r] := y. Jest to zgodne z zasadą przyjętą przez nas w lej książce, nakazującą oddzielanie poziomu abstrakcyjnego, charakteryzującego rodzaj i własności obiektów oraz operacji, od ich konkretnej realizacji. Funkcje określone na obiektach struktury danych są. również abstra kcyjnymi strukturami danych, dopuszczającymi różne implementacje. Instrukcja przypi sania J{x) :=yjesl abstrakcyjną operacją na obiekcie będącym funkcją (odwzorowaniem)/
1.5.5.
D rzew o Drzewo to dowolny niezorientowany graf spójny i acykliczny. (Przypominamy, że spójność oznacza, iż każde dwa wierzchołki grafu są połączone ścieżką utworzoną z. kra wędzi grafu. Acykliczność oznacza brak cykli prostych utworzonych z krawędzi grafu). Drzewo z korzeniem to drzewo z wyróżnionym jednym wierzchołkiem nazywanym
34
i. P o d sta w o w e z a sa d y a n a lizy algorytm ów
korzenieni. Jeżeli z kontekstu będzie wynikać, iż chodzi o drzewo z korzenieni i korzeń drzewa jest ustalony, to będziemy po prostu pisać ,,drzewo” . Zakładamy Drogi Czytelniku, że znasz podstawowe pojęcia dotyczące drzew. Przypo mnijmy je na przykładzie drzewa (z korzeniem) z rysunku 1.4. Wierzchołek z ma trzy następniki: i, u i w. Każdy wierzchołek, który ma następnik, jest wierzchołkiem wew nętrznym, w przeciwnym razie jest liściem (na przykład y). Zbiór następników wierz chołka x w drzewie T będziemy oznaczać jako childrenr (.v). (Będziemy pomi jać indeks T wtedy, kiedy będzie jednoznacznie wiadomo, o które drzewo chodzi). Każdy wierz chołek z wyjątkiem korzenia ma poprzednik (na przykład wma poprzednik /.<). Poprzed nik wierzchołka x w drzewie 7’będziemy oznaczać jako pr(x). (Będziemy pomijać indeks T wtedy, kiedy będzie jednoznacznie wiadomo, o które, drzewo chodzi). Potomkami wierzchołka z są zarówno on sam, jak i jego następniki i, u i w, ale także następnik u wierzchołka u. Z kolei za przodków wierzchołka i> uważa się jego samego, jego poprzednika u, a także wierzchołki z i ,v, leżące na ścieżce od v do korzenia. Głębokość (lub poziom) wierzchołka w drzewie to jego odległość od korzenia (,v na przykład ma głębokość 0, a v ma głębokość 3). Wysokość wierzchołka to maksymalna długość drogi od danego wierzchołka do liścia (na przykład wysokość ,v wynosi 3, a wysokość y - 0). Wysokość drzewa to wysokość jego korzenia (w naszym przykładzie, wynosi 3). .V
)•
Uys. 1.4. Drzewo o korzeniu ,v
Drzewo reprezentuje się zwykle albo za pomocą struktury dowiązaniowej (używając rekordów i dowiązań), albo za pomocą struktury indeksowej (używając tablicy). Gdy struktura drzewa jest ustalona, stosuje się na ogół reprezentację tablicową, a gdy re prezentowane drzewo może mieć dowolny kształt - dowiązańiową. Często przy przed stawianiu algorytmów nie jest istotne, jaka reprezentacja jest używana. W takim wypad ku będziemy stosować ogólną,, abstrakcyjną notację funkcyjną p(x) i children(x) zamiast konkretnych .\A.p i x*.children czy p\x] i chUdre.n\x\. CJ P rzykład :
Algorytm przechodzenia drzewa z korzeniem
W podanym tu algorytmie zakładamy, że vertex jest typem danych, reprezentującym wierzchołek drzewa, a previsii(v) i postvisit(y) to procedury specy fi kujące działania wy konywane ..odpowiednio -- przy wejściu do wierzchołka v i przy wyjściu z niego.
(.5. P od staw ow e stru k tu ry danych
35
procedure t r a v e r s e ( v : v e r t e x ) ; var iv : v e r t e x ; begin p r e v i s i t (v) ; for each w in c h i l d r e n ( v ) do t r a v e r s e (w) ; p o stvislt(v)
end t r a v e r s e ;
Aby przejść cale drzewo o korzeniu r wywołujemy traverse(r). Gdy procedura postvisii(v) nie ma treści, mamy do czynienia z przejściem drzewa metodą preorder, a gdy procedura previsit{v) nie ma treści - metodą postorder. Metody preorder użyjemy na przykład do obliczenia głębokości wierzchołków drzewa, a metody postorder do ob liczenia ich wysokości (zob. zadania 20 i 21). Szczególnym przypadkiem drzewa z korzeniem jest drzewo binarne, w który in dla każdego wierzchołka t> mamy \children(v)\ < 2. W drzewie binarnym każdy następnik wierzchołka v jest albo lewy, albo prawy (przy czym tylko jeden może być lewy i tylko jeden prawy). Używać będziemy następujących oznaczeń: Iąft(v) =
right (v) =
lewy następnik v, jeśli jest określony nil w przeciwnym razie prawy następnik v, jeśli jest określony nil w przeciwnym razie
Dla każdego wierzchołka v zbiór potomków jego lewego następnika oraz zbiór potom ków jego prawego następnika tworzą drzewa nazywane - odpowiednio - lewym i p ra wym poddrzewem wierzchołka v. □ PRZYKŁAD:
Algorytm przechodzenia drzewa binarnego
W podanym tu algorytmie zakładamy, jak poprzednio, że vertex jest typem danych, reprezentującym wierzchołek drzewa, a previsit(v), invisil(v) i pnxtvixit(y) to procedury specyfikujące działania wykonywane - odpowiednio - przy wejściu do wierzchołka v, po rozpatrzeniu wierzchołków w lewym poddrzewie a przed rozpatrzeniem wierzchołków w prawym poddrzewie i przy wyjściu z wierzchołka v. procedure b - t r a v e r s e ( v : v e r t e x ) ; begin
p r e v .i s .i t (v) ; if l e f t (v) & nil then b- t r a v e r s e { l e f t (v) ) ; in visitiv) ;
if r i g h t ( v) V- nil then b- t r a v e r s e ( r i g h t : ( v ) ) ; p o s t v i s i t (v) and b - t r a v e r s e ;
36
1. P od staw ow e zasad y an a lizy algorytm ów
Gdy procedury invisit i postvisit nic maju treści, mamy do czynienia z przejściem metodą preorder, a gdy previsit i postvisit nie maju treści - z przejściem metodą inorder. Gdy dotyczy to procedur previsit, i in visit mamy do czynienia z przejściem metodą postorder.
1.6 . Eliminacja rekursji Algorytmy rekurencyjne są naturalne dla wielu struktur danych, a dla grafów i dla drzew w szczególności. Należy jednak pamiętać, że realizacja rekursji na maszynie wy maga użycia stosu, a to pociąga za sobą zwiększenie złożoności pamięciowej, a także współczynnika proporcjonalności w złożoności czasowej.
□ P rzykład:
Przeglądanie drzewa binarnego metodą inorder
procedure i n o r d e r - ( v : v e r t e x ) ; begin if l e f t ( v ) i- nil then i n o r d e r ( l e f t ( v ) ) ; i n v i s i t (v) ; if r i g h t ( v ) nil then i n o r d e r ( r i g h t { v ) ) end i n o r d e r ;
Procedurę wywołujemy, przyjmując za parametr faktyczny korzeń drzewa r, czyli pisze my inorder(r). Przy symulacji wywołania.procedury rekurencyjnej inorder przez pro gram nierekurencyjny należy zapisać na stosie wierzchołek bieżący v oraz .miejsce, do którego należy wrócić po zrealizowaniu wywołania rekurencyjnego. W podanej dalej procedurze nazwa stack oznacza typ stosu, na którym umieszcza się pary wartości |y, /'], gdzie i>jest wierzchołkiem, czyli wartością typu vertex, a i etykietą (operacji push i pop używamy jako procedur, a operacji top jako funkcji). procedure n o n r e c - i n o r d e r l (r : v e r t e x ) ; label 1, 2, 3; var g : stack; v ; vertex; i : 1. .3 ; begin 1:
g := []; v := r ; (początek symulacji wywołanie! rekurencyjnego} if l e f t ( v ) ^ nil then begin (symulacja wywołania rekurencyjnego i n o r d e r I l e f t (v) )}
l.G. E lim inacja rekursji
87
p u s h (g , [ v , 2 } ) ;
{uniiessczenie na stosie b ie ż ą c e j wartości v i etykiety powrotu! v := luft: { v) ; {przygotowanie nowego parametru v) goto 1 end ;
:
i n v i s i ł: ( v) ; i f r i g h t ( v) / n i l th e n b e g in [symulacja wywołania re k u re n c y j nego i n o r d e r ( r i g h t (v) )]
push (q , [v, 3[); {uiiiieszczenie na stosie bieżącej w a rto ś c i v i etykiety powrotu)
v := r i g h t ( v) ; {p rzygotow anie riowego p aram etru | goto 1
end; 3:
[powrót do miejsca, gdzie nastąpiło wywołanie refcurencyjne)
i £ q y 11th e n b e g in [v , i | : = f r o n t (tj) ; [przywrócenie w a rto ś c i v z p o p rz e d n ie j instancji i uzyskanie informacji o miejscu powrotu]
p o p i q ); (skasowanie wierzchołka Stosu] g o to i {powrót) end end nonr e o - i n order! ; Zauważmy, że złożoność pamięciowa wzrasta o maksymalną liczbę elementów na stosie q, czyli w wypaulu: procedury inorder o O(n), gdzie // jest liczba wierzchołków w drzewie. Podany powyżej algorytm, będący przykładem zastosowania ogólnej metody eliminacji stosu, można nieco uprościć. Zauważmy, że gdy rozpoczynamy w danym węźle, nowt) .instancję., idąc w prawo, nie musimy już wracać do niego, gdyż nie. pozostało już w mm nic do wykonania. Nie trzeba zatem umieszczać pary [i\ 3| na stosie. Ponieważ powiol zawsze odbywa się. w miejsce oznaczone w algorytmie etykietą 2, na stosie wystarczy przechowywać jedynie wierzchołki, do których ma nastąpić powrót. p ro c e d u re nonrec- inorderl. (.r : v e r te x ) ; l a b e l 1, 2, 3 ; v a r g : stuck; v : v e r te x ; b e g in ■ II; : =r;
38
1. P o d sta w o w e za sa d y an a lizy algorytm ów 1:
{początek symulacji wywołania rekurencyjnego} if l e l : L { v ) # nil then begin (symulacja wywołania rekurencyjnego i n o r d e r ( l e f t ( v ) )} p u s h (q, v ) ; {wstawienie na stos bieżącej wartości v; powrót zawsze c.lo 2 ] v := l e f t(v) ; (przygotowanie nowego parametru v) goto 1 end ;
2:
i n v i s i t (v) ;
if r i g h t (vj ^ nil then begin (symulacja wywołania rekurencyjnego i n o r d e r ( r i g h Ctvj i j v := right (vj ; (przygotowanie nowego parametru} goto 1 end; [powrót do instancji, gdzie nastąpiło wywołanie rekurencyjne} i f. q 11 then begin v : = tr o n t ( c/j ; (przywrócenie wartości v z poprzedniej instancji} pop(g) ; {skasowanie wierzchołka stosu} goto 2 (powrót] end end nonrec- i n o r d e r : 2 ;
1.7.
Koszt zamortyzowany operacji w strukturze danych W wypadku struktur danych jest używany jeszcze jeden rodzaj złożoności, tzw. złożoność zamortyzowana (koszt zamortyzowany) operacji w strukturze da nych. Użycie struktury danych w algorytmie polega zwykle na wykonaniu na niej ciągu operacji o,, m, .... om (jedna po drugiej,' czyli w trybie on-line). Koszt i-tej operacji zapisujemy jako t, (I < i < ni). Na ogól to nie koszt rzeczywisty jednej operacji jest istotny, a koszt całego ciągu operacji, czyli
Czasami, gdy nic da się bezpośrednio uzyskać zadowalającego oszacowania rzędu wiel kości i (na przykład w sytuacji, kiedy koszt pewnych operacji jest mały, a innych duży),
1.7. K oszt zam ortyzow an y op eracji w stru k tu rze dan ych
39
warto skorzystać z metody wprowadzenia kosztu zamortyzowanego operacji. Polega ona na tym, że operacjom w wykonywanym ciągu przypisuje się koszty zamortyzowane o,, am tak, żeby albo Z a, ■= t
i ~i albo t =O Z Podstawową metodą liczenia kosztu zamortyzowanego jest metoda potencjału. Struk turze danych przyporządkowujemy potencjał
o kolejnych wartościach nieujemnych 0 = 0, d>j, .... tak żeby Cl j =
/ , -I- C p . -
cp. _ !
dla każdego i (1 < i < m). Suinuj ąc, otrzy m uj emy: m
m
m
S a, = S (/, + ,- - O,. . ,) = s t, -I- (d>„, - 0) I- I i=1 i= 1 a zatem
i - Z U S X a, i =I ! ■.=i
□ P rzykład:
Rozważmy .strukturo danych Stos z operacjami: (a) emptyv. utworzenie pustego stosu - koszt rzeczywisty 1; (b) push{x)v. wstawienie x na stosie - koszt rzeczywisty 1; (c) mpop(k)\\ usunięcie ze stosu k elementów - koszt rzeczywisty k. Za potencjał stosu przyjmijmy jego rozmiar: tp(Slos) = |Stos|. Wtedy koszt zamortyzowa ny operacji push wynosi 1 -I- 1 = 2, a koszt zamortyzowany operacji mpop k -i- ( - k) = 0. Jeśli w ciągu wykonywanych operacji jest n operacji push oraz / operacji mpop, to całkowity koszt ich wykonania jest < 2n. Gdybyśmy szacowali go z góry przez koszty
40
1. P o d sta w o w e za sa d y an alizy algorytm ów
pesymistyczne wykonania pojedynczych operacji, otrzymalibyśmy znacznie mniej do kładny wynik n + ki. Bardziej złożone przykłady użycia kosztu zamortyzowanego zamieszczmy w dalszej części książki.
1 .8 .
Metody układania algorytm ów Proces układania algorytmu rozwiązującego dane zadanie algorytmiczne ma charak ter twórczy, nie dający się zalgorytmizować. Istnieją jednak ogólne metody, które w pe wnych sytuacjach można zastosować. Ich użycie prowadzi często do skonstruowania szybkich algorytmów. W następnych rozdziałach zajmiemy się zastosowaniem takich właśnie ogólnych metod. W tym podrozdziale podajemy kilka z nich, z. którymi powinie neś się już zetknąć przy nauce programowania.
1. 8 . 1. M e to d a „ d ziel i z w y cię ża j” Problem rozmiaru /;. zostaje podzielony na kilka podproblemów mniejszych rozmia rów w taki sposób, że z ich rozwiązań wynika rozwiązanie zasadniczego problemu. Naturalną konstrukcją programistyczną jest w tym wypadku rekursja. Przykładem za stosowania Lej metody jest algorytm binarnego wyszukiwania elemenLu w liście uporząd kowanych wartości. Porównanie danego elementu z elementem znajdującym się pośrod ku ciągu sprowadza poszukiwanie do lewej lub prawej połówki zadanego ciągu.
1. 8 .2 . P r o g r a m o w a n ie d y n a m ic z n e Programowanie dynamiczne „poprawia” metodę „dziel i zwyciężaj” w sytuacji, kiedy wymaga ona wielokrotnego liczenia rozwiązań tych samych podproblemów. Oto funkcja rekttrericyjna, licząca współczynnik dwumianowy function w s p ( n , m : i n t e g e r )
; integer;
{0 'i w < u) begin if ( (ri -- in) or (in = 0) i then wsp 1 else w s p := w s p ( n — 1, m) I w s p ( n — 1 , m — 1) end
Wielokrotnie jest tu powtarzane rozwiązywanie tych samych podproblemów, na przykład
41
1.8. M etody układania algorytm ów
tv.yp(5, 3) = wsp(4, 3) + tt'.v/>(4, 2) = m».v/;(3, 3) + wsp(3, 2) -l- wsp(3, 2) + ii'.v/;(3, 1) = -■ wsp(3,3) + wxp(2,2) -I- it'.v/;(2, 1) + wxp(2,2) -I- wsp(2, I) + vr.y/;(2, I) -I- iiw/;(2,0) = —wsp(3,3) + >iiv/j>(2,2) I- ii'.v/;(l, I) + wxp(\, 0) -I- \vsp{2,2) + wsp( I, I) + w.v/»( 1,0) + -I- iv.y/)(I, 1) + wsp(\, 0) + ii'.v/h2, 0) Algorytm w takiej postaci ma złożoność wykładnicza. Aby uniknąć powtarzania wyli czania tych samych wartości, możemy zacząć proces obliczeń od rozwiązania najmniej szych podproblemów, zapisać rozwiązania w tablicy pomocniczej, a następnie użyć tych rozwiązań przy wyznaczaniu rozwiązań podproblemów większych rozmiarów - aż do (n\ otrzymania rozwiązania problemu wyjściowego. Aby na przykład obliczyć użyjemy dla I < i < u.
tablicy pomocniczej poin[0.M], licząc w i-tej lazie pom\j\ = U for 7 for
0 to n do pom|7| :■= i ; :• 7 to n do {po/»|;/|
fi-n
|
J, dla 0 < i
i - 1)
for j : = i . 1 downto 1 do pom|;j| :-- pom| j — 1] +po/ii|j"J; {po;n|mj =
}
Otrzymany algorytm ma złożoność czasową O(ir).
1. 8 .8 . M e to d a z a c lila n n a Gdy możliwych kombinacji danych, które mogą być rozwiązaniami, jest liczba wy kładnicza, rozpatrywanie danych w kolejności uporządkowanej prowadzi czasami do zadowalającego rozwiązania. W pewnych wypadkach prowadzi to do znalezienia peł nego rozwiązania, ale. częściej do znalezienia rozwiązania przybliżonego (to jest nieopty malnego). Jako przykład rozważmy problem zapełnienia plecaka. Manty danych n przedmiotów o nieu jomnych rozmiarach - odpowiednio ~ x,,x2, ...,.v„ oraz plecak o pojemności c > 0. Naszym zadaniem jest wybrać pewną liczbę przedmiotów .y, x , ......... ,v, tak, żeby .v, + .v, + ... + x, < c oraz żeby pozostające w plecaku wolne miejsce <: - (x, + xh -i- ... + xt ) było jak najmniejsze. W metodzie zachłannej pakujemy plecak, zaczynając od przedmiotów o najmniejszych rozmiarach. Znaleziony za pomocą iej metody wybór przedmiotów nie musi być optymalny, jak w wypadku plecaka o pojemności 3 i przed miotach o rozmiarach - odpowiednio.- I, I, 1,5, 1,5. O innym sposobie stosowania metody zachłannej możemy się przekonać, rozwiązując laki oto problem sortowania: dla danego ciągu wartości ze zbioru liniowo uporządkowanego
42
I. P o d sta w o w e zasad y an alizy algorytm ów
rt,, .... an należy znaleźć permutację o :[l, 2, .... //)■—>{1, 2, ..., n} taką, że f/„(l) < < ... < aaM. Zgodnie z metodą zachłanną znajdujemy najpierw laki indeks j, że dj jest najmniejszym elementem w ciągu, i przyjmujemy o (l) - j. Potem znajdujemy najmniejszy element z pozostałych wyrazów ciągu i otrzymujemy następny indeks cr(2). Powtarzamy tę procedurę aż do otrzymania wszystkicl) indeksów (o(3), ..., cs(/;)). Prob lem sortowania jest tematem następnego rozdziału lej książki.
1.8.4.
In n e m e to d y Wcześniej w tym rozdziale mówiliśmy już o jednej ważnej metodzie konstrukcji algorytmów: metodzie kolejnych transformacji (przy algorytmie przechodzenia grafu). Będziemy z niej też korzystać w następnych rozdziałach, ale przedstawimy również nowe metody. Warto przy okazji wspomnieć, że te same metody są stosowane przy układaniu algorytmów w różnych dziedzinach algorytmicznych. Choć następne rozdziały są. pogrupowane, tematycznie, powinieneś zwrócić uwagę na to, jak tych samych metod układania algorytmów i tych samych struktur danych (abstrakcyjnych typów danych) używa się w różnych sytuacjach.
Z a d a n ia 1.1. Porównaj rzędy wielkości następujących funkcji, porządkując je od najniższych do najwyższych: (a) / r lo g /i + /»V / r -l- n m (b) //"log/; (c) rr'1'/ log_/;_____ ( d) iiV'4 ii - l og/ / Ce) / / :VI + l og/ /
1.2. Dla jakich wartości /; (a) ir < 10/ilog ii (h) 2" < 10/r 1.3. Sprawdź, czy jeżeli/(n) -- C{"(//)) oraz //(/;) —0(i/(n)), to (a) J\n) + //(") = 0 {,(,'(//) +
Czy podobne zależności zachodzą dla rzędów wielkości LI i 0 ?
43
Z adania
1.4.
([BK11 Niech T(n) będzie funkcją niemalejącą o argumentach i wartościach natural nych, a f(x) funkcją niemalejącą o argumentach i wartościach rzeczywistych. Udo wodnij, że jeśli są spełnione następujące dwa warunki: (a) 7'(2‘) = 0(/-(2*)) (b) istnieją stale rzeczywiste jc() i c > 0 takie, że dla każdego rzeczywistego x t xn, mamy f(2x) < cf(x) to T(n) - (-Hf(ii)). Sprawdź, dla których z następujących funkcji zachodzi warunek (b): (a) ,v (b) ,vlog,v (c) .r (d) 2'
1.5. Rozwiąż (w sensie asymptotycznym) następujące równania rekurencyjne: (a) V'(») = 7'( Ln/2 J ) -l- n (b) T(n) = 37’(r«/2]) -I- n
(c) 'l'(ii) = 2T(Ln/2 J ) + 1 (d) T(n) = (e) T(n) = (f) T(n) = (g) Tfu) =
T( ii - I) + L log w J T(n - I) n T( u/4 J ) -l- T( L 3/;/4 J ) -I- n
L
7Tl«/3 j) -I- T( L n/ 2 J ) -l- n
(IV) T(n) = 7'(LVn J) -I- \."fn J
przy założeniu, że 7(1) = 1. Czy wynik ulegnie zmianie, gdy zamiast stałej 1 weź miemy dowolną stałą naturalną cl 1.6. ([AHU]) Podaj rozwiązanie następującego równania rekurencyjnego: b T(n) =
i i \ a T (lnlc}) +bn
dla
h
= 1
dla u > 1
gdzie a, b, c są dodatnimi liczbami całkowitymi. 1.7.
Podaj rozwiązanie następującego równania rekurencyjnego:
gdzie a, b są dodatnimi liczbami całkowitymi.
44
1. Podstawowe zasady analizy algorytmów
1.8. Podaj rozwiązanie następującego równania rekurencyjnego: T(n) =
■
dla u = i
7'(Ln/2 J ) +
LW n J
dla n > 1
gdzie a, b są dodatnimi liczbami całkowitymi. 1.9. .lale zmienią się wyniki zadań od 1.5 do 1.8, gdy zamiast równości będziemy rozpatrywać nierówność < ? 1.10. Zapisz algorytm wyszukiwania binarnego w ciągu uporządkowanym. Przy założe niu lego samego modelu probabilistycznego co w wypadku wyszukiwania sekwen cyjnego wyznacz funkcje VP(/i), /l(/i), A(n) i 8(n). 1.11. Stosując metodę „dziel i zwyciężaj” , ułóż algorytmy rozwiązujące następujące problemy: (a) sortowania; (b) wyznaczania dwóch największych elementów w ciągu; (c) wyznaczania największego i najmniejszego elementu w ciągu; (d) wyznaczania miejsca zerowego funkcji ciągłej/(a-) w przedziale [«, b\ z dokład nością e > 0; (e) mnożenia dwóch liczb binarnych; (Ij sprawdzania, czy istnieje takie i, że L[i\ = i w uporządkowanym ciągu liczb całkowitych L[ 1] < L|2] < ... < /,[/(]. Wyznacz pesymistyczną złożoność czasową i pesymistyczną wrażliwość czasową. 1.12. Udowodnij, że T log(/i. + I) | = LlognJ + I dla każdej całkowitej dodatniej liczby n , 1.13. I.BK.J Niech x, a, b, p, q i r będą zmiennymi całkowitymi. Udowodnij, że podane instrukcje są poprawne względem warunku początkowego jc > 0 i warunku koń cowego cr < x < (a -I- 1)2. (a) begin ,3
: =
0 ;
while (a -l- 1 )* (a -l- 1 ) < x do a : = a -I- 1 end; (b) begin a := 0 ; p ;= 1; r := ; t; while p < x do begin
a ;= a + 1; r ;= r -I- 2; p := end
p + r
Z a d a n ia
45
(c) begin a := O ; b X I- 1 ; while a -1- i ■/- b do begin p : ---- {a l-!:>}d i v 2 ; if p * p > >; than b := p else a := p
end ®nd ; (cl) begin g := :l.; while x > g*g do g := 2*g; a := 0 ; while g > 1 do begin. C[ := g div 2 ; if (a 'I- g) * (a l- g ) < x then a ;= a + g end end; (e.) begin g := 1; while x q do q 4*g; r := x ; a : = 0 ; while g > J. do begin q : g div 4 ; a :“ .a div 2 ; if 2*a + q S r then begin r : r - 2* a — g; a - . i I g end end end ;
1.14. Rozważmy obliczenie iloczynu n macierzy M x Al, x ... x M„, gdzie /VI jest macierz.i| maj<[Ci| ri _ , wierszy i i) kolumn. Przyjmujemy, że mnożenie macierzy rozmiaru k x l \ l x /u kosztuje/: • / ■iii jednostek. Korzystając z metody programowania dyna micznego, określ optymalne rozłożenie nawiasów w wyrażeniu M { X /W, x ... x Mn, niinimalizujuce sumaryczny koszt mnożenia wszystkich u macierzy. 1.15. Udowodnij, że pesymistyczna złożoność czasowa algorytmu NWD wynosi O(logrt), gdzie n = max(..v, y). (Wskazówka: Użyj liczb Hbonaccicgo).
46
1. Podstawowe zasady analizy algorytmów
1.16. Zaimplementuj w Pascalu kolejkę podwójni) za pomocą struktury dowiązaniowej lak, żeby koszt wykonania każdej operacji kolejki, podwójnej by 1 0(1). 1.17. Podaj implementację listy, w której każdą operację kolejki podwójnej, złożenie dwóch list i odwrócenie, listy można wykonać w czasie 0(1). Staraj się używać jak najmniej pamięci. 1.1 8. W pewnym programie ma zostać użyta tablica marrayf I ,./i| of integer. W wypad ku jakiej struktury danych można nie dokonać początkowego zainicjowania warto ści w tej tablicy, a jednocześnie odróżnić, czy wartość w tablicy została wstawiona tam przez instrukcję w programie, czy też jest to wartość przypadkowo znajdująca się w danym miejscu pamięci. Sprawdzenie powinno dać się wykonać w czasie. 0(1). Można użyć tablic pomocniczych pod warunkiem, że się. ich nie. zainicjuje. 1.19. Dokonaj eliminacji rekur.sji w procedurze b-traverse. 1.20. Ułóż algorytm rekurencyjny, który w danym drzewie z korzeniem 7'wyznacza głębo kość każdego wierzchołka. Wyeliminuj rekurąję, używając standardowej metody ze stosem. 1.21. Ułóż algorytm rekurencyjny, który w danym drzewne z korzeniem 7'wyznacza wyso kość każdego wierzchołka. Wyeliminuj rekursję, używając standardowej metody ze stosem. 1.22. Ułóż algorytmy rekurencyjne, które dla każdego wierzchołka u drzewa binarnego wy znaczają jego następnik w porządku: (a) preorder, (b) inorder, (c) postorder, zapisując go na polu nexl(v). 1.23. Napisz algorytm z procedurami rekurencyjnymi, wyznaczający dla danego drzewa binarnego T dwa wierzchołki ,v i y, między którymi odległość w drzewie jest największa. 1.24. Zapisz algorytm nonrec-inorder bez użycia instrukcji skoku. 1.25. Każde drzewo binarne można przejść bez korzystania z rekur.sji czy stosu (czyli z dodatkową pamięcią tylko 0(1)), jeśli jest dozwolona zmiana dowiązań dr/.ewowycli w trakcie wykonywania algorytmu (na koniec dowiązania mają być takie same jak na początku) ora/, jeśli dopuszczamy możliwość odwiedzania tego same go wierzchołka wielokrotnie. Jak tego dokonać? Ułóż algorytm.
Zadania
47
1.26. Rozważmy problem przejścia drzewa binarnego metodą preorder przy założeniu, że w każdym wierzchołku v drzewa mamy pole vi.sited(v) typu boolean, które może być użyte do zaznaczania faktu, że wierzchołek został już odwiedzony. Napisz procedurę przechodzenia takiego drzewa metodą preorder, używając 0(1) dodatkowej pamięci (a więc bez użycia rekursji). 1.27. Zaprojektuj strukturę danych, umożliwiającą wykonywanie w czasie. 0(1) następu jących operacji na stosie S o elementach ze zbioru liniowo uporządkowanego: (a) (nnpiy(S):: (b) pitshdt, ,S’):: (c) pop{S)\‘. (d) jindmin(S)::
sprawdzenie, czy stos .S'jest pusty; wstawienie elementu u na stos S\ usunięcie z S wierzchołka stosu; wyznaczenie najmniejszego elementu znajdującego się na stosie S.
1.28. Podaj strukturę danych, umożliwiającą wykonywanie w czasie 0(1) następujących operacji na początkowo pustym zbiorze S: (a) xelect(S):: wybranie dowolnego elementu zbioru S i usunięcie go z S; (b) search(i, S):: sprawdzenie, czy element: i należy do S; (c) inserl(i, S):: S: = S u {/} przy założeniu, że S cz {!, 2....... /?}. 1.29. Podaj strukturę danych, umożliwiającą wykonywanie w czasie 0(1) następujących operacji na początkowo pustym ciągu q\ (a) push(i, q):: (b) pop(c/):\ (e) searchd, q):\ (d) deleted, q)\\
wstawienie elementu i na początek listy q; usunięcie elementu początkowego listy q\ sprawdzenie, czy element /' znajduje się na liście
przy założeniu, że q c {I, 2,..., //.}. 1.30. Podaj strukturę danych, umożliwiającą wykonywanie w czasie 0(1) następujących operacji na początkowo pustej liście q: pudid:, i'/).: wsławienie elementu v na początek listy ą\ (b) pop{q)v. usunięcie elementu początkowego listy q\ (c) upt.omin(q):: usunięcie z listy q elementu najmniejszego i wszystkich elemen tów wstawionych na listę q po elemencie najmniejszym. 1.31. Licznikiem nazywamy strukturę danych, umożliwiającą wykonywanie operacji increment, polegającej na dodawaniu jedynki do początkowo wyzerowanej warto ści. Rozważmy reprezentację licznika za pomocą tablicy var A: array [0..r/\ j ol' 1 oraz zmiennej N wskazującej indeks najbardziej znaczącej jedynki. Wartość
48
1. Podstawowe zasady analizy algorytmów N
licznika ma być równa x - X A[i\2'. Na początku /l|7| = 0 dla 0 < i < rA oraz i=u
/V = - l . Rozważmy następującą implementację operacji increment. 1 : -- 0 ; while i < N do if ,A|i |- .1. then begin A[iJ := 0; i := i -I-1 end else b r e a k ; if i < rA then A[i] := 1 else e r r o r (' Przepełnienie licznika' ) ; i f i > N then N := i ;
Dobierz odpowiednio potencjał dla tej struktury danych i udowodnij, że koszt zamortyzowany operacji increment jest 0(1). 1.32. Do struktury danych z zadania 1.31 dodaj operację reset zerującą licznik. Przyjmij następującą jej implementację: N := -1;
Przy odpowiednio dobranym potencjale struktury danych udowodnij, że koszt za mortyzowany każdej operacji jest: 0(1). 1.33. Załóżmy, że implementujemy kolejkę za pomocą dwóch slosów, przy czym na jednym z nich umieszczamy elementy .lak, jakbyśmy umieszczali je na końcu kolejki, natomiast z drugiego stosu pobieramy elementy tak, jakbyśmy pobierali je z początku kolejki. Gdy drugi stos (służący do pobierania elementów, jest pusty, przepisujemy całą zawartość pierwszego stosu na drugi. Zapisz operacje kolejki w jej reprezentacji za pomocą dwóch stosów. Jaka jest pesymistyczna złożoność czasowa tych operacji? Zdefiniuj odpowiednio potencjał dla tej struktury danych tak, żeby koszt zamortyzowany każdej operacji kolejki był stały.
II
2
;? ^ - S o r t o w a n i e
tym rozdziale przedstawiamy podstawowy problem informatyczny, a mianowicie sortowanie. Podajemy zarówno ogólne algorytmy sortowania, takie jak sortowa nie przez selekcję (selcctionsorl), sortowanie przez wstawianie (insertionsorl), sortowanie szybkie (quicksort), sortowanie przez kopcowanic (heapsort), sortowanie przez scalanie (mergesorl), jak i algorytmy uzależnione od dziedziny, takie jak sortowa nie pozycyjne (radixsorl), a także algorytmy wykorzystujące pamięć zewnętrzną. Roz ważamy problem, ile co najmniej trzeba wykonać porównań, żeby posortować listę n ele mentów. Rozpatrujemy też problemy zbliżone do sortowania, jak scalanie i wyznaczanie /c-tego co do wielkości elementu.
W
Sortowanie to problem bardzo często rozwiązywany na komputerach. Jego popularność wiąże się z faktem, że łatwiej jest korzystać ze zbiorów uporządkowanych niż nieupo rządkowanych. Sortowanie definiuje się następująco: darni jest lista ą = [o,, c/J elementów zbioru liniowo uporządkowanego; trzeba dokonać permutacji ustawiającej elementy w porządku nicmalejącym a, ś «, < ... < a, . Spotyka się kilka modyfikacji problemu sortowania, które występują w praktyce: • elementy są rekordami danych; na rekordach jest określona funkcja klucza key (a): uporządkować rekordy względem wartości ich klucza tak, żeby i,) 2 keyin, ) < ... < key(a- ) ® elementy są parami |/q, p.J, gdzie Asjest kluczem, a p, jest dowiązaniem do rekordu dla I < i < iv, uporządkować elementy względem ich kluczy jak w wypadku pierwszej modyfikacji (unikamy przestawiania rekordów, które mogą być długie); ® elementy do posortowania mogą znajdować się albo w pamięci wewnętrznej (wówczas mamy do nich dostęp bezpośredni), albo w pamięci zewnętrznej;
50
2. Sortowanie
. o czasami wymaga się dodatkowo zachowania warunku stabilności, izn. zachowania początkowego ustawienia względem siebie elementów równych (rekordów o takich samych kluczach); gdy na przykład alfabetyczna listę studentów sortujemy względem wyników egzaminu, silą rzeczy wymagamy, aby w wypadku tej samej oceny nazwiska studentów były podawane w porządku alfabetycznym. Za operację dominującą będziemy przyjmować porównania elementów w ciągu. Za zło żoność pamięciową S(n) będziemy przyjmować ilość dodatkowej pamięci (oprócz n miejsc pamięci dla elementów w ciągu), potrzebnej do wykonania algorytmu. Będziemy też. zakładać, że elementy listy q są liczbami całkowitymi i że znajdują się w tablicy et, o|7| = «,■ dla I < i < n. Będziemy niekiedy przyjmować, że a[0| = — ■*>, «|/M- l| —+<*>, gdzie -w , +oo to liczby - odpowiednio - najmniejsza i największa w ustalonej reprezentacji liczb całkowitych. Przy analizie probabilistycznej (wielkości A(n) i S(n)) będziemy przyjmować, że danymi wejściowymi są permulacje liczb I, 2, .... ii oraz że każda laka permutacja jest jednakowo prawdopodobna. W kolejnych podrozdziałach rozważymy podstawowe algorytmy sortujące.
2 X
Selectionsort ~ sortowanie przez selekcję Sortowanie przez selekcję odbywa się w następujący sposób: trzeba wyznaczyć naj mniejszy element: w ciągu; zamienić go miejscami z pierwszym elementem w ciągu, wyznaczyć najmniejszy element: w a[2..n] i zamienić go z drugim elementem w ciągu itci., aż cala tablica zostanie posortowana. proc sćtura s e 1 e c ti on s or I:; irar i, j , w i n : i n t e g e r ; ba gin for i := 1 to n - 1 do begin {a|.l] S ... b a|i - 1J < a|i . . .i:ij} win : i ; for j := i -I 1 to n do if a [ j ] < a|i]; {zamiana miejscami afinioj z a|i]} end end s e l e c t i o n s o r t ;
Analiza złożoności algorytmu selectionsort: jest bezpośrednia. \V(ii) ■=A(n) = (ii — I) -l- ( ii —2) -I- ... + 1 =
ii(n - I) 2
I
ir — O(n)
2.:?. J n s e rtio n s o rt - s o rto w a n ie p rz e z w s ta w ia n ie
51
A(/;) = S(n) = 0 (w procedurze jest wykonywany zawsze ten sam ciąg operacji, nie zależnie od danych wejściowych) Sin) = 0(1) Do (a) (b) (c)
(mówimy, że algorytm sortuje w miejscu)
głównych zalet przedstawionego algorytmu należy: optymalność, jeśli chodzi o liczbę przestawień (tylko n - I); prostota implementacji; zadowalająca szybkość dla małych wartości ;i.
Algorytm selectionsort nie jest stabilny. Znalezienie kontrprzyldadu pozostawiamy Ci, Drogi Czytelniku, jako ćwiczenie (zad. 2.2). Kosztem dodatkowego wysiłku, zwiększa jąc jednak współczynnik proporcjonalności złożoności, można ten algorytm uczynić sta bilnym (zad. 2.3).
2 .2 .
Insertionsort - sortowanie przez wstawianie Sortowanie przez wstawianie odbywa się w następujący sposób; dla każdego i = 2, 3, .... n trzeba powtarzać wstawianie a|7| w już uporządkowaną część listy a\ 1] < . . . < a\i - 1]. procedure i n s e r t ! o n s o r t ; [a[0| =■■■■-■», aby uniknąć testu " j > 1" ) var i , j , v - . i n t e g e r ; begin for i := 2 to n do begin (a[0] < a[l] < . . . < a [ i - 1]} j := i ; v := a[i] ; while a [ j — 1] > v do begin (a[0] < . . . < a \ j - 1] < a [ j + 1] < . . . < a[i], j < i => v < a|j + 1], a [ j \ - wolne miejsce} a[j] := a [ j - 1]; j := j - 1 end; a [ j ] := v end end i n s e r t i o n s o r t ;
Analiza pesymistycznej złożoności czasowej jest bezpośrednia: =: 2 -l- 3 -I- ... -I- n
n(n -I- 1) 9
— ii1 0 (n) -I-
52
2. S o r t o w a n i e
(Liczymy również porównania z elementem o|()|). Mn) =
n(n + 1)
! - ( » - ! ) = — n 2 - 0(n)
2
,S'(«) = 0(1) (algorytm działa w miejscu) Zauważmy, że liczba porównań wykonywanych w algorytmie insertionsort jest propor cjonalna do liczby inwersji na liście ą, tzn. takich par, że r/[/j > u[i\ dla i < j. Jeśli ci;|g q jest prawie uporządkowany, tzn, gdy liczba inwersji jest 0(n), to złożoność pesymis tyczna tego algorytmu jest liniowa. Jest to duża jego zaleta, gdyż w praktyce sortowane dane są już często częściowo uporządkowane. Oprócz tego algorytm ten jest stabilny, prosty i łatwo implementowalny. Przeprowadzimy teraz analizę oczekiwanej złożoności czasowej. Niech X„ będzie liczbą porównań wykonywanych w algorytmie dla u-elementowcj permutacji liczb I, 2, n przy założeniu, że każda permutacja jest jednakowo prawdopodobna. Element «|7| z rów nym prawdopodobieństwem może zająć każdą z t pozycji na liście —oo —fl[Q] < a\ 11 < «[2] < ... < a\i - 1] (zad. 2.5). W i-tym kroku algorytmu wykonuje się więc średnio taka oto liczba por ównnń: ii (* + 1)/i
i H" 1
2
i
(Liczymy również porównania z elementem
A(n) = X
i+ I
1 II+ I
I
(n -I- 4) (n - I )
I = — / r -I- 0(n) ■1
Oczekiwana złożoność czasowa algorytmu insertionsort jest więc nieco lepsza niż al gorytmu selcctionsort, choć tego samego rzędu wielkości. Wyznaczenie oczekiwanej wrażliwości czasowej 5(h) = - - n m + O(n) 6
pozostawiamy Tobie, Drogi Czytelniku, jako ćwiczenie (zad. 2.4).
Q uicksort - so rto w a n ie szyb k ie
53
2.3. Q u ic k s o r t - s o r to w a n ie s z y b k ie Algorytm quicksort jest jednym z najczęściej uży wanych algorytmów .sortowania. Jest uważany za najszybszy algorytm sortowania dla „losowych” danych wejściowych. Jego konstrukcja jest oparta na zasadzie „dziel i zwyciężaj” . Lista wejściowa q zostaje w specjalny sposób podzielona na dwie części, po czym obie części są sortowane niezależnie. Oto ogólny schemat tego algorytmu. procedure q u i c k s o r t ; ( 1 , r : i n t e g e r ) ; [parametry I, r określają podciąg.do posortowania przez dane wywołanie procedury q u i c k s o r t ; w wyniku działania instrukcji if n > 1 then q u i c k s o r t (1, n ) ; zostaje posortowana cała lista wejściowa) var j : i n t e g e r ; begin (I < r } j
;= p a r t i t i o n ( l , r ) ; { p o d c i ą ł }
if j - 1 > 1 then q u i c k s o r t (I , j - 1) ; if r > j + 1 then q u i c k s o r t { j + 1, r) end q u i c k s o r t ;
Decydujące znaczenie dla poprawności i efektywności algorytmu nut funkcja partition, która przekształca tablicę «[/../•] w len sposób, że: (a) element v = a\j] znajduje się nu właściwym, ostatecznym miejscu w tablicy; (b) a\l). .... a\j ~ 1] < v; ( ej v
:,\j + I ],
u|r].
Jako element u, rozdzielający ciąg a\l...r], wybieramy u = «|7|. Przeglądamy ciąg rj|7 + 11, od lewej strony, dopóki nie znajdziemy elementu nie mniejszego niż a\l\. Potem przeglądamy a\l-t- i j, ..., a[r], tym razem od strony prawej, dopóki nic napotkamy elementu nie większego niż a[l|. Dwa elementy, na których się zatrzymuje my, zamieniamy miejscami. Powtarzając opisane działaniu, zapewniamy, że elementy na lewo od wskaźnika wędrującego ze strony lewej na prawą są nic większe niż u[/|, a ele menty na prawo od wskaźnika wędrującego ze strony prawej na lewą są nie mniejsze niż ti|7|. Kiedy oba wskaźniki się spotykają, proces dzielenia jest zakończony - wystarczy element rozdzielający v = a\t\ zamienić miejscami z ostatnim elementem lewej części. Oto polna postać procedury quicksort. proceduro q u i c k s o r t ( 1 , r : i n t e g e r ) ; {parametry 1 , .r określają podciąg do posortowania przez dane wywołanii- procedury q u i c k s o r t ; w wyniku działania instrukcj i. . i£ u 1 then q u i c k s o r t (1, n ) ; zos taje posortowana cała lista wejściowa przy założeniu, że a|n J 11-=+ ■»=>}
54
2. Sortowanie v a r
v ,
i ,
j
:
i n t e g e r ;
begin (początek funkcji p a r t i t i o n } 11 < r , a| i -I- I.J, . . . ,a[.r| < a|r -I 1.J) V := a| 1 1;i := i ;j ; ~~ r + 1 ; repeat repeat i : i I- 1 until a[i] k v ; repeat j := j - 1 until a|jj] 5 v ; i:E i < j then a |i | < - > a \ j ) ; {zamiana miejscami a |ij z a|j]} tint i 1 j < i ; a [ l\
:=
a \ j \ ; a [ j ]
: =
V;
[koniec procedury p a . r t i t i o n } if i - 1 > 1 then q u i c k s o r t { 1 , j - 1) ; if r > j -I- 1 then q u i c k s o r t ( j + 1, r) end q u i c k s o r t ;
(Warto zwrócić uwagę, że w wewnętrznych instrukcjach repeat istotne są nierówności nieostre, zobacz zad. 2.10). □ Przykład:
Weźmy ciąg 7, 1, 5, 8, 9, 6, 10, 2 i zbadajmy działanie funkcji partition. Klementem dzielącym jest v = 7. 7, I, 5, 8, 9, 6, 10, 2, -l-°° i
.1
7, 1,5, 8, 9, 6. / 7, 1,5, 2, 9, 6, i ./' 7, 1, 5, 2, G, 9, j i 6, 1, 5, 2, 7, 9, lewy policing
10, 2, -i-o° ./' 10, 8, +o° 10, 8, + « J^ > 10, 8, -l-oo
pmwy podciąg
Policzmy liczbę porównań dla wywołania quicksort^, r). Element dzielący v = a\l\ jest porównywany z każdym elementem w ciągu a\l -i- 1],..., «|r|, przy czym co najwyżej z dwoma elementami dwukrotnie, a zatem liczba porównań wynosi' r - / -i- 2. Równanie na liczbę porównań w przypadku pesymistycznym jest więc następujące: H'(0) = IłU ) = 0 W(n) = max (Wij - I) -I- Win - /)) + (n + 1) dla n > 1 i u
(*>
2.3. Quicksort - sortow anie szybkie
55
Zagnieżdżeń rekursji może być w najgorszym wypadku n - I. Ponieważ na każdym poziomie rekursji wykonuje się co najwyżej n + 1 porównań, iV'u/ i : (n — I )(/.' + 1) = :r -
I)
W poprzednich algorytmach współczynnik przy ir by! równy V2. Możemy zatem podej rzewać, że otrzymane oszacowanie nic jest dość precyzyjne. Spróbujemy zgadnąć do kładniejsze rozwiązanie równań (*), po czym przeprowadzimy dowód indukcyjny. Możemy oczekiwać, że z największą wartością będziemy mieć do czynienia w sytuacji skrajnej (funkcja ir jest wypukła, a zatem a:5 + y2 < (jc + y)2 + O2), gdy jedna z wartości j — 1 albo n —j jest zawsze równa 0. Zachodzi to wtedy, kiedy algorytm quicksort jest używany do posortowania ciągu już posortowanego o[1] < n[2) < ... < Otrzymuje my następujące równanie na liczbę porównań: I VP„(0) = VV(,(I) = 0 1 VP(,00 = W„(n - 1) -I- (77 + I)
dla n > 1
Stosując metodę rozwijania, mamy W/n) = (n + 1) +
ii
+ ... + 3 + VK,,(1) -
(n + 1) (// + 2) 1 t 3 -------- ----------- 3 = — i r + — n ~ 2
Pozostaje teraz wykazać za pomocą indukcji matematycznej, że dla każdego n S 0 f0 dla n = () W(n) = < i 3 —- ir -I- — // —2 dla n > 0
(**)
Dla n ~ 0,1 jest to oczywiste. Załóżmy, że (**) zachodzi dla wartości mniejszych od pewnego n. Wówczas wykorzystując wypukłość funkcji rr, otrzymujemy ),]/(„) = max (W(J - i) -l- W(n -./)) -I- (n + I) = i max (WQi - I), Wij - 1) + W(n -./)) + (/; -I- I) = 2s 7S'I - I = max I — (n - I )2 + j r (n - 1) - 2, max ( —(./ - 1)2 + ~ (j - I) - 2 -I12 2 2 j.s „_ I ( Z + y (« = rna.K !
- j f
+ y («
-j)
- 2 j+
(n +
(/; - I)2 + | ( / i - l ) - 2 , 3
J) - 2)2 +• — (« - 2) - 2j + (// + 1) = I
,
(n - I)2 + —••(/» - I) - 2 + n + I = — tr +
3
- 2
56
2. S ortow an ie
Z najmniejszą liczbą porównań mamy do czynienia w skrajnie przeciwnej sytuacji, gdy dla każdego rekurencyjnego wywołania j znajduje się w środku przedziału [/../•]. Otrzy mujemy wówczas I VV„(0) = W„(ł) - 0 i 1V » = W„( L(n - 1)/2 J ) + WuI' (n - l )/21) -I- (n -I- 1)
dla /i > 1
Rozwiązaniem tego równania jest funkcja VK„(») = 0(//log/t). Pesymistyczna wrażliwość czasowa jest zatem równa A(/t) = 0(tr - //log n) = O(ir) Z przeprowadzonej analizy pesymistycznej złożoności czasowej jeszcze nie widać, dlaczeg(I algorytm quicksort jest nazywany algorytmem sortowania szybkiego. Okazuje się, że jego oczekiwana złożoność czasowa jest dobra, bliska funkcji W («). Niech X„ będzie liczbą porównali, które są wykonywane w algorytmie quicksort w celu uporządkowania permutacji liczb 1, 2, ..., n, przy założeniu, że każda permulacja jest jednakowo prawdopodobna. kEj':
L e m a t 2.1.
!;
Podciągi powstające w wyniku podziału losowej permutacji są ciągami loso wymi.
Udowodnienie tego lematu pozostawiamy Ci jako zadanie 2. U). Najpierw policzymy oczekiwaną liczbę porównań przy założeniu, że elementem dzielą cym jest s e 11..//]. Wynosi ona (u-1- I) + oczekiwana liczba porównań dla quick sort ( I, s - 1) + oczekiwana liczba porównań dla quicksort (s + 1, n), co na mocy lematu 2.1 jest równe (n -I- I) -I- A(s — 1) + A(u —x). Ponieważ każde ,v e [1..//J jest jednakowo prawdopodobne (pojawia się więc z praw dopodobieństwem 1/«), oczekiwana złożoność czasowa spełnia następujące równanie: f/\(())= A (l) = ()
1 A(u) — (n -I- I) -l----Ż (A(s — I) -I- A(n —s)) l Równanie na A(n), dla n > I, daje się sprowadzić do A(n) = - - Z A(s - 1) -I- (n -I- I)
dla n > 1
2.3. Q uicksort - sortowanie* szybkie
Aby dokonać kolejnego przekształcenia, zapiszmy je w postaci dwóch równań: nA(n) — 2 2^ A( s — 1) -f- //(// -l- 1) ,V: |
li - I (ii - (),4 (n - I) = 2 X /t(.v - I) + (// - 1)// j..i
Odejmując te równania stronami, otrzymujemy nA (ii) -
- 1) /\ (/; - 1) = 2A(n - I ) + 2n
(ii
a stąd iiA(n) = (n + l)/\(// - i ) + 2u czyli A (ii) n+ I
A(n-l) n
^2 n +I
Stosując do lego równania metodę rozwijania, mamy A(a) -I- I
ii
=
A( ii - I) n
2 u+ 1
A(n — 2) 2 2 ,4(1) 2 2 2 —..- -i------ 1- —....— = .....- .....I----- 1.....- + ... -I--------n — I ii n -I- I 2 3 4 ii -I- I
f 3 4 = 2 /-/„ ,, 1, -----2 ,
I I I -udzie //„-■= I -I----2 1- -....I3 ... + — n
a zatem 3) A ( i i ) — 2 ( i i -I- ) )
/■ /
K
ii
+ I) //.. +
n+ 1
3 2
Na koniec korzystając ze związku asymptotycznego na ttn Hn = In// -l- y + 0 ( i r ‘)
(yjest siaki Eulera = 0,57)
otrzymujemy 2
2
A(n) ——...-..(n + I) log// + O (i i) = ----------//log// -I- O(n) log n ' log e W przybliżeniu współczynnik przy //log// jest równy 1,4.
58
2. S ortow an ie
Jako zadanie (zad. 2.7) pozostawiamy Ci wykazanie, że oczekiwana wrażliwość czasowa algoryimu quicksort wynosi
Współczynnik przy // jest w przybliżeniu równy 0,68. Jako zadanie (zad. 2.8) pozostawiamy Ci też wykazanie, że oczekiwana liczba przestawień wykonywanych w algorytmie jest nie większa niż połowa oczekiwanej liczby porównań. Zauważmy, że: (a) asymptotyczna oczekiwana złożoność czasowa algorytmu quicksort wynosi //log//, a więc mniej niż dla poprzednich algorytmów; (b) współczynnik przy //log// jest nieduży, około 1,4; (c) oczekiwana wrażliwość czasowa jest niewielka w stosunku do oczekiwanej złożono ści czasowej, co mówi o silnym skupieniu rzeczywistej liczby porównań wokół war tości oczekiwanej; (d) liczba innych działań w algorytmie nie jest znacząco większa od liczby porównań. Powyższe fakty tłumaczą, dlaczego algorytm quicksort jest uważamy za najszybszy al gorytm sortowania. Poważnymi jednak jego wadami są niestabilność i koszt O(ir) w przypadku pesymisty cznym. Pewną wadą jest też użycie rekursji, która jest tłumaczona na operacje na stosie. W złożoności pamięciowej musimy uwzględnić pamięć potrzebną na implementację sto su. Jeśli chodzi o przypadek pesymistyczny, głębokość rekursji wynosi n — I, a zatem S{n) = O (n) czyli złożoność pamięciowa jest większa niż w poprzednich algorytmach. Na szczęście można lalwo ją zmniejszyć. Zauważmy, że: (a) oba wywołania rekurencyjne w treści procedury quicksort są niezależne, tzn. mogą być wykonywane w dowolnej kolejności; (l:>) wywołania te znajdują się na końcu treści procedury, a zatem: jedno z nich może być zastąpione, iteracją; - zamiast: na stosie przechowywać miejsce powrotu z wywołania rekureneyjnego, można przechowywać parametry wywołania, które ma być wykonane po właśnie wykonywanym. Na stosie będą się teraz znajdować pary indeksów [/, j\ określające parametry wywołań algorytmu quicksort, które należy jeszcze wykonać. Przy realizacji wywołania (jiiicksort(l, r) z dwóch wywołań rekurencyjnyeh będziemy umieszczać na stosie większy z podciągów otrzymanych w wyniku podziału (a ściślej jego końce), pozostawiając mniejszy do prze!\va-
2.3. Q u i c k s o r t - s o r t o w a n i e s z y b k ie
59
rzania w kolejnej iteracji. Nazwijmy te podciągi bratnimi. Rozważmy pewien moment obliczeń i niech ,v,, sJ t..., sk będą długościami podciągów na stosie, a / ,, »2, ..., ik rozmiarami ich bratnich podciągów. Przyjmijmy t'„ = n. Zauważmy, że podciąg o długości ip gdzie j < k, został podzielony na dwa podciągi: jeden o długości ,v( ,, umieszczony na1Stosie, i drugi o długości przetwarzany w kolejnej iteracji /j = Sj , + t) + , + 1 oraz S j , > it + Zacliotlzi wówczas: (a) i j , (b) .s)
1 ''
Stosując indukcję, łatwo możemy teraz pokazać, że
2J~ 1
n
dla 1 < j < k
co dowodzi, że maksymalna głębokość stosu jest: ograniczona przez log//. Mamy zatem S(n) = O(log//) Oto zmieniona wersja algorytmu quicksort. procedure q u i c k s o r t 1 ; var 1 , r , j : i n t e g e r ; s : stack; begin i f n > 1 then begin
•••• := 111, n ||; repeat; Ii , r| ;= t o p i s ) ; p o p i s ) ; while 1 < r do begin j ; = p a r t i t i o n (1 , r ) ; i f j - 1 < r - j then begin p u s h i s , [j-i-l, r)) ; r := j - 1 end e ls e begin p u s h i s , 1.7., j - 1|) : end end u n t i l s = [] end end q u i c k s o r t : ! ;
60
2. S ortow an ie
Algorytm quicksort byl obiektem intensywnych badań, w wyniku czego zaproponowano kilka jego modyfikacji. <* Algqrytm insertionsort może być używany do sortowania danych o małych rozmia rach, na przykład < 20. • Gdy nie ma pewności, że dag wejściowy jest losowy, to lepiej albo używać generatora liczb losowych do określenia elementu dzielącego u := random(2', r) ; {generowanie losowego indeksu z |1, j.jj aj1 1 < — > aju];
albo jako element dzielący wybierać medianę z pewnej próbki elementów ciągu, na przykład pierwszego, środkowego i ostatniego. ® Można w ogóle pozbyć się stosu, organizując go wewnętrznie w sortowanym ciągu. Zmniejsza się w ten sposób złożoność pamięciową do 0(1), ale zwiększa się współczynnik proporcjonalności złożoności czasowej. Przy prezentacji tej metody przyjmiemy nastę pujące założenie upraszczające algorytm: elementy listy wejściowej q = [o,, .... an\ są różnymi liczbami całkowitymi. Zapiszmy jeszcze raz algorytm quicksort z rekursją zastąpioną przez stos, przy założeniu, że przy podziale zawsze umieszczamy na stosie końce prawego podciągu niezależnie od rozmiarów obu podciągów. procedure q u i c k s o r t 2; var.I, r , j : i n t e g e r ; s : stack; begin
0:
III, .nil; repeat {m> 2}
1:[1, r| := t ; o p ( s ) ; p o p ( s ) ; while r - 1 > in do begin
,I + r , niech a|/c| będzie medianą z a[l|, a|L------ J|, a[j;|; 2
a[/c| < - > a[l|; j := p a r t i t i o n ( 1 , r ) ; p u s h (s , [ j -I- 1, i-j) ; { j + 1 < r } r := 7 - 1
2: end;
wykonaj insertionsort dla a[l. . r \ ; 3:
u n t i l s = 11 end q u i c k s o r t k ;
Etykietami zostały oznaczone instrukcje operowania na stosie. W każdej chwili realizacji algorytmu lista (/jest podzielona na części przedstawione na rysunku 2.1.
2.it. Quicksort -■sortowanie szybkie
61
/ Część posortowana
Cześć właśnie sortowana
Podciągi do posortowania o końcach na stosie
Rys. 2.1. Stan w trakcie realizacji algorytmu quicksort?.
Zauważmy, że można wyeliminować lewy indeks z pary umieszczonej na stosie. procedure q u i c k s o r t 3 ; var I, r , j : i n t e g e r ; s : stack;
begin 0:
S — In]; r : ~ - 1; repeat (m >2} 1 : -r + 2 ;
1: 2:
r := to p (s ) ; pop(s); while r — 1 > m do begin ■i + r , niech a \ k \ będzie medianą z a|.I|, a| L-----J|, a[r| a(iv| < -• > a|l|; j := p a r t i t i o n (i , r ) ;
4: • 5:
6:
push(s, r ) ; r := 3 - 1 and ; wykonaj i n s e r t i o n s o r t dla a|I..r|; until s = [] end g u i c k s o r t 3 ;
Poprawność instrukcji przypisania / := r + 2 wynika z rysunku 2.2: I
r r-\-1
1-2 Następny, podciąg, dla którego indeks prawego końca bierzemy z.e stosu; indeks lewego końca oblicz,tuny, mając wartość bieżącego r
Rys. 2.2, Związek miedzy indeksami dwóch kolejnych insUmeji
indeks prawego końca podciągu do przetworzenia bierzemy ze stosu. Gdybyśmy potrafili, zaznaczyć prawy koniec podciągu w samym ciągu, stos nie byłby potrzebny (zaczynając od / = r + 2, szukalibyśmy pierwszego zaznaczonego miejsca).
, '
62
2. Sortow an ie
Załóżmy, że ciąg do posortowania składa się wyłącznie z dodatnich liczb całkowitych. Indeksy prawych końców podciągów znaczymy, zmieniając znak liczby na ujemny, proceduro q u i c k s o r t ^ ; var 1, r , j : i n t e g e r ; begin r := -- 1; a|ri| :- - afn]; r e p e a t (ni > 2}
.1 : =
r 1- 2
; r :=
1;
while a[r] > 0 do r: = r -I- 1 ; a[ri :-= — a[r] while r - .1 > m do begin , 1 -I- r . niech a\k] będzie medianą z a|I|, a| L------- J |, a(ij; a [ k \ < - > a|Ij; j
{>}
p a rtitio n d ,
r) ;
afr] := - a[.rj ;
r :=j- 1 end ; wykonaj inseruionsort dla a|./.. ,r|; until r -- n end q u i c k s o r t : - ' ! ;
Markowanie prawego końca można też wykonać przy użyciu samych elementów w ciągu (bez ich zmiany). Wystarczy w wierszu oznaczonym gwiazdką {*) wstawić w miejsce u |r| największy element, powiedzmy z. podciągu a\l..j - I j. przesyłając jednocześ nie «[/•] w miejsce a[/], o|/| w miejsce tt\j - l |, a a\j — I | w miejsce «[/c|. Szczegóły pozostawiamy Ci jako zadanie 2.12.
2 .4 . D o ln e o g r a n ic z e n ie na z ło ż o n o ś ć problemu s o r to w a n ia Zanim rozważymy następne algorytmy sortowania, zastanówmy się, jaka jest mini malna liczba porównań potrzebnych do posortowania ciągu ii różnych elementów q = |V(|i o j. Za model obliczeń przyjmiemy model drzew decyzyjnych. Drzewem sortującym listę q ~ |fl,, a2, ....
2.4. D o ln o og ra n icze n ie na złożon ość problem u sortow an ia
63
O P rzykład: Nu rysunku 2.3 jest widoczne drzewo sortujące 3-elementowy ciąg q = [as, a?, «,]. a l
Rys. 2 J . Drzewo sortujące ciąg 3-elememowy
Każdy algorytm sortowania przez porównania (na przykład selectionsort, insertionsort, quicksort) daje się rozwinąć w drzewo sortujące przy ustalonym rozmiarze n. Bez straty ogólności możemy założyć, że: (a) jest dokładnie u\ liści, tzn. tyle, ile różnych perm utac ji ciągu a v a2, .... att\ (li) każdy węzeł wewnętrzny ma dwa następniki tzn. każde porównanie jest istotne (oblicze nie może. dotyczyć pójścia w lewo bądź w prawo od danego węzła wewnętrznego). Klasę drzew binarnych spełniających podane warunki oznaczamy przez Bn. Zauważmy, że długość ścieżki od korzenia do liścia jest równa liczbie porównań wyko nywanych w ramach odpowiedniego obliczenia algorytmu. Znalezienie dolnych ograniczeń na liczbę porównań w przypadku pesymistycznym i przypadku oczekiwanym sprowadza się zatem do oszacowania z dołu: (a) h ( T ) .wysokości drzewa 7'; (b) d(T) =
X
clT(x) - całkowitej długości ścieżek zewnętrznych w 7’;
,i liść w 7'
w klasie drzew binarnych Wówczas dla każdego algorytmu sortującego przez porów nania, który rozwija się w drzewo sortujące, zachodzą, nierówności:
64
2. S ortow an ie
\V(n) > min h(T)
A (//.) > min Te.
den //!
□ P rzykład:
Dla drzewa binarnego z poprzedniego przykładu: h(T) = 3 d(T) = 3 + 3 + 2 + 2 + 3 + 3 = 16 a zatem oczekiwana długość ścieżki zewnętrznej c/(7)/3! = 16/6 = 2,66. _______ *1 Zauważmy, że: (a) 2',m > //! dla T <= B„ (dowód indukcyjny, że dla każdego węzła o wysokości h i licz bie potomków-!iści /, 2'' > /); "{
'i,
“t
"
(b) /t(7) > log//! = X log / > Jlogxdx = loge Jlnxdx = loge(.rln..v - ,t) = / =i i i = loge(/;ln // - // + 1) = //log/i - //loge + loge > //log// — J,45//. Stąd otrzymujemy JfjU
T w ie r d z e n ie 2.1. W każdym algorytmie sortującym przez porównania, w przypadku pesymistycz nym, jest wykonywanych co najmniej //log// - 1,45// porównań.
Tig
L e m a t 2.2. Wartość <7(7) jest najmniejsza dla tych drzew T e Bn, w których wszystkie liście znajdują się na dwóch sąsiednich poziomach.
D owód: Niech d rzew o T e Bn o wysokości d nie spełnia warunku w lemacie. Niech
z i / będą liśćmi na poziomie <7, a .t liściem na poziomie k < d — 1. Dokonajmy przekształcenia drzewa T w drzewo T' przez doczepienie liści z i t do węzła x Otrzymujemy drzewo należące do Bn i takie, że d(T) - d (T ) -
(2d + k) usuwamy dwa liście o głębokości <7 i jeden liść o głębokości k
—d —k - I = (d
1) - k > 0.
-
((d - 1) + 2(k + I)) = przybywa liść o głębokości r f - l i dwa liście o głębokości k + I
2,4. D olno o g r a n ic z o m e n a z ło ż o n o ś ć problem u K o d o w a n ia
Jak widać, pr/.y przejściu od '/' do T' całkowita długość ścieżek zewnętrznych zmniejsza się, u zatem d(T) nie mogło hyc minimalne. cl ido L e m a t 2.3. Niech-/ = ni. Dla drzew T e />’„ zachodzi d(T)> /[.log/J -i- 2(1 - 2l-ln,!,J) D ow ód :
Na mocy poprzedniego lemat u możemy założyć, że wszystkie liście (jest ich / = //!) w T znajdują się na dwóch sąsiednich poziomach. Niech // będzie wyso kości:! drzewa T. P rzypadek 1
/: t l.zn. // ł ) z/(7') = III = /log / Przypadek 2: /
*
2"
Nieclt /. będzie liczbą liści o głębokości i w drzewie T (i - li, h — I ). Wówczas zachodził związki: / = /*_, -I- /,,, 2''" 1 < / < 2" i h - 1 = |_ lo g /J. Z aiiważmy, że 2'' = lh -l 2/,, _ | = 2 / - lh, czyli /,, = 2(/ - 2* ~ '). Otrzymujemy więc d(T) = hl„ + (// - 1)/* _, = ( //— I)/ -I- 4 = (A - i)/ -I- 2(7 - 2"- ') = = L log/J/ -l-2(/- 2Li‘*,j)
cbdo I iłil
T w ie r d z e n ie 2.2. W każdym algorytmie sortującym przez porównania jest wykonywanych śred nio co najmniej /dog// — 1,45// porównań.
D owód: Niech T będzie drzewem sortującym ciąg
mocy lematu 2.2
66
2. Sortowanie = L log/;! J + 2 - 2 " Ii,,''"iJ - |1,‘!,,!> Llog«!.J >
> L/ilog/ł — l,45n + log e j > //log// — 1,45// ciule > | Przypomnijmy, że oczekiwana liczba porównań dla algorytmu 2 1,4/zlogn + Oin), a więc bliska dolnego ograniczenia.
quicksort
jest
2,5. S o r t o w a n ie p o z y c y jn e Wszystkie wielkości w komputerze są przedstawiane za pomocą słów binarnych. W ogólnych algorytmach sortowania nie jest to brane poci uwagę. Również w językach programowania wysokiego poziomu nie ma operacji na liczbach binarnych. Będziemy używać funkcji wycinani;! bitów w słowie binarnym, którą w języku Pascal można zdefiniować lak: function b i t s i x , k , j ; i n t e g e r ) : i n t e g e r ; begin b its { x div 2*) mod 2 1 end h i t s ;
Będziemy zakładać, że h jest długością słowa binarnego. Interpretację funkcji bits przed stawiamy na rysunku 2.4. bits (x, k,j) |/>-l
/.-+/'|
| k- 1
0|
Rys. .’..-I. 1'iinkcju bits wycina j lilnnv, |>oe/.yn;ij;ic od liilu o mmicrzi' /. z prawej strony
W szczególności bitsix, 0, 1) oraz i>its(x, b - 1,1) oznaczają, 'odpowiednio pierwszy bit z prawej strony i pierwszy bit z lewej strony. Najprostszą metodą sortowania pozycyjnego jest metoda liczników częstości. Załóżmy, że ni = 2*. Dla każdego j = 0, 1, .... ni — I liczymy, ile razy j pojawia się w ciągu wejściowym a,, a.„ ..., Na podstawie obliczonych liczników częstości umieszczamy każdy element a, we właściwym miejscu w ciągu wynikowym.
2.5. S ortow an ie p ozycyjne
67
procedure c o u n t s o r t ; I . ..n], tfl . ..n|, coun t[0. .m - 1]} var i, j , p : i n t e g e r ; begin for j := 0 to m - 1 do count[j] := 0; {inicjowanie} for i := 1 to n do c o u n t [ a [ i ] ] := count[a[i]] + 1; [ c o u i i t [ j ] to liczba wystąpień liczby j } for j := 1 to m - X do c o u n t [ j ] := c o u n t [ j - 1] + c o u n f|jj; [ c o u n t l j ] to liczba wystąpień elementów < j ] for i := n downto 1 do begin p := a[i|; t|count|p]| :=p; coun t[p] := count[p] - 1 end; for i := 1 to n do aji] := fcfij and co un t s or I:;
□ P rzykład:
Dla ciągu q - [1, 3, 1,2, 2, 1,3] po drugiej instrukcji iteracyjnej count[ 1] = 3, count\2\ = = 2, count\3\=2, a po trzeciej instrukcji iteracyjnej: counl\ I] = 3, count\2] = 5, (■ni//ii[3\ ----- 7. W ciągu wynikowym pierwszy element zajmie pozycje ą[l..count[l}]j dru gi - pozycje a\ęount\X\ + 1..counl[2]], a trzeci - pozycje a[co««/[2] + 1..co«n/[3]]. Ele menty ciągu q będą wpisywane do tablicy t w kolejności pokazanej na rysunku 2.5.
Rys. 2.f>. Wpisywanie elementów w odpowiednie miejsca ciągu wynikowego
68
2. S ortow an ie
Analiza-algorytmu countsort jest bezpośrednia: H/(/i, ni) —A(n, ni) = 0(n -l- ni) A(n, m) = 8(n, m) -■ 0 ni) - n -i- in -l- 0(1) Do zalet algorytmu countsort należy przede wszystkim szybkość działania, gdy m = 0(n). Wada natomiast jest dodatkowe obciążenie pamięci. Zauważmy, że w zapisa nej postaci algorytm len jest stabilny (dla stabilności istotna jest kolejność wpisywania elementów ciągu od a[n] do a |11). Algorytm countsort można stosować tylko dla niedużych wartości iii (tak, żeby można było użyć tablicy count o rozmiarze ni). Dla większych wartości ni można stosować podobną metodę, ale z dzieleniem liczb do posortowania na części, na których algorytm countsort może działać. Podzielmy słowa o b bitach na hic grup e bitowych (rys. 2.6). |//-1
I
Rys. 2.6. Podział słowa na
i 2e-1 b lc
e\e-\
Ol
grup ('-bitowych
Najpierw sortujemy ciąg liczb, stosując algorytm countsort względem ostatniej (najmniej znaczącej) grupy bitów, po czym sortujemy ciąg liczb względem przedostatniej grupy bitów. Powtarzamy tę operację, aż dojdziemy do pierwszej (najbardziej znaczącej) grupy bitów. Istotna jest przy tym stabilność algorytmu countsort: o ostatecznej pozycji ele mentu decyduje pierwszych e. bitów; jeśli są one takie same, to decydujące znaczenie mają dopiero następne grupy bitów. procedure radixsort; {/ii= 2“, a, l:|;l. .n|, counŁ[0 . .jii— 1]} var i, j, pass, nofpasses, key : integer; begin nofpasses b div e; if no £passes * e = b then no fpasses := nofpasses - 1 ; for pass := 0 to nofpasses do begin for j := 0 to w - 1 do coun t|j'J := 0; for i :=• 1 to n do begin key := bi ts (a| i ], pass * e, e) ; count[key\ := countficeyj -I- 1 end; for j ;= 1 to m - 1 do counfcl jj := count[j - 1| + count] j\; for i := n downto 1 do begin key bits (a[i j, pass * e, e) ; C[count[)cey]] := a[ij;
2.5. S ortow an ie p ozycyjne
couut\key\
69
coun tj key] —1
end; for i := 1 to n do a|i| := t[i | end end 'radixsox:!:;
W tym wypadku analiza złożoności obejmuje więcej parametrów: ( l> ] l> W(n, ni) = A(n, ni) - 0\ — ( i i + ni) = 0(n -I- w), gdy — = 0(1) l c J e A(//, ni) —8(n, ni) — 0 S(n, ni) = n -I- nt -I- ()(I )
□
F
kzyklad:
Niecli b = 8, e = 2, // = 4 oraz nflj =--01 1 10010 £/[2| = I 101 1 101 c/[3j = lOOOOOOO «[4| = 01 101001 Działanie algorylmu radixsorl przedstawiamy na rysunku 2.7.
/; = 8, <=2,n =4 41] = 01 11 ('121 = 11 01 <([31 = 10 00 "|4J = 01 10 10 01 01 i1
00 11 00 10
1 1 1
00 11 1 10 1 01
1
i 1 01 01 10 11
10 11 00 01
10 01 00 01
1 I
00 1 00 I 10 1 11
00 10 01 01
1
10 00 00 11
1 1
01 10 00 01
Rys. 2.7. D/.ialanic alyorylinu radixsorl
--------- p.
10 11 01 01
1 pass =2
I I
1
00 01 10 11
00 11 10 00
1 1 1
10 11 01 01
00 01 10 11
00 11 10 00
1
00 01 01 10
00 01 01 10
,7 0
2. Sortowanie
Gdy biły liczb są losowe, można znacznie przyspieszyć działanie algorytmu radixsort. Wystarczy (a) użyć radixsort dla najbardziej znaczących b/2 bitów; (b) wykonać inserlionsort. Zauważmy, że prawdopodobieństwo, iż dwie liczby będą miały takie same połówki lutów, wynosi 2~hn. Zatem algorytm inserlionsort jest stosowany do ciągu prawie upo rządkowanego, czyli działa szybko. Istnieje, leż wersja rekurencyjna algorytmu radixsort, w której „rozrzuca się” liczby względem c najbardziej znaczących bilów i stosuje się rekurcncyjnie algorytm radixsort (ewentualnie, znowu inserlionsort) do każdej podlisty. Algorytm radixsort-nie jest algorytmem sortującym przez porównania; wymaga zapisu elementów w układzie pozycyjnym. Do jego wad trzeba też zaliczyć: >» spory współczynnik proporcjonalności, który sprawia, że algorytm jest wolny dla ma łych rozmiarów //; w spore obciążenie pamięci, szczególnie wtedy, kiedy n jest duże. Główną zaletą algorytmu radixsort jest liniowość jego złożoności czasowej (przy od powiednich założeniach dotyczących związku n z długością słowa maszyny).. Dla „śred nich” wartości ii może on być lepszy niż algorytm tpiicksort. (Po odpowiednich modyfi kacjach) algorytm radixsort może być użyty do sortowania (a) słów w porządku alfabetycznym, (b) liczb rzeczywistych z pewnego przedziału.
2.6. K o lejk i priorytetowe i algorytm h eap sort Przez dynamiczny problem sortowania rozumiemy takie oto zadanie algorytmicz ne: podać strukturę danych dla elementów dynamicznego skończonego multizbioru S l>, względem którego są wykonywane następujące operacje: (a) constni(i(ij, S):: utworzenie multizbioru S = (a,, ...,
g.(i. K olejki p rio ry te to w e i a lg o ry tm h c a p s o r t
71
(.'/asami wymaga się wykonywania równic/, następujących operacji: wyznaczenie największego elementu w S (bez usuwania go); (ci' jh u ! n iiix ( S ) : : (ej ;<•/'/ .viwii.vi.v. .V):: usunięcie z S największego elementu w S i wstawienie na jego miejsce x; (!) deleleix, S):: ■V:= S - {.v); zastąpienie w .S' elementu x elementem y; (g) changed, .V, S):: S :=S{ u S, przy założeniu, że ój i J, są zbiorami rozłącznymi. (h) tmion(S,, S}, S):: Pięć ostatnich operacji jest definiowalnych za pomocą trzech pierwszych. Czasami jed nak lepiej jest implementować je osobno, żeby zmniejszyć ich złożoność. Strukturę. danych będącą rozwiązaniem dynamicznego problemu sortowania nazywamy kolejką priorytetową. Do elementarnych implementacji kolejki priorytetowej należą: » lisia nieuporządkowana, w której const mci ma złożoność 0(n), insert - 0(1), deletem a x - 0(n)\ jest ona godna polecenia wtedy, kiedy w ciągu wejściowym operacji jest dużo operacji insert, a mało deletemnw • lista uporządkowana, w której construct ma złożoność 0(//log/t). insert - 0(n), a deleIcmax - 0 ( I); jest ona godna polecenia wtedy, kiedy w ciągu wejściowym operacji jest dużo operacji deletenmx, a mało insert (na przykład 0(log/z)). Podstawową zaletą elementarnych implementacji kolejki priorytetowej jest ich prostota i niski współczynnik proporcjonalności w funkcjach złożoności. W tym podrozdziale zajmiemy się strukturą danych, tzw. kopceni, dla której operacja construct ma złożoność czasową O(n), a insert i deletenmx - O(logn). Algorytm heapsort, czyli algorytm sortowania przez kopcowanie, daje się zapisać za pomocą operacji kolejki priorytetowej. Oto ogólny schemat algorytmu- sortowania za pomocą kolejki priorytetowej: dana jest lisia ą = [a,, .... «„]; wykonaj: (a) constntct(q, S)\ (b) powtórz n - I razy deletenmx{S). ‘l Omawiany wcześniej algorytm select ionsort umożliwia sortowanie zgodnie z tym sche matem przy użyciu jako struktury danych listy nieuporządkowanej. Stosując implementację kopcową, otrzymamy algorytm sortowania heapsort o pesymis tycznej złożoności czttsowej O(uiogn). Przystąpimy teraz do zdefiniowania kopca. Przez k'»;:rec rozumiemy drzewo binarne, w węzłach którego znajdują się elementy reprezentowanego miilti/.bioru S i jest spełniony tzw. warunek kopca, a mianowicie:
72
2. S ortow an ie
15 10 5
8
6
Kys. 2.8. Przykład kopca
jeśli węzeł x jest następnikiem węzła y, to element w węźle x jest nie większy niż element w węźle y (rys. 2.8). Mówimy, że elementy są wpisane do węzłów kopca zgodnie z p o rzą d k iem k op co w y m , a drzewo ma u p o rzą d k o w a n ie k op cow e. Zauważmy, że: (a) w korzeniu drzewa znajduje się największy element (bądź jeden z największych elementów, jeśli jest ich kilka); (b) na ścieżkach w drzewie, od korzenia do liścia, elementy są uporządkowane w po rządku nierosnącym. Poziom 0
15 /
Poziom 1
!()' /
\
9 Poziom 3
2° węzłów
\
/ ii?
X6 \
3
4
I
/ / ^
5
21 węzłów
\ \ 2
2“ węzłów
/ //-(23- ł) węzłów
kys. 2.9. Kopiec zupełny
Przez k op iec zu p ełn y rozumiemy kopiec będący zupełnym drzewem binarnymi, tzn. takim, w którym wszystkie poziomy są wypełnione całkowicie, z wyjątkiem co najwyżej ostatniego - spójnie wypełnionego od strony lewej (rys. 2.9). Zależność między wysokością li a liczbą węzłów w kopcu zupełnym jest następująca: 2" - 1 < n < 2* " 1 - 1 czyli 2h < n < 2h"■1 Stąd h
= Lloguj
2.(i. K olejki p rio ry te to w o i a lg o ry tm h e a p s o r t
73
Kopiec zupełny ma regularny kształt, dzięki czemu może hyc reprezeniowany w tablicy bez dodatkowych dowiązań. Rozważmy numerację węzłów kopca zupełnego poziomami (od strony lewej do prawej). Z definicji kopca zupełnego wynika, że jest ona spójna. Obliczmy numer poprzednika i następnika węzła o numerze k. W tym celu rozważmy pod kopiec zupełny T \ kończący się na drugim następniku węzła o numerze k - I i taki, że węzeł o numerze /c jest liściem. Załóżmy, że lewy następnik węzła k (powiedzmy o numerze ,/j jest określony (rys. 2.10).
Rys. 2.10. Obliczanie numeru następnika węzła w kopcu
Zauważmy, że k — I to liczba węzłów wewnętrznych w T', a j - I to liczba wszystkich węzłów w V . Można łatwo udowodnić przez indukcję, że w drzewie binarnym, w którym każdy węzeł wewnętrzny ma dwa następniki, liczba wszystkich węzłów w T' jest równa 2 • liczba węzłów wewnętrznych w T' + I. Stąd ./ - I -- 2(k - I) -i- I, czyli / = 2k.
W
1 :
ł a s n o ś ć
Następniki węzła k (o ile istnieją) mają odpowiednio numery 2k i 2k + I. Ponieważ W
ł a sn o ść
L2/c/2 J = L (2k
+
I)/2_J =
k,
mamy
następną własność.
2:
Poprzednik węzła k (różnego oil korzenia) ma numer
L/i/lJ.
1/./.’ !
/k k c/’2k
'
2/
)I
R,vs. 2.11. Numery sąsiadów węzła w kupcu
Operacja inscn(.i, .V), czyli wstawienie elementu x do kopca zupełnego S, polega na umieszczeniu ,v w pierwszym wolnym miejscu ostatniego poziomu (lub następnego poziomu, gdy ostatni poziom jest całkowicie wypełniony) i przy wróceniu.zachodzenia warunku kopca, jeśli wstawiony element jest: większy niż element znajdujący się w po przedniku. Aby przywrócić zachodzenie warunku kopca, idziemy w górę. w stronę
74
2. S ortow an ie
korzenia, szokując miejscu, gdzie pasowałby wsławiany element. Wstawmy na przykład element I I do kopca zupełnego z rysunku 2.9 (rys. 2.12).
Rys. 2.12. Wsławiana' ciernemu ilo kopca
Rys. 2.J3. lak wyghsla kopiec po wsławieniu cie lnem u
11
Otrzymamy kopiec zupełny, przedstawiony na rysunku 2.13. Operacja insert składa się ze wstawienia elementu do liścia i dokonywanych później na ścieżce do korzenia zamian w celu przywrócenia zachodzenia warunku kopca. Użyjemy teraz procedury pomocniczej upheap (nad korzeniem ustawmy wartownika, przypisując mu wartość -i-oo). procedure upheap (k: : i n t e g e r ' ) ; va.r j.,v : integer; begin v : = a [A:]; a[0] := -K-«;
7. : k div 2 ; [warunek kopca jest zaburzony co najwyżej tylko dla v] while a [I] < v eto begin [węzeł 1 jest poprzednikiem węzła k} a|>| :“ a [71 ; k : - 1; 1 : = 1 div 2 end; a|7vj := v
end u p h e a p ; procedure i n s e r t (V : i n t e g e r ) ; begin
n : - n + 1; := v ; i i p h e a p (n ) rad /n . u e r c ;
:’osiewa/. wysokość kopca zupełnego wynosi h —I..Iog//J, pesymistyczna złożoność cza•..
75
2.6. K o le jk i p r i o r y te t o w e i a lg o r y tm h e a p s o r t
W i,„rr, 0 0 =
Llog IIJ
+
1
gdzie n oznacza liczbę elementów- w kopcu po wstawieniu y (liczone jest leż porównanie z wartownikiem). Aby wykonać operację deleiemcix{S), czyli usunąć z kopca największy element, wystar czy usunąć ten element z korzenia, wziąć prawy skrajny liść z ostatniego poziomu, usunąć go z drzewa, a element znajdujący się w tym liściu wstawić do korzenia. Ponie waż po tym wstawieniu warunek kopca może być zaburzony w jednym węźle, tym razem w korzeniu, należy tak opuścić w dól element wstawiony do korzenia, żeby przywrócić zachodzenie warunku kopca. Wykonajmy na przykład delctemart.il) dla drzewa z. rysun ku 2.13 (rys. 2.14).
2
Rys. 2.14. Usuwunic największego elementu w kopcu
Rys. 2.15. Wynik operacji ilcletemnx
Otrzymujemy kopiec zupełny, przedstawiony na rysunku 2.15. Użyjemy teraz procedury pomocniczej downheap. p ro c e d u re
downheap(k :
in te g e r );
la b e l 0; var i , j ,
v : integer;
begin v ;= while k < n div
2 do
begin ;/ := 2 * k ; \ j jest: następnikiem k \ i f j < n then i f a[j| < a [ j + 1| then j i f vet ei[j] then goto 0;
a[fc].:= a[ j j ; k
:== j
end ; 0: a[k \ := v end d o w n h e a p ;
j + 1;
76
2. S ortow an ie
Zauważmy, że na każdym poziomie drzewa (z wyjątkiem zerowego) w heap mogą być wykonane dwa porównania.
. n imię . nvn
function d e l e t e m a x : i n t e g e r : ; begin d e l e t e m a x := a[l]; a[l] :=o|n|;
n :—n - 1 ; d o w n h e a p (1)
end d e l e t e m a x ;
Pesymistyczna złożoność czasowa operacji deletemax, mierzona liczbą porównań, wynosi K
gdzie
= 2Llog/;J ii
oznacza liczbę elementów w kopcu po usunięciu największego ciernemu.
Pozostała nam jeszcze do omówienia operacja construct. Gdybyśmy realizowali ją za pomocą n operacji insert, jej złożoność czasowa byłaby ćż(//log«). Możemy jednak wy konać ją w czasie liniowym. Wystarczy konstrukcję kopca rozpocząć od dołu drzewa, tworzyć male pod kopce i łączyć je w większe - aż do powstania całego kopca. Załóżmy, że pod kopce o korzeniach 2 i i 2 i + I zostały już skonstruowane. Aby połączyć je i wsta wić kolejny element x w węźle i, wystarczy wywołać downheap(i) (warunek kopca jest zaburzony tylko dla węzła i). Na rysunku 2.16 widać wynik tej operacji.
R y s .
2.16. Wfijc/nnic węzła
i
do kopca
Oto procedura construct. procedure construct; {elementy listy
Zauważmy, że procedura downheap(i) wywołana dla węzła i z poziomu / wykonuje co najwyżej 2(// - /) porównań oraz że na poziomic l jest 2' węzłów (0 ź / < li). Mamy zatem
2.t). Kolejki priorytetowo i algoiyt.ni hcapsort w , , 00 -
77
>; 2' 2,(li - /) == X 2" “ '2/ = 2" -' 1 £ T 11 = 2** 1X X 2 ' = / = o
i= i
/ * i
i
i j
i
=2*' 1X X2-' <2*" 1X2-'■X 2"*<2"11X2~-/2=21'"2=/)(/,) / I/ ;
./ I
v n
i
I
Otrzymujemy wreszcie sam algorytm sortowania przez kopcowanic. procedure h e a p s o r l :; {afl. .oj - lista do posortowania) var m, i : i n t e g e r ; begin
m :- n ; constru ct;
for i :- .iidownto 2 do .j i| := d e l e t e m a x ; n :
in
end h e n p s o r t ;
Przeanalizowaliśmy już liczbę porównań wykonywanych w procedurze construct. Po zostaje nam jeszcze analiza liczby porównań wykonywanych w n - i wywołaniach pro cedury deletemax. Dla węzła /, usuwanego z poziomu /, w procedurze deletemax jest wykonywanych co najwyżej 21 porównań. Mamy więc razem co najwyżej /. - 1 (// - 21' -I- 1) 2/i + X (2/) 2' i :=I porównali. Obliczmy najpierw sumę: /i-l
/i - i /
X
/2' = X X 2 ' =
/ :(
/ : Iy- I
= X
1 i
2 “ -
X
/i - I /i - I X
X
Ii-l 2' = X
/ = I / =/
2-' = (/i - 1)2* -
/cl 2 h
/i- 1 ■j 2'
X
.V O
h- 1 2-' = X 2#(2*" 1~ J 11 - l) =
j.I
-i- 2
/ 1
Pesymistyczna złożoność czasowa algorytmu heapsort daje się zatem oszacować w na stępujący sposób: H'iui =--- 0< u : i- (n - 2" -I- 1) 2/i + 2 (// - 1)2'' - 2h 1 1 -i- 4 = = 2h(ti - 2" -i- 1 + 2A) + (Hu) = 2/ih.gii + 0(h)
Nie jest znana pełna analiza oczekiwanej złożoności czasowej tego algorytmu.. Wyniki przeprowadzonych badań wskazują, że współczynnik przy //logii jest bliski 2. Tłumaczy
Sortowanie
78
lo przewagę algorytmu quicksort nad heapsort. Zaletą (ego ostatniego jest to, że działa w miejscu, l/.n. ,S(n) = Oi I ) Używając kopca reprezentowanego w tablicy, każda operację kolejki priorytetowej, z wyjąt kiem imion, można wykonać w czasie O(logn), Opracowano wiele konkretnych struktur danych, które umożliwiają wykonanie również operacji imion w czasie ćż(logn). Będzie o nich mowa w dalszej części książki. 9 7
Drzewa tu r n ie jo w e i z a d a n ia s e le k c ji W operacji deleteniax na kopcu zupełnym jest wykonywana operacja downlwapO), wymagająca dwóch porównań na każdym poziomie kopca. Gdybyśmy zamiast przejścia od korzenia do liścia mieli przejście od liścia do korzenia (jak w insert), udałoby się nam zmniej szyć liczbę porównań dwukrotnie, ale kosztem skomplikowania struktury danych. Otrzymali byśmy strukturę danych zwaną drzewem turniejowym, gdyż można ją zinterpretować jako diagram rozgrywek pucharowych. Niech ą = [cip (/•,, ..., a j będzie listą różnych elementów. Przez drzewo turniejowe dla listy tj rozumiemy takie drzewo binarne, że o wszystkie poziomy w drzewie są. wypełnione całkowicie, z wyjątkiem co najwyżej ostatniego poziomu; •j każdy węzeł wewnętrzny ma dwa następniki; -• elementy a,, a;!, ..., un są zapisane w liściach, przy czym z każdym liściem ,v jest związany atrybut val(x), czyli element zapisany w liściu ,v; o na każdym poziomie węzły wewnętrzne oraz liście są połączone w listy cykliczne zgodnie z porządkiem od lewej strony do prawej; * uwzględniając uporządkowanie poziomami (jak w heapsort), węzeł o numerze i jest reprezentowany przez indeks i (rys. 2.17), przy.czym indeksuje się także puste miejsca w ostatnim poziomie; u dla każdego węzła wewnętrznego o numerze i są dodatkowo określone dwa ■atrybuty: mx(i) numer liścia zawierającego największy element w poddrzewie węzła /, t/.n. element max(w://(m..v(2/)), val(mx(2i + 1))) oraz imi(i) - numer liścia zawierające go element min(Va/(/n.;v(2/)), val(inx(2i -I- I))).
■' I..//2.J .; / >• 2/-I- I
Ity.s. 2.17. Numwncjn wę./.iów drzewa mniiejowcgo
2.7. D rzew u turn iejow e i zadaniu selekcji
79
□ P rzykład : Węzły wewnętrzne widoczne na rysunku 2.18 zapisujemy jako /': val{wx(i)), val(mn{i))% a liście i: \>al(i). /
g;3
9:7
1:9,8 \ \ ■ \
12:9
13:2
H.VS. 2.IK. Drzewo turniejowe
_______ BS Zauważmy, że jeśli /», gdzie wysokością, to
n
> I, jest liczbą liści w drzewie turniejowym, a
h
jego
21' - 1 < n < 2* czyli h - I log n I Operacje inscrl(x, S) wykonujemy w następujący sposób: tworzymy dwa nowe liście na ostatnim poziomie (jeśli ostatni poziom jest: wypełniony całkowicie, to otwieramy nowy poziom) i do jednego z nich wstawiamy ,v; wybieramy liść, powiedzmy /, z poziomu sąsiedniego, zamieniamy go na węzeł wewnętrzny, a wartość \>al(i) wstawiamy do dru giego nowego liścia; czynimy oba nowe liście następnikami /; aktualizujemy wartości na ścieżce w drzewie, idąc w stronę korzenia. Po wykonaniu na przykład iuscrl(5, S) wzglę dem drzewa z rysunku 2.18 otrzymujemy najpierw drzewo turniejowe przedstawione na rysunku 2.19, a potem, po aktualizacji wartości na ścieżce do korzenia, drzewo tur niejowe przedstawione na rysunku 2.20. 1:9,8
3:9,4
2:8.7
4:7,3
/ \ 8:3
\ 9:7
12:9
13:2 14:4
15:5
Rys. 2.19. Wstawianie cienieniu do drzewa turniejowego
80
2. S ortow an ie
:9,8
2:8,7 /
\
4:7,3
/ \ 8:3
58
/ \ 13:2 14:4/ 12:9.
9:7
Rys. 2.20. Drzewo turniejowe po wsławieniu elementu
15:5
Zauważmy, że. Wh
- Tlog/il
gdzie ;; jest liczbą liści po wstawieniu .v. Operację deleteniax{S) wykonujemy w następujący sposób: usuwamy liść zawierający największy element (»u(l) jest numerem największego elementu); aktualizujemy wartości na ścieżce od mx{ I) do korzenia, przy czym jeśli w/.v(l) nie należy do ostatniego poziomu drzewa, to najpierw w miejsce m.v( I) wstawiamy dowolny element z ostatniego poziomu, który przy pierwszym porównaniu okazał się mniejszy od swojego sąsiada. Ważne jest, by poprawianie zaczynać od dolnych poziomów drzewa. Na każdym ■/. h - 1 poziomów wykonujemy tylko jedno porównanie! Przeprowadzimy deletemax na drzewie turniejowym z rysunku 2.20. Najpierw otrzymamy drzewo przedstawione na rysunku 2.21, a następnie, po aktualizacji wartości na ścieżce, drzewo turniejowe przedstawione na rysunku 2.22.
Rys. 2.21. Usuwanie największego elementu w drzewie turniejowym
1:8,5
2:8,7
/
6:2 8:3
9:7
7:5,4 \
15:5
Rys. 2.22. Drzewo turniejowe po usunięciu największego elementu
2.7. D rzew a turn iejow e i zad an ia selek cji
81
.1:8,5
2:8,7
4:7
3:5,2
0:2
5:3
7:5,4-
14:4
15:5
Rys. 2.23. Powiórne usunięcie nujwięks/osjo cienieniu w drzewie tumicjowym
.15:5
Rys. 2.24. Drzewo lurnicjowc po powlórnyni usu nięciu największego ciernemu
1:7,5
3:5,'
2:7,3
4:7
7:5,4
5:3
// 14:4
\
Wykonajmy deleienui.x(S) jeszcze raz. Najpierw otrzymamy (przesuwając element 3 w miejsce usuwanego elementu 8) drzewo przedstawione na rysunku 2.23, a następnie, po poprawieniu ścieżki, drzewo przedstawione na rysunku 2.24. Zauważmy, że w ,m,
(") = I log" I “ I
gdzie n jest liczbą liści przed wykonaniem deletemax (współczynnik przy logn wynosi I a nie 2, jak to było w heapsorl!). Skonstruowanie algorytmu dla operacji construct tak, żeby WM l J n ) = n - 1 jak również zapisanie dokładnie algorytmów dla każdej operacji kolejki priorytetowej pozostawiamy Tobie, Drogi Czytelniku. (Jakie znaczenie mają listy cykliczne węzłów wewnętrznych i liści na każdym poziomie?) Algorytm tournamcnlsort. czyli algorytm sortowania turniejowego, ma ten sam schemat co heapsort: po wykonaniu const ruct(q, S) trzeba n — 1 razy powtórzyć operację deletemax(S). Jedyna różnica polega na użyciu do implementacji kolejki priorytetowej drzewa turniejowego zamiast kopca. Korzystając z oszacowali dla construct i delelenutx. osza cujmy pesymistyczną złożoność czasową algorytmu lournamentsort:
2. S ortow an ie
82
II
/I
vi-'(//) -- u - i + X ( T l o g /I - i) = Z I lo g /]
. .!
i 2
Zauważmy, że różnica z dolnym ograniczeniem daje sic lak oto oszacować z góry -i, 'g Z f log / I - X logi 5 n ~~ I :. i i >. Algorytm lournamentsorl: jest prawie optymalny, jeśli chodzi o liczbę porównań. Duża jednak liczba innych działań (chociaż oczywiście proporcjonalna do //log//) i spore wy magania dotyczące pamięci czynią len algorytm nieprzydatnym w praktyce. Problemami podobnymi do problemu sodowaniu są zadania selekcji. Dla listy ą •••• In,, u ....... a j i parametru k(l < k < n) wyróżniamy trzy najważniejsze zadania selekcji: » wyznaczenie k największych elementów (uporządkowanych); ® wyznaczenie k największych elementów (bez warunku uporządkowania); ■a wyznaczenie k-tego największego cienieniu. (Analogiczne zadania dotyczą najmniejszych elementów). Do realizacji pierwszych dwóch zadali możemy użyć dowolnej implementacji kolejki priorytetowej. Aby znaleźć k największych'uporządkowanych elementów na liście q, wykonujemy naj pierw conxtruct(q, ,V), następnie k - I razy dalciemax(S), a na zakończenie wywołujemy findiinix(S) (nie trzeba już ostatniego elementu usuwać z kolejki priorytetowej). Przyj mując implementację kolejki priorytetowej za pomocą drzewa turniejowego, otrzymuje my następującą pesymistyczną złożoność czasową (mierzoną liczbą porównań): /! W(ii)
-- (// - I) +
II (I
X i n- ki i
log / I - I) = (// --
k)
H-
X
I log / I
i - II - k l 2
Jako ćwiczenie pozostawiamy Ci udowodnienie, że dla k - 2 można otrzymać algorytm optymalny względem liczby porównań oraz że dolne ograniczenie na liczbę porównań wynosi (zad. 2.24 i 2.25) /i ( ii
-
k)
+ [
1
log i
}
i II - k ł 2
Aby znaleźć k największych elementów na liście q, niekoniecznie uporządkowanych, wykonujemy najpierw coiixmicl(
2.8. S zyb k ie algorytm y w y zn aczan ia k-tcgo n ajw ięk szego elem en tu w cią g u 83
kolejki priorytetowej za pomocą drzewa turniejowego, otrzymujemy następującą pesymi styczną złożoność czasową (mierzoną liczbą porównań): W(n) = (n - k) -I- (lc -1 )1 log(/t - I; i 1)1 (Operacja replacanax jest logicznie połączeniem delelemax i insert; przy implementacji za pomocą drzewa turniejowego można ją zapisać za pomocą jednego przejścia od liścia do korzenia). Do wyznaczania k największych elementów można też użyć dowolnego algorytmu wyznaczającego k-ty największy element. Po wyznaczeniu &-tego największego ele mentu wystarczy tylko jedno przejście po liście ą, żeby wypisać wszystkie k największe elementy.
.2 .8 .
S z y b k ie algorytm y w y z n a c z a n ia k -te g o n a jw ięk szeg o elem en tu w c ią g u Aby znaleźć lc-ty największy element na liście q (często &= Tn tl \ - przypadek mediany), możemy zastosować podobną metodę jak w algorytmie quicksort. Ciąg wej ściowy dzielimy na dwa podciągi względem pewnego elementu w ciągu. W zależności od liczebności podciągów /c-lego elementu szukamy albo w lewym, albo w prawym podciągu. Algorytm ten nosi nazwę algorytmu Hoare’a. function select (.7., r, k : integer) ; integer; U < k < r - 1 + 1; funkcja wyznacza k~ ty największy element: w tablicy a[I . .r); zewnętrzne wywołanie select(l, n, k) } var j :in teger; begin if 1 < r then begin j
: ~ p a r t i t i o n {1, r ) ; if k - 1 = r - j then select : = j else if k - 1 < r - j then select := select ( j -l- 1, .r, k) else select := select (1, j - 1, k - (r - j -I- 1) ) end else select := 1 end select;
Ponieważ wywołanie selcctif, r, k) pociąga za sobą co najwyżej jedno wywołanie rekurencyjne i to jako ostatnią instrukcję procedury, rekursję można w sposób standardowy zastąpić iteracją. Zapisanie algorytmu iteracyjncgo pozostawiamy Ci jako ćwiczenie.
2. S ortow an ie
82
II II W(n) = n - l + X (I log / I - 1) = X I l og/ 1 i ---2
i 2
Zauważmy, żc różnica z dolnym ograniczeniem daje się tak oto oszacować z góry n _ n X I log / I - X log / < II - 1 1= 2
i = 2
Algorytm tournamenlsorl jest prawie optymalny, jeśli chodzi o liczbę porównań. Duża jednak liczba innych działań (chociaż oczywiście proporcjonalna do /dog/;) i spore vv\ magania dotyczące pamięci czynią ten algorytm nieprzydatnym w praktyce. Problemami podobnymi do problemu sortowania są zadania selekcji. Dla listy q = [a,, a2, .... o j i parametru k( 1 < k < n ) wyróżniamy trzy najważniejsze zadania selekcji: o wyznaczenie k największych elementów (uporządkowanych); • wyznaczenie k największych elementów (bez warunku uporządkowania); • wyznaczenie k-tego największego elementu. (Analogiczne zadania dotyczą najmniejszych elementów). 1 I Do realizacji pierwszych dwóch zadań możemy użyć dowolnej implementacji kolejki priorytetowej. Aby znaleźć k największych uporządkowanych elementów na liście q, wykonujemy naj pierw constmcĄą, S), następnie k - 1 razy deletemaĄS), a na zakończenie wywołujemy findmax(S) (nie trzeba już ostatniego elementu usuwać z kolejki priorytetowej). Przyj mując implementację kolejki priorytetowej za pomocą drzewa turniejowego, otrzymuje my następującą pesymistyczną złożoność czasową (mierzoną liczbą porównań): W(n) = (n - 1) -I-
X /
/I
k i- 2
(F log /] - I) = (n - k) +
X
Tlog /I
i = II - k + 2
Jako ćwiczenie pozostawiamy Ci udowodnienie, że dla k = 2 można otrzymać algorytm optymalny względem liczby porównań oraz że dolne ograniczenie na liczbę porównali wynosi (zad. 2.24 i 2.25) (n - k) + f
n X
log / ]
1 = 11- k ■!' 2
Aby znaleźć k największych elementów na liście q, niekoniecznie uporządkowanych, wykonujemy najpierw construcl(q[I../» - k -I- 1], .S'), następnie dla i - n - k + 2 do n rel>h!ceiuax(ii:, S) (na miejsce usuwanego największego elementu wstawiamy kolejny, nie rozpatrywany jeszcze element listy q), a na zakończenie wykonujemy Jlndnmx(S). Przed stawiony algorytm nosi nazwę algorytmu Hadiana-Sobelu. Przyjmując implementację
2.8. Szybkie algorytm y w yzn aczan ia /.'-(.ego n ajw ięk szego elem en tu w ciągu liii
kolejki priorytetowej za pomoc;! drzewa turniejowego, otrzymujemy następującą pesymi styczną złożoność czasową tma-rzoną liczbą porównań): W{n) = (/; —k) + (k — I ) I log(/i - k -!- I) (Operacja replaceituix jest logicznie połączeniem delete.mux i insert', przy implementacji za pomocą drzewa turniejowego można ją zapisać za pomocą jednego przejścia od liścia do korzenia). Do wyznaczania k największych elementów można też użyć dowolnego algorytmu wyznaeziijącego k-ty największy element. Po wyznaczeniu /c-tcgo największego ele mentu wystarczy tylko jedno przejście po liście
2 .8 . Szybkie algorytmy wyznaczania fe-tego największego elementu w ciągu Aby znaleźć k Ay największy element na liście
Ponieważ wywołanie. selecUl, /'. k ) pociąga za sobą co najwyżej jedno wywołanie rekurencyjne i o jako ostatnią instrukcję procedury, rekursję można w sposób standardowy zastąpić iteracją. Zapisanie algorytmu iteracyjnego pozostawiamy Ci jako ćwiczenie.
2. S ortow an ie
84
' Równanie na pesymistyczną złożoność czasową jest nasiępujące: | W( I) ~ 0 ) Win) -- (n + I) -I- W(n - 1) ilia n > I Oto jego rozwiązanie: Win) = - ir + O(n) Pesymistyczna wrażliwość algorytmu na (Janc wejściowe wynosi A( i i ) = O(ir) (w przypadku optymistycznym wykonywanych jest tylko n + I porównań). 1
/'
/= /i—A+l
/>n-A-i-2
I - ------------- I - I ---------1--------I
n
1
A
Rys. 2.25. Podział rckiironcyjny w algorytmie Hoarc’a
Równanie rekurencyjne na oczekiwaną złożoność czasową w modelu permutacyjnym jest następujące (rys. 2.25): • f/U I) = 0 < /1(h) ---
(ii
V
I «i* -I- I) -I-.....-
/l(n
—
j)
|^
y_, A(j 11j « - k 1 2.
-I- —
11 j = I
— I)
d la
ii
>
1
Przez indukcję względem wartości n można udowodnić, że /l(n) < 4// Algorytm Hoare’a działa w miejscu, tzn. S(n) = 0(1) Algorytm Hoare’a, podobnie jak quicksort, jest szybkim algorytmem dla danych loso wych i lak jak quicksort ma pesymistyczną złożoność czasową 0 (/r). Przedstawimy teraz szkic algorytmu liniowego na wyznaczanie A-lego co do wielkości elementu w ciągu (algorytm Blurna-Idoyda-Pratta-Rivesla-Tarjana). Jego zasada dzia łania jest laka jak algorytmu Hoare’a. Różnica polega na specjalnym, wyważonym do borze elementu dzielącego tak, żeby każda z, części miała rozmiar nie większy niż 3/4 rozmiaru ciągu ulegającego podziałowi (p jest parametrem będącym stalą całkowitą, na przykład p = 50).
ii
r
2.8. S zy b k ie a lg o ry tm y w y z n a c z a n ia k-tago n a jw ię k sz e g o e le m e n tu w c ią g u 85 function s e l e c t : (k : i n t e g e r ,
g:
list)
: integer;
{ begin if n < p then {w tym wypadku nie stosujemy rekursjij begin posortuj g; , s e l e c t := k-ty od końca element na liście g end else begin podziel gna Ln/5 J 5-elementowych podciągów i (ewentualnie) jeden ciąg co najwyżej 4-elementowy; posortuj osobno każdy podciąg; niech M będzie ciągiem median tych podciągów; irl := select (I |Afj/2 I, M) ; niech g, , g 2 i g:ibędą ciągami elementów z. g,. które są odpowiednio < , = i > w; if k < (g: ,| then s e l e c t : = s e l e c t ( k , g3) else if | g,| -I- |g3| > k then s e l e c t := me ls e s e l e c t : select (k- |g2| - |g:,|,g,) end end s e l e c t ;
Otrzymujemy następujące równanie rekurencyjne na pesymistyczną złożoność czasową powyższego algorytmu. W(n) < c ,u dla r ;; I VKj/i) = U'i |/U | ! + maxfW^lg, I), W(|(/3|)) -I- ty; dla n > p j
gdzie c t i /■., są pewnymi stałymi. Zauważmy, że co najmniej (1/4)/; elementów jest niniejsza niż element dzielący lub równa mu i podobnie - co najmniej (1/4)/; elementów jest większa niż ten element lub równa mu (rys. 2.26).
Elementy, które są na pewno < elementu dzielącego
kys. 2.2/1. I'iu[/.ml
© IA © IA
o IA 9 IA o < © IA ' IA © © IA IA 0
.
0 IA © IA <
& IA & IA o
IA 0 IA ©
IA © IA 0
. ©IA o IA © IA o IA 0
..
0 IA 0
@ IA 9 IA
IA .. < 0 < © IA IA 9 0IA IA © .. ©
w/.yiędem mediany median
Elementy, które są na pewno > elementu dzielącego
S6
2. S ortow an ie
Stąd 1,/J, \th \ < ~ n , a żalem
W(n) < c2n -I-
-I-
/(j tl*a » - P
Przez indukcję dowodzi się, że 1I W(n) < 20 ai gdzie c - max(c,, c2). Dla liniowości rozwiązania równania rckurencyjnego jest istotne, że 1/5 + 3/4 < i. Po dział ciągu na 5-ciementowe podciągi jest związany z tym warunkiem. Przy zastosowa niu podziału na 3-elementowe podciągi algorytm przestaje być liniowy!
2. 9. Scalanie ciągów uporządkowanych Zadaniem pokrewnym do sortowania jest zadanie scalania, którego definicja jest następująca: mając dane dwa ciągi uporządkowane: a, < a, < ... < a„ i />, 2 b2 < ... < bm, połączyć je (scalić) w jeden ciąg uporządkowany c, < c, < ... ś c„ Zadanie scalania można rozwiązać za pomocą dowolnego algorytmu sortowania. Jest ono jednak prostsze niż zadanie sortowania. Istnieje algorytm, za pomocą którego roz wiązuje się je w czasie liniowym względem n -I- m. Wystarczy rozpatrywać elementy obu ciągów w kolejności od najmniejszego do największego, porównując bieżące elementy i przesyłając mniejszy z nich do ciągu wynikowego. Wygodnie jest użyć w tym wypadku implementacji dowiązaniowej list. Załóżmy dodatkowo, że listy kończą się specjalnym elementem +«*>. Przyjmijmy, że każdy węzeł struktury dowiązaniowej ma postać ..v:/cey(..v), ne.xt(x) function merge (a, b : l i s t ) Tar c , d : 1 i s t ; begin
: list;
d := [+o°|; C := ćl;
repeat if k e y ( a ) £ k e y ( b ) then begin n e x t (cl) := a; d := a;
a :=next(a) end else
2.:>. Scalanie ciągów uporządkowanych'
87
begin n e x t (<•?) :
i); d :
b;
/1 : = n ' I (b ) end until k e y (dl) = maxint; merge :-----next (c) and .merge;
Analiza złożoności jest oczywista: \V()i, m) = /\(/i, m) -- (,)(// + m) A(/t) = 8(/t.) = 0 S(n) - 0(//) (miejsce na dowiązania) Jeśli chodzi o złożoność czasowa, powyższy algorytm jest optymalny wtedy, kiedy m ~ 0(/i). Gdy natomiast n i m są różnych rzędów wielkości, w optymalnym algorytmie (opartym o metodę dzielenia binarnego) jest wykonywanych 0(in.(log (nhu) -I- I )) poró wnań, przy założeniu, że n > w (zttd. 2.34 i 2.35). Scalanie może być zastosowane do rozwiązania problemu sortowania metoda „dziel i zwyciężaj” . Oto ogólny schemat tego algorytmu. procedura i : i e r g e s o r t ( q : l i s t ) ; { Q = l'A ................t'„D ] begin if n > 1 then begin
podziel. q na dwie c z ęści g, i g;,o rozmiarach odpowiednio L-n/2.,1 i In / 2 I; posortuj rekurencyjnie g, i gz; scal g, i q:,, używając algorytmu liniowego scalania end end m e r g e s o r t ;
Otrzymujemy następujące równanie rcknrencyjne na pesymistycziiii (a także oczekiwa na) złożoność czasową: j W{ 1) = 0 | W(n) = VV(L/i/2j) -i- W(\'n/2"\) •!• //
dla n > I
Aby w rożwiązaniu tego równania uzyskać dokładny współczynnik mullyplikatywny przy /dog/ł, rozważmy równanie na liczbę poziomów rekursji:
(/(!) =() 1 /(/■/) = /(f/t/2'|) + 1
j dla // > i
2. S o rt o w a n i e
88
Dla n - 2L otrzymujemy i.(2k) - k. Liczba poziomów rekursji dla dowolnego n to /(?/) < /(2lln|!,|l) = | log// | < log// + I. Na każdym poziomie rekursji jest wykonywanych n operacji porównywania (z wyjątkiem ostatniego, na którym nie wykonuje się porów nali, i ewentualnie przedostatniego, na którym wykonuje się co najwyżej n porównali). Oirrymujemy zatem H'(/i) = AOi) —//log// + (Mn) A(//) —8(/t) = 0 S(n) - O( n)
Jeśli chodzi o liczbę porównań, jest to więc algorytm bliski optymalnego. Konieczna jest jednak dodatkowa pamięć 0(n). Algorytm ten wymaga leż sporo innych operacji poza porównaniami. Wyniki badań testowych stawiaj;) go między algorytmami quicksort i heapsort. Reknrsję możemy wyeliminować, zastępując ją kolejką podciągów pozostających do posortowania. Zauważmy, że drzewo wywołań rekurencyjnych algorytmu niergesort ma regularną postać (rys. 2.27). • I l :«l
A |'4:4|b 13:3)
ć |5:5) >’ ló:6.|
Kys. 2.27. Drzewu rekursji algorytmu mergesort
Algorytm składa się z dwóch kroków i jest w nim używana struktura danych kolejki list oraz liniowy algorytm scalania list uporządkowanych: (a) przejście ciągu wejściowego // z utworzeniem kolejki Q niepustych uporządkowa nych list elementów ciągu; (b) while |(?| > 2 do begin {Q jest: kolejką list: uporządkowanych] g ;-• f r o n (::((?) ; 0 pop(O) ; r := f r o n t {()) ; Q : ~ p o p ((?) ; s := m e r g e ( g , r) ; o i n j e c t (Q, s ) end
2.1,0. S o rto w a n ie z e w n ę trz n e
89
W punkcie (a) pozostał do określenia sposób tworzenia kolejki Q. M o ż l iw o ś ć l :
Q=
11'bi. K I , .... K I I
M o ż l iw o ś ć 2:
Przechodząc listę wejściową q, tak długo wstawiamy kolejne elementy na jedną listę, aż napotkamy element mniejszy od poprzedniego (rys. 2.28). 11 | 3 _5 | | 2 J7___9 | | 6
15 |
Rys. 2.28. Pod/.iał listy wejściowej na ci;|& list iipoi7.;|dkovv;myeh Jeśli j(Jj /, to potrzeba log / przejść przez kolejkę Q, żeby otrzymać jedną kolejkę. W każdym przejściu są rozpatrywane wszystkie elementy, a zatem \V(/7.) = A(n) - O(nlogl) ■ co w sytuacji, gdy ciąg wejściowy składa się z długich uporządkowanych fragmentów może wpłynąć na istotne przyspieszenie działania algorytmu mergesort.
2. 10. Sortowanie zewnętrzne Scalanie to podstawowy składnik algorytmów sortowania zewnętrznego, tzn. sor towania listy elementów q = [a,, a„ .... a j, gdy n przekracza rozmiar pamięci wewnętrz nej. Załóżmy, że elementy listy q znajdują się w pewnym pliku pamięci zewnętrznej i że z jednego pliku możemy sprowadzić do bufora pamięci wewnętrznej tylko jedną stronę składającą się - powiedzmy - z iii elementów. Przez blok będziemy rozumieć dowolną posortowaną część listy. Rozważone przez nas uprzednio algorytmy scalania dają się łatwo zmodyfikować do scalania bloków zapisa nych na różnych plikach zewnętrznych (elementy scalanych bloków są dostępne por cjami ściąganymi do pamięci wewnętrznej). W wypadku algorytmów sortowania zewnętrznego za główną operację dominującą przyjmuje się wykonanie, przesiania strony między pamięcią wewnętrzną a zewnętrzną. Algorytmy sortowania zewnętrznego składają się z dwóch głównych kroków: (a) tworzenia bloków początkowych i ich rozdzielenia do dwóch lub więcej plików; (b) scalania wielofazowego, tzn. powtarzania scalania bloków branych z różnych plików lak długo, aż pozostanie jeden blok. Rozpoczniemy ocl omówienia kroku drugiego.
90
2. S ortow an ie
2 . 10 . 1 .
S c a la n ie w ie lo fa zo w e z 4 p lik a m i
Załóżmy, że bloki zostały rozłożone na pliki P„ i l\. Pliki P, i P, są początkowo puste. /, := 0; /, := i; {numery plików wejściowych, Izn. otwartych do czytania} / := 2 ;/, := 3; (numery plików wyjściowych, Łzn. otwartych do pisania} while jest więcej niż jeden blok do (a) scal pierwszy blok na P, z pierwszym blokiem na P:< i powstający blok zapisz na P;. ; (b) scal następny blok na P( z następnym blokiem na P^ i powstający blok zapisz, na P(. ; (c) powtarzaj kroki (a) i (b) (kładąc powstające bloki na przemian na pliki P( i PjJ, aż zostanie osiągnięty koniec plików P, i Pfi; (cl) przewiń wszystkie pliki do początku; dodaj liczbę 2 mod 4 do i,, j odwracając w len sposób rolę plików wejściowych i wyjściowych. □ P rzykład:
Niech n = 16 i niech bloki początkowe mają długość 2. : U, 7] [3, 41 [10, 12] [2, 5] /(: [2, 61 |3, 5] [1. 9] [2, 8J P.,: Po pierwszym wykonaniu pętli while, otrzymujemy Ań Pr P,: [I, 2, 6, 7] [1, 9, 10, 12| Py [3, 3, 4, 5] [2, 2, 3, 8] Powtarzając dwukrotnie pętlę while, otrzymujemy najpierw P„: [I, 2, 3, 3, 4, 5, 6, 7] P,: [1, 2, 2, 5, 8, 9, 10, I2| P,: P3a potem P, P I, 2, 2, 2, 3, 3, 4, 5, 5, 6, 7, 8, 9, 10, I2| P.,
2.10. Sortowanie zewnętrzne
9.1.
Zauważmy, że w wyniku zrealizowania jednej fazy liczba bloków jesl zmniejszona dwu krotnie. Jeśli zatem na początku mamy // elementów pogrupowanych w (//////) bloków rozmiaru ///., to potrzeba log (nhn) faz, żeby otrzymać jeden blok obejmujący cały posor towany ciąg wejściowy. Złożoność drugiego kroku sortowaniu zewnętrznego wynosi zatem: //log (////;/) -I- O(n) porównań i 2 (//////) log (nhn) + O(nlni) przesłań. Powyższą metodę wielofazowego wyważonego scalania można uogólnić na przypadek, gdy ma się do dyspozycji 1k plików (k wejściowych i k wyjściowych, k > 2). Złożoność drugiej fazy sortowania zewnętrznego zmniejsza się do //log,,.(nhn) -l- (){n) porównań i 2 (nhn) log, (//////) -l- O (nhn) przesłań. Można także zmniejszyć liczbę używanych plików do 3 bez istotnego pogorszenia złożo ności czasowej.
2 . 10 . 2 .
S c a la n ie w ie lo fa zo w e z 3 p lik a m i Załóżmy, że. mamy do dyspozycji trzy pliki / ’,, / ’,, Pj i że początkowe bloki zostały utworzone na Pn i / ’, w taki sposób, że P„ zawiera a P, /■', , , bloków, gdzie /■’, jest /-tą liczbą Fibonacciego dla pewnego / > 0. j/ą, = 0, /-, = 1 lS,i = S + S . , /j := 0; /, ./ 2;
dla / > 0
I; (pliki wejściowe,} (plik wyjściowy}
while jest więcej niż jeden blok do (a) scal pierwszy blok na /( z pierwszym blokiem na /J, i powstający blok zapisz na (b) powtarzaj krok (;t), aż zostanie osiągnięty koniec P( \ (c) przewiń do początku pliki P^ i /(; dodaj do /j, i, i j liczbę I mod 3.
□ P rzykład:
/ = 6 (kj oznacza pojedynczy blok) Na początku mamy u u
P-,
u
e
u
j
u
u u
o; oj u r j u u
u
u
u
u
u
u
u
92 25 D;
W ż t
i fc-
Z* S ortow a n ie
Polem kolejno U
u u
u u
l.J
U u
1} u
u
u
u
u
U
u IV u
u u
u
I'r
IV
KJ
>v
>V
IV l\r u l\: u
u
u f; '
u
a na końcu IV
/v u Pv
_____ta . i
Można pokazać (wykorzystując własności liczb Fibonacciego; zad. 2.40), że złożoność scalania wielofazowego z 3 plikami wynosi w przybliżeniu 1,04 n log («//») + O(n) poró wna/? i 2- l ,04 (n/m) )og (u/m) + 0(/>h») przesłań (każdy element jest uwzględniony w przesłaniach około 2,08 log (nim) razy), a zatem jest tylko nieznacznie gorsza niż w wypadku scalania wielofazowego z 4 plikami. Do omówienia pozostaje jeszcze, pierwszy krok sortowania zewnętrznego, a mianowicie, tworzenie bloków początkowych. Wykorzystamy tu sortowanie wewnętrzne. IVIktoda I
Powtarzać podane działania aż do osiągnięcia końca pliku wejściowego: wziąć porcję kolejnych i i i elementów; posortować je, używając jednego z algorytmów sortowania wewnęirznego; przesiać utworzony blok do odpowiedniego pliku. Micron a U
Do tworzenia bloków użyć kolejki priorytetowej, na przykład drzewa turniejowego (z najmniejszym elementem w korzeniu) jak w algorytmie sortowania turniejowego, przy czym na miejsce usuwanego najmniejszego elementu wsławiać kolejny element ciągu.
2.10. S ortow an ie zew n ętrzn e
93
Można w ten sposób tworzyć bloki o rozmiarze większym niż. m (średnia długość two rzonego bloku wynosi faktycznie 2///). Wobec nic zmieniającej się struktury drzewa turniejowego można je całe reprezentować w tablicy o rozmiarze 2iii - 1.
□ P rzykład:
Załóżmy, że iii - 4 i że chcemy dokonać podziału na bloki dla następującego ciągu wejściowego: 3, 7, H), 1, !2, 4, 8, 6, 2, )!. Budujemy drzewo turniejowe (rys. 2.29) dla pierwszych 4 elementów ciągu.
7 H)
Rys. 2.29. Początkowe drzewo turniejowe
Usuwamy najmniejszy element 1, a na jego miejsce wstawiamy kolejny element 12. Po wykonaniu modyfikacji otrzymujemy drzewo turniejowe, przedstawione rut rysunku 2.30a. Usuwamy najmniejszy element 3, a na jego miejsce wstawiamy kolejny element 4. Po wyko naniu modyfikacji otrzymujemy drzewo turniejowe przedstawione na rysunku 2.30b. Usuwamy najmniejszy element 4, a na jego miejsce wstawiamy kolejny elements. Po wyko naniu modyfikacji otrzymujemy drzewo turniejowe przedstawione na rysunku 2.30c.
Rys. 2.30. Kolejne drzewa turniejowe
94
2. S ortow an ie
Usuwamy najmniejszy cienieni 7, a na jego miejsce wstawiamy element 6 (ponieważ ostatni element bloku bieżącego 7 > 6, a 6 należy już do następnego bloku, co zaznacza my za pomocą ’). Po wykonaniu modyfikacji otrzymujemy drzewo turniejowe przed stawione na rysunku 2.30d. Usuwamy najmniejszy element 8, a na jego miejsce wstawiamy kolejny element 2 (ponieważ ostatni element bloku bieżącego 8 > 2, a element 2 należy już do następnego bloku, co zazna czamy za pomocą ’). Po wykonaniu modyfikacji otrzymujemy drzewo turniejowe przedsta wione na rysunku 2,3'0e. Usuwamy element 10, a najego miejsce wstawiamy kolejny element 11 (ponieważ 11 > 10, a I I należy do bloku bieżącego). Po wykonaniu modyfikacji otrzymujemy drzewo turniejowe, przedstawione, rysunku 2.30f. W ten sposób zostają utworzone dwa bloki: [1, 3, 4, 7, 8, 10, I I, |2] i [2, 6|.
Pesymistyczna złożoność czasowa pierwszego kroku sortowania zewnęlrzego wynosi 0((n/m)in\ogrn) = O(nlogm) porównań i 2 (nim) -I- 0(1) przesłań. (Przy metodzie I zakładamy, że jest stosowany algorytm sortowania wewnętrznego dzia łający w czasie 0(/ilog//.)). Wyższość metody II polega na tworzeniu bloków o długości średnio dwukrotnie większej, co prowadzi do zmniejszenia o 1 liczby przejść przez pliki. Złożoność całego algorytmu sortowania zewnętrznego wynosi zatem 0(«logm) + -l- O (/ilog (iihu)') = 0(»log/i) porównań (a więc tyle samo co dla algorytmów sortowania wewnętrznego) i 0 ((n /m )(i + log (////»)) przesłań. W wypadku sortowania zewnętrznego czynnikiem decydującym o szybkości działania algorytmu jest liczba operacji zarządzania plikami w pamięci zewnętrznej. Liczbę tę można zmniejszyć, używając większej liczby plików do scalania, jak również drzew Lurniejowych do tworzenia bloków początkowych.
Zadania 2.1. Algorytm bubblcsort polega na wielokrotnym przejściu sortowanego ciągu z doko nywaniem zamian sąsiednich elementów, gdy nic są one we. właściwym porządku. Opracuj i dokonaj analizy tego algorytmu. 2.2. /.najdź przykład na to, że algorytm seleclionsort nie jest stabilny. 2.3. Zmodyfikuj algorytm seleclionsort tak, żeby by 1 stabilny. Oszacuj, ile dodatkowe go czasu i/lub pamięci wymaga taka zmiana.
95
Zadni?in
2.4. ( |BK]) Używając melody funkcji tworzących, wyprowadź dla algorytmu insertionsort wzór na oczekiwaną liczbę porównań i na odchylenie standardowe liczby porównań. 2.5. Udowodnij, że w algorylmie insertionsorl kolejno rozpatrywany elcmenl r/|'l może z jednakowym prawdopodobieństwem zająć jedną z i pozycji w uporządkowanym ciągu
I ] < / \ |/ j
... = /![/] = i' <
A \ j
-I- 11, ...,
A
[/•]
gdzie / < i < j < r. 2.14. W algorytmie heapsort w fazie poprawiania kopca element a\ I ] jest. z reguły mały. Jak można zmodyfikować fazę poprawiania kopca, aby zmniejszyć średnią liczbę porównań, uwzględniając powyższe spostrzeżenie. 2.15. |K | Algorytm shellsorl polega na wielokrotnym zastosowaniu algorytmu insertionsort w następujący sposób: ustalić ciąg przyrostów k] > k? > ... > /q - I; w /-lej fazie, gdzie I < i < /, sortować podciągi ciągu bieżącego złożone z co kr lego elementu (a więc sortować cały ciąg w ostatniej fazie). Zapisz ten algorytm.
96
2. Sortow aniu
2.16. Opracuj rekureneyjną wersje algorytmu coimtsorl (w której przetwarzanie zaczyna się od najbardziej znaczących bilów). 2.17. NiccJi .Sj, S„ .... St będą zbiorami liczb całkowitych /. przedziału od I do //, speł niającymi warunek:
i .. i
-//
Opracuj algorytm sortujący każdy zbiór .Sj w łącznym czasie. (Ąit). 2.18. Opracuj szybki algorytm sortujący 100 000 ułamków postaci p/2'1, gdzie 0 < />,(/ < 10. 2.19. Niech x,, ,v2, .... ,\n będzie ciągiem słów, z których każde nut długość k nad alfa betem ///-literowym. Opracuj algorytm sortowania, działający w czasie proporc jonalnym do sumy długości tych słów (to znaczy O(nk), traktując /n jako stałą). 2.20. Opracuj algorytm sortowania słów zmiennej długości nad alfabetem " stałej liczbie. m znaków, działający w czasie proporcjonalnym do sumy długości yen sin";. 2.21. Niecił .v,, .... xn będzie losowym ciągiem liczb rzeczywistych z przedziału [u, /•>]. Uwzględniając rozrzucanie i scalanie z algorytmu radixsort, opracuj algorytm sor towania, mający średnią złożoność czasową ()(u). 2.22. Udowodnij, że do wyznaczeni;.! największego elementu w ciągu trzeb;! użyć co najmniej n — I porównań. 2.23. Udowodnij, że do posortowania //-elementowego ciągu, który zawiera k różnych elementów rg, .... ak i każdy element: występuje ni razy, czyli k I jH j =
II
i= I
trzeba wykonać co najmniej log —..— porównali. "o "/,• J 2.2d. Udowodnij, że do wyznaczenia dwóch największych elementów w //-elemen towym ciągu (uporządkowanych) trzeba wykonać co najmniej // + I log// I - 2 porównań. 2.25. Udowodnij, że do wyznaczenia k największych elementów w //-elementowym, li ciągu (uporządkowanych) trzeba wykonać co najmniej (//--/<)■ i- }_, | l og/ | i n- A!•;> porównań.
97
Z adania
2.26. Zaprogramuj algorytmy kolejki priorytetowej dla realizacji na drzewach turnie jowych. 2.27. Ułóż algorytmy dla następujących operacji wykonywanych dla kopca: (a) deleted, ,Sj:: (b) clianiy(i, k, S)::
.S' := S - {«[/'] j; delete(a[i], S)\ «|/J := k; insert{a\iJ, S)\
2.28. Drzewem lewicowym nazywamy drzewo binarne, w którym dla każdego wierz chołka .v prawa skrajna ścieżka od x do liścia jest najkrótszą ścieżką od .v do liścia w poddr/ewie wierzchołka ,v. Opracuj implementację kolejki priorytetowej, korzy stając z drzew lewicowych z elementami wpisanymi do nich w porządku tworzą cym kopiec. Złożoność każdej z operacji: insert, deletemin i imion powinna wyno sić O(lov.n). 2.29. Podaj strukturę danych umożliwiającą wykonywanie w czasie O(logn) następują cych operacji na początkowo pustym zbiorze S: (a) insert(v, $):: S :-- .$' u {u}; (b) delelemedian(S):: usunięcie z S mediany elementów zbioru S\ przy założeniu, że elementy zbioru S pochodzą z dowolnego zbioru liniowo upo rządkowanego. 2.30. Udowodnij zachodzenie nierówności A(n) < 4/; dla algorytmu Hoare’a. 2.31. Zaprogramuj algorytm Bluma-FIoyda-Pratla-Rivesta-Tarjana. I, 2.32. Dla algorytmu z zadania 2.31 udowodnij zachodzenie nierówności W(n) < 20cn. 2.33. Czy w algorytmie, z zadania 2.31 podział na 5-elementowc podciągi jest istotny? 2.34. Ułóż algorytm scalania dwóch ciągów uporządkowanych o długościach n i ni, gdzie n > iii , za pomocą 0(in.(log(nhn) + I)) porównań. 2.35. Udowodnij, że w modelu drzew decyzyjnych scalenie dwóch ciągów uporząd kowanych o długościach n i ni wymaga wykonania i i (in (log (nhn) -I- I )) porów nań. 2.36. Opracuj stabilny algorytm sortowania ciągu zero-jedynkowego. Algorytm powi nien działać w miejscu (złożoność pamięciowa - 0(1)) i mieć pesymistyczną zło żoność czasową D(//log/i).
98
2. S ortow an ie
2.37.
Udowodnij, że w modelu drzew decyzyjnych wyznaczenie /c-(ego co do wielkości n V,<; ..k -I- I jY\ elementu w /j-eiemenlowym ciągu wymaga wykonania O log k - I j{ porownan.
2.38.
Zaprogramuj metodę tworzenia bloków początkowych za pomocą drzewa tur niejowego.
2.39. Czy jest możliwe posortowanie n elementów za pomocą tylko jednego pliku ze wnętrznego. Ile jest potrzebnych operacji wymiany bloków? 2.40. Podaj dokładną analizę sortowania wielofazowego z 3 plikami.
o najczęściej wykonywanych operacji na zbiorze elementów należą wstawianie elementu do zbioru, usuwanie elementu ze zbioru i wyszukiwanie elementów o zadanych własnościach (na przykład najmniejszego elementu lub elementu o po danej wartości klucza). Struktura danych umożliwiająca wykonywanie takich operacji nazywa się słownikiem. Ważnymi przykładami zastosowań słownika są bazy danych, tablice, identyfikatorów w kompilatorach i komputerowe słowniki języków naturalnych.
D
Przez problem słownika rozumie się następujące zadanie: podać strukturę danych umożli wiającą wykonywanie następujących operacji na zbiorze skończonym S: (a) const ruct(S):: S '■= 0 ; (b) search{v, ,Sj:: sprawdzenie, czy element u należy do ó'; jeśli tak, wyznaczenie miej sca, gdzie znajduje, się v; (c) insert(w, ó'):: S := S u (v); (d) delete(v, S):: S := S - fv}.
U w aga !
Specyfikacja operacji s
100
3. Słowniki
3. lo I iiip le m e n ta c ja lis t o w a n ie u p o r z ą d k o w a n a Najprostsza implementacja listowa polega na zapisie elementów zbioru ,V= [a|, u,, .... a j w dowolnej kolejności na liście / = [a,, a2, .... a j. Z kolei lista może być przechowywana albo w (abliey, albo w strukturze dowiązaniowej. Dla każdej opera cji (b)-(cl) otrzymujemy (zad. 3.1) W(ii) = A(ii) -- ()(n) (zakładając wyszukiwanie .sekwencyjne elementu na liście). Zaleta jest tu prostota struktury danych i niewielka ilość dodatkowej pamięci; wadą natomiast jest długi czas działania. Implementację tę stosuje, się, gdy dane są zawsze przetwarzane sekwencyjnie w dowolnej kolejności. Warto zwrócić uwagę na tę odmianę bezpośredniej implementacji listowej, w której element będący argumentem operacji search lub insert jest ustawiany na począł ku listy. Gdy rozkład częstości dostępu do elementu nie jest równomierny, ułożenie elementów na liście staje. się zgodne z rozkładem częstości; elementy będące często argumentami ope racji znajdują się, na ogól blisko początku listy i w związku z tym dostęp do nich jest szybki (zad. 3.2). Implementacja la nosi nazwę samoorganizujących się lisi.
3.2. uporządkowana Elementy na liście mogą być leż uporządkowane. Wygodnie jest wówczas używać tablicy. Elementy zbioru S zapisujemy w tablicy a\\..n\ tak, że ,/| I j < a\2] < ... < «|>/j. Wygodnie, jest rozszerzyć tablicę a o dwie pozycje skrajne: z a\k \ i w zależności od wyniku porównania albo szukać u w a[l] < ... < u\k\, .albo w a\k\ < ... < o|rj. Niech cul(a, /, r) będzie funkcją wyznaczającą indeks k (zakładamy, że / + I < /'). function s e a r c h ( v : T) : i n t e g e r ;
wax
k,
1, l- : in te g e r ;
begin jn > 1 j 1 := 0 ; r ;
n + 1;
r e p e a t { a |i| < v < a |r j a r > 1 + 1} k
;
= cut (v
,
1
, .r) ;
[X < k < r )
if v < a|/r| then r := k else I := k
3.2.
Im p le m e n ta c ja lis to w a u p o rz ą d k o w a n a
until r = 1 + 1 ; if v - a[I| then search end search ;
:=
101
1 else search :=
0
Rozważymy dwie metody wyboru funkcji cm. IVIictoiu
I
lest to tzw. wyszukiwanie binarne. cni(v, /,/•)=
L (/
-l- r)/2i
Załóżmy, że chcemy wykonać search{5, S) dla elementów a zapisanych w tabeli 3.la. Olr/.yniamy wyniki przedstawione w tabeli 3.Ib. Tabela 3.1. 0
Dane i wyniki dla algorytmu wyszukiwania 1
— 1X1
0
2
3
4
5
6
7
S
y
10
1
5
6
i
8
9
16
34
56
00
/
0
0
2
3
3
r
11
5
5
4
k
5
5 2
3
4
(b )
Do Ciebie należy (zad. 3.3) wykazanie, że dla operacji search W(n) = logn + 0(1) A(n) = logu + 0(1) Działanie wyszukiwania binarnego można zobrazować za pomocą drzewa binarnego, w wierzchołkach którego znajdują się elementy tablicy a, ułożone w kolejności porówny wania ich z szukanym elementem v. Dla n = 15 otrzymujemy drzewo przedstawione na rysunku 3.1. Drzewo to nosi nazwę drzewa obliczeń funkcji search.
Rys. 3.1. Drzewo \vyszukiwani;i binarnego
102
3. Słow n ik i
Realizacja funkcji .search polega na przejściu jednej gałęzi od korzenia do liścia, z jednoczes nym porównywaniem v z wartościami zapisanymi w wierzchołkach. Gdy u jest mniejsze, idziemy do lewego następnika danego wierzchołka w drzewie, a w przeciwnym razie - do prawego. Obliczenie funkcji .search to ciąg par (/„ t>,),.... vK), gdzie /, oznacza instrukcję, a i>j wartościowanie zmiennych. Ponieważ każde drzewo binarne o n wierzchołkach ma wysokość nie mniejszą niż Llog n J , przedstawiona metoda wyszukiwaniu binarnego jest optymalna w przypadku pesymistycz nym. Jeśli natomiast chodzi o przypadek oczekiwany, istnieją lepsze metody wyszukiwania, w których brana jest pod uwagę wartość v w stosunku do przedziału poszukiwań. M etoda O
Jest to tzw. wyszukiwanie interpolacyjne. Niech S c (c, d), gdzie c, d to liczby rzeczy wiste, c < cl, |5j = n. Załóżmy, że elementy zbioru S są dobierane z rozkładem równo miernym w przedziale (c, d). Niech v g (c, cl). Rozważmy zmienną losową X, oznaczają cą liczbę elementów zbioru S, które są < tą (tzn. jeśli v e S, to a[X\ - u) (rys. 3.2). Prawdopodobieństwo, że element zbioru S jest < a, wynosi !> -
C
a\ 11 r/[2]...................................... r/|;/| c
t'
d
Itys. 3.2. Położenie elementu w przedziale (o,
Wybór elementów zbioru S następuje zgodnie z rozkładem Bernoulliego (mając element v, losujemy kolejno a[ Ij , ..., a[n] z prawdopodobieństwem sukcesu q), a zatem ave(A') —ć/n I var(/Y) = ę/( 1 - q)n < —n Wynika stąd, że z dużym prawdopodobieństwem wartość v jest położona w okolicy elementu r/« l] w tablicy a. W wypadku wyszukiwania interpolacyjnego kładziemy 'jdjOJ = c, a[n
-I-
IJ = cl
Wartość funkcji cut obliczamy w następujący sposób (zakładamy, że I -I- I < /•): .v := / +
c ~ a\.l\ Cr ~ l ) ; <4r\ - a ll)
cui(v, I, /j := if ,v = I th en / -I- I e lse i f s = r th en r - 1 else ,v;
1.03
3.2. I m p k u u e n t a c j a lis to w a u p o r z ą d k o w a n a
Pesymistyczna złożoność czasowa i oczekiwana złożoność czasowa operacji search su następujące; W(n) = n -l- ()(I) A (ii) - log log/; -l- 0(1) Załóżmy, że chcemy wykonać searcli(5, .S') dla elementów a zapisanych w tabeli 3.2n. Otrzymamy wyniki przedstawione w tabeli 3.2b. Tnlicki 3.2. I);me i wyniki dla algorytmu wyszukiwania inturpolncyjnego 0
1
0
1
! 2
3
4
5
(»
7
8
9
1(1
5
6
7
S
9
10
12
14
(a) 0
0
3
11
4
4
4
3
d>)
Dowód wzoru na A(n) dla wyszukiwania interpolacyjnego jest skomplikowany matematy cznie i nic przytoczymy go tutaj. Rozważymy za to pewna modyfikację wyszukiwania interpolacyjnego, dla której łatwiej jest udowodnić, że A(n) = 0(log log//). Najpierw pokażemy, jak można intuicyjnie wyprowadzić algorytm, korzystając z drzewa obliczeń .funkcji search, a następnie sformułujemy algorytm, korzystając z reprezentacji tablicowej. Wróćmy do drzewa obliczeń funkcji search. Załóżmy, że. ma ono wysokość rzędu logaryt micznego. Obliczenie funkcji search polega na przejściu ścieżki od korzenia do liścia. Aby przejść tę ścieżkę w czasie podwójnie logarytmicznym, należy zastosować przejście binar ne, jak w wyszukiwaniu binarnym (dzielne za każdym razem na połowę długość odcinka, który pozostał do przejścia). Problem jednak polega na tym, że ta ścieżka nic jest dana z gó ry. Wzór interpolacyjny pozwala nam zbliżać się do szukanych wierzchołków na lej ścieżce z dodatkowym kosztem, którego wariość oczekiwana jest siała. Od korzenia będziemy schodzić na poziom środkowy drzewa, a następnie będziemy porównywać u z pewna 1icy.bii korzeni dolnych poddrzew. która okaże, się stała w przypadku oczekiwanym, po czym bę dziemy powtarzać szukanie e.lemenlu v w odpowiednim dolnym poddrzewie (rys. 3.3).
(log II . W, (log II Rys. 3.3. Podział drzewa obliczeń funkcji
se a rch
104
it. Słowniki
Zauważmy, że w poddrzewie o wysokości (1/2) log n jest około 2,,,M"i!'' = V/» wierzchoł ków. Udowodnimy następujący lemat. Ł
L e m a t 3.1. Oczekiwana liczba porównań potrzebnych do wyznaczenia właściwego dolnego pocldrzewa jest < 2,5.
Zanim przejdziemy do dowodu lematu, zauważmy, że wynika z niego następująca zależność: |/U I) = I, A (2) = 2 1A(n) < 2,5 + /l(| Vn~ 1) ,Sląd A ( i i )
<
2,5 1 log log//
I -I-
dla n > 3 2 dla
n > I.
.Dowód: Najpierw określimy nieco dokładniej (używając reprezentacji tablicowej) na szkicowany powyżej algorytm. procedure quadratic binary search (szukamy r> w oblicz
-------- —/r, a- c jeśli v > //[/ip to wyznacz najmniejsze i takie, że v < a
z?|()...// -I- 11, y e
(r, tl)\
p
p o w tó rz, r e k u r e n e y j n i e s z u k a n i e i/ w //[I
^-y p
-l- (/ -
I ) ' / / / ] . . ,|
p
-i- / V/i | |;
jeśli i' < a |- -|, lo wyznacz najmniejsze, i takie, że u > powtórz, rekurencyjne szukanie v w j = \X - p\ > (j - 2) Vh
dlii j > 3
czyli Pr (Y > j) — Pr(|A' - p\ > ( / - 2 ) V / / ) ora/. Vv(Y> I) - IV (V > 2) - |
105
8.3. D rzew a poszu k iw ań b in arn ych
Do zmiennej losowej X stosujemy nierówność Czebysz.ewa (t = (/ —2) V/i ) var(A) Pr (| X - uve(/Y)| •> /) < -----^---Dla j > 3 mamy żalem Pr ()' >./)
<
var(A')
—:- - 4 rr...< ii
- 2 ) 'n
— li 4
( j ~
2f
n
4(/ - 2 ) ’
Wracając do tego, co jest naszym celem, a mianowicie oszacowania ave(lj, otrzymujemy ave•((>0 =
=
X
I
./Pr (Y =./) = X X Pr (Y =./) = X X Pr (Y =./) = ./>!/= i i im i ■i
Pr(K> /) +
X
(i:, 4(1 - 2)
I v, I I TT • = 2 + — X - r r < 2 -l- — < 2,5 41 i I Ji4 6 cbdo
Pozostałe operacje, dotyczące słownika, a mianowicie insert i delcie w implementacji za pomocą lab) ic uporządkowanych (niezależnie od wyboru funkcji cut) są nadal pracochłonne: W(n) = A(n) = O(n)
3 .3 .
Drzewa poszukiwań binarnych Uogólnieniem wyszukiwania elementu w tablicy uporządkowanej jest wyszukiwanie elementu w tzw. drzewach poszukiwań binarnych, czyli w skrócie BST (od ang. Binary Search Trees). Przez wzbogacenie struktury danych uzyskujemy możliwość szybszego wykonywania operacji wstawiania i usuwania elementu ze zbioru. Zakładamy, że każdy węzeł ,v drzewa, będący obiektem typu nade, ma trzy atrybuty: ,r. left(.x), kcy(x), right(x) Klemenly w kopcu .są, uporządkowane zgodnie z porządkiem kopcowym (rys. 3.4n). W drzewach BST mamy natomiast do czynienia z porządkiem symetrycznym. W porząd ku tym dla każdego węzła „v jest spełniony następujący warunek: jeśli węzeł y leży w lewym poddrzewie ,v, to key(y) ś key (x); jeśli y leży w prawym poddrzewie x, to key(x) < ke.yiy) (rys, 3.4b).
106
3. S łow niki
.v \riylil(.\) (a)
(b)
Uys. 3.4. (a) I’oi/diilek kopcowy; (b) porxmlck symetryczny
..5 I
\» '\ s
% Kys. 3.5. Drzewo poszukiwań binarnych z zaznaczonymi wierzchołkami zewnętrznymi
Drzewem poszukiwań binarnych (drzewem .BST) nazywamy dowolne drzewo binarne, w którym elementy zbioru są wpisane do wierzchołków zgodnie z porządkiem symet rycznym (rys. 3.5). Wierzchołki zaznaczone na rysunku 3.5 przez H to wierzchołki zewnętrzne. W struk turze dowiązaniowej są one reprezentowane przez nil. Wierzchołkom zewnętrznym od powiadają przedziały wartości, na które zostaje podzielona przestrzeń wszystkie!) kluczy przeą jducze obecne w drzewie (czyli elementy znajdujące się. w drzewie). Wstawiając, nowy element, umieszczamy go w wierzchołku zewnętrznym reprezentującym przedział wartości, do którego należy dany element. Operacja search dla drzewa BST jest uogólnieniem funkcji search dla tablicy uporząd kowanej. function s e a r c h (v : T; r : n o d e ) : n o d e ; {T j e s t dowolnym typem liniowo uporządkowanym; r j e s t korzeniem drzewa BST} var x ; n o d e ; begin x := r ; while {x* nil)and(key(x) -r v) do if v < k e y { x ) then x := l e f t { x ) else x := r i g h t ( x ) ; s e a r c h :-- x {jeśli element v znajduje się w drzewie, to x-f- nil i k e y ( x ) =,v; jeśli elementu v nie mu w drzewie, to x = nil] end s e a r c h ;
3.3.
107
D rzew a p o sz u k iw a ń b in a rn y c h
■>"' U\va<;a Zamiast nil można użyć wierz,cholka-wartownika, powiedzmy o nazwie sentinel. Wtedy przed pytki while należy wstawić instrukcję ke.y(sentinel) t>. Warunek while uprości się do key(x) -f- v. Zapisanie algorytmów słownika z użyciem war townika pozostawiamy Ci jako ćwiczenie. Pierwsza czynnością w operacjach insert i delcie jest wykonanie operacji search. Po trzebny jest przy tym poprzednik końcowego węzła ,v. W tym celu zmodyfikujemy naszą operację search. function s e a r c h (v : T ; r : n o d e ; v a r y : n o d e )
: node;
{r j e s t dowolnym typem lin io w o uporządkowanym; r j es t korzeniem dr zewu BST; y j e s t p o p rzednikiem w ie rz c h o łk a wyszukiwanego p rz e z se a rc h ] var X ; bogiń
n o c ie ;
x := r; V : n i l ; while (y. -A n i l )and ( k e y ( x ) Z v ) do begin y
x;
if v < k e y (x ) then x := l e . l : t ( x ) ols© x := r i g h t : ( x ) ; end ; s e a r c h := x
end s e a r c h ;
Operacja insert polega na wykonaniu scan:h(v, r, y), utworzeniu nowego wierzchołka ,v, wstawieniu tam elementu v i dowiązaniu x do y. procedure i n s e r t (v ; var r : n o d e ) ; •var x , y : n o d e ; begin if s e a r c h (v, r, y ) = nil then begin n e w (x ) ; l e f t ( x ) : —n i l ; k e y ( x ) v; r i g h t ( x ) := n i l ; if v =■ nil then r := x else .if v < k e y (y) then left (y) := x else r i g h t (y) := >: end end i n s e r t ;
Operacja construct jcsi trywialna. Oto ona: procedure c o n s t r u c t (var r : n o d e );
b e g in r := nil end c o n s t r u c t ;
.108
Ii. Słowniki
Drzewo przedstawione. na rysunku 3.5 można skonstruować za pomocą następującego ciągu operacji: const rurt(S)\ insert) 10, .V); inscri(7, S); inscn(5, S): insert W, S); /«.vert(8, .V); insert)2, S)\ insert) 15, .S’); insert)\2, .S'); t/i.vm( 14, 5’); insert))8, .V); To samo drzewo można też utworzyć za pomocą innych ciągów operacji. Zauważmy, że przestrzenie probabilistyczne drzew losowych BST i drzew BST tworzonych przez loso we permutacjc są, różne. Dla n = 3 na przykład jest pięć różnych drzew BST (rys. 3.6). W modelu drzew losowych BST każde drzewo ma więc prawdopodobieństwo 1/5, nato miast w modelu permutacyjnym drzewo 71 ma prawdopodobieństwo 1/3, a pozostałe 1/6. Udowodniono, że w modelu drzew losowych BST oczekiwana wysokość /(-wierz chołkowego drzewa BST jest / H Vn ). W naszej analizie przyjmiemy model pemiulacyjny drzew BST.
o. I 2
•< n
3
Rys. 3.6. 1’iędoe.lemciUowc drzewa UST
Operacją delete, zajmiemy się za chwilę, a teraz przeanalizujemy czas działania operacji scorch (a więc także insert). Czas działania operacji search jest proporcjonalny do głębokości wierzchołka ,v (wewnętrznego lub zewnętrznego), będącego wynikiem funkcji search. Stąd : W )n )
0 )tt)
(ii oznacza liczbę wierzchołków w drzewie BST). Aby obliczyć A(n), oznaczmy przez G(n) oczekiwaną sumę. głębokości wierzchołków zewnętrznych w drzewie BST, utworzonym z drzewa pustego przez wykonanie, ciągu ii operacji insert iiiscrl)ar .V); in s e r l( a .S'); ...; inserl)an, S); gdzie, u,, ....
" ; ... i
D rzew a p o sz u k iw a ń b in a rn y c h
Element y
109
El eroen ty
I. •••> ./- I
n
Rys. 3.7. Struktura //-wierzchołkowego drzewa I5ST
Olrzynitine równanie jest takie jak dla algorytmu quicksort, .lego rozwiązanie wygląda zatem tak: (!(ji) ~ 1,4/i l o g / t + O( n )
Oczekiwana złożoność czasowa operacji insert i search w sytuacji, kiedy nie ma elemen tu w zbiorze, wynosi /!(//) = — = 1,4 log ii -i- <7(0 n+1 Aby wyznaczyć oczekiwaną złożoność czasową operacji search w przypadku, gdy szu kany element znajduje się w drzewie, wprowadźmy dwie zmienne losowe: Z„ oznaczają cą sumę głębokości wierzchołków zewnętrznych (G(/t) = ave(Z„)) i Wn oznaczającą su mę głębokości wierzchołków wewnętrznych. Przez indukcję względem liczby wierzchołków w drzewie możemy pokazać (zad. 3.7), że Z„ = W„ -l- 2/i Stąd ave(Z„) = ave(VK„) -l- 2n Oczekiwana złożoność czasowa operacji search w przypadku, gdy element: znajdzie się w drzewie, wynosi zatem M n, ■
ave(Z„) ( ii -t- I) ave( VV„) ZZ (żTd-ljn" n
1,4 logo - 0(1)
czyli tyle samo co w przypadku, gdy szukanego elementu nie ma w drzewie. Nietrudno leż wykazać (zad. 3.8), że 6(n) = OO/log/i) Zajmiemy się, teraz operacją delete(v, S)\\ S := S - {w)
110
3. S łow n ik i
Usunięcie elementu ze zbioru wiąże się z usunięciem wierzchołka z drzewa. Trudność polega na tym, że nie można swobodnie usuwać wierzchołków z. drzewa, gdy mają one następniki. Można sobie z tym poradzić, zastępując wierzchołek x zawierający element v wierzchołkiem zawierającym albo element bezpośrednio poprzedzający v w zbiorze S, albo element bezpośrednio następujący po t> (rys. 3.8). Aby zagwa rantować losową posłać drzewa BST po usunięciu wierzchołka, losujemy, który wierz chołek wybrać. Fizycznie usuwamy wierzchołek, który, ma co najwyżej jeden wewnę trzny następnik (co najmniej jeden z następników jest wierzchołkiem zewnętrznym reprezentowanym przez nil). Oto algorytm delete. Jego tekst jest dosyć długi ze względu na dużą liczbę możliwych przypadków.
.V
Rys. 3.8. Operacja usuwania elementu /. drzewa BST
3.3. Drzewa p oszu k iw ań binarnych
111
procedure delete(u : T; var r : n o d e ); var A', y, z, t . -n o d e ;
h : 0 . . j. ; begin x := s e a r c h ( v , r , y ) ; if X b nil then begin if i l e f t i x ) ~ n i l )or ( r i g h t : ( x ) = nil) then begin if (l e f t : (x) = n i l )and {r i g h t (x) --nil) then z := nil else if l e f t(x) = nil then z := r i g h t ( x) else z := l e f t ( x ) ; if y —-nil then r := z else if x = l e f t (y) then l e f t (y) := z else r i g h t : (y) := z end else begin -|l e f t ( x) h nil i r i g h t (x) * nil] b := r a n d o m {2) ; {jeśli b = 0, to w miejsce v ws tawiamy element: bezpośrednio poprzedzający u w zbiorze S; jeśli h --- 1, to w miejsce v wstawiamy element: bezpośrednio następujący po v w zbiorze S] if b = 0 then begin z := l e f t (x) ; if r i g h t (z) = nil then l e f t (xj := l e f t (z) else begin repeat t := z; z := r ig h t { z ) until ri g h t (z ) = n i l ; right(t)
\=le£t(z)
end end else begin z := r i g h t (x) ; if l e f t (z) = nil then r i g h t (xj := r i g h t : (z) else begin repeat Ł := z ; z := l e f t (z) until l e f t ( z ) = n.il; I eft (t) := r i g l i t (z) end end; keyix)
end end end d e l e t e ;
k e y (z )
Ry.s. 3.9. Wynik u,snniyci;i clcmenlii JO pi/rz. przesunięcie elementu 9
Rys. 3.JO. Wynik usunięci;) cienieniu 10 prze/, przesunięcie cienieniu \ ' . l
Aby na przykład wykonać operację, deleui 10, S) względem drzewa BST podanego na rysunku 3.5, możemy w miejsce zwalniane przez. 10 przenieść *), uzyskując drzewo widoczne na rysunku 3.9, albo 12, uzyskując drzewo widoczne na rysunku 3.10. Pesymistyczna złożoność czasowa przedstawionego algorytmu delcie wynosi oczywiście \V(//) = O(n) Z badań wynika natomiast, że oczekiwana złożoność czasowa A(n) -■ ć)(log /<) chociaż nie jest to fakt malemaiyez.nic udowodniony. Możliwa jest prostsza wersja operacji delete, nazywana opóźnionym usuwaniem. Za miast pozbywać się wierzchołka x zawierającego usuwany element: v, przyjmujemy wie rzchołek ,v z.a „usunięty” i pozostawiamy go w drzewie. Oczekiwana złożoność czasowa wszystkich operacji .słownika wynosi zatem O(logm), gdzie m jesl liczbą wykonanych operacji insert. Gdy liczba pozostawionych w drzewie „śmieci” slajc się zbyt duża lub gdy jeden z wierzchołków znajduje się w zbyt dużej odległości od korzenia (na przykład > dog n dla pewnej stałej c), należy przechodząc cale drzewo, utworzyć z nie usunię tych elementów nowe drzewo BST. Można się postarać, żeby miało ono jak najmniejszą wysokość (żeby było zupełnym drzewem binarnym). Zapisanie algorytmu rekonstruują cego zupełne drzewo BST pozostawiamy Ci jako ćwiczenie (zad. 3.1 1).
3.?}. D rzew a poszu k iw ań binarnych
3.3.1. AVI.: Drzewa BST są prosi ii i efektywną implementacja słownika w przypadku oczekiwa nym. W przypadku pesymistycznym każda z operacji search, insert i delete ma jednak złożoność 0(«). Chcąc uzyskać czas działania O(logn), trzeba dodatkowo zadbać, żeby drzewa BST pozostawały w postaci gwarantującej wysokość Q(logn), gdzie // jest liczbą wierzchołków. Istniejewiele odmian takich drzew. Najprostsze z nich to drzewa AVL. Dnow BST jest .'krzewem AVL wtedy, kiedy dlii każdego wierzchołka wysokości dwóch jego poddr/.ew różnią sic co najwyżej o I. L
L em a t 3.2. Wysokość drzewa A VL o n wierzchołkach (// > I) jest nie większa niż 1,45log n.
D o w ó d : Niech Tlt będzie drzewem AVL o wysokości It, które ma najmniejszą możliwą
liczbo wierzchołków wśród drzew AVL o wysokości h. Niech nh będzie liczbą węzłów wewnętrznych w Th, a /»,, liczbą węzłów zewnętrznych. Oczywiście Ti / r= fei
/( = - 1
m .., = 1
//= 0
"'<>= 2
T* A
AA
Rys. 3.11. Konstrukcja minimalnych ilr/.ew AVI,
mh = nh -i 1. Zauważmy, że (rys. 3.11) /«,, = Fł +J dla h ż I, gdzie A,, jest liczbą Fibonacciego o numerze h
l
l'li =
! 'h -
i ‘k l ' h
- 2
Ó lh
I' >
I
Z własności liczb Fibonacciego wynika, że &'• - ’ < Fh < Qh "■1
dla li £ 2
114
3. S łow n ik i
gdzie © = — (1 + V 5 ). Stąd +
dlii li > I
Mamy zatem h -I- 1 < log0m(l < lng„(/t,, + 1) < log„(/t -l- 1) < 1,45log n + I
dla h > 1 cbdo
Poza atrybutami left, key i riyjit każdy wierzchołek drzewa AVI. ma równicz arrybul h/'W = /tŁ(x) - /i;((x) gdzie h,(x) i hR(x) to odpowiednio wysokość lewego i prawego poddrzewa wierzchołka x. Z definicji drzewa AVL wynika, że dla każdego wierzchołka x bf(x) e {-1, 0, +1} Realizacja operacji search dla drzewa AVL jest taka sama jak dla zwykłego drzewa BST (w wyniku search drzewo się nie zmienia). Natomiast operacje insert i delete dla drzewa AVI. przebiegają z początku tak samo jak dla drzewa BST, po czym następuję faza przywracania struktury drzewa A VL, jeśli została zaburzona (w wyniku insert może się zwiększyć wysokość drzewa, a w wyniku delete zmniejszyć). Wstawiając na przykład
0 3
10 0
13
10 0
/
0 2 Rys. 3.12. Wsławianie cienieniu 2 do drzewa AV1,
element 2 do drzewa AVL widocznego na rysunku 3.12 (atrybuty hf są podane obok wierzchołków), otrzymujemy drzewo BST, w którym jeden z wierzchołków ma atrybut h f równy 2. Należy dokonać przesunięcia w drzewie wierzchołków, czyli tzw. rotacji ''cokół wierz chołka 5, likwidując nadmierną różnicę wysokości poddrzew (rys. 3.13).
115
Dr/.ewa poszu k iw ań binarnych
0 8
0 2
5 0
Hi 0
Rys. 3.13. Wynik poprawienia drzewa AVI,, z rysunku 3.12
Naszkicujemy teraz operację inseri(u, S) dla drzewa AVL. Po wstawieniu nowego węzła x za pomocą zwykłego algorytmu insert dla drzew BST przesuwamy się z powrotem po ścieżce od x w stronę korzenia, dokonując odpowiednich zmian atrybutu />/'(wierzchołki tej ścieżki stają się „cięższe”) do chwili napotkania takiego wierzchołka y, że albo (a) y jest korzeniem, a nowa wartość bf(y) jest różna od 2 i -2, albo (b) nowa wartość bf(y) = 0, albo (c) nowa wartość bf(y) - 2 lub -2. W przypadku (c) w wierzchołku y następuje zaburzenie struktury drzewa AVL i należy dokonać odpowiednich przesunięć wierzchołków w drzewie, aby tę strukturę przywrócić. +2
/ A
(a) Kolacja pojedyncza w y (przypadek symetryczny, gdy /;/(>') = -2 , bf(z)~- I)
(l>) Rotacja podwójna w y (przypadek symetryczny, gdy /»/(»=-2, bf(z) = -i-I; wartości hjil\c {- I, + 1])
Rys. 3.14. Rotacjedr/.cw AVL
H i)
S łow nik i
Z dokładności;] do symetrii mamy do czynienia /, jedną z dwóch sytuacji przedstawio nych na rysunku 3.14; obok zapisujemy odpowiednie, operacje (nazywane rotacjami) przywracające, strukturę; drzewa AVL w wierzchołku y.
Ii
L em a t 3.3. Rotacja pojedyncza i rotacja podwójna prowadź;) od drzewa. BST do drzewa BST.
]'e' U waga Należy pamiętać, żeby przy dokonywaniu rotacji doczepić do poprzednika węzła r właściwy następnik (z lub /). Zauważmy, że wykonanie rotacji przywraca danemu poddrzewu jego wysokość przed rozpoczęciem wykonywania operacji insert. Dla pozostałych wierzchołków na ścieżce do korzenia atrybut bf pozostaje nie zmieniony, Aby umożliwić przejście ścieżką /, powrotem do korzenia, należy podczas wykonywania operacji search umieszczać odwiedzane wierzchołki na stosie. Oto algorytm opisujący przechodzenie ścieżką w górę drzewa AVI., z odpowiednią aktualizacją atrybutu bf. | t o s s -z.awierći ścieżkę ort korzenia do poprzednika wstawionego do drzewa wierzchołka x ; a t o p -jest: nazwą procedury, która kończy operację i/!.•;,•/•/.) :i. £
S
-
0 th a n
s
op;
:= x ; {iisobno rozpn truj emy poprzednik ws towianego wierzchołka} :: := f r o n t ( S ) ; pop(S) ; b f { z ) < > 0 Chon begin b f ( z ) : — 0; s t o p end; if Ł-lewy następnik z then b £ ( z ) := -1-1 else b f ( z ) : -j ; |w poniższej pętli jest powtarzana operacja modyfikacji a trybutu b f ; wierzchołki t , z , y tworzą łańcuch przesuwający się w górę drzewa] while S 0 do bogiń y
f r o n t (.'/) ; p o p ( S ) ; case b f { y ) of 0: if z-lewy następnik y then b f ( y ) +1 else b f (y) ;=-l; KI, : if z-prawy następnik y then begin b f ( y ) 0; s t o p e n d else if /./ (::) • I I then begin rotacja pojedyncza (y, z) ; s t o p end e l s e begin rot,icja. podwójna ( y , , t) ; s t o p end;
.‘i..‘i. I/rix’.wa poszu k iw ań b in arn ych
117
-1: if s-lewy następnik y then begin b f ( y ) :=0; s t o p e n d else if l > f ( = -1 then begin rotacja pojedyncza (y, z); stop end else begin rotacja podwójna (y, z, t) ; s t o p and; end; I: :
z ; z := y
end ;
Pełna implementacją algorytmu insert, jak również zaprojektowanie algorytmu delete, pozostawiamy Ci jako ćwiczenie (zad. 3.12-3.13). Otrzymujemy takie oto twierdzenie.
T w ie r d z e n ie 3.1. W wypadku drzew AVL każdą z operacji search, insert i delete można wykonać z pesymistyczną złożonością czasową O(logn). Zaimplementowanie drzewa AVL wymaga 0{n) dodatkowej pamięci na atrybuty left, right i h f gdzie n jest maksymalną liczbą elementów w zbiorze S.
3 . 3 .2 .
S « m o o rg a n iz u ją c e się d rz e w a BST Zamiast wymagać, żeby każda operacja słownika była wykonywana w czasie O(logn), wystarczy zwykle zażądać żeby m operacji słownika mogło być wykonanych w łącznym czasie <3(mlog n) (a więc z kosztem zamortyzowanym O(logn) dla każdej operacji w ciągu). Czas taki zapewnia struktura danych o nazwie samoorganizujących się drzew BST, w której nie trzeba używać dodatkowego atrybutu w rodzaju atrybutu hf dla drzew AVI,. Podobnie jak dla samoorganizujących się list, wierzchołek zawierający argument operacji jest przesuwany do korzenia drzewa odpowiednio za pomocą wielo krotnego stosowania obu przedstawionych wcześniej rodzajów rotacji. Operacje search, insert i delete zapisuje się za pomocą pomocniczej operacji splay o po danej tu specyfikacji: splavU, ,S’):: Drzewo BST S zostaje przekształcone w drzewo BST S', reprezentujące ten sam zbiór elementów co S. Jeśli l jest w S, to korzeń drzewa S' zawiera /. Jeśli / nie ma w S, to w korzeniu drzewa S ' znajduje się taki element /', że między wartościami min(/', /) i max(/', /) nie ma elementu z S (jeśli są dwa (akie elementy, to jeden z nich jest wybierany dowolnie). Operacjo search, insert i delete można zrealizować, używając operacji splay (rys. 3.15).
1 1 $ I 3. S łow n ik i
searchO,
splay(l, S) + sprawdzenie, czy w korzeniu powstającego drzewa znajduje się /
deleliil, S):
I xplay(l, S)
Usunięcie /
splay(l, St)
A Rys. 3.15. Zapisanie operacji słownika za pomocą operacji
s p la y
Operacja splay(l, ój polega najpierw na realizacji operacji search{l, ój dla zwykłych drzew BST do wyznaczenia ścieżki od korzenia do wierzchołka x zawierającego element /. Następnie przechodzimy z powrotem po ścieżce od x do korzenia drzewa, wykonując ciąg rotacji pojedynczych, w wyniku których wierzchołek a: zostaje przesunięty do korze nia. Rotacje pojedyncze są wykonywane parami. Wykonanie jednej pary nazywa się krokiem rozchylającym (rys. 3.16). Jeśli odległość x od korzenia jest nieparzysta, to na końcu jest wykonywana jedna rotacja pojedyncza. Przykład wykonania operacji splay przedstawiamy na rysunku 3.17. Jako koszt operacji f, co oznacza się przez cost(f), przyjmuje się liczbę kroków roz chylających (licząc również rotacje pojedyncze kończące splay). Koszt jedne; operacji może być nawet 0(n), ale rozłożony na cały ciąg operacji jest tylko 0(lo g «). Aby to wykazać, każdej operacji przyporządkowuje się -- oprócz jej kosztu - także kredyt na jej wykonanie (rzędu logn). Również drzewom BST przyporządkowuje się kredyt. Pełnią one funkcję banków użyczających operacjom kredytów, gdy ich koszt jest wyższy niż
Drzewa p oszu k iw ań binarnych
1J 9
10 / c/y / /o ' o // / /'
10
/ .v/>/«v(l, S) -------- f>-
■Z
Rys. 3.17. Wynik wy konania operacji s p l a y
przyznany im kredyt. Gdy koszt wykonania operacji jest mniejszy niż przyznany jej kredyt, nie zużyty kredyt jest odkładany w „banku” drzewa BST. Kredyt drzewa BST definiujemy w nastypuj;|cy sposób:
C(S) = L LlogwOv)
I2 0
:i. ,S łow nik i
gdzie ii’(.v) jest liczbą poloników „v (wliczając' w to .v). Zauważmy, że kredyt drzewa początkowego C(A'n) = Q j że dla dowolnego drzewa BST S mamy 0 < C(S) < nlog n. Kredyt operacji f przekształcającej drzewo BST S w drzewo BST A'' definiujemy w na sil,'-pujący sposób: i if) = cost(j) ! C(S') - CIS) Można udowodnić laki oto lemat (zad. 3.14). :;:Ł j
L e m a t 3.4.
.....
j
Jeśli /
— sp la yd ,
Aj), lo <■(/) < 31og// -I- I.
Wypływa stąd' wniosek: kredyt operacji s e a r c h jesi < 3log// -i- 1 (jedna operacja s p la y )', kredyt i n s e r t < 4log ii -1- 1 (kredy! s p l a y -I- wzrost kredy (u drzewa o log//); kredyl d e l e t e < 6log n -I- 2 (dwie. operacje s p l a y ) . 11
T w ie r d z e n ie 3.2.
i
Kosz! wykonania m operacji search, insert i delete jest Oimlogn).
D ow ód:
Niech cr = J\; będzie ciągiem operacji search, insert i delete. Niech f , przekształca drzewo BST ,Sj , w Aj. Wówczas III
III
III
cnsf(a) = Z, cost{/]) =■ £ c(J\) - £ (ś '(.V, ) - C(S, _ ,)) ----i= I
i- i
i-l
= £ c(J]) - C(Aj„) -I- ('(Aj,) - Oimlogn)
Ct OA O Tliło
M ie s z a n ie Mieszanie jest zupełnie odmiennym rozwiązaniem problemu słowniku od drzew BST. Wykorzystuje się w nim własności numeryczne przechowywanych elementów (w drzewach BST jedyna informacja o elementach pochodzi z wyników porównali). Najprostszy schemat przechowywania n elementów polega na potraktowaniu i-tego cie nieniu zbioru c/y jako indeksu i zamarkowaniu należenia do zbioru A' na pozycji /\|fl;j pewnej tablicy A. Aby slwierdzić, czy element ,v należy do Aj należ)’ sprawdzić pozycję /l|'.v|. Metoda la staje się niepraktyczna, gdy uniwersum możliwych elementów jest zbyt duże. Można ją jednak nieco zmodyfikować.
121
3.4. M ieszanie
Najpierw należy obliczyć wartość pewnej funkcji odwzorowującej uniwersum elemen tów w zbiór indeksów tablicy. Funkcję tę nazywamy funkcją mieszającą. Obliczona wartość daje nam indeks w tablicy, pod którym możemy znaleźć poszukiwany element. Może się przy tym okazać, że na pozycji o danym indeksie znajduje się kilka elementów zbioru S. Zjawisko to nazywamy kolizją. Elementy o tej samej wartości funkcji miesza jącej są trzymane na jednej liście reprezentowanej bezpośrednio lub w sposób ukryty w jednej tablicy. Mieszanie, może być uważane za metodę wypośrodkowania wymagań pamięciowych i czasowych: ® jeśli nic ma ograniczeń pamięciowych, to element v pamiętamy pod adresem u; wy szukiwanie wymaga wówczas czasu 0(1); • jeśli nic ma ograniczeń czasowych, to ..., an przechowujemy w postaci liniowej, używając w ten sposób minimum pamięci. Przy mieszaniu staramy się mieć .szybki dostęp do elementów zbioru i używać jak naj mniej pamięci. Mieszanie to klasyczna metoda informatyczna w tym .sensie, że została dokładnie zbada na i jest powszechnie stosowana. Jej użyteczność została potwierdzona zarówno przez analizy teoretyczne, jak i testy.
3.4.1.
i,
W ybór fu n k c ji m ie sz a ją c e j Funkcja mieszająca jest funkcja, odwzorowującą uniwersum, z którego pochodzą elementy zbioru .V, w zbiór adresów [0..m — IJ dla pewnej liczby naturalnej m. Idealna funkcja mieszająca powinna być: ® łatwo obliczalna; ® losowa, tzn. każdy indeks.[O..in - I] powinien być jednakowo prawdopodobny jako wartość funkcji. jeśli elementy zbioru S mają '/.łożoną postać, to funkcję mieszającą definiuje, się jako złożenie dwóch transformacji: (a) transformacji elementu w jedno słowo maszyny; (b) transformacji słowa w indeks z przedziału [()../;/.- I). W pierwszym wypadku stosuje się takie operacje na słowach bitowych, jak różnica symetryczna słów.. W drugim wypadku stosuje się często jedną z dwóch funkcji: (a) //,(A') - k mod ii i (gdzie m to zwykle liczba pierwsza); (b) h.,(k) - M najbardziej znaczących bitów liczby (ck) mod b, gdzie m = 2lW , b = 2”, B ■• długość słowa maszyny; postać dziesiętna stałej c nie powinna kończyć się na ... .v2l z cyfrą parzystą ,v (|K|).
122
3. Słowniki
3.4.2. S tr u k tu r y d a n y c h sto so w a n e do ro z w ią z y w a n ia p ro b le m u k o liz ji M
et o d a
I
Jest to tzw. metoda łańcuchowa. Polega na utrzymywaniu dla każdego indeksu ie listy elementów, które mają tę samą wartość funkcji mieszającej (rys. 3.18). Użyjemy tablicy - i], nazywanej tablicą mieszania, do zapisu wskaźników do tych list. H Lista elementów w S lak ich, że //(v)=0 Lisia elementów w S takich, że //(vj) = 1
Lista elementów w S takich, że /j(v)=/a- 1
mRys. 3.18. Rozwiązywanie problemu kolizji metoda łańcuchowa
Operacje słownika polegają na wyznaczeniu listy /-/[//.(p)j, a następnie na wykonaniu odpo wiedniej operacji na tej liście z argumentem w W przypadku pesymistycznym złożoność czasowa metody łańcuchowej jest zatem taka sama jak bezpośredniej implementacji listowej: W(n, m) = 0(n) dla operacji search, insert i delete; W(n, m) = 0(m) dla operacji construct. Aby wyznaczyć oczekiwaną złożoność czasową, zauważmy, że wstawianie elementów na listy odbywa się zgodnie ze schematem Bernoulliego. Przy założeniu, że. prawdopodo bieństwo otrzymania danej wartości funkcji mieszającej jest takie samo dla każdego elemental, prawdopodobieństwo trafienia elementu na daną listę wynosi Mm. Przy n loso waniach oczekiwana liczba elementów trafiających na jedną listę wynosi tilm. Mamy więc dla operacji search, insert i delete Mn, m) = - - -l- 0(1) m Ponieważ metoda łańcuchowa wymaga pamięci na przechowywanie dowiązali, złożo ność pamięciowa wynosi S(n, in') =■ in -I- u -I- 0(d )
123
3.4. M ieszan ie
Mieszanie, a w szczególności metodę łańcuchową, stosuje się zwykle, gdy /; < m. Dla metody łańcuchowej oczekiwana złożoność czasowa jest wtedy 0(1). Wymaga to jednak wcześniejszej znajomości maksymalnej wartości i i . Jeśli maksymalna wartość // nie jest dana z góry, stosuje się dodatkowo metodę reorganizacji struktury danych, gdy n staje się większe niż m. Aby utrzymać rząd wielkości złożoności czasowej, podwaja się wartość i i i (tzn. i i i ' = 2 n i ) . Rlernenty z tablicy /•/ miesza się raz jeszcze w nowo utworzonej, dwa razy dłuższej tablicy //'. Koszt: jednej reorganizacji jest proporcjonalny do ///.' + « = czyli m(l> = 2 'm, jest proporcjonalny do
Koszt / reorganizacji,
i Z m 2‘ =
111( 2 "
' - 2) ^ 0 ( m m)
i= i Reasumując, jeśli liczba elementów w zbiorze S jest zawsze nie większa niż rozmiar tablicy bieżącej, to koszt wszystkich reorganizacji jest liniowy względem największego rozmiaru tablicy mieszania. Zaletą metody łańcuchowej jest jej prostota i dobra oczekiwana złożoność czasowa. Wadą natomiast jest stosunkowo duże dodatkowe obciążenie pamięci. W następnych dwóch metodach rozwiązywania problemu kolizji w ogóle nie używa się dodatkowej pamięci; mają one za to trochę gorszą oczekiwaną złożoność czasową. Dla ich popraw ności istotny jest fakt, że i i i > u. Jest w nich przestrzegana ogólna reguła zwana regułą adresowania otwartego. Oto ona: elementy są przechowywane w tablicy mieszania /:/[().. m - IJ; w razie zaistnienia kolizji należy użyć innego wolnego miejsca w tablicy mieszania i i [O .. in — 11. IVIktoda III
Jest to tzw. metoda adresowania liniowego. Można ją opisać w następujący sposób: jeśli miejsce H[h(v)} jest zajęte i H[h{v)] ż- v, to szukaj miejsca v (lub miejsca do wsta wienia v) pod kolejnymi adresami h(v) -I- I, //(V) -i- 2, ... (mod ///) Oto implementacje operacji construe/ i insert. procedure h a s h e o n ;;t r u c i : ; var i : i n t e g e r ; begin for i : -- U bo im - .1. do H|i| :.. !■■•; ■[-1®1OKiiacsa wolne miej. see w tablicy)
n := O end h a s h e o n s t r u e t ; function Lu::: reinsert i o : integer) : i n t e g e r ; var i : i n t e g e r ;
124
Słowniki begin };i < m j-
i : ~= h(v) ; while (//| i ] i v) and {li\ i | / i := (i I I(mod in; i f- Hi (| Z v then begin H| i | :== v;
ćto
n : • n -I- 1 and ; h a s h insert : ■/ end h a s l i i n s e i ' t . ;
W trakcie zapełniania się tablicy znajdujące się w niej elementy często grupują się razem, przez co operacje wykonują .się znacznie wolniej. Oto metoda, dzięki której unika się grupowania elementów. Zamiast przyrostu I używa się w niej wartości, która zależy losowo od samego elementu r. iv; KTOOA Jest: to t/.w. metoda mieszania podwójnego. Zamiast przyrostu I bierzemy przyrost określony przez drugą funkcję mieszającą h'(v). Funkcja ta powinna spełniać następujące warunki: (a) //'(u) > 0; (b) /i'(V) względnie pierwsza z ni (najlepiej, gdy m jest liczbą, pierwszą); (c) li' istotnie różna od Ir, na przykład dla funkcji //, z podrozdziału 3.4.1 można wziąć funkcję li \k ) = m — 2 — k mod (in ~ 2) gdzie ni jest liczbą pierwszą, function h a s h i n s e r l : (v ; i n t e g e r ) var u, i. ; i n t e g e r ; begin i : = h ( v ) ; u :---h ' ( v ) ; while ( Hi i ) Z- v) and (//|i | z i ( i I u) mod m; if H|i.| i- v then begin
H| I := V; n : = n -l- I and ; tu i s l i i u s e r t
end h a s h i n s e r l : ;
i
: in teger;
cj0
3.4. Mieszanie
125
Zajmiemy się teraz, analizą oczekiwanej złożoności czasowej dla operacji imerl(v, .S’), gdy t> .S’, przy metodzie mieszania podwójnego, przyjmując przybliżające rzeczywisty model probabilistyczny założenie, że adresy (h(v) + lh'(v)) mod m tworzą losową permulację liczb 0. 1..... ni - I (zakładamy dodatkowo, że operacja delete nie była w ogóle wykonywana). Niech C~(», nt) będzie oczekiwaną liczbą porównań wykonywanych w operacji inxeri(v, .S’), gdy w tablicy !I\() ..m — I] znajduje się już n elementów. Wówczas C -(O, m) = I oraz C (/), ni) — 1 -I- — C (n — 1, ni — I) m dla n > 0, gdyż co najmniej jedna próba jest zawsze wykonywana. Natomiast z praw dopodobieństwem u/iii (oznaczającym prawdopodobieństwo, ż.e pierwsze sprawdzane miejsce jest zajęte) branych jest pod uwagę pozostałych in - I pozycji tablicy U oraz pozostałych n - I elementów zbioru. Stosując indukcję matematyczną ze wzglę du na wartość ///, łatwo można pokazać, że rozwiązaniem powyższego równania jest funkcja: ni -i- 1
C (//. ni) m
—n -I- 1
Mamy zatem
C (n, iii )
—
m+ I m —n + I
I+ n 1----1 I ------ni m
1 —a
ll
gdzie a = — oznacza współczynnik zapełnienia tablicy mieszania, /// Zajmiemy się teraz analizą oczekiwanej złożoności czasowej operacji searchiy, S), gdy e e S. Skorzystamy z powyższego wzoru dla przypadku, gdy v 0 S, i ze spostrzeżenia, że liczba prób potrzebnych do wyszukania v jest równa liczbie prób wykonywanych przy wstawianiu v do zbioru ,V. Ponieważ element: v mógł być z jednakowym prawdopodobieństwem wstawiany do zbio ru ,V, gdy zbiór ten miał k = 0, 1, ..., n - 1 elementów, mamy
126
3. S łow n ik i 1 ,lv,1 C ( / / ,
III) =
-
X
ni +1
III + 1
-- ---------- 7 —
n k=0 in —k + 1
=
---------------- ( / ■ / „ , ,
s — (In(«/ + 1) —ln(m —n + 1)) ---(X
n
1 1 __________ , ) s =
.
-I- I
in
a
-
I
ni - n -i- 1
a
In -
I- a
W tabeli 3.3 podajemy zebrane wyniki analizy oczekiwanej złożoności czasowej z uży ciem przedstawionych metod rozwiązywania problemu kolizji. Tabela 3.3. Oczekiwana złożoność czasowa w wypadku różnych metod mieszania Liczba prób dla
Liczba prób dla
xearch(v, S), v 6 S
scarch(v, S), v i
Metoda łańcuchowa
U 1+ —
1 + 0.
Adresowanie liniowo
1 I --- 1----------
1 2
2
2 (1 - a )
1 2(1 - a f 1
1 ln(l - a) a
Mieszanie podwójne
i'
I- a
Podstawową zaletą metod opartych na regule adresowania otwartego jest niewielkie ob ciążenie pamięciowe, (S(n, m) = m - //)). Mają one jednak także poważne wady, których nie ma metoda adresowania łańcuchowego. Otóż zawsze, musi zachodzić zależność: n < in, a poza tym gdy n jest bliskie i i i , algorytmy stają, się, podobne do wyszukiwania liniowego na liście. Operacja delete, jest bardziej skomplikowana (szczególnie w wypad ku mieszania podwójnego), zachodzi bowiem konieczność oznaczania pozycji tablicy mieszania jako „miejsca wolnego, ale kiedyś zajętego” .
3.5. Wyszukiwanie pozycyjne Omówimy teraz metodę wyszukiwania, w której wykorzystuje się postać elementów zbioru S jako słów binarnych, podobnie jak w metodzie sortowania pozycyjnego. Załóżmy, że słowa binarne mają długość niaxb -l- 1, v = ..., .. , ... pn)2, v, e (0, 1). Będziemy używać tej samej funkcji wycinania bitów co przy sortowaniu pozycyjnym, tzn. lń ts { v ,
k , j )
=
(i;, ,
, ...
v
t
, iv )2
Rozważymy teraz kolejno trzy rodzaje drzew poszukiwań pozycyjnych, realizujących wyszukiwanie pozycyjne.
;».5. W yszukiw anie p ozycyjne
127
3.5.1. D rz e w a R ST Drzewa poszukiwali pozycyjnych, w skrócie drzewa RST (od ang. R a d ix S e a r c h Trees), są alternatywną strukturą danych do drzew poszukiwań binarnych (RST). Roz gałęzienia s.j dokonywane nie przez porównania wartości elementów, ale na podstawie wartości kolejnego bitu wyszukiwanego słowa v. Na pierwszym poziomie drzewa decy dujące znaczenie ma pierwszy bit tzn. elementy zbioru .V z bitem 0 na pierwszej pozycji trafiają do lewego poddrzewa, a elementy z bitem I do prawego (rys. 3.19). U' ,0
I
Elementy z pierwszym bilem = 0
Elementy z pierwszym bilem = I
Rys. 3.19. Ko/d/iiil elementów w drzewie RST w ziile/nośei oil pierwszego bilu
i joi 0 / /
\
i N
oon 0 / / 0001
lotu 0 / /
\ I \
Ki l l
tlił
Rys. .1.20. Przykładowe drzewo RST
Na drugim poziomic w drzewie decydujące znaczenie ma drugi bit ild. Jak poprzednio, zakładamy, że każdy węzę! ,v drzewa RST ma atrybuty: leji(x), key(x), riyiii(x). Poszukiwanie elementu i wsławianie go do drzewa RST odbywa się w podobny sposób jak w wypadku drzewa RST. f u n c tio n digi. ta ls e a r c h (v : in te g e r ; r : no d e) : node; {r jest. korzeniem drzewa RST} var b : in te g e r ; x : node; b e g in b : = maxb; x : = r ; w h ile ( x i n i l ) and ( key( x) -A v) do b e g in if b i t s I v, b , 1) = 0 then x := l e h t ( x ) else x := r i g h t b
■■ = b
■■
1
end; d ig i ta ls e a r c h
x
{ (v e S
v ) v (v cS S
A
key(x)
end d ig ita ls e a r c h ;
a
x
= nil) }
(x) ;
128
3. S ł o w n i k i function d i g . i. tai i user i:(v ■ i n t e g e r ; var r : no c i e )
: no c i e;
RS')’1 var y : node; b : i n t e g e r ; bogiń [pierwsza część algorytmu to wyszukanie miejsca w ć , : z c W - ' wś t avji iżn ia e 1eme u t.u vj b •■= n i axb; x : = r; y := nil ; wbiła (x n i l ) and (k e y ( x ) * e) do begin
y : =; X; if b i t s i v , b , 1) « 0 than x := l e i : !:( ;;) else x := r i g h t (-"<) ;
b := b - 1 and; if x ■-=n i l then {jośli v nie ma w drzewie, to zostaje wstawiony ł|0 nowego węzła, który jest: doczepiany do y \ begin now {::) ; keyin') v ; l e f t ( x ) := n i l ; r: i . g ht : ( x ) :-n.il; if y /■ nil then if bits (v, h + 1 , 1) = 0 then l e l : l : { y ) : else riq/i (:(y) else r x end ; d igi ta n s er t ; x: end d i g i t a l i n s e r L ;
fi PiV/.YKiAl): Załóżmy, żc /-lii literę alfabetu reprezentujemy za pomocą słowa binarnego odpowiadają cego liczbie / i że drzewo RRT tworzymy przez wykonanie wslawicń liter w następującej kolejności: S 1001 I Z I 1010 U 10101 K 0101I A 00001 J 01010
Otrzymujemy drzewo RST przedstawione na rysunku 3.21.
K A
Z .1
U
Itys. 3.21. Drzewo t.ST
129
3.5. Wyszukiwanie pozycyjne
Operacja delete jest nieco proslsza niż dla drzew BST. Na miejsce usuwanego elementu m ożna.jeśli nie jest on w liściu - wstawić element z dowolnego liścia w poddrzewie. Zapisanie tej operacji pozostawiamy Ci jako ćwiczenie (zad. 3.18). Pesymistyczna złożoność czasowa operacji słownika na drzewach RST nie jest najlepsza. Mamy bowiem W(n, inaxb) = 0(min(n, inaxb)) porównań słów (czyli 0{maxb • min(//w.v/;, //)) porównań bitów). Załóżmy, że ovnly zbioru S są losowe, tzn. że. na każdej pozycji h z jednakowym prawdopn,.!; i,i ar,iwern może wystąpić 0 lub 1. Niech p, oznacza prawdopodobieństwo, że w n I losowaniach bitów j — I razy padnie 0, a n —j razy I. Gdy zostanie wylosowane 0, wstawiamy element do lewego poddrzewa, a gdy I - do prawego. Otrzymujemy
Dla drzewa RST o // wierzchołkach z prawdopodobieństwem /z w lewym poddrzewie znajdzie się j - 1 wierzchołków, a w prawym i i - j. Niech G(n) oznacza sumę długości ścieżek od korzenia do wierzchołka wewnętrznego w losowym drzewie RST (o n wierz chołkach). Mamy wtedy
[ r;tn) = o, C(i) - o | (Hu) =
(ii
- I)
>, /z {
dla n > I
Można udowodnić |BKR], że G(n) = plogn + O(n) Oczekiwana złożoność czasowa operacji AUi) ~ log;/ -l- 0(1) porównań słów.
search,
gdy
r s S,
wynosi
zatem
Tę samti oczekiwaną złożoność czasową otrzymuje się dla pozostałych operacji słow nika. Złożoność pamięciowa wynosi oczywiście .V(//> . O(n)
‘'
Podstawową wadą drzew poszukiwań pozycyjnych jest: konieczność porównywania na każdym poziomie drzewa całych slow binarnych. W wypadku następnych dwóch struktur danych porównywanie słów odbywa się tylko raz, na zakończenie poszukiwania.
130
3. Słowniki
3 . 5 .2 . D r z e w a T R IE W drzewie. TRIE elementy zbioru S są zapisywane w liściach. Węzły wewnętrzne mają tylko dwa atrybuty: left i riiftil. Liść ma tylko atrybut key. Oprócz tego jest potrzeb ny atrybut logiczny typu Boolean: . . leajlx) = true = „vjest liściem Zasada wpisywania elementów do drzewa jest taka sama jak w wypadku drzew RST (rys. 3.22).
Elementy /. pierwszym bitem = 0
Elementy z pierwszym bilem = I
Rys. 3.22. Zasada wpisywania elementów ilu drzewa TRIE ze względu na pierwszy bil
.1
K
Rys. 3.23. Drzewo TRIU
Wprawiając litery z poprzedniego przykładu do pustego drzewa TRIE, otrzymujemy drzewo przedstawione na rysunku 3.23. Oto algorytm wyszukiwania elementu w drzewie TRIE. function t r i e s e a r c h (v : i n t e g e r ; r : nocie) : nocie; {r jest korzeniem niepustego drzewa TRIE} Y a r b ; i n t e g e r ; x : node; begin b := inaxb; x := r; while not l e a f ( x ) do
3.5. Wyszukiwanie pozycyjne
131
begin if b i t s { v , b , 1) = 0 then x b := b - 1 end ;
left(x)
e ls s e x
righ t(x) ;
ti:iesearch : - x
{(
Jako ćwiczenie pozostawiamy Ci zapisanie pozostałych algorytmów słownika dla drzew TRIU Zaleta drzewa TRIO jest to, że poszukiwany w drzewie element v jest. porów nywany tylko z jednym elementem w drzewie, mianowicie w odpowiednim liściu. Do wad drzewa TRIE należy pojawianie się gałęzi, na których węzły mają tylko jeden następnik (zwiększona złożoność pamięciowa). Wadą jest także niejednolitość formatu węzłów w drzewie (węzły wewnętrzne, liście). Zamiast rozgałęzień binarnych używa się często rozgałęzień o stopniu wyższym, na przykład względem liter alfabetu (rys. 3.24):
A
B
...
Z
Rys.
3.24. Rtr/.galęziciiia względem
liler
allnhclti
Rozwiązanie to daje szybkie algorytmy, ale kosztem większego obciążenia pamięci. Możliwe jest też rozwiązanie hybrydowe, które polega na tym, że na górnych poziomach drzewa stosuje się duży stopień rozgałęzienia, a na dolnych niski. Operacje search, insert i delete mają następującą złożoność (konieczne jest założenie, że wash 4 I > log, /i): Md,//) = ćż{min(///i7.v//, //)
-I-
tnaxb) porównań bilów;
/Mn) = 0 (lo g n) porównań bitów, gdy log// < tnaxb; .?(//) = 0(n(maxh - log// + 2)) wierzchołków. Jako ćwiczenie (zad. 3.20) pozostawiamy Ci skonstruowanie drzewa TRIE o // liściach, które ma S(n) = Q(n(/naxh - log// -I- 2)) wierzchołków.
8 . 5 .8 .
D rze w a P A T R IC IA Kolejne drzewo poszukiwań pozycyjnych jest modyfikacją drzewa TRIE bez jego' wad, czyli długich gałęzi i niejednorodności węzłów. W drzewie PATRICIA jfcst tylko // węzłów do reprezentowania n elementów. Ma ono zaletę drzewa TRIE, a mianowicie
.1.32
.'i. Słowniki
porównanie kluczy odbywa się. tylko raz, (na koniec wyszukiwania). Aby skasować dłu gie gałęzie, na ścieżce od korzenia pozostawia się tylko porównania bitów, na których różnią się reprezentowane słowa. Aby odróżnić na przykład trzy słowa bitowe (przypo minamy, że pierwszy bil z lewej strony ma numer maxb, a porównywanie zaczynamy od bitów o najwyższych numerach): 0001000, 0000011, 0000001, wystarczy najpierw po równać bity nr 3; w wypadku bilu 0 na tej pozycji kolejne bity porównujemy na pozycji nr I (rys. 3.25). Bil nr 3 () \ I / "'w Bit nr i 000i000 0 ./ \ I \ OOOOtjOl
000001 I
Rys. 3.25. Pierwsze drzewo PATRICIA
i
Aby mi iknuć niejednorodności węzłów, utożsamiamy każdy liść z pewnym wierzchoł kiem wewnętrznym, wpisując zapisany w liściu element zbioru S do odpowiadającego mu wierzchołka wewnętrznego, W len sposób każdy węzeł wewnętrzny je.sl, traktowany dwojako: w fazie wyszukiwania jako węzeł określający rozgałęzienie, a w 1'azie iden tyfikacji (dojścia do liścia) - jako liść. Trzeba przy tym dodać jeden węzeł, gdyż jest tylko i i - I węzłów określających rozgałęzienie. Zbudujmy drzewo PATRICIA dla zbio ru S 7. poprzedniego przykładu (rys, 3.26).
A
.1
1C
S
II
Rys. 3.26. Drugie drzewu PATRICIA
Każdy węzeł drzewa PATRICIA ma 4 atrybuty: dowiązania drzewowe lefl i right, at rybut key i atrybut b (numer bilu, na podstawie którego następuje rozgałęzienie). Za kładamy, że element zidentyfikowany (przez wyszukiwanie) w danym węźle jest wpisa ny do jednego z przodków danego węzła. Łatwo jest. wtedy odróżnić zwykłe dowiązanie drzewowe. od dowiązania identyfikującego element: dowiązanie od węzła x do y iden tyfikuje element = b ( x ) < h ( y ) . Algorytm wyszukiwania elementu w drzewie PATRICIA jest prosty. function p a t r i . c i a s e a r c h ( v : i n t e g e r ; r : n o d e ) {:r jest: korzeniem drzewa PATRICIA}
wter x, y : nocie;
: node;
3.5. W y szu k iw an ie p o zy cy jn e
133
begin
.v : ■r; repeat
V '■= if b i c s (v , b { x ) , 1) = 0 then x := l e J : ' t ( x ) else x := r i g h t t x ) until )>{y) < b { x ) ; p a tri.c i a s e a r c h := x [ v jest w S m k c y ( x ) = v) end p a t r i c i a s e a r c h ;
Przejdziemy teraz do algorytmu wstawiania nowego elementu do drzewa PATRICIA. Załóż my, że do drzewa zbudowanego powyżej chcemy wstawić nowy element X = 11000. Stosu jemy algorytm patriciasearch, żeby wyznaczyć słowo binarne, które ma te same wyróżnione bity co X. Dochodzimy do Z = 11010. X i Z różnią się dopiero na bicie o numerze 1. Dowiązujemy węzeł zawierający X do węzła zawierającego Z (rys. 3.27). ()......3 lz 2 1U '
.11X
0/ \ l X 3
Z
Rys. 3.27. Wstawienie elementu X
|K
0/ \l 2 1U
I 11
0 / \l I
o|.I
y K
Rys. 3.28. Wsliiwienie elementu I
Załóżmy teraz, że chcemy wstawić element I = 01001. Operacja patriciasearch prowadzi nas do słowa J = 01010, Pierwsze miejsce, na którym I i .1 się różnią, to I. Odróżnienie słów I i .1 miisi zatem nastąpić między węzłami zawierającymi K i .1 (rys. 3.28). Dochodzimy do takiego oto algorytmu.
.
function p a t r i c i a i n s e r t { v : i n t e g e r ; r ; no c i e ) : no c i e ; {.r jest korzeniem drzewa PATRICIA] var x, y, : n o d e ; i. ; i n t e g e r ; begin I: := p a t r i c i a . s e a . r c h (v , r ) ; jol omen t: wd aje te sarnę wyniki porównań co k e y { t ) )
134
3. S łow n ik i while b i t s (v, i, 1) = b i t s ( k e y ( t ) , i, ;|)
A" : = r ; repeat z := x ; if b i t s ( v , b ( xj , 1) = 0 then x •until (b ( z ) < b ( x ) ) or ( b ( x ) " < i) ;
le£t(x)
else x : = r i g h t : ( x )
{miejsce elementu v jest: na dowiązaniu między z a x] new(y) ; ’k e y ( y ) :== v ; b ( y ) := i; if b i t s ( v , b ( y ) , 1) = 0 then begin left(y) y;
r i c/i i fc ( y ) : = x end else begin r i g h t ( y ) :=y; l e f t ( y ) :=x
end ; if b i t s ( v ,
b ( z ) , 1) = 0 then peitriciain sert y end pa t r i c i a i n s e r t ;
= y els© r i g h t (z) : = y ;
Drzewo puste, jest najwygodniej reprezentować przez drzewo składające się z jednego wierzchołka ze słowem nie występującym w zbiorze S (na przykład złożone z samych zer). W wypadku operacji search i insert drzewa PATRICIA osiągają najlepsze rzędy wielko ści złożoności z obu poprzednich rodzajów drzew: W(n) = 0(nun(maxh, n) 1- maxi)) porównań bitów; A(n) - O(logn) porównań bilów, gdy log/i < maxh\ S(n) = O(ii). Opracowanie algorytmu dla operacji delete pozostawiamy Ci jako ćwiczenie (zad. 3.21). Dotychczas zakładaliśmy, że rozważane słowa binarne mają tę samą długość. Przed stawione algorytmy można uogólnić do przypadku, w którym słowa mają różną długość, pod warunkiem, że żadne słowo nie jest prefiksem drugiego. Można leż zamiasL alf abetu dwuliterowego użyć alfabetu o dowolnej liczbie liter.
3 .6 .
Wyszukiwanie zewnętrzne Zajmiemy się teraz problemem wyszukiwania przy założeniu, że elementy zbiotu S znajdują się w pamięci zewnętrznej.
3.G. W y szu k iw an io z e w n ę t r z n e
135
Zakładamy, żc pamięć zewnętrzna jest podzielona na bloki, a każdy blok jest identyfiko wany przez swój adres, Form at rekordu lo lista atrybutów, a rekord to lista wartości, po jednej wartości dla każdego atrybutu. Plik jest listą n rekordów (o tym samym formacie). Stanowi on reprezentację zbioru elementów, a rekordy reprezentację elementów. KUucz to pod lista formatu rekordu, złożona z takich atrybutów, których wartości w rekordzie identyfikują jednoznacznie rekord w pliku. Zakładamy, żc jeden blok mieści k rekordów (/< > 0). Jeden plik zajmuje zwykle pewną liczbę bloków. Za operację, dominującą przyj mujemy przesianie jednego bloku między pamięcią wewnętrzni} a pamięcią zewnętrzną. Rozważymy kilka struktur danych rozwiązujących problem wyszukiwania zewnętrznego.
3 ,6 . 1.
P lik i n ieu p orząd k ow an e
■
W pliku nieuporządkowanym rekordy są ułożone w dowolnej kolejności. Wykona nie operacji search wymaga n/k przesłań, n wykonanie operacji insert i delete 1 -I- n/k przesłań. Podstawową wadą jest w tym wypadku długi czas działania operacji; pod stawową zaletą natomiast nieużywanie dodatkowej pamięci i prostota algorytmów. Stru ktura danych jest użyteczna, gdy rekordy pliku są zawsze przetwarzane sekwencyjnie, bez, wykorzystywania porządku.
3 . 6 ,2 .
P lik i z fu n k cją m iesza ją cą Załóżmy, że mamy określoną funkcję, mieszając;}: Ir.
zbiór wartości klucza
—
> {(), I, ....
iii
- Ij
Przyjmujemy metodę łańcuchową rozwiązywania kolizji. Tablicę mieszania, a także po szczególne listy trzymamy w blokach pamięci zewnętrznej. Załóżmy, żc i i i ~ n/k. Wów czas w jednym bloku mieści się średnio jedna lisia. W przypadku oczekiwanym operacja search wymaga przesiania dwóch bloków, a operacje insert i delete - trzech. Zaletą tej metody jest jej szybkość działania w przypadku oczekiwanym. Do wad nato miast należy długi czas działania w przypadku pesymistycznym oraz fakt, żc nie można przetwarzać'rekordów pliku w postaci uporządkowanej względem wartości klucza.
3 .6 .3 . S e k w e n c y jn e p lik i in d e k s o w a n e W tej metodzie rekordy są przechowywane w pliku głównym w postaci uporząd kowanej względem wartości klucza. Oprócz pliku głównego z rekordami jest tworzony
1 3 (i
S łow nik i
(1,1)
(9, 2)
...'............... ... \ ..
'
1 1 r 2
5
s
|
(31,4)
(17, 3)
................
2; 9
10
13
15 | 3:
17 IS 45 49 1 4
51 53 80
Rys. 3.29. .Sekwencyjny plik indeksowy
plik pomocniczy nazywany indeksem rzadkim. Dla każdego bloku pliku glownego w indeksie rzadkim znajduje sip para (u, />). gdzie b jest adresem bloku, a v najmniejszym kluczem wśród rekordów w tym bloku. Na rysunku 3.29 widać Iragmenl indeksu rzad kiego i pliku głównego dla k —4 i wiirlości klucza będących liczbami naturalnymi. Zauważmy, że plik główny zajmuje n/k bloków, a indeks rzadki - n/Ic (przyjmujemy, że każdy blok indeksu rzadkiego mieści k rekordów). Operacja search ma więc średni koszt: przy wyszukiwaniu sekwencyjnym - 1 -i- —
przesłań;
przy wyszukiwaniu binarnym - 2 -i- log przy wyszukiwaniu interpolacyjnym - 2 -l- log log Operacje insert i delete są nieco bardziej skomplikowane. Operację delete wykonuje się przez, oznaczenie rekordu jako usuniętego. Operację insert wykonuje się przez do czepienie dodatkowych bloków do już istniejących. Powoduje, to skomplikowanie stru ktury opisanej powyżej i pogorszenie złożoności czasowej. Aby uzyskać strukturę dy namiczną, należy rozbudować indeks rzadki do poslaci drzewa, przy założeniu, że bloki nie muszą być całkowicie wypełnione rekordami. Podstawową zaletą tej implementacji jest możliwość przetwarzania rekordów w pliku w postaci uporządkowanej względem wartości klucza.
ft.6.4. B -drzew o ja k o w ielop oziom ow y in d e k s rz a d k i Przyjmujemy, że w jednym bloku znajduje się między I k/2 I a k (gdzie k > 3) rekordów (zakładamy jak poprzednio, żc blok pliku głównego i indeksu rzadkiego może pomieścić k rekordów), z wyjątkiem bloku znajdującego się w korzeniu drzewa, który może. zawierać między 2 a k rekordów. Każdy blok indeksu jest węzłem wewnętrznym drzewa, zawierają cym między I k/2 I a k dowiązań do węzłów poziomu niższego (z wyjątkiem korzenia). Bloki pliku głównego są liśćmi. Wszystkie liście znajdują się na lej samej głębokości w drzewie. Drzewo zbudowane w opisany właśnie sposób nazywa się B-drzcwem (rys. 3.30).
i 3.6. W yszukiw anie zew n ętrzn e
1
137
100 Poziomy indeksu rzadkiego
\
I
3
I
8
..7 ..... /
\
4
25
100
\ 8
6
9
11
25 33
15 18
Poziomy pliku głównego Rys. 3.30. B-drzewn jako indeks rzadki
Załóżmy, że chcemy wstawić do drzewa element 10. Posługując się indeksem rzadkim, schodzimy do bloku zawierającego elementy 8, 9, II. Możemy wstawić do tego bloku 10 po przesunięciu I I w prawo. Załóżmy teraz, że chcemy wstawić do drzewa element 2. Blok, do którego pasuje 2, jest już całkowicie zapełniony. Tworzymy nowy blok, który „stawiamy” obok bloku zawie rającego I, 3, 4, 6, i dzielimy elementy 1, 2, 3, 4, 6 między te dwa bloki. W jednym bloku na przykład zapisujemy 1,2, a w drugim 3, 4, 6. W ten sposób blok indeksu, zawierający klucze I, 8, 15, 25, powinien jeszcze objąć klucz 3. Znowu więc zachodzi konieczność utworzenia nowego bloku, tym razem na poziomie, indeksu rzadkiego. W je dnym bloku pozostawiamy klucze I, 3, a do drugiego wpisujemy 8, 15, 25 (rys. 3.31).
1 8 .r \ ..
8 '
15 25 \ \
8 9 11
9
15 18
25 33
Rys. 3.31. Wsławianie cienieniu do ll-drzewa Powtarzamy te kroki do czasu, aż w kolejnym bloku znajdziemy wolne miejsce albo rozbijemy korzeń na dwa nowe bloki i doczepimy je do nowego korzenia. Koszt operacji search, mierzony liczbą przesłań, szacuje, się przez (dla k > 3) I + logt/.
n m
i < 2 -i----------- - log log* -- I
138
3. S łow n ik i
Koszt operacji insert i delcie jest natomiast tr/.y kroi nie większy (dla każdego poziomu jest możliwe przesianie istniejącego bloku w obie strony i przesianie do pamięci zewnęt rznej nowo utworzonego bloku). Podstawową zaletą B-drzew jest możliwość stosunkowo łatwego wykonywania operacji insert i delete z dobni pesymistyczna złożonością czasową. Rośnie za to obciążenie pamięci, spowodowane dostawianiem nowych poziomów w indeksie rzadkim oraz tym, że bloki mogą być teraz wypełnione tylko w połowie. Ilość dodatkowej pamięci dla plik.; głównego wynosi ii/k, a dla indeksu rzadkiego 2(n/(k/2)2) = 8nile (dla sekwencyjnego pliku indeksowego tylko n/lc bloków), B-drzewa użyte jako indeks rzadki mają jeszcze jedną wadę, a mianowicie uniemożliwia ją używanie wskaźników z zewnątrz do rekordów pliku głównego, ponieważ przy wyko nywaniu operacji insert i delete rekordy te są przesuwane. Kolejna omawiana struktura danych eliminuje tę ostatnią wadę, nic kosztem jeszcze większego obciążenia pamięci.
3 .6 . 5 . B -drzew o ja k o w ielo p o zio m o w y in d e k s g ę sty Indeks gęsty tworzy się z par (v, />), gdzie v jest wartością klucza rekordu, a b iden tyfikatorem (adresem) rekordu, biorąc pod uwagę wszystkie rekordy pliku głównego. Plik główny pozostaje nieuporządkowany i nie wchodzi w skład B-drzewa. B-drzewo jest budowane tylko dla rekordów indeksu gęstego. Obciążenie pamięci rośnie o n/k (dodat kowy jeden poziom liści w B-drzewie). Koszt search rośnie o jedno dodatkowe przesia nie, ;i koszt insert i delete o trzy dodatkowe przesiania (w przypadku pesymistycznym). W tej strukturze danych do rekordów mogą być kierowane dowiązania z zewnątrz (rekor dy nie są przesuwane w pliku głównym). Możliwe leż staje się zbudowanie kilku indek sów ze względu na różne klucze.
Zadania 3.1. Opracuj bezpośrednią implementację, listową słownika z wyszukiwaniem sekwen cyjnym elementu na liście. Udowodnij, że dla każdej operacji search, insert i delc ie pesymistyczna złożoność czasowa i oczekiwana złożoność czasowa są O(n). 3.2. (| 13KRI) Opracuj samoorganizującą się. implementację, listową słownika, przy któ rej argument operacji insert i search jer! zawsze umieszczany na początku listy. 3.3. Udowodnij, że dla wyszukiwania binarnego zachodzą związki: W(n) — log// -i- 0(1) i A(n) - log// + 0(1) (oddzielnie dla przypadku, gdy r e S, i dla przypadku, gdy w
Z a d a n ia
li! O
.1.4. Skonstruuj dane dla wyszukiwania interpolacyjnego, wymagające wykonania D.ia) porównań. 3.5. Do pustego drzewa BST 3 wykonaj ciąg operacji inserl(4, ,Y); insertd,
/;),vcn(IO, .S’); inserl((>, .S'); inxerl( \ , ,V);
3.6. Udowodnij, że lewe i prawe poddrzewo drzewa BST, otrzymanego przez wykonanie inserting S); insert(n2, 3');
insen(an, ,S);
gdzie
Ułóż algorytm rekonstruujący dime drzewo BST do postaci zupełnego drzewa BST (liście znajdują się na dwóch sąsiednich poziomach w drzewie).
3.12. Zaprogramuj algorytm insert dla drzew AVI... 3.13. Zaprogramuj algorytm delete dla drzew AVI,. 3.14. ([BKR, lemat 2 .11) Udowodnij lemat 3.4 o kredycie operacji spiny.
a: ;H s;: (;
> u, ć
.'MO
Słowniki
• 3.15. 2-3 drzewem nazywamy drzewo, w którym każdy wierzchołek wewnętrzny ma 2 lub 3 następniki, a wszystkie liście leża na tym samym poziomie. Używając 2-3 drzew, opracuj implementację słownika, w której każda z operacji: insert, delcie, search oraz: (a) concatenate (,Sj, S.,, />):: przy założeniu, że wszystkie elementy w słowniku .Sj poprzedzają elementy w słowniku ,V, połączenie obu słowników w jeden słownik S: (b) split (S, x, ,Sj, S ,)■.■. rozdzielenie słownika S tut dwa słowniki, przy czym w słowniku ,Sj mają być zapisane wszystkie elementy < ,v,. a w słowniku S2 wszystkie elementy > ,v daje się wykonać w czasie O(lngii),
;
/; ki';
3.16. Zaprogramuj algorytm delcie w wypadku różnych metod rozwiązywania problemu kolizji. 3.17. Istnieje metoda rozwiązywania problemu kolizji (nazywana algorytmem Brc-nta.), która ma na celu przyspieszenie wyszukiwania elementu kosztem dodatkowej pra cy przy wstawianiu. Gdy miejsce, gdzie chcemy wstawić dany element, jest już zajęte, musimy zdecydować, czy bardziej opłaca nam się. kontynuować obliczanie adresów dla wstawianego elementu, czy może lepiej jest wstawić go w dane miejs ce, a przesunąć element, który był tam poprzednio (minimalizując sumę. długości wszystkich list kolizji). Okazuje, się, że przy takim podejściu C'(n. i i i ) < 2,4‘) (|K|). Opracuj len algorytm dla metody mieszania podwójnego. 3.18. Zapisz algorytm delete dla drzew RST.
Ój
3.19. Zapisz algorytmy słownika dla drzew TRIK. 3.20. Skonstruuj drzewo TRIK o ków.
ii
liściach, zawierające 0(n(maxh - log/;)) wierzchoł
3.21. Opracuj algorytm delete dla drzew PATRICIA. 3.22. Zaprogramuj operacje słownika, używając struktury danych B-drze.w. 3.23. Wykonywany jest ciąg operacji słownika: const nicl(S); inxer/(()i)l I, S)\ inserl( 101 I, ,V); insert{’0010, .S’); inserti0100, .S’); insert(() III, ,S'); z użyciem następujących struktur danych: i
(a) drzewa BS1 (porządek allabelyczny słów), (b) drzewa AVI, (porządek alfabetyczny słów),
Z adania
141
(c) tirzewa RST, (cl) drzewa TRIE, (e) drzewa PATRICIA.
1<
Narysuj odpowiednie drzewa. 3.24. Zaprojektuj strukturę danych umożliwiająca wykonywanie w czasie <9(log//) na stępujących operacji na zbiorze S: (a) makesel(S):: i (b)inxeriiix, >'), S):: (c) rninx(S):: (cl) miny(S):: (e) searclaix, S):: (f) searchyfy, S)::
S := 0; S := S u {(.v, >■)}; usunięcie z .S' pary (x, y) o najmniejszej pierwszej składowej; usunięcie z S pary (v, y) o najmniejszej drugiejskładowej; wyznaczenie takiej pary (a, b), że x = «; wyznaczenie takiej pary (o, b), że y - b.
Elementy składowe par z S pochodzą ze zbiorów liniowo uporządkowanych. Wy znacz złożoność pamięciową implementacji. 3.25. Zaprojektuj strukturę danych umożliwiającą wykonywanie w czasie 0(log//) na stępujących operacji na zbiorze S: (a) (b) (c) (d) (e) (Ij
conslruct(S):: insen(S, ,v):: dclaUĄS, .v):: searches, .vj:: dem(S, /):: nurnb(S, x)::
utworzenie ciągu pustego .S; S S u {.vJ; S := S - {>};
sprawdzenie, czy x znajduje się w zbiorze ,S’; wyznaczenie /-lego co do wielkości elementu zbioru .S'; wyznaczenie numeru elementu x w zbiorze. S (względem wielkości).
Zakładamy, że elementy zbioru S pochodził z dowolnego liniowo uporządkowane go iiniwersum U. 3.26. Zaproponuj efektywną strukturę danych do wykonywania ciągów następujących operacji (dla elementów x pochodzących z, dowolnego zbioru liniowo uporząd kowanego): (a) initializiiiion:: (b) i.n$ert(x, ,Sj):: (c) delctemin{Si)\\ Ul) fiiid(x)::
Si 0 dlii / = I, 2,..., //; .Sj. := S, u {.v} pod warunkiem, że x nie występuje w żad nym zbiorze Sj, 1 ś j < //; usunięcie ze zbioru .Sj najmniejszego elementu; wyznaczenie numeru zbioru, do którego należy element x.
.laka jest złożoność operacji?
142
3. Słowniki
3.2'iM Zaprojektuj strukturę danych umożliwiającą wykonywanie w czasie 0(log n) na stępujących operacji na ciągu S: (a) canstnict{S)'.\ (b) inscrl^S, i, x)
utworzenie ciągu pustego S; wstawienie x na /-te miejsce w ciągu S, tzn. ,V; warunkiem, że i < |,Sj + I;
(c) swn(S, i, j)::
obliczenie sumy £ Sk. k i
,v, pod
Zakładamy, że elementy ciągu są liczbami całkowitymi. 3.28. Do operacji słownika .search, insert i delete dodajemy operację: between(x, y) = |{« e S: x < a < _y}| Podaj implementację słownika, przy której każda operacja ma pesymistyczną zło żoność czasową O(log/i). 3.29. Zaprojektuj strukturę, danych umożliwiającą wykonywanie w czasie 0(log») na stępujących operacji na zbiorze S zawierającym przedziały liczb rzeczywistych [/, 'j: (a) (b) (c) (d)
einpty(S):: add(S, /):: clelete(S, /):: is(S, x)::
(c) inlerseęt{S, /)::
S '.= 0 ; S := S u {/}; 'S ■■=S ~ {/}; sprawdzenie, czy element x należy do jakiegoś przedziału zbioru -S\ sprawdzenie, czy przedział / ma niepuste przecięcie z jakimś przedziałem należącym do S.
Z ło ż o n e .struktttry-'
4
jdaULych i l a z b i o r ó w ' e l e m
W
e n
t o w
ą
.ję :
rozdziale tym przedstawimy i zbadamy dwie struktury danych umożliwiające .wykonywanie różnych operacji na zbiorach rozłącznych.
4 . 1; P r o b le m su m ow an i^ z b io r ó w r o z łą c z n y c h Problemem ściśle związanym z przechowywaniem i wyszukiwaniem informacji jest problem sumowania zbiorów rozłącznych. Przedstawimy go w najprostszej postaci, kiedy przechowywanymi elementami są liczby naturalne. Jeśli chodzi o inne elementy, to byłoby potrzebne, wzajemnie jednoznaczne odwzorowanie tych elementów w zbiór {1,2, n j dla pewnego naturalnego n. Niech U — {Aj, Aj, .... Aj.} będzie podziałem zbioru E = (I, 2,..., w}0. Problem sumowa nia zbiorów rozłącznych polega na podaniu struktury danych reprezentującej podziały zbioru E, umożliwiającej wykonywanie następujących operacji: (a) initialization:: U {{I }, {2}, ...,{//}} (utworzenie/; jednolitych zbiorów); (b) union(A, Ii):: U (U —{/t, />’}) cj {/\ u IIj , pod warunkiem, że A, li e. U i A * B\ (c) find(x):: wyznaczenie w rodzinie zbiorów U zbioru do którego należy element x e E.*1 11 Podziałem zbioru /-. - (1, 2, .... n } nazywamy dowolna rodziny. spełniający następujące warunki:
(1) S p o d la 1 < /
S
, - /•'.
U
—
{.S',,
S 2,
.... .V,} podzbiorów zbioru
144
-i. Złożone struktury danych dla zbiorów elementów
Na poziomie implementacji będziemy zakładać, że .v. A, B s;i wskaźnikami do węzłów w strukturze, danych. Operacja initialization będzie wykonywana raz, na początku. Bę dziemy też zakładiić, że jest wykonywanych ii — I operacji imion (tworzących zbiór E) oraz pewna liczba ni operacji fint! przemieszanych z operacjami union. a IPkzYKŁAD: Rozważmy problem wyznaczania klas abstrakcji najmniejszej relacji równoważności, zawierającej zadany zbiór par C = {(ii,, />,): I < / < nr, u-,, b, e E] Oto algorytm zapisany przy użyciu operacji initialization, find i union, dzięki któremu można len problem rozwiązać. ini I i,i I iza tzion ; for i := 1 to m <:lo if .find(a.) ■/■ l i i i ć H b , ) then nu i"O ( / (,/,) , fincHb,)
)
Aby sprawdzić, czy liczby „v i y są w lej samej klasie abstrakcji, sprawdzamy predykat: jincH.y) -jincKy) W szczególności możemy powyższy algorytm zastosować do wyznaczenia wszystkich' spójnych składowych gralit niezorientowanego G (l7, C). Zbiór krawędzi C traktujemy jako ciąg par równoważnych wierzchołków, Izn. mających się znaleźć w lej samej klasie abstrakcji - spójnej składowej grafu G. Warto porównać len algorytm- z algorytmem wyznaczania spójnych składowych (klas równoważności), opartym na przejściu grafu metodą DFS. czyli w głąb (algorytm len znajduje się w rozdziale poświęconym algorytmom grafowym). Rozwiązanie z użyciem lej metody ma charakter statyczny off-line - wszystkie krawędzie grafu muszą być dane z góry, a spójne składowe są produkowane na koniec działania algorytmu. Rozwiązanie za pomocą zbiorów rozłącznych ma charakter on-line - krawędzie grafu mogą być doda wane na bieżąco i w każdej chwili możemy uzyskać informację, czy dane dwa wierz chołki należą do lej samej spójnej składowej danego grafu.
4 . 1. 1 .
Im p lem en tacja l is to w a Zbiór /l {«,, ..., f/(} reprezentujemy jako strukturę dowiązani tworzących listę jed nokierunkową, z dodanym dodatkowym dowiązaniem prowadzącym od każdego elemen-
4.1. Problem sum ow an ia zbiorów rozłącznych
Rys. -1.1. Reprezentacja listowa zbioru
A
= {«,,
n„
.... a
145
J
l,u listy do głowy listy. Głowa listy jesl osobnym węzłom, w którym dodatkowo zapisuje my jeszcze liczbę, elementów na liście. Na rysunku 4.1 jest przedstawiona laka lista. .lako format elementu listy (element) przyjmujemy
11
,.v: key(x), heacł(x), next(x) Atrybuty elementu listy oznaczaj;) odpowiednio klucz elementu, dowiązanie do głowy listy oraz dowiązanie do następnego elementu listy. Jako formal głowy listy (heatl^f) przyjmujemy y: firstly), coiint(y) przy czym atrybuty oznaczają odpowiednio dowiązanie do pierwszego elementu na liście oraz liczbę elementów na liście. W wyniku operacji i n i t i a l i z a t i o n powstaje n jednoelemcntowych list, a jej koszt jest OIn). W algorytmie korzystającym ze zbiorów rozłącznych muszą być gtlzieś zapisywa ne głowy bieżącego zbioru list (na przykład w tablicy var A': arrsiyl!../;] of l i c i n L . / ; ) . W przytoczonych lu algorytmach nie bierzemy pod uwagę tej zewnętrznej struktury danych. n i t i a i i z a Izi o n ; v a r x : elem en t; y : h e a d - f ; i : in te g e r ; procedure i
begin fo r
i := 1
t o n do
' -g in new(x)
; {elem en t}
koy(x) n e w l y ) ;
:- i ;
n e x t lx) :- n i l ;
{g ło w a l i s t y )
f i r.Rt(y) := x; count(y ) := 1; in
ond GIM ;
'.id ( .y) : = y ;
146
4. Z łożone stru k tu ry d an y ch dla zbiorów elem en tó w
Operacja find jest bardzo prosta. function f i n d ( x ) : h e .a d _ f ; begin
f i n d : = h ea cl (x ) end;
W algorytmie union zawsze przyłączamy listę krótszą do dłuższej. Chodzi bowiem o zmniejszenie kosztu łączenia. procedure u n i o n (y, z : head... f) ; var x ; e l e m e n t ; begin if c o u n t (y) > c o u n t {z) then y < - > z; {zamiana y i z) x := f i r s t (y) ; while n e x t (x) ź nil do begin h e a d (x) : = z ; x := n e x t(x) end; h e a d ( x ) := z; {połącz dowiązaniem koniec listy y z początkiem listy n e x t ( x ) := first(s) ; firs t (z) ;= firs t (y) ; c o u n t (z) := c o u n t {y) + count(z) end;
z\
□ P rzykład:
Rozwiążmy problem wyznaczania klas równoważności dla następujących danych: n = 6, /- = {!, 2, 3, 4, 5, 6}, k = 4, C = {(1, 2), (4, 5), (5, 6), (4, 1)}. Kolejne listy w struk turze danych są przedstawione na rysunku 4.2.
Koszt operacji initializationJest 0{n), a koszt find - 0(1). Operacją dominującą w union jest operacja head(x) := z, wykonywana dla każdego elementu listy krótszej, gdy jest ona przyłączana do listy dłuższej. W przypadku pesymistycznym koszt pojedynczej operacji union jest 0(n). Gdy jednak jest wykonywany ciąg n - I operacji union, tworzących cały zbiór, koszt ten faktycznie jest 0(/dog/i), a nie 0(ir). Liczymy bowiem, ile razy dany element może mieć zmieniane dowiązanie do głowy listy. Za każdym razem element przechodzi do listy o rozmiarze co najmniej dwukrotnie większym. Zmian takich może być co najwyżej logn. Uwzględniając wszystkie elementy, otrzymujemy koszt wykona nia n - 1 operacji union 0(n\ogii). Inaczej mówiąc, koszt zamortyzowany pojedynczej operacji union jest ć?(log/t).
‘1 .1. Problem sumowania zbiorów rozlicznych
Rys. 4 . 2 . (a) zbiór (b) zbiór (cj zbiór (d) zbiór
Implementacja listowa algorytmu równoważności: list po wykonaniu operacji i n i t i a l i z a t i o n ' , list po wykonaniu u n i o n { f i n d { \ ) , f i m l ( 2 ) ) \ u n i o n { j i n d { 4 ) , list po wykonaniu n n i o n ( J i n d ( 5 ) . f i i t ( l { 6 ) ) \ list po wykonaniu i i n i o n { f i n d { \ ) , f i n d ( 4 ) )
jin d {5 ))\
147
118
4. Złożona .struktury dan ych dla zbiorów eJemontów
T w icrd y.en ie 4.1.
']#
Koszt ciągu operacji initialization, n — 1 operacji union i nt operacji find (za kładając, że operacja initialization jest wykonywana na samym początku) jest ()(m -i- //log//).
4.1.2. Im p le m e n ta c ja drzew ow a Zbiór A = {«,, tij} reprezentujemy jako struktur*; dowiązań tworzących drzewo - z. dowiązaniami w górę od następnika do poprzednika danego węzła.
□
P
rzykład:
Zbiór ,V= {I, 2, 3, 4, 7, 9, 10, 15, 16} może być reprezentowany jako drzewo przedstawione na rysunku 4.3.
ikys. 4.3. Drzewo reprezentujące zbiór
S
Jako Ibrmat elementu drzewa (node) przyjmujemy ,v: key(,\), p(,v), sizn(x) Atrybuty cienieniu drzewa oznaczają odpowiednio klucz-wierzchołka, dowiązanie do poprzednika wierzchołka w drzewie oraz rozmiar wierzchołka - określony dla korzeni drzew. Są dwie możliwości zdefiniowania rozmiaru wierzchołka: albo utrzymywać w polu xir.e(x) liczbę wierzchołków w drzewie, albo utrzymywać w tym polu wielkość mającą związek z wysokością tworzonego drzewa. Druga możliwość daje rozwiązanie bardziej oszczędne pamięciowo. Do przedstawiania algorytmów wygodniej nam jednak będzie zastosować pierwszą.
149
<1.1. P r o b l e m s u m o w a n ia z b io ró w ro'/h\(;zny<*h
W wyniku operacji initialization powstaje n jeclnoelementowych drzew, a jej koszt jest ()(n). W algorytmie korzystającym ze zbiorów rozbieżnych muszą być gdzieś zapisywa ne korzenie bieżącego zbioru drzew (lasu), na przykład w tablicy var R'. array[l..nj oi node. W przytoczonych tu algorylmnch pomijamy ten aspekt. procedure ini ti.a l i . za ti on; var x : n o d e ;
i : in te g e r ; b e g in
for i : = 1 to n do begin
new(x) ; size(x) :=1;
p( x ) : = nil end end ;
W algorytmie union przyłączamy mniejsze drzewo do większego. procedure u n i o n ( x , y : n o d e ) ; begin i f s i z e l x ) > s iz .e(y) then x < - > y ; p{x)
:=
y;
s i z e (y) :=
1j
s i z e ( x ) -I- s i z e ( y )
end ;
Koszt operacji union jest 0(1).
Li
L em a t 4.1. (1) Każde drzewo o wysokości /; ma co najmniej 2* węzłów. (2) Każde drzewo o n węzłach ma wysokość co najwyżej log n.
O dw ód :
Stwierdzenie (2) wynika bezpośrednio z (I). Dowód (I) jest indukcyjny wzglę dem liczby wykonanych operacji anion. Po zainicjowaniu wszystkie drzewa są jednoelementowe i mają wysokość 0. Załóżmy, że drzewo o wysokości /», i licz bie wierzchołków //, przyłączamy do drzewa o wysokości h2 i liczbie wierzchoł ków tu («., < u-,). Gdy li, > //,, wysokość się nie zwiększa. Gdy lu K /», wyso kość nowego drzewa zwiększa się do //, + I. Korzystamy z założenia indukcyj nego »», > 2*' i ii, > 2'K\ otrzymując n, -l- «, > 2n s > 2 • I 1'1 > 2*1+ 1 cbdo 1
150
4. Z łożone stru k tu ry d an ych dla zbiorów elem en tów
Rys. 4.4. Wynik kompresji ścieżki
Operacja Jind(x) polega na przejściu od danego węzła x do korzenia drzewa, z przekaza niem korzenia jako wyniku operacji. Z lematu wynika, że drzewa maja wysokość loga rytmiczną, a więc także koszt operacji find jest logarytmiczny. Przy okazji przechodzenia ścieżką do korzenia można spłaszczać bieżące drzewo, przyłączaj.p' wszystkie wierz chołki ścieżki bezpośrednio do korzenia drzewa (rys. 4.4). Ta dodatkowo wykonywana operacja nazywa się kom presją ścieżki. function f i n d { x : n o c i e ) : no c i e ; begin if p ( x ) ---nil then, f i n d x else begin p { x ) : find (p (x) ) ; find
p(x)
end end;
.lako ćwiczenie pozostawiamy Ci wyeliminowanie rekursji z, powyższego algorytmu. Ponieważ wysokość tworzonych w wyniku operacji union drzew jest rzędu logarytmi cznego, pesymistyczny koszt operacji find jest (9(log/i). Okazuje się jednak, że koszt zamortyzowany pojedynczej operacji find jest prawie stały, ii mianowicie jest rzędu wielkości funkcji log';/ = min{7: log<,)/; < 1} gdzie log0’/* oznacza /-krotne złożenie funkcji log, na przykład log* 2<,55:'6 = 5, log'22h:,5;’6 -■ 6. Dla rozmiarów danych, dlii których stosuje się strukturę zbiorów rozłącz nych, log* jest funkcją „praktycznie” stalą (o wartości mniejszej lub równej 5). Udowod nimy teraz takie oto twierdzenie.
4.1. Problem sum ow an ia zb iorów rozłącznych
jjjj
151
T w ie r d z e n ie 4.2. (Twierdzenie Hopcrofta-Ullmana) Koszt ciągu operacji initialization, a następnie - przemieszanych -n - 1 opera cji imion i /// operacji find jest ()((m + //) log"//.).
D ow ód :
Dowód wymaga wprowadzenia kilku pomocniczych pojęć. Przez /-tą warstwę lim kej i log" będziemy rozumieć Aj = {n: log'/i = /] Zauważmy, że liczba u należy do warstwy Aj,,,,,,, oraz że log’log// < log1// - I czyli że liczba logn należy do warstwy o numerze nie większym niż log"//. I Potrzebna nam będzie jeszcze własność charakteryzująca warstwy funkcji log'. Rozważymy mianowicie funkcję /•", która w przeciwieństwie do funkcji log'/; rośnie bardzo szybko. | /•(()) = 0 1 /''(/z) = 2 '1" " 11
dla // > 0
Z, przedstawionych definicji wynika, że log*// -- / c: /'"(/) < //.< /■'(/ + I) dla / > 0 oraz / —0, gdy n ~ 0, 1 Stąd otrzymujemy następującą własność warstw Aj: | Aj, = {(), I } l Aj = {//: F(i) < n < F(i -I- l)}
dla / :> 0
innym ważnym pojęciem potrzebnym w dowodzie twierdzenia Mopcrofta-Ullmana jest pojęcie: rząd wierzchołka. Rzędem wierzchołka x, co oznaczamy jako r(x), nazywamy wysokość wierz chołka x w drzewie uzyskanym w wyniku wykonania danego ciągu operacji initialization, imion i f i n d . bez stosowania kompresji dla operacji linii.
□ PuZYKi.AD:
Prześledźmy działanie algorytmu prowadzącego do wyznaczenia klas abstrakcji przy użyciu drzewowej struktury danych. Niech n — 16, (..’= { (!, 2), (ó, 4),
1.52
•!. Złożone struktury danych dla zbiorów elementów
(2, 4), (5, 0), (7, 8), (6, ii). (4, ii), (9. 10), (II, 12). (10, 12), (13, 14), (15 (14, 16), (12, 16), (I, 5), (10, i), <9. 3)J. Na rysunku 4.5 jest przednim końcowe drzewo, olrzymrme bez stosowania kompresji ścieżek w operat yiW. Wysokości wierzchołków w tym drzewie to ich rzędy.
R/.ail wierzchołków
Jiys. 4.5. Drzewo utrzymane licz sin.smvnni.'i .'.ompresji ścieżek
Na rysunku 4.6 dla odmiany widać drzewo otrzymane z zastosowaniem kom presji ścieżek w operacjach.//W , łącznie zc w szystkim i krawędziami tworzony mi w trakcie wykonywania algorytmu. Wierzchołki sq narysowane na w ysoko ści odpowiadającej ich rzędom. W każdej chwili wykonywania ciągu operacji fin d i imion rzędy wierzchołków mają następujące własności: v dla każdego wierzchołka ,v rząd r(x) jest na początku równy O, a następnie zwiększa się o I do czasu, aż /Hx) stanie się różne od nil; kiedy do tego dojdzie, r(x) nic ulega już zmianie; ^ dla każdego wierzchołka x nie będącego korzeniem r(x) < rt/>(x)); ■-> dla każdego wierzchołka .r nie będącego korzeniem rząd /■(/>(x)) zwiększa się. przy każdej zmianie />(.tj; «> dla każdego k. > 0 liczba wierzchołków o rzędzie k wynosi co najwyżej n/21'; " liczba wierzchołków w drzewie o korzeniu x wynosi eo najmniej 2'lx>;
4.1. Problem sum ow an ia zbiorów rozłącznych
153
Rząd wierzchołków
Rys. 4.6. Wszyslkit: krawędzie zoslaly utworzone z zastosowaniem kompresji śeie/.el;
• rząd każdego wierzchołka wynosi co najwyżej log/?; <' rzędy wszystkich wierzchołków znajdują sie w warstwach o numerach od !) do log1// — I. Korzystając z podanych powyżej własności warstw funkcji log' i rzędu wierz chołków przeprowadzimy dowód twierdzenia Hopcrofta-Ullmana. Zastosujemy l/.w. metodę księgowania kosztu. Każdej operacji przyporządkowujemy pewną liczbę kredytów do pokrycia kosz tu jej wykonania. Operacja initialization dostaje n kredytów, union - I, a find - 1 + log’//. Jeśli find wymaga większego kosztu, musi uzyskać dodatkowy kredyt z banku kredytowego. Dpdąlkowynt kredytem są obciążane wierzchołki. Wystarczy pokazać, że łączna ilość dodatkowego kredytu, którym są obciążane, wierzchołki, jest 6>(//log*//). Inaczej mówiąc, ze strukturą danych wiążemy potencjał 0(/ilog*//) na opłacanie „nadmiernych” kosztów wykonania niektórych operacji find. Przyjmujemy, że operacja find wykorzystuje przede wszystkim przyporządkowany jej koszt, tzn. koszt zamortyzowany, a jeśli lego kosztu nic wystarczy, to część brakująca jest pokrywana z ogólnego potencjału struktury danych.
154
4. Z łożone stru k tu ry d an ych dla zbiorów elem en tów
Z kosztu przydzielonego operacji find jest pokrywany koszt rozpatrzenia korze nia i jego bezpośredniego następnika oraz koszt rozpatrzenia każdego wierz chołka u na ścieżce związanej z realizacją find - takiego, że oba rzędy r(v) i r(p(v)) należą do różnych warstw. Łączny koszt przydzielony operacji find jest zatem 0(in\og‘n). Jeśli natomiast oba rzędy r(v) i r(p(v)) należą do tej samej warstwy, to jednostką dodatkowego kredytu obciążamy wierzchołek v (rys. 4.7). Wierzchołek, którego rząd r(v) leży w /-tej warstwie, może być obciążany jednostką dodatkowego Rząd wierze 11oIkó w
Warstwa A
Rys. 4.7. Rozdział kosztu rozpatrywania krawędzi wychodzących z danego wierzchołka
kredy iu co najwyżej F ( i + I) - /■'(/) - I razy (I raz, gdy / = 0), ponieważ war tość r(p(v)') zwiększa się przy każdej kompresji v do korzenia. Obciążenie do datkowymi kredytami wierzchołków, których- rzędy Jeżą w /-tej warstwie, wy nosi razem co najwyżej ® n il • 0
dla / = 0 dla / = I IV i i)
• (/'’(/ + I) -
F (i)
- I)
Z q -■/•"(/) -ł
ll
ll 2‘‘
n < /''(/ + I) 9 /■'(») i T"
i
dla / > I
Sumując po wszystkich warstwach, otrzymujemy oszacowanie, całkowitego kre dytu, którym są obciążane wierzchołki:
155
4.2. Złączał ne kolejki priorytetowe ii
—... I-
l!A' 1, n i >
0(/ilogl/()
|
Dowód twierdzenia Hopcrofta-UIimana został w ten sposób zakończony.
4, 2. Z lą c z a ln e k o le jk i p r io r y te to w e Zbiory informacji przechowywane w komputerze maju czasami postać zlączalnych ze sobą kolejek priorytetowych (elementem usuwanym ze zbioru jest element o naj mniejszym bądź największym priorytecie). O kolejce priorytetowej - jako rozwiązaniu dynamicznego problemu sortowania - była już mowa w rozdziale 2. Użycie kopca zape wnia koszt
utworzenie zbioru pustego .V= 0 ; S a-- .VWJ {.a:}; usunięcie z S' najmniejszego elementu (tzn. o najmniejszym klu czu, ewentualnie jednego z najmniejszych, gdy jest ich kilka); wyznaczenie najmniejszego elementu w S (bez usuwania go);
(d) findinin(S):: (e) deiete(x, .Si:: ■V: ■ I.vj: (0 decreasc(x, k, SY.\ deletc(x, .S'); key(x) :- k\ inserf(x, S) (pod warunkiem, że k < keyix));" (g) M//ńi/i(.V,, .V,, S):: S :- .Sj u .S', przy założeniu, że ój i .S', są zbiorami rozłącznymi.
Zauważmy, że implementacja kopcowa zastosowana w algorytmie heapsorl. (w podroz dziale 2.6) zapewnia czas O(logn) dla wszystkich operacji z wyjątkiem (g). Zanim żuci i.iiujcmy kolejkę dwumianową, przytoczymy kilka innych potrzebnych pojęć.■ '* Operacja
d e c re a se
jesl .szczególnym przypadkiem operacji
c h a n g e
z podrozdziału 2.6.
156
4. Z łożon e stru k tu ry dan ych dla zbiorów elem en tów
Drzewem dwumianowym fit stopnia k > 0 nazywamy (a) (liz.cwo />’„ składające się. z pojedynczego wyżła; (IV) drzewo Bk powstające z dwóch drzew Iik , przez dołączenie korzenia jednego z nich jako następnika korzenia drugiego (rys. 4.8). I{i;:
/
( r i. -i
IU's. 4.8. Slrufchira drzewa /\-
-}
\ i
( r
( )
( )
1
B j : (..>
!
-B:iS ..)
liys. 4.V. Przyklndy drzew dwmniimowycli dla /. - 0, I. 2, 3
Przykłady drzew dwumianowych dla k = 0, 1, 2, 3, widać na rysunku 4.9. Drzewa dwumianowe Bt (k > 0) mają następujące własności: liczba węzłów w drzewie Bt wynosi 2k\ ■> wysokość drzewa Bk wynosi k; >-■ w drzewie B, jest dokładnie
węzłów o głębokości i dla 0 < i < k\ [i ) >•’ maksymalny slopień węzła w drzewie Bk wynosi k\ drzewo B, daje się pr/.edslawić tak jak na rysunku 4.10.
'X . Rys. 4.10. Rozwiniecie drzewu /Z
11
r 157
4.2. ZiącKn.'ns- kolejki p rio ry teto w e
(6) Rys. 4 .!!. Drzewo dwumianowe
/. olemcniami w porządku kopcowym
Do drzew;: dwumianowego elementy wpisujemy zgodnie z porządkiem kopcowym, z l:ym żc najmniejszy element umieszczamy w korzeniu (rys. 4,1 I). Kolejką dwumianową nazywamy zbiór drzew dwumianowych spełniających następują ce warunki: (a) każde, drzewo dwumianowe ma uporządkowanie kopcowe; (b) dla każdego k jest co najwyżej jedno drzewo dwumianowe stopnia k. Z definicji wynikają następujące własności kolejki dwumianowej dla n-elementowego zbioru S : ® w kolejce jest / drzew dwumianowych stopnia odpowiednio L lo g u j = A', > lc? > ... > k, > 0 gdzie u = 2*' + 2*2 + ... + 2*': ® liczba drzew w kolejce, dwumianowej wynosi co najwyżej Llog/iJ -I- I; ® każdy węzeł ma stopień co najwyżej Llo g u j;
o najmniejszy element znajduje się w jednym z korzeni drzew dwumianowych. Przyjmujemy następujący schemat węzła w kolejce dwumianowej (zarówno w drzewie dwumianowym, jak i w kolejce korzeni drzew dwumianowych): kt:y(x) - wartość klucza elementu x, l>(.\) - poprzednik .v, cliild(x) • pierwszy następnik .v, sihling(x) •- kolejny sąsiad ,v (mający ten sam poprzednik), gdy x nie jest korzeniem, bądź korzeń następnego drzewa dwumianowego w kolejce, gdy x jest ko rzeniem,
I
158
'I. Z łożone struktury d an ych dla zbiorów elementów
5 I
10 0
Rys. 4.12. Przykład kolejki dwumianowej i jej reprezentacji
Na rysunku 4.12 jest przedstawiona przykładowa kolejka dwumianowa i jej reprezenta cja) w postaci struktury dowiązaniowej. Zauważmy, że połączenie dwóch drzew Bk _ , w drzewo Bk daje się wykonać w stałym czasie. Naszkicujemy teraz implementacje operacji kolejki priorytetowej dla kolejki dwumiano wej, pozostawiając Ci zapisanie ich w całości.• • uni.on(S,, S2, S):: Połącz drzewa dwumianowe z kolejek S t i S2, naśladując binarne dodawanie liczb, tak jak to widać w przykładzie przedstawionym na rysunku 4.13; czas wykonania O(logz)). • findmin(S):: Przejdź kolejkę korzeni drzew dwumianowych i wyznacz korzeń o naj mniejszym kluczu; czas wykonania 0(log n).
4.2. Ziąezalne kolejki priorytetowe
Rys. 4.13. Leczenie kopców z wykorzystaniem dodawania liczb binarnych
159
4. Złożone stru k tu ry d a n y c h dla zbiorów elementów inserl(x, ,V):: Utwórz jednoelementową kolejkę dwumianów;) o,, (jedynym węzłem jest .v) i używając union, połącz ją z .S’; czas wykonania ćjlog//). *> ilelelcinin(S):: fii) Wyznacz korzeń x drzewa dwumianowego T z najmniejszym kluczem. Usuń T z głównej listy, otrzymując kolejkę, dwumianową S'. (b) Usuń x z T. Odwróć porządek na liście następników ,v, tworząc kolejkę dwumiano wą S". (e) Wykonaj union(S', S", Aj. Przykład działania lego algorytmu jest przedstawiony na rysunku 4,14. Operację (a) można wykonać w czasie <9(log//), ponieważ lista korzeni kopców ma długość co naj wyżej Llog n J I- I. Operację (b) leż daje się wykonać w czasie £ż(log n), ponieważ każdy węzeł mti co najwyżej L log n i następników. Operacja union, jak pokazaliśmy wcześniej, również daje, się wykonać w czasie O(logu). Litezny zatem czas wykonania operacji deleleinin jest O(logn). drcreasckey(x, k, Aj:: ( a) k e y ( a j : = k;
(b) Wykonaj openttję podniesienia x w drzewie dwumianowym tak jak dla zwykłego kopca (z algorytmu heapsort). Przykład działania tego algorytmu jest widoczny na rysunku 4.15. Ścieżki do korzenia kopca mają długość co najwyżej I. log;/ J, a zatem koszt tej operacji też wynosi 0(log//). Operacja delete sprowadza się do wykonania najpierw operacji decrettsekey (zmniej szenia wartości klucza w węźle ,v do -«>), a następnie wykonania operacji deleleinin (usunięcia najmniejszego elementu Mamy więc: •j delrie(x, Aj:: (a) decreasekeylx, -•», Aj; (b) tleleieinin(S). Wynika stąd, że koszt wykonania operacji delcie leż jest 0(log//). Udowodniliśmy w len sposób, że używając kolejek dwumianowych, każdą z operacji kolejki priorytetowej - insert, findiniii, deleleinin, imion, decreusekcy i delcie ~ można wykonać w czasie 0(log/i). Kolejki priorytetowe mają zastosowanie przy rozwiązywaniu wielu problemów. W rozdziale dotyczącym algorytmów grafowych dowiesz się, że używa się ich do wyznaczania minimal nego drzewa rozpinającego dany graf i do wyznaczania ścieżek o najmniejszej długości, łączących dany wierzchołek z. pozostałymi. Korzysta się z nich i ■ - co ważne - przy wyznaczaniu optymalnego kodu dwójkowego dla alfabetu, w którym symbolom przyporząd kowano częstości ich wystąpień w komunikatach (zoli. rozdział o algorytmach tekstowych).
4.2. Zląezalne kolejki priorytetow e
161
tlcki<'iriiii(S): :,y
:V "!...........................
(7)
... / (8)
.
_/ (.1)
(2)
1 9)
(4 i
:^]0i.......
:x3)
M 21.......... M 5'
I
(4)
(7)
(10)
1
(8)
(
\
(O'
20)
==>#'
(S )
(20>
(9 )
( i )
Rys. 4 . 1 4 . Usunięcie najmniejszego elementu /.osiuje sprowadzone do złączenia dwóch kolejek dwumianowych
Witrlo jeszcze wspomnieć o strukturze danych, będącej modyfikacją kolejki dwumiano wej. Chodzi o kopiec Fihonttceiego. Zamiasl w operacji union od razu łączyć poszczegól ne drzewa w kolejce, łączy się jedynie listy. Do właściwego złączenia drzew dochodzi dopiero '.-•lody, kiedy trzeba wykonać operację de leiamin. Otrzymujemy strukturę danych,
162
4. Złożone struktury danych dla zbiorów elem en tów
decrease (x, 2, S ):
Rys. 4.15. Operacja d e e r e a s e k e y po lega na podniesieniu węzła w drzewie dwumianowym
w której koszt zamortyzowany wszystkich operacji kolejki priorytetowej jest 0(1). Wy jątek stanowią operacje, delełemin i delete, których koszt jest Otjog/;). Zainteresowanego czytelnika odsyłamy do książki [CLRJ.
4.1. Korzystając z 2-3 drzew (zdefiniowanych w zadaniu 3.15), opracuj implementację kolejki priorytetowej, w której każda z operacji insert, delełemin, union, delete i change daje się wykonać w czasie 0(log/;). 4.2. Zaproponuj efektywną strukturę danych do wykonywania ciągów podanych tu ope racji na dynamicznie zmieniającym się podziale zbioru [1, 2, /;}: (a) initialization:: (b) union{A, B):\
utworzenie podziału {{!}, {2}, {/«}}; połączenie dwóch zbiorów A i B bieżącego podziału w jeden;
163
Z adania
(c) find(x):: (d) findmiii::
wyznaczenie zbioru, ilo którego należy element ,v; wyznaczenie zbioru bieżącego podziału, który ma najmniej elementów.
.laka jest złożoność operacji? 4.3.
Używając algorytmu z przykładu z początku rozdziału, wyznacz spójne składowe grafu G = (Iż, Ii), gdzie: \ż = {1, 2, 3, 4, 5, 6, 7, 8, <■), 10) /■: = {(I, 2), (3, 4), (5, ó), (7, 8). (I, <)), (10, 3), (4, 7), (8, 10)} Wykonaj obliczenia, raz posługując sip implementacją listową, a raz posługując implementacją drzewową z kompresją ścieżek w operacji find.
się
4.4. [AHU| Zaprojektuj strukturę danych, reprezentującą zbiory drzew (lasy), w której można efektywnie wykonywać następujące operacje: (a) iniiialization(n):: utworzenie a jednoelementowycht drzew; zapisanie elemen tu i w korzeniu /-tego drzewa; (b) link(x, >•’):: przyłączenie krawędzią wierzchołka ,v (będącego korzeniem) do wierzchołka y (znajdującego się w innym drzewie niż x); (c) deplh(y):: obliczenie głębokości wierzchołka y. Ciąg k operacji link (k < n - I) i iii operacji depth powinien dać się wykonać w cza sie 0((ni -I- /ijlog*//). 4.5. [AUUj Zaprojektuj strukturę danych, reprezentującą zbiory drzew (lasy), w której można efektywnie wykonywać następujące operacje: (a) initiaUzalioii(n):: utworzenie n jednoclementowych drzew; zapisanie elemen tu i w korzeniu /-tego drzewa; (b) link(x, y):: przyłączenie krawędzią wierzchołka x (będącego korzeniem) do wierzchołka y (znajdującego się w innym drzewie niż ,v); (c) lc n (x , y):: wyznaczenie najniższego wspólnego przodka .v i y. Ciąg k operacji link (k < n -- I) i ni opornej i lai powinien dać się wykonać w czasie OUm •! n)log‘«). 4.6. Udowodnij, że w wypadku implementacji drzewowęj z kompresją ścieżek koszt wykonania ciągu operacji: initialization, n — I operacji union, a następnie iii opera cji find, jest OOr, + ii). 4.7. Dla danej wartości n skonstruuj ciąg operacji union tworzących drzewo om - I elementach i wysokości |_ log u ..!.
"ó T icch / będzie pewnym ustalonym skończonym zbiorem symboli (liter). O ile nie \ jest to wyraźnie zaznaczone, przyjmujemy na ogól, że zbiór / jest: ograniczony przez pewną stalą. Zbiór len nazywamy alfabetem, a ciągi symboli - tekstami lub słowami. Długość tekstu (o liczba jego symboli. W szczególności tekst o długości zero sldada się z zerowej liczby symboli; nazywamy go słowem pustym. Długość tekstu ,v oznaczymy przez |,v|. Dwie typowe reprezentacje tekstów l.o reprezentacja listowa i reprezentacja tablicowa. Reprezentacja listowa jest użyteczna zwłaszcza wtedy, kiedy dokonujemy operacji wsta wiania i usuwania części tekstu. Reprezentacji) tablicowi) jest bardziej naturalni) dla statycznych operacji, polegających na sprawdzaniu pewnych własności lub obliczaniu pewnych parametrów dla jednego lub wielu tekstów. Przyjmiemy drugą reprezentację, ponieważ w rozdziale tym będziemy się zajmować właśnie tego typu problemami. 'Danymi wejściowymi będzie jeden lub kilka tekstów. Jako rozmiar u danych wejścio wych będziemy przyjmować maksymalną długość tekstu, jeżeli będziemy mieć do czy nienia z co najwyżej dwoma tekstami, lub sumę długości tekstów wejściowych w wypad ku większej liczby tekstów. Podstawowym problemem dotyczącym tekstów jest problem wyszukiwania wzorca. W dalszych rozważaniach będziemy go nazywać WW. Problem WW polegli na znalezieniu wszystkich wystąpień tekstu ,v, zwanego wzorcem, w tekście v. Przyjmujemy, że |.\-| = ni, |y | = n oraz n > ni. Problem WW łatwo jest uogólnić na przypadek dwuwymiarowy. Teksty x i y są wtedy tablicami składającymi się z symboli, a naszym zadaniem jest znalezienie wszystkich wystąpień ,v jako podlabiicy y.
5.1. P ro b le m w y sz u k iw a n ia w z o rc a
165
Problem VVW daje się rozwiązać w czasie liniowym ze względu na całkowity rozmiar wejścia. (W przypadku dwuwymiarowym zakładamy, żc tablice ,v i y są kwadratowe, a więc rozmiar wejścia jest 0(n2)). Wyszukiwanie wzorca było wszechstronnie badane ze względu na duże zastosowanie praktyczne. Przedstawimy kilka algorytmów dla problemu WW: N (algorytm „naiwny” ; bezpośredni naturalny algorytm o złożoności teoretycznie zbyt dużej), KMP (algorytm Knutlia-Miorrisa-Pratta), KMR (algorytm Karpa-M il lera-Rosenberga), KR (algorytm Karpa-Rabina), GS' (uproszczona wersja algorytmu Galila-Seiferasa, działająca jedynie, dla szczególnej klasy wzorców) oraz BM (algorytm Boyera-Moore’a) i jego modyfikację BM', działającą w czasie oczekiwanym rzędu istotnie mniejszego niż liniowy. Omówimy ponadto rozszerzenie algorytmu KMP na przypadek wzorców dwuwymiarowych. Każdy z tych algorytmów ma pewne wyróżniające go własności. Najbardziej skompliko wanym algorytmem dla problemu WW jest algorytm Galila-Seiferasa, który minimalizu je jednocześnie koszt czasowy i pamięciowy. My zajmiemy się pewną prostszą jego wersją algorytmem GS' (pełną znajdziesz, w książce [BKR]). Rozważymy również problem WW' dotyczący wyszukiwania wzorca z .symbolami nieznaczącymi (symbole takie pasują do każdego innego symbolu). Przedstawimy algorytm FP (Fishera-Patersona) rozwiązania dla tego problemu.
O o 1L e
Problem, wyszukiwania wzorca Nieci) ,v będzie wzorcem, a y tekstem, w którym szukamy wzorca. Dla słowa z przyj mijmy <;(/.../! = z[i)z[i -l- I ]••?.[/], gdzie i < /. Mówimy, że x występuje w y na pozycji /, gdy y\i..i -I- w — 11 -- .v -- inaczej mówiąc, gdy począwszy od /-tej pozycji, wzorzec „pa suje” tekstu. Z lego powodu problem WW jest również nazywany problemem dopa sowywania wzorca.
5.1.1. A lg o r y tm N ( „ n a iw n y ”) Schemat: najbardziej bezpośredniego algorytmu, zwanego „naiwnym” , wygląda nas tępująco: begin i := X; while i < Ti - ni -l- 1 do begin i f x [ l . .ruj = y|i . . i -I- m - 1] then w r ite (i) ; i := i + X end ; end;
166
5. A lgorytm y tek sto w e
Pełny algorytm otrzymamy, rozpisując instrukcję sprawdzenia równości tekstów. Przyjtnijmy dla uproszczenia, że x[m -1- 1] i yf/t -I- 1] są specjalnymi, różnymi symbolami. Zabezpieczamy się w ten sposób przed błędem wynikającym z przekroczenia zakresu tablicy. Przyjmijmy również następujące założenie: jeżeli w czasie sprawdzania warunku logicznego a zostaje przekroczony zakres tablicy, warunkowi a jest przypisywana war tość false i obliczenie jest kontynuowane. I | {Algorytm N (algorytm „naiwny"; zgodność wzorca jest sprawdzana od
początku wzorca} begin i := 1; while i < n — m -I- 1 do begin j := 0; while >:[;/ -I- 1] = y [ i -I- j ] do j ;= j + 1 ; if j = m then write (i ) ; i :== i -I- 1; {przesunięcie - 1] end end [algorytm M} ;
Liczba wykonanych porównań symboli może być rzędu n \ na przykład dla .v = 0"/2l i y - O'1" 11 (w tym wypadku m = ul2 -1- 1; załóżmy, żc n jest parzyste). Jednakże średnia złożoność algorytmu N jest liniowa. Oszacujmy ją dla przypadku, gdy allabel jest dwu literowy (dla większej liczby liter jest analogicznie). Niech teksty x i y będą losowymi tekstami. Wówczas prawdopodobieństwo, że test ,v[/ -I- 1] = y[i. + j -l- 1] będzie pozytywny wynosi 1/2. Łatwo już teraz policzyć, że ocze kiwana liczba takich testów, które wykonamy dla ustalonego /, nie przekracza 2. Średnia złożoność (mierzona liczbą porównań) algorytmu N nie przekracza zatem 2n.
5 . 1 .2 ,
A lg o ry tm KMP (K nutha-M orrisa-Pratta) W algorytmie N początek wzorca przesuwamy zawsze o jeden, podczas gdy moż liwe jest czasami dużo większe przesunięcie. Taką wielkość przesunięcia można ob liczyć, korzystając z informacji o maksymalnej wartości j (w algorytmie informacja ta w ogóle nie jest wykorzystywana przy dopasowywaniu wzorca do następnej pozycji /; wartość j jest zerowana). Niech P\j\, gdzie j > I, będzie, maksymalną długością właściwego sufiksu słowa ,v| l../|. będącego jednocześnie jego prefiksem. Formalnie /■’[/] -- max {() < k < j: aj l../y| jest sufiksem ,v[l../j}. Przyjmujemy P[l | = /-’[OJ = 0. Jeśli tablica P jest już policzona, to problem WW można efektywnie rozwiązać za pomocą poniższego algorytmu KMP, którego struktura jest
167
5.1. Problem w y szu k iw a n ia w zorca
podobna do simktury algorylmu N. Wartość przesunięcia początku wzorca w y jest określona teraz wzorem: przcsunięciiij) = rnax(1,y - / ’[/]). {Algorytm KM? (Knutha-Morrisa-Pratta)j bogiń i := :l; j : 0 ; while i < n - in + 1 do M - ł . . / ’| / | |
:.j 7 -
P | j | i- 1 . .
j]
• • V-|
i . . :i.
r
P[j]
- 1 ]}
begin j := /-I /I; while x [ j -I- I |---y|i -I j ] do j if j = m then w r i t e (i ) ; i m i +prse:i.iiiiecie(j) ;
j I 1;
end and {algorytm KM?} ;
W algorylmie tym jest wykonywanych co najwyżej 2n porównań symboli. Wystarczy oszacować liczbę wykonań instrukcji j :---j -l- 1. Jeśli tą liczbą jest p , to wykonujemy co najwyżej n -i- p porównali (liczba p odpowiada liczbie wszystkich porównali mających wynik pozytywny; liczba porównań z wynikiem negatywnym jest: oczywiście ograniczo na przez n - in -I- 1). Rozważmy wartość sumy s = i + j. Wartość ta nic zmniejsza się (obserwowana tuż przed wykonaniem porównania). Największą możliwą wartością .v jest n, a za każdym razem, gdy wykonujemy j := j -t- I, ,v zwiększa się o 1. Instrukcja j := j + I jest zatem wykonywana co najwyżej n razy. Złożoność (pesymistycznego przypadku) algorytmu KMP jest liniowa. Pozostaje jeszcze problem obliczenia tablicy P. Dla każdego / > 2 obliczymy / j/|. korzystając /. następującej obserwacji: niech i > I będzie minimalne, takie że AjPu,U -• 11 + 11 = ,\j/|. Jeśli takie i istnieje, to P\j\ —PU)\j — 11 -I- I; w przeciwnym ra zie P\j\ — 0 (gdzie Pu) oznacza /-krotne złożenie fu u kej i P). {Algorytm obliczania tablicy /’) begin .P[0 | P|l] := 0; I: := 0; for j := 2 to uido begin {obliczamy wartość P|iJ] It - P|j - l|| while (/; > 0 ) and (/: I 11y x| j|) do I: : Ijij ; i .i: ;,jt + 1| = x[g| then (: : t -I- 1; P|;/] := t; end end {algorytm obliczania Pj;
Złożoność lego algorylmu można oszacować, stosując zasadę magazynu. Ilekroć wyko nujemy / := 7j7], pobieramy z magazynu niezerową liczbę przedmiotów / - /j7|. Do
1.68
£>. A lg o ry tm y te k s to w e
magazynu wkładamy jeden przedmiot, gdy wykonujemy / :-= / -i- I. Początkowo magazyn jest pusty. Inaczej mówiąc, wartość / interpretujemy jako liczbę przedmiotów w pewnym magazynie. Oczywiście w sumie włożymy co 'najwyżej m przedmiotów, a zatem wyj mować będziemy je również co najwyżej ni razy. Wynika stąd, że liczba wykonań instrukcji t / j / | nie przekracza ni, a więc złożoność całego algorytmu jest liniowa względem ni. Tablicy /■’ można użyć w sposób bardziej bezpośredni (chociaż bardziej abstrakcyjny) do rozwiązania problemu WW. Weźmy tekst // —.v$y, gdzie $ jest specjalnym znakiem występującym tylko na jednej pozycji. Obliczmy tablicę / ’ dla tekstu u. Związek P\i + ni + 11 -- ni zachodzi zatem wtedy i tylko wtedy, gdy ,v występuje w y począwszy od pozycji i - ni -I- 1 dla ni < i < n. Algorytm KMP nm bardzo istotną zaletę, a mianowicie jego złożoność nie zależy od rozmiaru alfabetu wejściowego (jest liniowa ze względu na //, i to nawet wtedy, kiedy aliabel ma rozmiar rzędu //). W oryginalnej wersji ałgorylmu KMP zamiast tablicy / ’ używa się nieco bardziej skom plikowanej (do zdefiniowania i obliczenia) tablicy NEXT, w której NEXT\j] jest ma ksymalną długością k właściwego suHk.su słowa .vj I../), będącego jednocześnie jego pre fiksem (jak dotychczas te same własności spełniała wartość P\j\), i taką, że ,r|/c -i- 11 Z ,v|/ -I 11, jeśli 0 < / < ni. Przyjmijmy .V/ó\'7 j()J = /j()| = 0. Załóżmy, że w chwili zwiększania j wczytujemy kolejny symbol tekstu wejściowego y. Podobnie jak porzodnio przyjmujemy, że x[in -l- 1] jest nowym specjalnym symbolem (różnym od #). Przez KMP' oznaczmy taką wersję algorytmu KMP, w której tablicę P zastąpiono lablict! NEXT. [Algorytm KMP' - vie rsja on-line algorytmu KMP; „na wejściu" j e s t ciąg n kolejnych symboli te k s tu y, cjalnym symbolem II j begin
z a
kończony spe
i := 1; j := 0; read (symbol) ; •wbiła s y m b o l Z ' II' do begin j
: = ./ V G a T |j |;
■ w hile .'d i -1- :l.| =.-. s y m b o l d o b e g i n j ■- j -I- 1 ; r e a d ( s y m b o l . ) e n d i if j = 0 then r e a d ( s y m b o l ) ; i f j ~ m th e n w r i t e (1) ;
i
i -I- mar (1, j - MEX'Ąj\) I
and e n d {algorytm KMP'};
5.1. Problem w yszu k iw a n iu w zorca
169
Poprawność i rząd złożoności algorytmu nie ulega zmianie. Algorytm KMP' jest trochę szybszy (dla pewnych danych). Ma pewną zaletę. użyteczną w sytuacjach, kiedy cały tekst y nie jest dany od razu. a mamy jedynie kolejne jego symbole, on-line. Kolejne symbole tekstu y pobieramy w czasie wykonywania instrukcji read{symbol). Interesuje nas odpowiedź na pytanie: ile maksymalnie czasu upływa między kolejnymi instrukc jami czytani;) read(syinbol). Oznaczmy ten maksymalny czas przez delayOn), a przez dalayl’ini) -- maksymalny czas przerwy między czytaniami, gdyby zamiast NEXT byk\ wykorzystywana poprzednia tablica P. Załóżmy dla uproszczenia, że alfabet wejściowy jest dwuelementnwy. Drastyczną różnicę między wielkością dcltiyim) a delayP(in) widać na przykładzie. x y - a " '~ 'b a . Załóżmy, że wczytaliśmy właśnie symbol ’!>’ i że j = in - I. Manty wtedy NEXT\in - lj = 0, a więc kolejne czytanie odbywa się w na stępnym przebiegu instrukcji ..dopóki” (w h ile). Gdybyśmy zastosowali tablicę / ’, to zanim otrzymalibyśmy j = 0, musielibyśmy wykonać liniową liczbę iteracji. Wynika stąd, że delayP(ni) = Jeżeli jednak używamy tablicy NEXT, to mamy di’lay(in) — D(log ni) Dla pewnych wzorców zachodzi ponadto delay(m) = Q(log/n). Przykładem takich wzor ców są słowa Fibonacciego (zdefiniowane dalej). Dla wzorców Fibonacciego o długości oszacowanie delay(ni) = £ł(logw») wynika ze wzoru: N E X T \F k
- IJ =
Fk
- I
gdzie P\ jest /c-lą liczbą Fibonacciego (/;j = l<\ = I, l<\ .,. = Fk ., , -i- F,). Oznaczmy fc-te słowo Fibonacciego przez fibk. Niech /(/>, = b, fib-, = a i niech fihk ., , = fibk ,. !fibb
dla k > 0
Mamy wtedy j7/g == ab,fibĄ = abti,/ibf = abaab, fihb —ahaababa. Słowa Fibonacciego mają wiele ciekawych własności. Po obcięciu na przykład dwóch ostatnich symboli stają się słowami symetrycznymi. Zachodzi również równanie Jll’k Ifil’l: ■■/il'J'i'; ,. , z dokładnością do zmiany kolejności dwóch ostalnich symboli.
5.1.3. A lg o ry tm , lin io w y d la problem u w y s z u k iw a n ia w z o rc a
d w u w y m ia ro w e g o , czyli a lg o ry tm Balcera Pokażemy zastosowanie algorytmu KMP do rozwiązania problemu szukania wzorca dwuwymiarowego. Istotną własnością algorytmu KMP będzie tu niezależność kosztu od rozmiaru alfabetu (alfabet będzie rzędu m).
170
5. A lgorytm y tek stow e
Załóżmy, że mamy dane tablice x: a rray f 1 I ,.m] of char i y. a r r a y [1 ..n, i ../tj of char, gdzie n > m. Przypuśćmy, że mamy r różniących się między sobą kolumn lablicy-wzorca ,v. Oznaczmy je przez .fi, x 2, .... -vr. Każda kolumna jest tekstem długości m. Zastanówmy się najpierw nad rozszerzeniem algorytmu KMP na przypadek szukania wieki wzorców. Niech X - {,r„ .v2, x r} będzie zbiorem wzorców o tej samej długości m < n. Dla każdej pozycji i w tekście chcemy nie tylko sprawdzić, czy któryś ze wzorców zaczyna się od tej właśnie pozycji, ale również znać numer takiego wzorca. Reprezentujemy zbiór X drzewem T, którego krawędzie, są etykietowane symbolami, Każdy węzeł odpowiada prefiksowi pewnego wzorca, a prefiks jest dany ciągiem etykiet na ścieżce od korzenia do tego węzła. Korzeń drzewa root odpowiada prefiksowi pustemu, a liście drzewa - wzorcom. Tablicę P definiujemy podobnie jak w algorytmie KMP. Dla węzła a -ż root (będącego prefiksem niepuslym pewnego wzorca) P[v) jest maksymalnym właściwym sufiksem u słowa v, który jest prefiksem pewnego wzorca ze zbioru X. Tablicę P wy znaczamy podobnie jak w przypadku jednowymiarowego wzorca. Koszt jest liniowy względem sumarycznej długości wszystkich wzorców. Jako zadanie pozostawiamy Ci opracowanie dokładnej konstrukcji tego algorytmu. Mając dane drzewo T i tablicę P, szukamy wzorców w tekście y podobnie jak w algoryt mie KMP. Tym razem „dopasowujemy” maksymalną ścieżkę w drzewie T do tekstu. Czytając kolejne symbole tekstu, poruszamy się ścieżką, której etykiety krawędzi od powiadają czytanym symbolom. Jeśli kolejny symbol w tekście nie jest etykietą żadnej krawędzi wychodzącej z bieżącego węzła drzewa T, to stosujemy tablicę / ’. Otrzymuje my algorytm umożliwiający policzenie w czasie O(n) dla każdej pozycji i numeru wzor ca, którego wystąpienie zaczyna się na lej pozycji (lub stwierdzenie braku wystąpienia), .lego twórcami są A. Aho i A. Corasic. Opracowanie dokładnej konstrukcji tego algoryt mu, który będziemy dalej nazywać .algorytmem AC, pozostawiamy Ci jako zadanie. Przejdźmy ponownie do szukania dwuwymiarowego wzorca. Niech y,, _y2........y,. będą kolumnami tablicy-tekstu y. Wprowadzamy nowy alfabet (0, 1, 2, .... r\. Slosnjemy algorytm AC i w każdej pozycji kolumny y,. wpisujemy numer wzorca ze zbioru X ko lumn, który zaczyna się na tej pozycji („chodzimy po” kolumnach z góry na dól). Jeśli wzorzec nie występuje, to wpisujemy 0 i ostatnich m — 1 wierszy tablicy y odcinamy. W ten sposób z tablicy y otrzymujemy tablicę y", której elementami są numery kolumn tablicy x lub zera. Teraz istotnie korzystamy z tego, że algorytm KMP ma koszt niezależny od rozmiaru alfabetu. Niech /j, będą numerami (odpowiadającymi kolejności słów ze zbio ru X) kolumn tablicy ,v. Tworzymy wzorzec x" -=./1./2 W każdym wierszu tablicy / szukamy wzorca x*. Wystąpienie, tego wzorca w Mym wierszu na /-lej pozycji oznacza, że tablica ,v „pasuje” do tablicy y, począwszy od pozycji (/, /), a zatem możemy x przyło żyć do y lak, że lewy górny róg ,v wypada na pozycji (/,/). Pokażemy działanie tego algorytmu na następującym przykładzie. Niech x, y będą (od powiednio) tablicami:
5.1. Problem wyszukiwania wzorca ba ab
171
ab a ba b ti b b
Numerujemy kolumny tablicy ,v przez I, 2. Tablicę reprezentuje lcraz tekst o długości 12 nad alfabeletn [0, I, 2). Szukamy wzorców ba i ab w kolumnach tablicy y i zastępujemy elementy tej tablicy numerami wzorców, które zaczynaj;! się na danej pozycji (z góry na dól w kolumnie). Ostatni wiersz y obcinamy. Otrzymujemy następującą tablicę y"; 2 12 I20 Wierny teraz, że x zaczyna się. na drugiej pozycji pierwszego wiersza i pierwszej pozycji drugiego wiersza, gdyż w tych miejscach występuje wzorzec 12. Całkowity koszt algorytmu jest. O(ir). Algorytm ten nazywamy algorytmem Bak, od skrótu nazwiska jego twórcy T. P. Hakera.
5.i:4. A lg o r y tm G S ' ( w e r s ja algorytm u G alila-Seiferasa dla p e w n e j k l a s y w zorców ) Jednym z najciekawszych algorytmów dla problemu W Wjest algorytm GS (Galila-Neiferasa), który umożliwia rozwiązanie tego problemu wczasie liniowym ijednocześnie w pa mięci 0(1). Jest w nim używanych tylko kilka zmiennych całkowitych (o zakresie jO../i|). Przez pewien czas uważano nawet, że algorytm taki w ogóle nie jest możliwy. Podamy tu pewna bardzo słabą jego wersję, (algorytm GS'). Pełny algorytm znajdziesz w książce j BKRj. Mówimy, że wzorzec x jest łatwy, gdy żadne słowo niepuste postaci imw nie jest jego prefiksem (na przykład wzorzec ,v = abababba nie jest. łatwy, a ,v abhababba tak). W wypadku łatwych wzorców można skonstruować algorytm podobny do algorytmu KMP, w którym tablicę P zastępuje się przez pewne przybliżone oszacowaniu: /'[/I < 2/73, przesmięcie(j) > j/3.
. {Algorytm GS';wersja a 1g o r y tanu Gal, ila -Se i terasa dla la twych wzorców} b o g iń i ;= 1 ; while i < u - m I X do begin j : 0; while >.j 1 -1- 1| — yj i. r j -1- 1) do / : j 1 i; i i j — m then ner i te (i ) ; :i := i 1 max { 1, I j / 3 I)
end end (algorytm GS'j ;
.1.72
5. A lgorytm y lekHtowe
Poprawność algorytmu dla łatwych wzorców wynika wprost z. definicji laiwego wzorca. Złożoność algorytmu jest liniowa. Wystarczy pokazać, że liczba wykonanych instrukcji / := / -i- I jest liniowa. Rozważmy sumę ,v = 3/ + ./. Można przeprowadzić podobne rozu mowanie co przy omawianiu algorytmu KMP. Algorytm GS' dla wzorców x, które nie s;| łatwe, może dać odpowiedź • ovsq Dla ,v = (HutCHiaah i y =•■atiutinaaah na przykład zostanie wypisanych zero wystąpień wzorca ,v, podczas gdy ,v występuję w y począwszy od pozycji i - 2. Dla / otrzymamy przesunięcie, równe 2 i następu;) badaną pozycją w y będzie i - i -I- 2 = 3, a pozycja i = 2 zostanie pominięta jako możliwa początkowa pozycja wystąpienia ,v w y. Przykładem interesującego zbioru łatwych wzorców jest zbiór słów Fibomtcciego [//’/;,, fih,, Dowód lego, że słowa Fibonaccicgo st| łatwymi wzorcami, pozostawiamy Ci jako zadanie. Możesz skorzystać z następującego wzoru na elementy tablicy P (dla wzorców Fibonaccicgo): />[/) = j - Fk ,
tli a Fk < / < Fk „ gdzie k > I
5 . 1 .5 .
A lgorytm KMR (ICarpa-M illera-Roscnberga) Algorytm K.1VIR (Karpa-Mi Ilera-Rosenberga) można zastosować do problemu W W w sposób pośredni. Algorytm len umożliwia wykrycie wszystkich możliwych powtórzeń (ego samego tekstu w danym słowie. Bezpośrednim zastosowaniem algorytmu KMR jest obliczanie najdłuższego powtarzającego się podslowa tekstu wejściowego x w czasie D(/dog//), gdzie. |.v| ~ n. Problem WW daje się jednak leż rozwiązać tym algorytmem w tym samym czasie, a przejście od przypadku jedno- do dwuwymiarowego jest natych miastowe (przy stosowaniu rozszerzenia algorytmu KMP do przypadku dwuwymiarowe go przejście takie, jest bardziej skomplikowane). Widzimy więc, że algorytm jest gorszy od KMP o czynnik log//; „nadrabia” to jednak prostotą i możliwościami zastosowania w praktyce. Załóżmy, że słowem wejściowym jest tekst w i że mamy zadaną pewną, liczbę r. Jeżeli wszystkimi różnymi podslowami długości r w tekście w są słowa . a : , , , v ;!, . . . , xk (w ko lejności leksykografie/,nej), to dla każdej pozycji 1 < / < |w | - / ■ -i- I algorytm KMR umożliwia obliczenie wartości numeĄi] równej numerowi podslowa w[i..i -I- r - I], Równość mtmer\i\ = k zachodzi wtedy, kiedy ,xk h'|/../ -i- r — I]. Inaczej mówiąc, algorytm umożliwia obliczenie klas równoważności. Dwie pozycje i, j: I .< i oraz j < |u ’| - r -I- !, są równoważne, wtedy, kiedy iiiini('r[i\ - iiiuncr\j\. Dla vr ~ dba bab i /■~ 4 na przykład mamy .v, =- nbab, .c, = baba oraz. (przykładowo) iwinrr\3 \ -- I. Pokażemy teraz, jak zastosować tego typu obliczenia do wyszukiwania wzorca. Weźmy w~ i r = m. Przyjmijmy xk = ,:v, gdzie k jest numerem wzorca. Mamy niiim:r\i] - k
5. f. Problem wyszukiwania wzorca wtedy i tylko wtedy, gdy wzorzec x występuje w y na pozycji dla I <, i Algorytm KMR umożliwia zatem także rozwiązanie problemu WW.
173 <
n- m
I,
Załóżmy dla uproszczenia, że /- jest potęgą dwójki (tutaj założenie takie nie jest równie naturalne co zazwyczaj). Niech numer [i] będzie numerem podstawa o długości />, za czynającego się od pozycji i w tekście n\ Tablica numer ma długość ju'| - p -I- I. Algorytm KMR umożliwia obliczenie tablic numer■ kolejno dla /:>= I, 2, 4, ..., r (dla kolejnych potęg dwójki, aż do r). Wykorzystywany jest następujący oczywisty laki: (*) n u m e rk i| = numeruj] wtedy i tylko wtedy, gdy (numerki] = numer,,\j]Sauimerl]i -I- //) = numeruj + p)). Pokażemy działanie tego algorytmu na przykładzie tekstu w = abbababba. Niech /• = 4. Mant)' 5 podstaw długości 4. Oto one: -Vi = uhab, x> = abba, x, = baba, x, = habb i x5 = bbah. Naszą końcową tablicą powinna być zatem następująca tablica: numer,, = [2, 5, 3, I, 4, 2| Na początku obliczamy tablicę numer,. Obliczenie takie jest bardzo proste, gdyż nu mer,[i] jest numerem symbolu nj/|. W naszym wypadku numer, = 11,2 , 2, 1,2, I, 2, 2, 11. Pokażemy teraz, jak obliczać tablicę n u m e r mając daną tablicę numer■, i korzystając /. faktu (*). Aby obliczyć-tablicę numer,,, tworzymy ciąg trójek (numer-,] i), numer.,]i -I- 2], i), .a następnie sortujemy ten ciąg Icksykógraficznie w czasie liniowym. W naszym wy padku numer, - [I, 3, 2, 1,2, 1,3, 2], a ciągiem trójek jest ciąg: (1,2, I), (3, I, 2), (2, 2, 3), (I, 1, 4), (2, 3, 5), (1,2, 6) Po posortowaniu otrzymujemy ciąg: (1,1, 4) | (I, 2, 1), (i, 2, 6) 1 (2, 2, 3) j (2, 3, 5) | (3, I, 2) Dzielimy ten ciąg na grupy trójek mających te same dwie pierwsze współrzędne. Jeżeli trójka (/,„ lc2, i) jest w lc-tej grupie, to numerk i] = k, ;t zatem numer,,[4] = 1, numer,] 11 = 2 itd. Schemat algorytmu KMR jest następujący: | a :i.cio r y tm KMB (Karpa-M i l i e r a -R ose nbe rg a ); o b l i e z o n a z o s t a j e t a b l i c a numer) begin
for i :=-. 1 to |tv| do iiu n ie rji | := numer symbolu U'|il; P := 1;
174
5. A lgorytm y tek sto w e repeat oblicz tablicę n u m e r - , , k o r z y s t a j ą c z faktu {korzystamy z sortowania kubełkowego; koszt = D(| nj )} p := 2*p; until p = r; for i := 1 to |w| do jiumerjiJ := nuirier,.|ij end (algorytm KMR} ;
Algorytm KMR ma złożoność O(nlagn), gdzie |vt| = n. Chociaż nie jest to złożoność liniowa, algorytm KMR jest bardzo interesujący. Jeśli /• nie jest potęgą dwójki, to można wykonać algorytm dla rozmiaru r' będącego największą potęgą dwójki nie przekraczającą r. Potem należy skorzystać z faktu podob nego do faktu (*), a mianowicie: numerki] = numeruj] wtedy i tylko wtedy, gdy (niunerr.[i] = nuiner^\j\Smuinerr\ i + r - r'\ = nwnerr,\j -l- r - r'J). Algorytm KMR można bez istotnych zmian stosować dla tekstów dwuwymiarowych. Rozważmy pod tablice rozmiaru p x p, gdzie p jest potęgą dwójki. Wartość elementu numer [i,j] jest teraz numerem podtablicy, której lewy górny róg znajduje się. na pozycji (/,./') w początkowym tekście dwuwymiarowym. Można sformułować fakt analogiczny do (*). Trzeba będzie rozważyć numery podtablic zaczynających się na pozycjach (/,./), (/ -l- p, /), (/, / -I- p) oraz (i! -i- j -l- p) przy przejściu od rozmiaru /> x p do rozmiaru 2p x 2p (pozostawiamy to jako zadanie).
5.1.8. A lg o ry tm K R (K a rp a -R a b in a ) W algorytmie tym stosujemy funkcję mieszającą h, dzięki której można obliczyć pewną wartość (kod) podslowa y, - y[i..i -I- in — I] tekstu wejściowego. Jeśli /;(>>,) = li(x), to z bardzo dużym prawdopodobieństwem zachodzi równość'.x = y,, a więc wzorzec x występuje w y na i-tej pozycji. Efektywność algorytmu wynika z możliwości szybkiego obliczenia wartości H(y, ,), jeżeli wartość h(y.) jest znana. Działanie algorytmu polega na obliczaniu Łych wartości kolejno dla i = 1,2, ..., n —w -I- 1. Załóżmy (dla uproszczenia), że nasz alfabet składa się z cyfr 0, I, ..., /-— I. Niech ą będzie pewną, bardzo dużą liczbą pierwszą (ale nie za dużą, na przykład największą liczbą pierwszą taką, że q(r -I- 1) nie powoduje nadmiaru). Jeśli z jest tekstem o długości m, to definiujemy -I-.... + ::[/»]) mod ą
5.1. Problem w yszu k iw a n ia wzorcu
175
Inaczej mówiąc, wartością funkcji mieszającej h jesl wartość tekstu traktowanego jako liczba zapisana w systemie o podstawie r mod q (aby nie było nadmiaru). Prawdopodo bieństwo, że wystąpi kolizja (wartości funkcji h będą takie same dla dwóch różnych argumentów) jesl bardzo male, gdyż wynosi \/q, a q jest bardzo duże. (Algorytm KR (Karpa-Rabina) } begin 111
h ( x )
;
dftf := J ; li?. := ! j ( y | l . .jn)) ; ( k o s z t O( m) } i := 1 ; while i < n - m I 1 do begin if h 2 - h i then if x y|i, i -I ill - 1] then write(i) (koszt -■ 0 ( m ) }; {oblicz nowe Ii2 = h(y[i + 1. .i + mj) wiedząc, że poprzedn io h 2 = h ( y | ./ . . i - l - u i - 1 I) = S pi J | i " ' +y|.i -l- 1 |r"' -I . . . -l-y|-i l iii J I) mod cfj Ii2 (( h 2 y|i] * dM) * r -I-y[i + m|) mod <;;
i : = i -I- 1 end end (algorytm KR);
Podobnie jak w algorytmie KMK, tak i tutaj uogólnienie na przypadek dwuwymiarowy (dla problemu VVVV) jesl bardzo łatwe. Algorytm KR jest algorytmem praktycznym. W oryginalnej jego wersji liczba pierwsza r/ jest wybierana losowo.
5.1.7.
A lgorytm BM (Boyera-M oore’a) Algorytm ten, podobnie jak algorytm KMP, powstaje przez pewne ulepszenie al gorytmu „naiwnego” . Ulepszenie to polega na pełniejszym wykorzystaniu gromadzonej informacji w algorytmie. W algorytmie KMP informacją tą była wartość j. Podobnie będzie i tutaj..z tą różnicą, że zaczniemy od nieco innej wersji algorytmu „naiwnego” . {Algorytm N' (wersja algorytmu N) ; zgodność wzorca jest sprawdzana od końca} begin i : = :t ; while i < n -- iii r 1 do begin 7 :■ :ni; while x[j] = y[i I-j -• 1J do j := j - 1; if v ■ 0 than w r i b e { i ) ; i := i i- :1; {przesunięcie =1}
'Jlii end {algorytm N'j ;
.176
!>. A l/'orylm y tekstowo
W algorytmie tym nie jest wykorzystywana informacja związana ze zmienną /. Przed zwiększeniem wartości i poprzednia wartość j jest tracona, ale wiemy, że są wtedy spełnione, warunki: (a) .\|/ -t- 1..///1 ~ y|; +./../ I i i i -• I |; (do tekstu pasuje końcowa część wzorca, począw szy od pozycji j + I we wzorcu j (li) t(/i ■/■ r | / •I-./ -- 1 1;
Oznaczmy przez ,v wartość przesunięcia początkowej pozycji przyłożenia wzorca. Mamy obecnie ,v = |. Korzystając jednak z informacji wyrażonej przez warunki (a) i (b), może my czasami zwiększyć wartość przesunięcia ,v. Zastanówmy się, jakie warunki wystarczy nałożyć na wartość ,v, żeby zagwarantować to, iż nie zostanie pominięty żaden wzorzec zaczynający się w jednej z „przeskoczonych” pozycji. Najpierw jednak zanalizujmy, w jaki sposób warunki (a) i (b) wpływają na wysląpienie wzorca na pozycji / -ł- ,v. Jeśli wzorzec zaczyna się od pozycji i -i- ,y, lo: uv/;-/(,v, /): przesunięty wzorzec jest zgodny z jego częścią „pasującii” oslatnio do tekstu y, a bardziej formalnie: dla każdego / < k .< zachodzi ,v > k lubx[k - ,v] = ,\:[/r|; nv//'2(.s', /): ,v > j lub ,v[/ - ,v| -A .\j/] (warunek ten wynika z niezgodności ostatnio czyta nych symboli tekstu i wzorca). Hiecli
cll(j) = min (,y > I: zachodzi warl{s, j)) z/2(/) —min {s > I: zacltodzi warJ(x,j) i hw2(.v, /'){ Rozważmy następujący przykład: ,v —cubabababa. Mamy d l(9) c/2 daje większe przesunięcia.
i!2(9) = li, a więc
Jeśli w algorytmie N' przyjmiemy wartość przesunięcia s -■ (I2(j), lo otrzymamy al gorytm BM. [Algorytm BM (wersja algorytmu M ) ; zgodność wzorca jest sprawdzana od końca]
b e g in i : = 1; while i < n ••••m I .!. do begin j m; while ;s| /) = yj.i -I-j .1.)do j j 1; :i.:E j = 0 then ivri te (i ) ; i := i -I-Ć12 (.;/); [przesunięcie = ĆI2 (j )} end end (algorytmu BM j;
Okazuje, się, że w algorytmie BM jest wykonywanych czasami znacznie mniej niż ii po równań, a jednocześnie, maksymalna liczba porównań jest liniowa (dowód wykracza poza ramy tej książki).
5.1. Problem w yszu k iw ań i:1, w zorca
1.77
.
Niech .V cababababa i y = aaaaaaaauubabababa. W tym wypadku liczba porównali symboli w algorytmie BM jest równa 12. Gdybyśmy jako przesunięcia użyli dl(j) a nic d2(j), to wykonalibyśmy 30 porównań. Okazuje się, że w wypadku użycia clI(/) liczba porównań wynosi £l(ir). Można to zobaczyć dla ,v = ca(ba)k i y = crk ' x(ba)k. Algorytm BM wymaga wcześniejszego slablicowania wartości d'2(j). Koszt takiego ob liczenia jest liniowy. Można w tym celu wykorzystać algorytm obliczania tablicy P (potrzebnej w algorytmie KMP) dlii tekstu x l! (gdzie R jest operacją odwracania tekstu), jest wtedy widoczny pewien związek między algorytmami KMP i BM. Opracowanie takiego algorytmu pozostawiamy Ci jako zadanie. Przedstawimy teraz algorytm koncepcyjnie podobny do algorytmu BM. Nazwiemy go algorytmem • Wzorzec jest przykładany do tekstu i „dopasowanie” wzorca jest sprawdzane od końca. Przyjmijmy dla uproszczenia, że alfabet jest dwuliterowy. Niech r = 2\ log//; |. jżlaoryl:m BM'} beeri"
i : - : .; while i : u do ■ begin, if y[i -- r + 1. . i] jest podslowem wzorca x then oblicz wszystkie wystąpienia wzorca na pozycjach [i --ni-l 1 . .i - r I-1], korzystając z algorytmu KMP (koszt = O(m) } else {wzorzec nie rozpoczyna się od żadnej z pozycji 1i - m -t 1 . .i - r -I- 1 1}; i :- i -I- m - r -i- i ; end end (algorytm BM'};
1I
Załóżmy, że koszt sprawdzania, czy y[/ —r -i- I../J jest podslowem wzorca ,v, jest O(logw) (jeśli korzysta się z uprzednio policzonej struktury danych 7’(.v) lub G(x), o któ rych będzie, mowa później). Przypuśćmy, że y jest tekstem losowym. Wzorzec ma co najwyżej m różnych podslów długości r, a prawdopodobieństwo, że dane słowo długości r (mamy 2' > nr takich słów) jest podslowem wzorca, jest co najwyżej Ilin. Oczekiwany koszt obliczeń dla ustalone go i wynosi zatem 0(ni/ui -l- r): Ponieważ mamy O(nhii) takich wartości i, oczekiwany koszt algorytmu BM' wynosi ()((n\ogin)/ni). Dla m = n m na przykład otrzymujemy oczekiwany koszt algorytmu BM' rzędu n >,2\ogn (miwet wtedy, kiedy uwzględnimy w koszcie tworzenie T(x)). Pominięcie czasu wstępnego obliczania l \ x ) ma sens przy szukaniu lego samego wzorca, wielokrotnie (na przykład przy szukaniu słów kluczowych w tekście programu).
178
5. A lgorytm y tek sto w e
5.1.3. A lg o ry tm F P (F ishera-P atersona) W algorytmie KMR dochodziło do pewnego sprowadzenia problemu WVV do próblemu sortowania. W algorytmie FP (Fishera-Patersona) dla problemu WW' (wyszukiwa nia wzorca z symbolami nieznaczącymi) jest z kolei wykorzystywana redukcja problemu tekstowego do problemu arytmetycznego. Pokażemy, że złożoność problemu WW' jcsl (asymptotycznie) nie większa niż złożoność mnożenia dwóch liczb całkowitych. Oznacz my przez <|> symbol nieznaczący, który ma tę silną własność, że pasuje do każdego innego symbolu. Przez s oznaczmy relację „pasowania” . Dla dwóch symboli z i .v mamy ,v ~ z, gdy ,v - z lub ,v = (|), lub 7 = . Relację = rozszerzamy na teksty. Dwa teksty u i n> (o lej samej długości) są w relacji = (piszemy u m w), gdy dla każdej pozycji i mamy «|t| h wfi]. Relacja = ma jednak nieprzyjemną własność: nie jest przechodnia. Z równości a = b, b a c nie możemy wcale wnioskować, że a ~ c. W tym momencie nici rudno uwierzyć, że poprzednio skonstruowane algorytmy dla problemu WW nie działają dla problemu WW' (gdyby sprawdzanie równości symboli zastąpić w nich sprawdzaniem relacji a między symbolami). Co więcej, dla problemu WW wystarczy zawsze 0{n) porównań (=). Jeśli natomiast jedyną informację o słowach x i y otrzymujemy za pomocą poró wnań -a, to możemy pokazać, że potrzeba Q(/r) takich porównań. Weźmy mianowicie Przypuśćmy, że za pomocą pewnego algorytmu można sprawdzić, czy x s y[i..i + n - 1J dla i = l../i + 1, korzystając jedynie z odpowiedzi na pytania typu: „Czy aj//| ~ y[i/]?” . Oczywiście zostaną wypisane wystąpienia na pozycjach I, 2, .... ii -I- I. Przypuśćmy teraz; ż.e w algorytmie nie ma pytania: „Czy x[p] = dla pew nych 1 < p < n i n -i- 1 < ej < u -i- p. Zmieńmy symbole 4/?] i.y|//] w następujący sposób: -V|/ij := o, y[q] := b (gdzie a i b są różnymi symbolami znaczącymi). Ponieważ zestaw odpowiedzi na zadane pytania się nie zmienił, algorytm daje ten sam wynik: wystąpienia na pozycjach 1, 2, ..., n -I- 1. Nie jest to jednak poprawne; na pozycji q -- p + I wzorzec x nie występuje (nie zachodzi x = y[q - p -l- I ,.q - p -i- »]). Jeśli jedynymi informacjami wejściowymi są odpowiedzi na porównania, to potrzeba £2(if) tych porównań. Pokażemy, że korzystając z pełnej informacji o danych wejściowych oraz kodując teksty x i y jako liczby binarne, możemy osiągnąć złożoność rzędu dużo mniejszego niż. ir, a mianowicie tego co koszt, mnożenia liczb. Nie będzie to jednak koszt liniowy. Problem istnienia algorytmu o złożoności liniowej dla problemu WW' jest otwarty. Wszystko wskazuje, że takiego algorytmu w ogóle nie ma. Zaczniemy od algorytmu, w którym dwa teksty są mnożone tak jak robi się to w szkole, czyli metodą „w słupkach” . W metodzie tej podpisujemy jedną liczbę pod drugą, a na stępnie wymnażamy cyfry jednej liczby przez cyfry drugiej, stosując operację *. Otrzy mujemy kilka „pięter” poziomo zapisanych liczb. Od strony lewej do prawej sumujemy kolejno cyfry w kolumnach, stosując operację +. Można powiedzieć, że pełna operacja to para «*, +», ponieważ, operacje * i -l- określają procedurę mnożenia „w słupkach” .
179
5.1. Problem w y szu k iw a n ia w zorca
Podobnie możemy zdefiniować operację «s, otrzymujemy teksl h> = w «=, a» r, laki że U’[/vj gdzie
a
a ».
Jeśli mnożymy teksty /./ i e, to w wyniku
A (//[/) I i ii =I:
jest operacją koniunkcji.
Operację «=, a» nazywamy mnożeniem tekstowym. Przypuśćmy, że mamy znaleźć wystąpienia wzorca x - bua w tekście y = baaba. Mnożymy X1' ~ aub przez y. baaba s
,i
ii
b
10 0 1 0 0 MOI 0! I U I 0 0 10 0 I 0
Łatwo sprawdzić, że wzorzec,v występuje w tekście yod pozycji /, gdzie I gwekloru(v) będzie wektorem logicznym, którego i-tą składową jest lnie, gdy i’|)j = a, gdzie v jest danym tekstem, a a symbolem alfabetu. Przez Xrl ,,(//, v) oznaczmy iloczyn logiczny: Aj, ,,(//, v) ~ logwektor„(u) «a , v » b>gwekU>rh{v) Wartość iloczynu tekstowego v « -, a » u jest równa negacji alternatywy logicznej wszys tkich wektorów Aj, y), gdzie (a, b) są wszystkimi parami wzajemnie różnych (a z- b) symboli znaczących (różnych od <[)). Problem WW' ma zatem len sam rząd złożoności co obliczanie iloczynu logicznego wektorów. Pokażemy, w jaki sposób można łatwo sprowadzić obliczanie iloczynu logi cznego wektorów do obliczania iloczynu arytmetycznego dwóch liczb. Niech n będzie długością mnożonych wektorów u i v. Każdy bit b tych wektorów zastąpmy ciągiem I log/; I -i- ł bilów 00...0h. Otrzymamy ciągi u i u' o długości 0(//lug/i); potraktujmy je jako liczby (w zapisie binarnym). Mnożymy arytmetycznie u i / . Weźmy i-tą grupę bilów o długości | log// I -i- 1. Zauważmy, że /-ty bit iloczynu logicznego wektorów jest niczerowy wtedy i tylko wtedy, gdy wartość takiej /-tej grup}' bilów jest: niezerowa (nie składa się z samych zer).
.180
W.
A J^orytm y ic k s io w e
Ponieważ istnieją algorytmy mnożenia liczb binarnych w czasie 0(/i'), gdzie r < 2 | AMD'| problem W\V' można rozwiązać w czasie G((/dog»)'), a zatem asymptotyczny koszt prob lemu \VW' jest rzędu mniejszego niż n ’. Nie jest znany algorytm o koszcie liniowym (z czego nie wynika wcale nieistnienie takiego szybkiego algorytmu).
D r z e w a srafiltsow e i g r a fy p o d s ló w W tym podrozdziale przedstawimy dwie interesujące struktury danych dla tekstów, a mianowicie drzewo sufiksowc T(x) i graf podslów G(x). Struktury te służą, do repre zentacji zbioru wszystkich podslów danego tekstu x. Rozmiar reprezentacji będzie linio wy, chociaż liczba wszystkich podslów może być nieliniowa. Dla liniowości czasowej procesu tworzenia tych struktur danych istotne jest, żeby rozmiar alfabet u był ograniczo ny przez pewną stalą. Zakładamy w tym podrozdziale, że tak jest. Jeśli rozmiar al label u jest „duży” i wynosi k, to należy wymnożyć złożoność każdego algorytmu przez logi. Drzewa sufiksowe i grafy podslów mogą być użyte do tworzenia algorytmów liniowych dla problemu W W. Można je ponadto wykorzystać do rozwiązywania innych problemów. Zbiór wszystkich podslów Sub{x) danego tekstu x o długości n może się składać z liczby rzędu ir słów. Okazuje, się jednak, że jest możliwa zwarta reprezentacja takiego zbioru. Naszym przykładowym tekstem w tym podrozdziale będzie tekst .1: - a a c c a c d .
5 .2 . 1 .
N ie z w a r ta r e p r e z e n ta c ja
d rz e w a ,
suifiksow ego
Będziemy rozważać acykliczne grafy zorientowane, których krawędzie są etykieto wane symbolami alfabetu lub podslowami tekstu ,v, Htykiely te nazywamy wartościami krawędzi. Grafy te będą mieć wierzchołek zwany korzeniem, z którego prowadzi ścieżka do każdego innego węzła. Przez wnrl(p) oznaczymy dla każdej ścieżki p tekst będący złożeniem etykiet (wartości) krawędzi na ścieżce p (\varl(p) jest wartością ścieżki p). Powiemy, że grał' G reprezentu je Sub(.\), jeżeli Sub(x) = {wart(p): p jest ścieżką o począt ku w korzeniu grafu G) i różne, ścieżki (zaczynające się w korzeniu) mają różne, wartości. Przez G(x) oznaczymy grał' reprezentujący Sub(x). Zajmiemy się teraz tworzeniem go. Miech 77(v) będzie drzewem reprezentującym Sub(x), inaczej mówiąc grillem, lamy reprezentuje Sub(x) i w którym dwie różne ścieżki nic prowad/.;| od korzenia do tego san legi >w ierzchol ka. Grafy 77(,v) i G(x) dla przykładowego tekstu są przedstawione na rysunkach 5.1 i 5.2. Zauważmy, że G(.*j ma tylko 10 węzłów, podczas gdy 77(.v) ma ich 24.
c .
Rys. 5.2. (!r;if G (krawędzie są zorienlowane z yóry na doi)
182
5. A lgorytm y tek sto w e
Drzewu TJ(x) ma takie oto własności. Liczba podslów x jest równa liczbie węzłów drzewa. Dla każdego węzła v przyjmijmy, ża warl(v) = wcirt(p), gtlzie p jest ścieżka od korzenia do t\ Wartościami liści tego drzewa są zatem sufiksy tekstu x. Ponieważ mamy n sufiksów, liczba liści wynosi co najwyżej n. Drzewo 7 7 ( a) nazywamy niczwartym drzewem sufiksowyni. Drzewo sufiksowe 7( a ) jest zwartą (skróconą) reprezentacją 7 7 ( a ). Łańcuchem nazywamy maksymalną ścieżkę w 7 7( a ) (być może nie od korzenia), której wszystkie węzły, z wyjątkiem końców, mają stopień równy I (mają jeden następ nik). Zauważmy, że liczba krawędzi TI{x) wynosi 23, natomiast liczba łańcuchów jedy nie 10. W ogólności liczba ta jest zawsze 0(n). Drzewo 7 ( a ) jest drzewem, które powstaje z 77( a ) przez zastąpienie każdego łańcucha p przez pojedynczą krawędź, której wartością (etykietą) jest para [/, j\, gdzie x[i..j\ = wart(p). Stosujemy tu zatem zwarty zapis podslów; gdybyśmy bowiem wpisy wali cały tekst wart(p), to rozmiar reprezentacji 7( a ) byłby dalej rzędu /; . ' j/. /] będziemy utożsamiać z podsłowem .v(/../j. Drzewo T(x) (dla naszego przykładowego tekstu x) jest przedstawione na rysunku 3 .1. Drzewo to ma 1 1 węzłów.
5 ,2 .2 .
T w o rzen ie d rzew a su fik sow ego Zajmiemy się teraz złożonością procesu tworzenia drzewa sufiksowego 7(.v). Dla uproszczenia przyjmijmy, że dany tekst kończy się zawsze specjalnym symbolem, wy stępującym jedynie na końcu (w ntiszym przykładzie symbolem d). Niech suj'{i) będzie /-tym su (iksem x, czyli niech suf{i) = x[i..n) dla / = 1, ..., n. Bezpośrednim algorytmem jest skonstruowanie najpierw 7 7( a ), a następnie przekształcenie 7 7 ( a) na 7( a ). Można to łatwo wykonać w czasie O(ir). Wąskim gardłem tej metody jest rozmiar 7 7( a ); dla a = a"b"a"b"d bowiem rozmiar ten jest rzędu n2. Jeśli nawet przyjmiemy, że rozmiar 7 7 ( a) jest liniowy (dla szczególnej klasy tekstów a), lo projekt algorytmu tworzenia 7 7 ( a) w czasie proporcjonalnym do rozmiaru 7 7 ( a) jest nietrywialny. Będziemy budować 7(a), nie korzystając z 7 7 (a ), ale cały czas 7(a) będziemy traktować jako reprezentację 7 7 ( a). Algorytm polega na tworzeniu drzew sufiksowych T(siif(i)) w kolejności i - n, n - I, ..., I. Zakładamy, że w danym momencie mamy już drzewo T
=
T { x iif( i
-I- 1)).
Przez • oznaczymy operację konkalenacji (złożenia) tekstów. Na ogól tekst)' pisze sit? obok siebie, ale czasami dzięki użyciu • zapis tej operacji staje się jaśniejszy. Niech LINK i 77ńS7'będą tablicami pomocniczymi, takimi że: (a) IJNK\a, «J = v, gdy wart{v) = a • wari(u) dla węzłów u i t> drzewa T i każdego symbolu a; jeśli takiego węzła v nie ma, to przyjmujemy UNK\a, u] = nil;
5.2. D rzew a n ufik so w e i .grafy p o d sió w
183
(b) TliSTla, u] ~ true, gdy słowo a • wart(a) jest prefiksem wartości pewnego węzła drzewa T: w przeciwnym razie TEST\a, //] = false. WjuTiv/rd-iiTsv też pewne funkcje pomocnicze, określone częściowo (gdy istnieje węzeł spełnia ją c y zadane warunki) dla .symboli z alfabetu i węzłów drzewa T. Oto one: (a) Jirxliext(a, w) = pierwszy węzeł ii na drodze od węzła w do korzenia, dla którego TE$T\a. u] = tnie; (h) Jirsilink(a, w) = pierwszy węzeł ii na drodze od węzła w do korzenia, dla którego LlNK\a, m| ^ nil (nil, jeśli nic ma takigo u): (c) na.srtu, x', ,v) = następnik węzła ii, do którego prowadzi krawędź o wartości zaczynającej się na tę samą literę co wartość pierwszej krawędzi na ścieżce od ,v' do ,v; (d) breakpoint^, t\ z.) - w, gdzie w jesl takim nowym węzłem, że wart(w) - z; funkcja ta jest jedynie określona dla takich węzłów ii, w, że r jest następnikiem (synem) »; fjest właściwym prellksem warl(v), a mirl(u) jest właściwym prellksem z. Efektem ubocznym działania funkcji breakpoint jest utworzenie odpowiednich krawędzi z u do u1 oraz z n> do r, jak również nadanie tym krawędziom właściwych wartości (uzyskane tirzewo ma reprezentować Sub{xuf(i))). Inaczej mówiąc, operacja breakpoint polega na podzieleniu krawędzi od u do t> na dwie krawędzie przez utworzenie nowego węzła, który jest wartością lej operacji. Ścieżka prowadząca do tego nowego węzła odpowiada słowu z. {Algorytm tworzenia drzewa T(x)} begin
T :=
) ; v := jedyny liść'7'; inicjowanie tablic LINK i TEST dla T; {koszt 0(1)} lor i :-■ n - 1 downto 1 do {T =
T(suf(i
-i-
i) ) , w a r t (v) = s uf (. i + i) ; obliczamy Ti, s u i: ( i ) )}
begin
a := nji]; ii a nie występuje w s u f ( i -I- 1) then begin. dla każdego węzła w ha ścieżce od u d o korzenia Wykonaj TES‘Ą a , w] := true; v l v; v := nowy węzeł o wartości s u f ( i ) , będący następnikiem korzenia; LINK[a, vl] v; end else begin x J;j r n t t e s t ( a , v ) ; x ' := f i r s I: l i n k (a, v) ; if x = nil then h e a d 1 : -- korzeń else headl ; —L I N K [ a , x ' \ ; if x ~ x ' then h e a d t - h e a d ! else
184
5. Algorytmy te Icytowo begin if .'-i == nil then h e a d d ; - następnik korzenia, taki że etykieta prowadzącej do niego krawędzi zaccyna się symbolem a else heads := nan.:;t ( h e a d l , :<) ; head b r e a k p o i n t ( h e a d l . , h e a d ' d , a - ■■■aa: i ; end; {aktualizacja tablic L I M?i TEST) T E S 1 ] b , h e a d ] := TBST[b, h e a d l ] dla każdego symbolu b; Tl;,'S'.r|a , iv| := true dl a każdego węzła w na ścieżce od vdo
LIWKjći, x] :=
h e a d ; v l : ~- v ; v :- nowy węzeł będący następnikiem h e a d ;
wa. rt(v) := s u f ( : i ) ; ijJWKta, v.7..| := v
{ and I and ; a nd (algorytm);
Niech dijpth(v) oznacza głębokość węzła v w drzewie 7(. Można udowodnić, żc ilepih(JJNK[/h(v) + i , co pozostawiamy Ci jako ćwiczenie. Wynika to siąd. że T-~ TLsuKi. -I- !.)). Przyjmijmy, że rozmiar alfabetu jesl niewielki (ograniczony przez stalą). Niech v(i) będzie równe wartości r po zakończeniu iteracji dla danego /. Łatwo zauważyć, że koszt jednej iteracji jest proporcjonalny do różnicy \(|.v|) można było sprawdzić wystąpienie zadanego wzorca ,v w tekście. Drzewo T(y) umożliwia rozwiązanie lego problemu. Można jednak sformułować bardziej skomplikowaną wersję problemu: znaleźć pierwsze wysląpienie x lub wszystkie wystą pienia (w drugim przypadku koszt ma być proporcjonalny do |.v| plus licz,ba wystąpień). Strukturę.T(.v) można też zastosować do obliczenia tak zwanego drzewa pozycyjnego. Miech dla każdej pozycji i słowo i deut(i ) będzie najkrótszym słowem, które zaczyna się
5.2. D rz e w a sufiksowc i grały podsiów
185
w tekście ,v na pozycji i i nie występuje na żadnej innej. Mówimy, że słowo takie identyfikuje pozycję /. Drzewo pozycyjne jest drzewem, którego krawędzie są etykieto wane symbolami lub podslowami, a zbiór wartości ścieżek od korzenia do liści jest równy zbiorowi identyfikatorów pozycji. Utworzenie drzewa pozycyjnego, przy założe niu, że drzewo sufiksowe jest dane, pozostawiamy Tobie, Drogi Czytelniku. Z grubsza biorąc, dla każdego liścia v wystarczy zastąpić etykietę ] krawędzi prowadzącej do i‘ przez aj/|.
5 .2 .3 . T w o rz e n ie g r a f u p o d s ió w
Przejdziemy teraz do utworzenia grafu G(x) w czasie 0 (|a '|). Najpierw oszacujemy rozmiar lego gra fu i zbadamy jego strukturę (pomijając szczegóły algorytmiczne). Niech end-pas(z) oznacza zbiór pozycji, na których kończy się wystąpienie słowa z w ,v. Rozważać będziemy tylko słowa z. należące do Sub(x), Niech na przykład z = ac i niech x będzie naszym przykładowym tekstem auccacd. Wtedy e.nd-pos(z) = (3, 6). Załóżmy, że dla słowa pustego zbiór końcowych pozycji jest równy zbiorowi wszystkich pozycji. Dla podsiów tekstu x mamy 10 różnych zbiorów końcowych pozycji: (I, 2, 5j, (2), {31, {4}, {5}, {6}, (7), {3, 6), (3, 4, 6} oraz zbiór wszystkich pozycji. Ten ostatni zbiór odpowiada korzeniowi grafu acyklicznego G(x). Zbiór V węzłów grafu G(x) można utożsamić ze zbiorem wszystkich zbiorów end-pos dla podsiów x. leżeli v = end■po: i, te - cnd-pos(::/i). to etykietą krawędzi od r do w jest symbol a. Podstawową własnością grafów Gis) jest icli maty rozmiar. Graf podsiów ma bowiem co najwyżej 2ii węzłów. Wynika to stud, że rodzina zbiorów end-pos ma strukturę drzewiasta. Jeśli weźmiemy dwa różne zbiory /. tej rodziny, to albo są one rozłączne, albo jeden zawiera się w dru gim. Możemy zatem zdefiniować drzewo podzbiorów odpowiadających węzłom z V7. Podzbiór VI jest synem \72, gdy VI
186
5. Algorytmy tekstowe.
Dany węzeł v = odpowiada zbiorowi pozycji P(v) - end-po.s(.\[i..j\). Ze wszyst kich słów o tym samym zbiorze pozycji końcowych l\v ) słowo x[i..j\ jest najdłuż sze. Zbiór takich słów (mających ten sam zbiór end-pox) jest postaci: j.v|/../j, x\i + 1-/1, ..... v[A.,/lj. Niech si/J{v) = ,v|/c + 1.../], jeżeli k < j. VY przeciwnym wypadku suf(\>) będzie korzeniem grafu G(x), a więc słowem pustym. Zauważmy, że posługujemy się tutaj trzema re prezentacjami węzłów grtilu podsłów: zbiorami pozycji P(v), słowami ,i|/../j i numerami węzłów iz Pierwsza reprezentacja jest teoretyczna; służy jedynie do wytłumaczenia dzia łania algorytmu tworzenia grain G(x). i Rozważmy dla przykładu węzeł v = aac = ,.vj I ..3] w grafie CĄaaccacd). Mamy wtedy P(i’) = {3 j, suf(\>) = vl - ac - x\2.3\; P(vl) - {3, • 6 ], sufivf) — v2 = c = .v|3..3|; P(v2) = {3, 4, 6} i suf{v2) jest korzeniem. Dla węzła v = x[i..j\ oznaczmy przez length(v) liczbę j — i + I. Inaczej mówiąc, length (a) jest długością najdłuższej ścieżki od korzenia do węzła t>. Algorytm, który teraz podamy, będzie typu on-line. Oznacza to, że tworzymy kolejno grały Gfi/,), C(«|02), G(« ,«•//•,) ..., wczytujtic kolejne symbole tekstu a la2...an. Na koniec lego podrozdziału przedstawimy nieformalny schemat prostego algorytmu działającego ofl'Jlirie. W algorytmie tworzenia grafit podsłów będziemy dla każdego węzła obliczać wartość suf(v) i umieszczać ją w tablicy SUF{y]. Przez nast(v, a) oznaczmy taki bezpośredni następnik w węzła v, że krawędź od v do w jest: etykietowana symbolem a. Jeżeli takiego następnika nie ma, to przyjmujemy tuist(v, a) — nil. {Algorytm tworzenia grafu podsłów) begin a : 1 1; oblicz G(a) ; r o o t := korzeń grafu G(a) ; l a s t := liść grafu G(a) ; {węzeł bez następników] for i ; = 2 to |.\r| do begin a := >:|i]; u twórz nowy węzeł l a s t l nie mający następników; p := SUFjlastU while (p ^ .root) and (nasi; (p, a) = nil) do begin n a s i : [ p , a) '• l a s t l ; p : ~ SC/Fjpl oncl; w := nasi; (p, a ) ; if u’= nil then {p = r c oi .J begin nasi; { r o o t , ni := l a s t l ; S U Ą l a s t l ] := r o o t end e 1 se if l e n g t h (p) -I- 1 = l e n g t h (w) then SUTĄ Lastl] else
w
5.2. 'Drzewa sul'iksowe i g rafy po d sló w
187
begin utwórz kopio i" węzła w; r ma te same nas topniki co w; l e n g t h ( r ) := l e n g t h (p) il; .5i7F|w| SUlĄ l a s 1:1 \ : - - r ; niech w.l będzie poprzednikiem węzła w, takim że l e n g t h { w l ) = l e n g t h (w) . 1; skieruj krawędzie prowadzące do w (oprócz tej prowadzącej z w l ) do nowego węzła r; vl :-p| ; while ( l e n g h t (\/i )-l-1 ■/- ' Le ngt h (w) ) and ( n a s t (v l , a ) - w) do begin nasi: (v l , a) := r; v.L :- SUlĄ vl | o n d ; end; l e n g t h ( l a s t l ) := l e n g t h ( l a s t ) -I- .7.;n a s i: (. l a s t , a ) : l.i.-:: :; last:
last!
end end {a !.i. 1•'; m kons trukc j i ti(x)};
Pewnych douuikcwyeh wyjaśnień wymaga operacja tworzenia nowych węzłów, ponie waż dla każdego węzła chcielibyśmy znać odpowiadający mu tekst .r|/..y|. Oczywiście można pominąć taką informację, i identyfikować węzły przez ich numery, ale identyfika tor tekstowy może się przydać w różnych zastosowaniach. Jest on również pomocny do zrozumienia działania algorytmu, .leżeli tworzymy nowy węzeł lastl, to odpowiada mu tekst .\:|l../j. Natomiast węzłowi /■ odpowiada ,v|7 - / -I- I../J, gdzie / jest wartością Ittig//;(/•), obliczoną przy tworzeniu węzła r. Kluczową rolę. w algorytmie odgrywa tab lica SUF. Po zakoiiczeniu danego przebiegu instrukcji „dla” (for) SUF\last\ jest węzłem odpowiadającym najdłuższemu sufiksowi ,\;|l../|, który występuje wcześniej w słowie ,\| i ,.i\ (nie tylko na końcu). Zastanówmy się, w jaki sposób liczymy wartość SUI'}lastl\. Rozpoczynamy kolejny przebieg instrukcji iteracyjnej „dla” (for) dla danego i. Oblicza my węzeł p. Przypuśćmy, że 'mist(p, a) -f- nil. Wtedy /; odpowiada najdłuższemu sufik sowi .\:|,Y,,/ — l | słowa , v [ I - IJ, dla którego słowo ,v[.v../] = x[s..i. - IJ ■a występuje w ..v| I../] nie tylko na końcu. Nowy węzeł r - nasi(p, a) jest wartością SUlĄlast l\. W jed nym przebiegu instrukcji „dla” tworzymy jeden lub dwa nowe węzły (węzły r i lastl). Oczywiste jest znaczenie nowego węzła lastl, gdyż odpowiada on zbiorowi pozycji, zawierającemu ostatnią pozycję i. Kluczowe znaczenie dla zrozumienia algorytmu ma tworzenie nowego węzła r. Węzeł taki tworzymy, gdy lenglh(p) + I < leiigih(w), gilzie p i w są takie jak w algorytmie. Niech wart (w) będzie słowem odpowiadającym węzłowi w. Wiemy, że sufiks y tego słowa o długości length(p) -I- I jest taki, żc i e etuUpus(y), podczas gdy i <& eiici-pos(warl(w)). Ścieżki o wartościach wariiw) i >>, prowadzące od korzenia w dól grafu, muszą za Lem prowadzić do różnych węzłów (obecnie obie prowadzą do w). Dlatego też trzeba utworzyć nowy'węzeł r, do którego prowadzi ścieżka z etykietą y. Wiemy, że y = y'a oraz y' prowadzi do p. Wystarczy jedynie skierować krawędź o etykiecie a z węzła p do nowego węzła r. Algorytm ten ma złożoność liniową. Wystarczy jedynie pokazać, że całkowita liczba wykonanych instrukcji p SUF\p\ jest liniowa. Zauważmy, że każcie wykonanie lej
ii. i
li. Algorytmy tekstowe
instrukcji powoduje zmniejszenie głębokości węzła p w grafie. Jednocześnie w jednym przebiegu insirukeji „dla” zwiększamy tę głębokość o I, gdyż w chwili, kiedy wy konujemy p SUI']lnsi\, głębokość SUf'’\ltisi\ jesl co najwyżej o I większa od bie żącej głębokości p. Możemy teraz znslosownć zasadę magazynu, podobnie jak przy obliczaniu lubliey / ’ w algorytmie Knulha-Morrisa-Prntla. Koszt konstrukcji grafu G(.\j jest zatem ()(|.v |). vit a I’ G(.y). podobnie jak drzewo 7T.v), możemy stosować do obliczania npo\vt{x) - naj dłuższego powtarzającego się podsłowa .r, waxMi/ix(x) - maksymalnego leksykograficznego sidlksu oraz liczby podslów słowa ,v. Pokażemy Ci teraz, jak stosować (7(.v) do znalezienia liczby podslów. Liczba ścieżek prowadzących do węzła v, nie będącego korzeniem, jest równa lengihiy) —lenglh(SUF[v}). Wystarczy zatem zsumować te liczby po wszystkich węzłach. Algorytm tworzenia (7(a) ma jeszcze inne nieoczekiwane zastosowanie. Załóżmy, że tekst x zaczyna się symbolem występującym jedynie na pierwszej pozycji. Jako elekt uboczny algorytm wyznacza drzewo sufiksowc 7'(.\y'j, a więc daje alternatywny liniowy algorytm obliczania drzew sufiksowych. Korzeniem drzewa T(xR) jest korzeń (7(,vj. Poprzednikiem (ojcem) węzła w jest węzeł r = ,57//;'| ic |. Jeśli i> -ź w i węzłowi n> od powiada słowo b v .,bm, a węzłowi e słowo b, + , to krawędź od y do w jest ety kietowana słowem b ,b ,.. Pozostawiamy Ci obliczanie tych etykiet w postaci ,v*|/.»..«/], jak również pełni] konstrukcję algorytmu obliczania 7tv*) na podstawie al gorytmu obliczania G(.v). Graf 07( a ) nie zawsze jest grafem minimalnym, reprezentującym zbiór słów ,Sub(x). Weź my tekst x = ab" ' Mamy 2n — I zbiorów end-pos dla tego tekstu, a więc G(x) ma In - I węzłów. Łatwo natomiast podać graf reprezentujący Sub(x) i mający jedynie u -i- b węzłów. Jest to minimalna możliwa liczba węzłów. Algorytm tworzenia 07(a) można zmodyfikować i otrzymać algorytm tworzenia grafu minimalnego w czasie linio wym. Jest to jednak dość skomplikowane, a liczba węzłów niewiele się zmienia (G(x) ma cu najwyżej 2ii - I węzłów, a graf minimalny co najmniej n •!■ I. W wielu wypadkach ( 7 ( a ) jest grafem minimalnym, reprezentującym podsłowa ,v). Opiszemy teraz nieformalną konstrukcję, typu off-line (oznacza to, że najpierw wczytu jemy cale słowo wejściowe) grafu (7(.r). Załóżmy, że tekst x kończy się specjalnym symbolem, nie występującym nigdzie indziej w a . Niech Ti będzie niezwurtym drzewem sufiksowym grafu 07(.v). Przypomnijmy, że każdemu prefiksowi każdego sufiksu x w 77 odpowiada dokładnie jeden węzeł. Wprowadzamy następującą relację równoważności na zbiorze wierzchołków drzewa 77. Dwa wierzchołki są równoważne, gdy izomorficzne są poddr/ewa, których są one korze niami. Oznaczmy przez compntsĄTl) graf, który powstaje z 77 przez sklejenie wszyst kich równoważnych wierzchołków (wierzchołek w budowanym grafie odpowiada klasie równoważności wierzchołków 77). Zauważmy następujący fakt (którego udowodnienie
■5.3. luno algorytm y tek.si.mro
189
pozostawiamy Tobie, Drogi Czytelniku): compres.s(TI) jest (z dokładnością do izomor fizmu) grałem podslów słowa ,v. W celu otrzymania efektywnej konstrukcji nie możemy zaczynać od grafu 77, ponieważ może on mieć rozmiar kwadratowy. Możemy natomiast zrobić prawie laką samą kon strukcję, posługując się (zwartym) drzewem sufiksewym 7'= 7'(.v). Niech GI - comprcsĄT). Grał' 67 różni się od G(x) jedynie tym, że etykiety (będące podstawami ,.v) niektórych krawędzi w grafie GI mogą mieć długość większą niż 1, podczas gdy w grafie podslów 6 wszystkie etykiety są pojedynczymi literami. Opiszemy (nieformalnie) procedurę Up<.huc(G/). która zamienia GI na G przez dodanie pewnej liczby nowych wó r ęhióków mk, żeby wszystkie etykiety były jednoliterowe. Procedura ta działa lokalni'. <:T każdego wierzchołka grafu GI. Przetwarzanie wierzchołka v ozna czmy przez l..oc(ii'Jp,l(iii:(y). Z, krawędzi w grafie GI, prowadzących do wierzchołka v, wybierzmy krawędź (w, v) o najdłuższej etykiecie z ~ b {b.,...bt. Wtedy etykieta każdej krawędzi prowadzącej w GI do v jest sufiksem z. Utwórzmy k — I nowych wierzchoł ków u,, i',, .... vt , oraz krawędzie (rę, rę ,) z etykietami , dla / = O, .... k — 1, gdzie v„ = u>, vt = w Rozważmy kolejno każdą krawędź (>/, v). Jeśli etykietą tej kra wędzi jest hj>: , |...bk, to poprowadźmy krawędź (u->', vj ,) etykietowaną b.. Usuńmy następnie, krawędź (u/, v), jeśli ma ona etykietę dłuższą niż I. Algorytm ten umożliwia tworzenie grafu podslów w czasie liniowym przez obliczenie izomorficznych poddrzew w drzewie sttfiksowym. Klasy izomorficznych poddrzew można obliczyć w drzewie w czasie liniowym.
5. 3. Łsi;o.€ a lg o r y tm y t e k s to w e Poza wyszukiwaniem wzorca można rozważać wiele innych problemów związanych z tekstem. W tym podrozdziale zajmiemy się algorytmami umożliwiającymi ich roz wiązanie.
5.3.1. O b lic z a n ie n a jd łu ż s z e g o w sp ó ln e g o p o d slo w a Mając dwtt teksty: ,v i y, oznaczamy" przez nwiexl(x, y) najdłuższe podstawo wy stępujące jednocześnie, w x i w y. Żeby obliczyć mvtext{x, y), utwórzmy drzewo sufiksowe (lub graf podslów) dla słowa v$y#. Niech n ~ | y| , a m ~ |jr|. Wystarczy znaleźć węzeł v o najdłuższym wari(v), z którego prowadzi ścieżka do liścia odpowiadającego pewnemu sufiksowi sitj(i) dla i < m oraz ścieżka do liścia suflj) dla / > m. Ścieżka prowadząca do v odpowiada słowu nwlext(x, y).
190
5. Algorytmy tekstowo
5 .3 ,2 .
O b liczan ie n ajd łu ższego w sp óln ego p o d c i ą g u Przez, nwpodciąg(x, .y) oznaczmy najdłuższy wspólny podciąg lekst.ów ,.v i y. Problem len jest tylko pozornie podobny do poprzedniego. Nie znamy dla niego algorytmu linio wego. Tworzymy tablicę A rozmiaru n 'A-m,■laką z t A \ i , j \ jest długości;: 9 ; ova /iw/w/. ck[g{y\I../], .v| I../I). Niech ,v - ti{...am, y = b v ..ba. Elementy tablic) wyznać,.;.:ny kolej no ze wzoru: A\i, j\ — max(/t|7 - 1, / 1, /V[/, / - l |, A[i — 1, / — 11 -l- S(/z, //,)) II ' ........................................................ gdzie 5 jest funkcją dającą I dla równych argumentów, a 0 dla różnych. Koszt, obliczania tablicy jest 0{n x /«). Po obliczeniu tablicy cofamy się, od elementu A|/i, w\ /.godnie z wyborem maximum we wzorze na /![/, j], W ten sposób obliczamy n\vpodci
5.3.3. W y s z u k iw a n ie s łó w p o d w ó jn y c h Problem wyszukiwania słów podwójnych (słów typu xv) w czasie liniowym jest nietrywialny (ma raczej znaczenie dydaktyczne). Bezpośredni algorytm sprawdzania w wypadku każdego podslowa danego tekstu, czy jest ono słowem podwójnym, czy nie, ma złożoność rzędu //'. Stosując algorytm KMP, można tę złożoność zmniejszyć do ir. Można to zrobić w następujący sposób. Niech pref(it w, e) oznacza długość najdłuższego prefiksu słowa u[ | u j który jest prefik sem v. Inaczej mówiąc, jeżeli potraktujemy « jako tekst, w którym szukamy wzorca u, to definiowana wartość jest równa długości maksymalnego prefiksu wzorca, który „pasuje” do tekstu, począwszy od pozycji i. Wartości lej funkcji łatwo umieścić w tablicy o tej samej nazwie w czasie liniowym jako efekt uboczny algorytmu KMP. Niech x będzie słowem długości //. Słowo to zawiera podstowo podwójne wtedy i tylko wtedy, gdy dla pewnych dwóch pozycji / < k zachodzi prej[i, ,v, , v | 1 | > k - i. Problem obliczania
5.3. In n e algorytm y tek stow e
m
predykatu sąuarefree (,v nie zawiera słowa podwójnego) można zatem sprowadzić do liczenia liniowej liczby tablic typu pref. Opiszemy teraz algorytm dotyczący rozwiązywania tego problemu w czasie O(nk)gn). Oznaczamy go przez ML od nazwiska jego autorów M. O. Maina i R. .1. Lorentza. W algorytmie ML podstawowa operacji] jest iest(u, w). Chodzi tli o sprawdzenie, czy słowo Hi’ zawiera podslowo podwójne tnr. które zaczyna się w //, a kończy w \>. Opiszemy jedynie tę część działania tej operacji, która prowadzi do wykrycia takiego słowa iwr, którego środek znajduje się w v i które ma ustaloną długość 2k {k ~ | tr|). Tę nowi) (pod)operację. opiszemy jako riglutest{u, w, k). Będzie nam potrzebna funkcja .yu/'analogiczna do funkcji pref. Wartość sufi, v, u) jest: długością maksymalnego sufiksu słowa v kończącego się, na /-lej pozycji, będącego jednocześnie snfiksem słowa u. Funkcję tę można stablicować (umieszczając wartości w tablicy o tej samej nazwie suf) w czasie liniowym. Łatwo zauważyć, że riglutestf, v, k) = tnie wtedy i tylko wtedy, gdy prcj\k, r, i»j > 0 oraz prej[k, v, ej + suj\k, v, u] > k. Mając już tablice pref i suf możemy obliczyć predykat rightiesliii, e, k) w czasie. 0(1), Podobnie możemy zdefiniować predykat lefitest(u, a, k) umożliwiający wykrycie słowa podwójnego długości 2k w uv o środku w u. Ponieważ predykat lesi ma wartość tnie, gdy któryś z predykatów lefuesi i riglitiesi ma wartość true dla pewnego k, można go poli czyć w czasie liniowym (obliczając riglittest i left test dla k — 1 Ot o nieformalny schemat algorytmu ML. { A lg o ry tm ML; s p r a w d z a n ie , b e g i n i f | x . j 5/ 2 then s p r a w d ź else b e g i n
c z y tek st: w -z a w ie ra s ło w o p o d w ó jn e } b e z p o ś r e d n io ;
( c z a s 0 ( 1)}
s p r a w d ź r e l a . i r o n c y j n . i e , c z y t e k s t x | l . . L n / 2, J ] z a w i e r a s ł o w o p o d w ó jn e ; s p r a w d ź r e k u r e n c y j n .: i e , c z y x [ \ . n / 2 j -I- 1 . . n ' | z a w i e r a s ł o w o p o d w ó jn e ; sprawdź, c z y z a c h o d z i c e s I: ( x [ l . .L n / 2 J ] , x | L n / 2 J -I- 1 . .n | ) ( c z a s 0(n)j e n d
e n d | a d- LI o r y tin M L } ;
Złożoność algorytmu jest Ofilogn). W algorytmie są wykorzystywane dodatkowo tab lice rozmiaru 0(n). Pokażemy teraz, jak obliczać predykat right test w czasie liniowym z wykorzystaniem dodatkowej pamięci rozmiaru jedynie 0(1). Prowadzi to do algorytmu o tej samej strukturze co algorytm ML, działającego w tym samym czasie 0(/dog/i), ale wymagającego dużo niniejszej pamięci. W algorytmie są wykorzystane pewne prosie własności kombinatoryczne. struktury wystąpień słów podwójnych.
i;)2
5. Al-'oryiray tekstowe
Niech u i i>bgiln dwoma słowami nic zawierającymi słowa podwójnego i nieci) righiOu r) b(,’d/.ie takim predykatem, że righl(u, y) = tnie wtedy i tylko wtedy, gdy słowo itv zawiera słowo podwójne o środku w tekście, y. Podobnie możemy /.definiować left {u, u). Zauważmy. ż,e: MW(//, u) = (Jąfl(u. >’) v ns;Jil(u, i1)). function r i g h t ( u , v : t e k s t ) : b o o l e a n ; |u - a t a, . . . a m, v ~ b, . . .b } begin i ■.--■■n; I: : = n -I- X ; r i g h t falce; while i > 1 and not r i g h t do begin j :-- i ; while ( j -t 1 ) and (jn - i + j > 1) and (a,,.. (. if '/-- 0 then r i g h t :■- t r u e elf.:a berylu k
, = b i) do ;/ ;= j - 1 ;
: i +j;
if < t then begin t : = k;
while (C > i) and (Jbt =; ..s , ,) do t := t - 1; if t i then r i g h t : t r u e ; and; if not r i g h t then i
max (.7 ,I i / 2 I) - J.
end end;
Pozostawiamy Ci udowodnienie, że, podany algorytm ma złożoność liniową ze w z g l ę d u na długość w. Poprawność algorytmu wynika z dwóch własności słów podwójnych. - Słowo podwójne zz występujące w uv rna środek w słowie u, gdy t/v = ypitz.)'-, i |v,zj > j u | dla pewnych słów y, i y3. ^ Niech n i u będt[ tekstami nie zawierającymi słowa podwójnego i takimi, że złożenie u v zawiera słowo podwójne o środku w v. Załóżmy ponadto, żc ?:yz jest najkrótszym prefiksem y, takim że y jest niepuslym sufik.sem u (a więc yzyz jest słowem podwój nym o środku w v). Wówczas słowo z jest najdłuższym słowem, które jest jednocześ nie prefiksem i sufiksem zyz, a y jest najdłuższym wspólnym sufiksem tekstów u i z.v. Załóżmy dodatkowo, żc y,, i',, y', z' są takimi słowami, że |y,| — |y.,|, rpdyT.z'jest prefiksem z, zy jesi właściwym prefiksem y,z'y', y' jest sufiksem u, a z' jest najdłuż szym wspólnym sufiksem słów y,z' i ^z'yVjZ'. Wtedy zy jest właściwym prefiksem y,z' lub | zv| < |y|Z 'v'|/2, a ponadto zyz jest właściwym prefiksem u,z'yV.. : /owod tych technicznych własności słów podwójnych pozostawiamy Tobie, Drogi Czytelniku.
5.3. Inne algorytm y tekulowo
193
Pokażemy teraz, jak można przedstawić problem słów podwójnych, używając struktur (.lanych 7'(.v) i G'(,v). Zakładamy, że alfabet: jest rozmiaru 0(1). Do rozwiązania problemu wystarczy znaleźć w drzewie Tl(x) laki węzeł u, że w poddrzewie o korzeniu v znajdują się dwa różne liście. xiif(i) i -sitj(j), takie że wartość' \ i - j \ jest: równa długości słowa odpowiadającego ścieżce od korzenia 77(r) do węzła u. Jeśli takiego węzła nie ma, to słowo ,v nie zawiera słowa podwójnego (squarefree(x) = true). Zamiast: drzewa 77 Cr) trzeba użyć zwartej reprezentacji T(x) danej drzewem sufiksowym. Znalezienie takiego węzła w cz.asie liniowym jest jednak dosyć trudne. Pokażemy Ci teraz, jak zastosować grał' G(r) do znajdowania słów podwójnych w czasie liniowy in. Zakładamy, że rozmiar alfabetu jest ograniczony przez stałą. Rola G(x) sprowadza się tu do faktoryzaeji tek stu r, która polega na obliczeniu ciągu niepuslych słów (tę, u,..... v ), zdefiniowanego w następujący sposób: niech | ty ■... • ty _ , | = /, gdzie u, = r[1 j, a ty jest najdłuższym prefiksem ii tekstu r|7 + I ,./j], występującym co najmniej dwa razy w tekście r[ I../(]//; jeśli takiego u nic ma, to ty = r |i -I- I], Niech pos(vk) będzie ostatnią pozycją / < i, taką że wystąpienie ty w r zaczyna się na pozycji /; jeśli takiej pozycji nie ma, to przyjmu jemy / = O, Fakloryzację tekstu i obliczenie wartości pox można wykonać w czasie liniowym, korzy stając z grain G(x). Graf len będziemy tworzyć stopniowo, konstruując kolejno G’(t>, ■... • vk) dla k = I, 2, i i i . Znalezienie ty, jeżeli mamy G(ty • ... • ty _,), sprowa dza się, do przejścia pewnej ścieżki w G(v, • ... • vk _,). Można teraz pokazać, ż e r ztiwiera słowo podwójne, gdy dla pewnego I < k < ni zachodzi + |vt | > | v, ■ ... ■ vk .. 1 1 (\y „nakłada” się na siebie, przez co otrzymujemy słowo podwójne) lub left(vk ty), lub nghl(yk _ ,, ty), lub righl(vt • ... ■ ty _ ,, ty • ty). Algorytm obliczania left jest analogiczny do obliczania right. Przypominamy, że koszt nyju(u, ty) był liniowy ze względu na długość r. Fakt ten ma kluczowe znaczenie dla liniowości czasu całego algorytmu. Całkowity koszt jest zatem liniowy ze względu na sumaryczną długość ty ,..., ty, a więc liniowy względem długości n tekstu wejściowego r. Dokładną konstrukcję algorytmu pozostawiamy Ci jako zadanie.
5 . 3 .4 .
W y sz u k iw a n ie słów sy m e try c z n y c h Nieeli wK oznacza odwrócenie słowa. Przez słowa symetryczne (tzw. palindromy) rozumiemy słowa niepuste postaci ww". Do znajdowania słów symetrycznych - podobnie jak do znajdowania słów podwójnych możemy skorzystać z drzewa sufiksowego lub grafu podslów. Istnieje jednak prostszy
194
5. Algorytmy tekstowe
algorytm, umożliwiający obliczenie dla każdej pozycji i maksymalnego promienia A'(/] palindronui o środku na pozycji i (promieniem palindromu wy" jest długość w), a do kładniej A’|7j = max{A > 0: x[i - k -I- l../j = (.r|( + 1../ I- /c|)"} [Algorytm Manachera; obliczanie promieni słów eymetry zi.yoh} bogiń {przyjmijmy, że x [ l ] = $, x [ n j = #}
i?[l] := 0; i := 2 ; j := 0; while (i < n ) do begin while x[i - j] = x|i + j + i] do j := j -I- 1; l?li| :=j; k := 1 /
while (i?|i - k l z fij.-ÓI - k ) and ( k < i ) do begin R [ i -I- Jc] lej, :?[7| - i) ; A := k < ... end; j : = m a x { j - k , 0) ; i ;= i -i- k end 'end {algorytm Manachera} ;
Poprawność algorytmu wynika z następującej własności promieni paiindromów: jeśli -im k = 1../?{/] mamy A|7 —k\ ■■£A[7] - k, to R[i -\- k| = mini. Aj/ - k), A17) k). Operacjami dominującymi w algorytmie są porównania ,\j/ - /| = ,v|7 -i- j -1- lj. Porów nań takich z wynikiem negatywnym jest wykonywanych co najwyżej ;i, a dla każdej wartości i co najwyżej jedno. W porównaniach z wynikiem pozytywnym wzrasta war tość sumy i + j. Wartość la (przy porównywaniu symboli) nie maleje. Ponieważ ma ksymalną wartością i -i- j jest n, całkowita liczba wykonanych porównań symboli nic przekracza 2n.
5.3.5. R ó w n o w a ż n o ść c y k lic z n a Dwa słowa są cyklicznie równoważne, gdy są równe w sensie list cyklicznych, co zapisujemy jako x = y. Problem polega na sprawdzeniu, czy dwa dane słowa są cyklicz nie równoważne. Wystarczy skorzystać z następującego faktu: x = y wtedy i tylko wtedy, gdy .r występuje (jako wzorzec) w tekście yy. Zakładamy tutaj, że |.v| = | y | . Problem sprowadza się więc do problemu WW i jego koszt: jest liniowy. Istnieje jednak prostszy algorytm, który nie wymaga wyszukiwania wzorca i korzystania z dodatkowej tablicy (pamięć 0(1))Niech < będzie dowolnym porządkiem liniowym na zbiorze symboli.
195
In a u algorytm y tek sto w e [Algorytm sprawdzania, esy u s w; n i och begin i := 0; j := 0; k 1; while (i < n) and {j < u) and (k < n) d.o begin
, y — mili, n -- | u| }
k :• I ;
while i -I /;|= y\ j + /;|do k k I- 1; if ( k < n) then begin if ;v|i + k] > y U + kj then i := i -I-k else j {niezmiennik *}
j + k end;
and ; if (k > J)) then return {u m w) else return (nie z.-ic)iod.v.i u ~ iz) end {algorytm) ;
Niech-»(W- u\k + I..//j//| I ../c), a więc niech ntk>powstaje przez cykliczne przesuniecie u. Niech Dl = { I ś k ś u : w**" 11 » n(/)dla pewnego,/} i niech D2 = {I < k < u : uu""11 » vc(/) dla pewnego,/}, gdzie » oznacza rozszerzenie > na słowa (leksykografieznic). Poprawność algorytmu wynika sUjd, że jeśli DI lub D2 jest zbiorem { 1 , 2 , « } , lo nie zachodzi // = ir. Ponadto zachodzi niezmiennik *: (1,2,.... /} c £>/, { 1, 2 , c D2. Algorytm ma złożo ność liniową. Największa liczba poróvvnań symboli jest wykonywana dla słów u i w postaci (odpowiednio) I I... 1201, I i 1... 120.
3 , 3 .6 .
A lg o ry tm I l i i f f m a n a Kompresję tekstów można rozumieć na różne sposoby. Dla nas będzie ona oznaczać redukcję binarnego zapisu danego tekstu. Załóżmy, że dla danego tekstu x = i alfabetu I chcemy znaleźć jednoznaczny kod /; symboli u; alfabetu słowami binarnymi /;(«,.) tak, żeby długość tekstu //(«,) • ... • /<(«„) była minimalna. Rozważamy tutaj klasę takich kodowali, że /;(<«,) nic jest prefiksem h{ai) dla żadnych dwóch różnych symboli i dj. Zbiór kodów symboli możemy reprezentować drzewem, którego ścieżki odpowia dają kodutti poszczególnych symboli. Rit 0 oznacza „idź w lewo” w drzewie, a bit I - „idź w prawo” . Rozważmy przykład ,v = abedededdb. Jeżeli zakodujemy każdy symbol ciągiem dwubitowym, to otrzymany kod będzie miał długość 2n = 20. Przedstawimy teraz algorytm opracowany przez D, Huffmana. Istota tego algorytmu sprowadza się do kodowania częściej występujących symboli krótszymi ciągami binarnymi, a rzadziej występujących symboli - dłuższymi. Przedstawimy na naszym przykładzie działanie algorytmu w wersji rekurencyjnej. Częstość występowania w słowie x poszczególnych symboli jest nastę pująca: częstością)
I, cz.ęst<>ść(b)--- 2, częstością) — 3, częsiośc(d) =: 4.
.1-9(3
5. Algorytmy tekstowe
Znajdujemy dwa symbole, których suma czystości jest minimalna. Są to symbole a i b. Zamieniamy je, oba na jeden nowy symbol, na przykład na a. Otrzymujemy tekst, w któ rym czystość występowania symboli jest następująca: (.sytytośćU') = 3, częsiość^:) ~ 3, częslość(d) - 4. (Częstość występowania symbolu e jest sumą częstości występowania „sklejonych” symboli). Obliczamy teraz rekurencyjnie minimalny kod dla symboli e, c i d. Nietrudno zauważyć, że jest on następujący: h{e) =.00, h(c) - 01 i //((/) = 1, gdyż częstość d jest: maksymalna. Następnie „rozklejamy” symbol a. Kody symboli a i b są takie same jak symbolu c, z wyjątkiem dodania jednego bitu rozróżniającego te symbole. Mamy więc //(«) = h{c)0, /;(/>) = //(«)l, czyli ostatecznie: h(a) = 000, h(b) — 001, h(c) -- 01, /<(//) = I. Po zakodowaniu długość tekstu binarnego wynosi 19, a więc mniej niż długość uzyskana kotłowaniem bezpośrednim (gdy każdy symbol ma tę samą. liczbę bilów). Schemat algorytmu HulTmana w wersji rekurcncyjnej wygląda następująco: {Algorytm Huf finana; kongres ja zapisu binarnego tekstów naci alfabetem o liczbie .symboli > 2 } begin if 1 iczba symboli = 2 then k o c i każ' i g o symbolu składa się z jednego bitu else begin nieci) a i b będą symbolami o najmniejszej częstości; zastąp je nowym symbolem a' o częstości będącej sumą częstości symbol i a i b; wywołanie rekurencyjne algorytmu Huf finana; nieci) h będzie otrzymanym kodem; Ji(a) : ~ - h ( a ' ) 0 ; h ( b ) :== /j(a')l end {algorytm Huffman,)};
Operacjami dominującymi w algorytmie są: pobranie elementu o minimalnej częstości i wstawienie nowego elementu o danej częstości. Operacje te można wykonywać w cza sie ć)(log//) za pomocą kopca jako pomocniczej struktury danych. Całkowity koszt: al gorytmu HulTmana jest 0(/dog/i).
5.3.7. O b liczan ie lek sy k o g ra ficzn ie m akaym alno
1
:1
Przez m a x s u f i k s ( x ) oznaczmy leksykograficznie największy sufiks siowa x , na przy kład iiutxsiifikx(ahabbaaaaaa) - hhaaaaaa.
5.3. In n e a lg o ry tm y te k s to w e
197
Bardzo łatwo można go obliczyć, slosujac strukturę danych T(x) lub G(x). Wystarczy tylko przejść leksykograficznie największą ścieżką. Istnieje jednak znacznie prostszy algorytm, w którym nie korzysta się z żadnej z tych struktur - nawet z tablicy pomocni czej (jedynie z kilku zmiennych). Miech < będzie relacją porządku liniowego w alfabecie. {Algorytm Duval a ; obliczanie n i a x s u f i k s ( x ) ] begin i : b then begin j := j + k ; k := 1; p := j - i end e ls e i f (a = b ) and ( k i- p ) then k := k + 1 e ls e begin j : = j -I- p ; I: := 1 end; *I end ( in s tr u k c ji while] ; m a x s i i f i k s := s u £ { i + 1) end. (algorytm Duvala);
Poprawność algorytmu wynika z pewnych własności kombinatorycznych funkcji max.su/iks. Nieci: inaxxufikx(x) = z = (iifv, gdzie w jest najkrótszym okresem z, a v jest prefiksem właściwym u (być może pustym). Niech per{x) = u i rest(x) — u. Zauważmy, że maksymalny sufiks tekstu' xa jest zawsze sufiksem tekstu maxsufiks(x)u. Niech a' będzie takim symbolem, żc i:cxt(x)u' jest prefiksem per(x). Funkcje per i rest mają następujące własności: (a) maxxujikx{xa) = if a < a' then maxsufikx{x)a else maxsujiks(resl(x)a)\ (b) pc.r(xa) = if a < a' then inaxxufikx(x)a else if a = a' then per(x) else per(rest(x)a)\ (c) rexl(xa) = if a < a' lub (a = a' oraz rext(x)a = per{x)) then słowo puste else if (a = a' oraz rest(x)a < per(x)) then rexl(x)a else rexl(rest(x))a. Poprawność algorytmu wynika z tych własności, ponieważ za jego pomocą można ob liczyć ilcracyjnie maxsufik.six'a), mając policzony maxxufiks(x'), gdzie x' = x[\..j + k] i a jest następnym symbolem tekstu (a = x[j + k -l- 1]). W danym momencie inaxxufikx(x') = ,v|7 -l- | ../ -I- Ic|, rexl(x') = ,v:[/ -I- 1../ + A:| oraz p jest długością okresu per(x'). Czas działania algorytmu jest liniowy;, można to udowodnić, korzystając z faktu, że wartość sumy i + j -l- k wzrasta w każdym kroku. Jej minimalną wartością jest 2, a ma ksymalną 2 |.v |. Efektem ubocznym algorytmu Duvala jest: dokładne obliczenie okresu słowa w (per(x)) w przypadku, gdy x ma okres krótszy niż połowa długości słowax Takie słowa x nazywamy mocno okresowymi. Niech inaxxufikx(x) = i/v, gdzie u jest najkrótszym okresem maksymal nego sufiksu. W algorytmie Duvala oblicza się również w, e i v, a zatem* = w//v. Czwórkę wartości (w, //, e, v) 'nazywamy dekompozycją stifiksową. Oto jej własności.
198
5. A lgorytm y tekstowe
(1) Jeśli słowo x jest mocno okresowe, to per(x) --- Ji./J. (2) Jeśli dana jesl dekompozycja sufiksowa, to sprawdzenie, czy |/t| jest okresem całe go słowa x, można wykonać w czasie ()(\ u | ). (3) Jeśli słowo x jest mocno okresowe i mamy dekompozycję sufiksową x, to dekom pozycję sufiksową słowa x obciętego o per(x) można obliczyć w czasie stałym. Korzystając z powyższych własności, można stosunkowo łatwo napisać algorytm dla problemu wyszukiwania wzorca, działający w czasie liniowym i z wykorzystaniem (do datkowej) pamięci stałej. Algorytm ten jest symulacją algorytmu KIVIP bez dodatkowej tablicy P. Symuluje algorytm Duvala. Oblicza się w nim dekompozycję sufiksową słowa 41.../' + 1], mając daną dekompozycję sufiksową słowa ,v[!../]. Został opracowany przez M. Crochemorc’a. W algorytmie tym przyjmujemy: przesunięcie'(j)
per(x\ I../|), jeśli słowo ,v|ł../j jesl mocno okresowe U /2J w przeciwnym wypadku
fAlgorytm Crochemore 'aj begin {x jest wzorcem, i := 1; j := 0; while i < n — m -I- 1 do [niezmiennik: znamy dekompozycję sufiksową słowa x|l. .-j|i begin while x [,7 -I- 1| = y|i + j \ do begin
j ■= i + 1; oblicz dekompozycję sufiksową x|;L..j], korzyf!|-.rjqr z dekompozycji suf iksowej słowa x| ł . . - 1| [części owa symulacja algorytmu Duvala} end;
if j - m then w r it e ( i ) ; {wzorzec zn a le zio n y }
o b lic z p r z e s u n ię c ie ' (j ) , k o r z y s ta ją c z dekompo i'" i i suf iksow ej x|l . . j ]; i := i -I- p rzesu n ięc ie -:’ {j) ; if p r z e s u n i ę c ie ' (j) < \..j/2 j { p rz e s u n ie c ie = o '=" słowa xj l . . yj) then begin obi icz dekompozyc j ę su i:i knową
x |l . . j p r z e s u n ię c ie ' (;/) |, k o r z y s ta ją c (3) ; J := j - p r z e s u n ię c ie ' (j) ; end else i := 0 end end {algorytm Crochemore ’ a j;
z
W1 a s n o ś c i
5.3. la n e algorytmy tekstowe
1° 9
5.3.8. J e c ljM łz iia e z iie k o d o w a n ie
Niech dany będzie ciii;.: tekstów: kod{ I), kodil), .... kotlik). Możemy go (niklować jako funkcję kod, która każdemu elementowi z alfabetu {I, .... k\ przyporządkowuje tekst. bunkcję tę możemy rozszerzyć na zbiór wszystkich słów mul allabelcm (I, stępujący sposób:
k } w na
kod ii.i....i ' = kodiit)kod(i?)...kodii,) Kodowanie jest jednoznaczne wtedy, kiedy funkcja kod' jest: różnowarlościowa. Chcemy sprawdzić, czy zadany ciąg, tekstów jest kodem jednoznacznym. Rozmiarem problemu jest suma długości tekstów kodii)......kotlik), którą oznaczamy przez n. Sprowadzimy ten problem do znajdowania ścieżki w pewnym grafie skierowanym. Węz łami grafu są sufiksy. slow kod{I), kodii)...... kod(k). W szczególności przyjmujemy, że węzłem jest słowo puste. Przyjmijmy również, że xitf , ,(//) jest słowem pustym, gdy |//| = p . Oznaczmy zbiór węzłów przez V. Zauważmy, że | Vj < n 1 I, gdzie n zostało zdefinio wane jako sumaryczna długość słów kodowych. Niecli zapis ,v//v oznacza tutaj operację obcinania słowa :v o słowo y, tzn. .v//v = z, jeżeli x = yz. Operacja ta jest zdefiniowana częściowo: wynik jest określony, gdy y jest prefiksem ,v. Niech A7//j;(/j oznacza sufiks; rozpoczynający się od pozycji-/.: w słowie kod(i). Dla każ dego elementu v = xitfk(i) i słowa kotlij) tworzymy krawędź skierowaną: (a) (xu/t(i), sufr(j)), jeżeli xuj]{j) = kod(j)Hv i operacja ta jest określona; (b) (xu)\(i), xu/ji)), jeżeli xiifj:) ---- vllkodif) i operacja ta jest określona. Niech Vtl oznacza zbiór węzłów początkowych. Są to węzły odpowiadające wszystkim Iowom niepusl cm postaci kodii)//kotlij) dlii i Aj. Niech węzłem końcowym będzie sło wo puste. Nietrudno zauważyć, że kod nie jest jednoznaczny wtedy i tylko wtedy, gdy w odpowiadającym mu grafie istnieje ścieżka ml węzła początkowego do węzła końcowego. Otrzymany graf G ma co najwyżej ir krawędzi. Koszt przeszukania G jest więc lego samego rzędu. Mniej oczywisty jest. koszt tworzenia grafu O. Musimy umieć szybko odpowiedzieć na pytanie, czy para słów (V, u) jest krawędzią, co sprowadza się jednak tlo problemu WW. Istnienie na przykład krawędzi na mocy warunku (b) (xu/ji) ~ vUkodij) 1 operacja // jest określona) jest równoważne występowaniu wzorca kotlij), od pozycji r w słowie, kodii). Łatwo obliczyć wszystkie problemy WW dla tekstów kodii) i wzorców
200
!>. A lgorytm y te k s to w e
'kodij) w (sumarycznym) czasie O(ir), stosując wielokrotnie jeden z algorytmów wy szukiwani;! wzorca w czasie liniowym. Reasumując: problem jednoznaczości kodowania możemy rozwiązać w czasie 0(ir).
S-.3.9.
Liczeni© lic z b y p o d słó w /.alóźmy. że rozmiar alfabetu jest stały, Pokażemy, jak obliczać w czasie, liniowym liczl)i,', wszystkich różnych podsłów danego słowa ,v. Oznaczmy tę liczbę przez cot,iiii(x). Pokażemy najpierw, jak można skorzystać z grafu podsłów G(x). Ponieważ liczba ścieżek prowadzących do węzła v nie będącego korzeniem jest. równa Ien^l!i(r)dength(SUF[v]), wystarczy zsumować te liczby po wszystkich węzłach t», W ten sposób otrzymamy liczbę COIIIIl(x).
Jeśli mamy G(,x) bez tablicy SUF, to coiini(x) możemy obliczyć jako liczbę, wszystkich ścieżek w grafie G(x), prowadzących od korzenia do jakiegokolwiek innego węzła. Licz bę ścieżek w grafie skierowanym acyklicznym łatwo obliczyć, korzystając z sortowania topologicznego takiego grafu. Liczbę .ścieżek: rozpoczynających sv od każdego węzła liczymy, przeglądając węzły „od końca” . fen sam problem można rozwiązać za pomocą drzewa sufiksowego. Niech 7’(.v) będzie drzewem sufiksowym. Wagę krawędzi definiujemy jako długość słowa odpowiadającego tej krawędzi. Liczba c o u n l i x j jest sumą wag wszystkich krawędzi drzewa sufiksowego.
b a d a n ia 5.1. Posługując się tablicą P, skonstruuj algorytm sprawdzania w czasie liniowym, czy dany tekst zaczyna się słowem postaci ww lub słowem postaci inni'. 5.2. Skonstruuj algorytm liniowy obliczania tablic pre/[*, u, r], sit/\*, u, g|, /.definiowa nych przy omawianiu słów podwójnych. (Zmodyfikuj algorytm KMP). 5.3. Skonstruuj algorytm, który umożliwia sprawdzenie w czasie, liniowym i z wyko rzystaniem pamięci pomocniczej 0(1), czy dany leksl jest słowem Fibonacciego. 5.4. Udowodnij, że delayUn) = O(logm), gdzie ihliiy(w) jest funkcja zdefiniowaną przy analizowaniu algorytmu KMP'. 5.5. W algorytmie KMP' czasami „przeczekujemy” 0(log/«) kroków przed wczyta niem kolejnego tekstu. Przerób len algorytm lak, żeby odstępy czasowe między kolejnymi wczytaniami były ograniczone przez pewną stalą i żeby po wczylaniu
Zadania
201
kolejnego /-tego symbolu dochodziło do wypisania informacji, czy wystąpienie wzorca kończy się na tej pozycji (w zależności od tego, czy wzorzec jest, czy go nie ma, zostają wypisane, „true” lub „false” ). Załóżmy, że pamiętamy w tablicy tylko wzorzec i kilka zmiennych roboczych. Tekst wejściowy y jest wczytywany on-line. 5.6. Udowodnij, że po obcięciu dwóch ostatnich symboli słowa Fibonacciego są palindromami oraz .,. , =jih„ , ,fib„ z dokładnością do zamiany dwóch ostat nich symboli. 5.7. Niech wzorzec ,v będzie słowem Fibonacciego. Udowodnij, żc NEXT[Fk - 11 = ~ ć; I oraz P[j\ - j - /'). ... , dla F\ < j < /'). , h gdzie k > I. 5.8. Skonstruuj algorytm liniowy obliczania tablicy P dla drzewa 7'prefiksów wzorców ze zbioru wzorców X. 5.9. Napisz pełną wersję algorytmu AC (Aho-Cornsicka) szukania wielu wzorców w czasie liniowym ze względu na sumaryczni) długość wszystkich wzorców i teks tu do sprawdzenia. 5.10. Napisz pełną wersję algorytmu Bak (opracowanego przez T. Bakera) szukani,a wzorców dwuwymiarowych w czasie liniowym ze względu na całkowity rozmiar danych (//’ dla tablicy o wymiarach n x //). 5.11. Udowodnij, że algorylm GS' ma koszt liniowy.
5.12. Zastosuj algorylm KMR do znalezienia maksymalnej długości /-takiej, że w tekś cie występuje.dwukrotnie (A-krotnie) to samo słowo o tej długości. 5.13. Napisz wersję algorytmu KMR dla przypadku dwuwymiarowych i - ogólnie - A-wymiarowych wzorców (A jest stalą całkowitą). Koszt algorytmu ma być 0(/jlog//), gdzie // jest: całkowitym rozmiarem danych wejściowych. 5.1-1. Skonstruuj algorytm liniowy obliczania tablic <7/ i <72, wykorzystywanych w al gorytmie BM (skorzystaj z modyfikacji algorytmu KMP i liczenia tablicy P). 5.15. Podaj wzór na liczbę wykonanych porównań symboli w algorytmie BM dla v = ca{bnf i y ~ irk 1 '{baf. Ile zostanie wykonanych porównań, gdy zamiast cl2 użyjemy w algorytmie jako przesunięcia wartości <7/7 5.16. Udowodnij, że obliczanie iloczynu logicznego ma ten sam rząd złożoności co obliczanie iloczynu tekstowego (definicje podane przy opisie algorytmu FP). 5.!7. Udowodnij, żc liczba krawędzi grafu podslów G(x) nie przekracza 3|.-v|.
202
5. A lgorytm y tek stow o
5.18. Skonstruuj pełny algorytm liniowy obliczania drzewa sufiksowcgo na bazie al gorytmu tworzenia G(.v) (jako elekt uboczny tego algorytmu). Załóżmy, że rozmiar alfabetu jest 0(1). 5.19. Niech T będzie drzewem sufiksowym dla wzorca x kończącego sio specjalnym
symbolem. Niech sexl będzie laką tablicą, że sexl[a, i’J jest węziern odpowiadającym minimalnemu słowu y, dla którego a • wart(v) jest prefiksem (niekoniecznie właściwym). Jeżeli takiego węzła nie ma, to xext[a, fj = nil. Oblicz tablicę sexl w czasie liniowym. Skonstruuj algorytm obliczania drzewa sufiksowcgo V'(,v), w którym zamiast z tablic TEST i LINK korzysta się jedynie z tablicy sexl (liczonej w czasie algorytmu). Koszt algorytmu ma być liniowy. 5.20. Niech ,v kończy się specjalnym symbolem. Udowodnij, że tablica scsi Jia T(.\) daje reprezentację grafu G{xK) w sensie uast{a, v) —sext\a, r|. Napisz alternatywny algorytm liniowy tworzenia grafu G(x), będącego efektem ubocznym obliczania d rzc wa sufi kso weg o. 5.21. Niech G będzie grafem podslów tekstu $..v, gdzie $ występuje jedynie na początku tekstu, a więc G - G($.v). Podaj prostą transformację grafu G w graf G' = G(x). 5.22. Podaj pełny algorytm liniowy sprawdzania, czy dany tekst zawiera słowo podwój ne. 5.23. Algorytm Manachera umożliwia obliczanie promieni palindromów parzystych (o długości. parzystej, postaci w R). Podaj wersję tego algorytmu dla obliczania promieni palindromów nieparzystych (postaci vavK) w czasie liniowym. 5.24. Skonstruuj algorytm liniowy sprawdzania, czy dany tekst jest złożeniem pewnej liczby palindromów o długości parzystej (słów postaci ww11). Skorzystaj z tablicy promień i pa Iind romów. 5.25. Niech depth(y) oznacza głębokość węzła v w drzewie sufiksowym T. Udowodnij nierówność: deplh(UNK\u, aj) < depth(v) -I- I. 5.26. Skonstruuj algorytm liniowy sprawdzania, czy tekst jest złożeniem trzech palindromów parzystych. 5.27. Podaj pełny algorytm rozwiązywania w czasie liniowym problemu obliczania odległości redakcyjnej. Obiicz w czasie liniowym minimalny ciąg operacji re dakcyjnych. 5.28. Zmodyfikuj algorytm obliczania odległości redakcyjnej lak, żeby cl la danych dwóch tekstów ,v i y była liczona minimalna odległość między x i pewnym pod-
Zadaniu słowem y. Znajdź podslowo nic wzorcti z błędami).
203 y
o minimalnej odległości od x (jesi to tlopasowywa■■
5.29. Udowodnij poprawność algoryi.mii Crochemore’a obliczania wartości iiiaxxiifikMx). 5.30. Oszacuj dokładnie maksymalną liczbę porównań symboli w algorytmie sprawdza nia równoważności cyklicznej dw'V.h tekstów ( ii ~ w) za pomoett podanego na sir. 195 algorytmu. 5.31. Udowodnij, że aby sprawdzić jednoznaczność kodu kod( I) i kod(2) dwóch elemen tów, wystarczy jedynie sprawdzić, czy nie. zachodzi równość: kod( I)kod(2.) —■ ~ kod(2)kod( i j. ' 5.32. Napisz itoracyjiifj wersję algorytmu HulTnmna. Udowodnij, że wyznaczone, w wy niku działania algorytmu kodowanie jest optymalne. 5.33. Udowodnij, że righlicsi(u, e, Aj - true wtedy i tylko wtedy, gdy pnj'\k. r\ r| > 0 oraz prefj A, v, v| -l- suf\k, g, nJ > k. I )c. inieje tablic pref i suf oraz clo li n ic:j;i predy katu riyjuiesl są podane, przy omawianiu słów podwójnych. 5.34. Skonstruuj algorytm rozwiązywania problemu wyszukiwania słów podwójnych w czasie. O(nlogn) i z wykorzystaniem pamięci 0(1). 5.35. Udowodnij własności słów podwójnych podane przy omawianiu algorytmu ob liczania riglu(u, i’) w czasie liniowym i z wykorzystaniem pamięci stałej. Pokaż również, że. algorytm jest poprawny i jego złożoność jest liniowa względem |e |. 5.36. Udowodnij, że, ,v zawiera słowo podwójne, gdy dla pewnego I < k < m zachodzi pax(vt) -l- | ty. | > |ty • ... ■ty || (ly „nakłada” się. na siebie, przez co otrzymu jemy słowo podwójne) lub left(iy _ ,, ty), lub Hg/i/(iy .. ,, ty), lub r/gl/tie, • ... - ly ly _ py). Zapis r, • ... • ly . py oznacza faktoryzację tekstu ,v. 5.37. Podaj pełny algorytm fakloryzncji tekstu. 5.38. Udowodnij, że. jeśli Aj/ — /oj Z- Aj/j -- k dla k - l../\j/j, to Aj/ ■(- k] = inin(Aj/ - Aj, RU) - A). ' 5.39. Udowodnij, że, algorytm Manachera ma złożoność liniową. 5.40. Skonstruuj algorytm liniowy obliczania minimalnego grafu reprezentującego zbiór Sub(x) wszystkich podslów tekstu ,v. „Sklej” równoważne węzły grafu (7(.v). Węzły vl, \>2 odpowiadające podslowom ,\7 i x2 (w takim sensie, że warl(v/) = xl,, a \mri(\>2) - x2) są równoważne, gdy Sub(x)//xl —Sub(x)l/x2. Klasy równoważno ści są tutaj co najwyżej dwuelemenlowc. Zakładamy, że alfabet ma rozmiar 0(1).
204
'i. A lgorytm y tekstowe
-(.41. Załóżmy, że alfabet jest „mały” i że niektóre pary symboli są przemienne (na przykład ab = ba). Dwa słowa są równoważne, gdy możemy otrzymać jedno z drugiego za pomocą pewnej liczby zamian sąsiadujących ze sobą symboli, które są wzajemnie przemienne. Jeśli na przykład w alfabecie [a, />, cj jedyną parą przemienną jest para symboli a i b, to abbarh :£i baabcb i nie zachodzi acb ~ hen. .Skonstruuj algorytm liniowy sprawdzania, czy dwa slowtt długości n są równoważ ne. Zauważmy, że dla danego tekstu możemy mieć wykładniczą liczbę tekstów równoważnych. 5.42. Załóżmy, że. wagi elementów są posortowane. Pokaż, jak zaimplementować wtedy algorytm IluITmana lak, żeby działał w czasie liniowym (wskazówka: skorzystaj ze stosu; nowe elementy są przesyłane na stos, który automatycznie jest sor towany). 5.43. Napisz dokładną implementację algorytmu Crochemore'a rozwiązywania problemu WW w czasie liniowym i z wykorzystaniem (dodatkowej) pamięci stałej (w rozdzia le tym opisaliśmy jedynie nieformalny schemat lego algoryimu). Jest lo przykład konstrukcji algorytmu metodą „transformacyjną” . Algorytm Duvala jest przekształ cony na algorytm dopasowywania wzorca. W algorytmie Dm ab. liczymy leksykograficznie maksymalny sufiks, podczas gdy w problemie, YVYV lo nas nie interesuje (korzystamy jedynie z ubocznego efektu algorytmu Duvala). W swoim programie powinieneś użyć jedynie kilku zmiennych całkowitych. Zakładamy, że wzorzec ,v i tekst y są umieszczone w tablicach, które mogą być tylko odczytywane (nie mogą byc modyfikowalne). - Tablice te nie są traktowane jako „pamięć” , a jako dane wejściowe.
tandardowym modelem obliczeń sekwencyjnych jest maszyna ze swobodnym dostępem do pamięci. Model ten jest oznaczany jako RAM (skrót od ang. Random Access Machine). Niestety w wypadku obliczeń równoległych nie ma podobnego (w porównaniu z sytuacją w obliczeniach sekwencyjnych) standardu. Spowodowane jest lo tym, 'że istnieje niewiele komputerów naprawdę równoległych (z dużą liczbą proceso rów), a poza tym nie wiadomo, jaka będzie w przyszłości najlepsza technologia tych maszyn. Jeśli jednak chodzi o przedstawianie i analizowanie algorytmów (mniej o reali zację), to najbardziej rozpowszechnionym modelem obliczeń równoległych jest maszyna równoległa ze swobodnym dostępem do pamięci, oznaczana jako PRAM (skrót od ang. Parallel Random Access Machine). IP
PRAM jest „wyidealizowanym” modelem obliczeń. W modelu tym pomija się wiele szczegółów technicznych, zwłaszcza te, które są związane ze wzajemną komunikacją i synchronizacją procesorów. Przyjmuje się, że każdy procesor może komunikować się z innym w stałym czasie przy użyciu wspólnej pamięci. Wynikająca stąd bardzo duża liczba połączeń nie jest realizowalna przy współczesnej technologii. Jednakże model ten jest bardzo przydatny do opisu algorytmów równoległych, właśnie z powodu pominięcia tyci) szczegółów technicznych. Co więcej, PRAM daje się symulować (implementować) na bardziej realistycznych modelach. Złożoność niewiele się wtedy pogarsza, czas zwię kszał się o czynnik logkn, a w wielu typowych sytuacjach rząd złożoności w ogóle się nie zmienia (na przykład w wypadku sortowania i mnożenia macierzy). Model obliczeniowy PRAM składa się z pewnej (z. reguły dużej) liczby procesorów. Każdy z nich jest maszyną ze swobodnym dostępem do pamięci (RAM). Procesory nicują synchronicznie; w jednym kroku jest wykonywana jedna instrukcja dla każdego pn >cesora. Żeby uniknąć wchodzenia w szczegóły technologiczne, opiszemy jedynie jed ną ogólną konstrukcję programistyczną, umożliwiającą bardzo proste wyrażanie rów noległości obliczeń. Konstrukcją tą jest równoległa wersja instrukcji „dla” (for)
206
(i. Algorytmy równolegle for each x G X do in parallel a k c j i ) . { x ) ;
gdzie. akcja(x) jest pewną operacją zależną od parametru a:. W wyniku wykonania na przykład instrukcji for each i e. [1. .nj do in parallel A| i j := 2 * i ;
każdemu z elementów tablicy A zostanie przypisana wartość 2*/, gilzie i jesl pozycją w tablicy. Istnieją pewne niebezpieczeństwa natury semantycznej, związane z tak ogólnym mode lem. Jeżeli na przykład i-ty procesor, gdzie / e I l. /i|, chce przypisać lej samej zmiennej x wartość 2*/, to nie wiadomo, jaka wartość będzie Taktycznie przypisana (co może prowadzić do niedelerminizmu). Sytuacje takie nazywają się konfliktami zapisu. Jest wiele sposobów poradzenia sobie z nimi. Można na przykład przyjąć zasadę, że z proce sorów usiłujących dokonać zapisu w to samo miejsce dokonuje zapisu procesor o naj mniejszym numerze. Najprościej jednak założyć, że konflikty zapisu są w ogóle za bronione. Nie ma natomiast większego problemu (z punktu widzenia semantyki pro gramów) z konfliktami odczytu: wiele procesorów może jednocześnie czytać wartość Lej samej zmiennej. (Należy jednak pamiętać, że przy implementacji na bardziej realis tycznych komputerach równoległych oba typy konfliktów stwarzają problemy o podob nej skali trudności). W rozdziale tym za podstawowy model obliczeniowy przyjmujemy maszynę PRAM bez konfliktów zapisu. Głównym zagadnieniem w teorii algorytmów równoległych jest rozstrzygnięcie., czy pro blem obliczany w czasie Sekw(n) sekwencyjnie (na jednym procesorze) da się rozwiązać w czasie istotnie mniejszym pray użyciu wielu procesorów. Problem ten można nazwać problemem efektywności zrównołeglania algorytmów sek wencyjnych. Przez „istotnie mniejszy” rozumiemy zwykle czas wielomianowo-logarytmiczny T(n) = \ogk(Sekw(n)), a przez „wiele” procesorów - wielomianową (wzg lędem //) licz bę procesorów P(n). Całkowita liczba operacji w algorytmie równoległym wynosi T(n)*i\n i. a optymalność tego algorytmu wyraża się ilorazem Sekw(ri)/(T(n)P(n)). Im większy jest ten ilorliż, lym lepszy jest algorytm równoległy. Algorytmy optymalne można zdefiniować jako algorytmy ze stałym ilorazem oplymalności. Są to algorytmy równoległe, których symulacja na jednym procesorze daje algorytm o złożoności równej asymptotycznie najszybszemu znanemu algorytmowi sekwencyjnemu dla danego problemu. W tym rozdziale zajmiemy się najpierw zrównoleglaniern możliwie najprostszych al gorytmów sekwencyjnych. W tym celu za model obliczeń sekwencyjnych przyjmiemy (tak zwane) proste programy sekwencyjne. Są to programy iteracyjne (nierekureneyjne)
6.1. R ó w n o le g le o b lic z a n ie w y ra ż e ń i p ro s ty c h p r o g r a m ó w
207
. ciągi instrukcji przypisania, licz instrukcji warunkowych. Okazuje się, że lego typu uproszczone obliczenia .sekwencyjne są wystarczająco ogólne do badania problemu olektywności zrównolcglania algorytmów sekwencyjnych. Algorytmy sekwencyjne o bardziej skomplikowanej strukturze (rekursja, instrukcje wa runkowe) su w ogólnym przypadku bardzo trudne do analizy możliwości ich zrównofeglenia. Są jednak pewne łatwe szczególne przypadki. Algorytmy sekwencyjne o stru kturze rekurencyjnej dajij się łatwo zrównoleglić, gdy głębokość rekursji jest logaryt miczna oraz wywołania rekurcncyjnc są niezależne (mogił być wykonywane, jednocześ nie). Typowym przykładem jest obliczenie iloczynu n liczb. Przyjmijmy dla uproszcze nia, że, // jest potęga dwójki. W algorytmie, rekunencyjnym jest osobno obliczany iloczyn pierwszych n/2 liczb i iloczyn ostatnich n/2 liczb. Następnie, w jednym kroku jest ob liczany wynik: końcowy z zastosowaniem jednej operacji mnożenia. Równoległa wersja różni się od sekwencyjnej jedynie tym, że. wywołania rckureneyjne. są wykonywane jed nocześnie. Zamiast mnożenia dwóch liczb możemy wziąć dowolną operację łączną i wykonywalną na RAM w czasie jednostkowym, a zatem obliczanie iloczynu, sumy, maksimum i mini mum z. n liczb możemy wykonać na maszynie PRAM w czasie (ż(logn), korzystając z n procesorów. Pozostawiamy Ci jako ćwiczenie redukcję procesorów o czynnik logu bez zmiany złożo ności czasowej (asymptotycznie). Zamiast maszyny PRAM można tutaj przyjąć znacznie prostszy model obliczeniowy: drzewo procesorów (komunikacja tylko między procesora mi poprzednik (ojciec) - następnik (syn) w drzewie). W rzeczywistości taka komunikacja odpowiada strukturze rekurencyjnej obliczania iloczynu n liczb. Obliczanie iloczynu dwóch macierzy o rozmiarze ii x ii sprowadza się do obliczenia ir iloczynów skalarnych. Każdy taki iloczyn liczy się. jak sumę n liczb, a więc wystarczy n procesorów do policzenia jednego iloczynu skalarnego w czasie. <7(log/i). Ponieważ, mamy ir takich iloczynów do policzenia, możemy stwierdzić, że iloczyn dwóch macie rzy o rozmiarze n X n można obliczyć tut maszynie PRAM w czasie O(logu), korzystając z ni procesorów.
8. 1.
Równolegle obliczanie wyrażeń i prostych.' programów sekwencyjnych Przykładem programu sekwencyjnego dającego się. łatwo zrównoleglić jest instruk cja jednoczesnego (wielokrotnego) przypisania: .... xn) : (ml), vttl,, .... w«/„)
208
I*. Algorytmy równolegle
gdzie vril.. si] wyrażeniami zawierającymi jedynie stale. W algorytmie sekwencyjnym jest wykonywań veil kolejno n elementarnych instrukcji przypisania: .r, := i a, : val2; i val„ ; Algorytm równoległy składa się z jednej instrukcji: for ciach i
g
|1. ,n] do in parallel x, := v a l s
Zajmiemy się teraz, nieco bardziej skomplikowanymi programami. Programy te, jak rów nież sposób ich zrównoleglenia, będą pewnym uogólnieniem instrukcji jednoczesnego przypisania. Ciąg sekwencyjny instrukcji przypisania x, := lij. gdzie I < i < n, nazwiemy prostym programem sekwencyjnym, jeżeli IV, jest wyrażeniem zawierającym jedynie zmienne o numerach mniejszych niż. i (zmienne o wartościach wcześniej.obliczonych). Załóżmy dla uproszczenia, że wyrażenia takie zawierają co najwyżej dwie zmienne i operacje -i- , - , * lub /. Dla prostego programu sekwencyjnego P przez grajlP) oznaczymy graf acykliczny, którego korzeniem (wierzchołkiem bez poprzedników) jest zmienna x„, a następnikami (lanego węzła x, są zmienne występujące w wyrażeniu IV,. Zbiorem węzłów V grafu są jedynie węzły osiągalne z korzenia (zmienne „biorące” faktyczny „udział” w ob liczaniu wyniku). Zmienne, odpowiadające tym węzłom (jak również, te węzły) nazy wamy aktywnymi. Na początku zajmiemy się jedynie prostymi programami sekwencyjnymi, obliczającymi wartości wyrażeń arytmetycznych. Załóżmy (do odwołania), że każdy rozważany prosty program sekwencyjny P ma strukturę drzewiastą, tz.n. graJiP) jest drzewem. Program laki oblicza wartość wyrażenia, przechodząc drzewo wyrażenia metodą „z dołu do gó ry” . Węzły drzewa odpowiadają zmiennym programu. Ponieważ zmienna odpowiada korzeniowi drzewa wyrażenia, a jej wartość daje wartość całego wyrażenia, naszym celem jest obliczenie jedynie wartości ,v(l. Działanie algorytmu przedstawimy na następującym przykładzie programu P obliczania wartości wyrażenia W = 2(3(„v/ + x2) -l- 2((2 * x3) -i- 2)) -I- x4 z następującymi wartościami x l, ,v2, ad, x4: xJ — I. x2 - 2, x3 = 3, x4 - 4. Program P obliczania Wjesl takim oto prostym programem sekwencyjnym:
6.3. R ó w n o le g li) o b lic z a n ie w y r a ż e ń i p r o s t y c h p r o g r a m ó w
209
program P x l := 1; x 2 2 ; x3 := 3 ; x4 := 4 ; x5 :- 2 * x 3 ; x6 := x5 -I- 2 ; :s7 := x l + x2; x8 := 3 * x7 -I- 2 'i*xć>; x9 2 * x8 + x4
Algorytm równoległego wykonywania P będzie podobny do równoległego wykonywania programu wynikającego z instrukcji jednoczesnego przypisania. Będziemy starali się wykonać jednocześnie obliczenia wyrażeń IV, stojących po prawej stronic instrukcji przy pisaniu. Jednakże nie zawsze (nie. dla każdego i) będzie Lo w pełni możliwe. Jeżeli wartości pewnych zmiennych występujących w W, nie są policzone, to wydaje się, że, nie można obliczyć wyrażenia VI7,, Co to znaczy dokładnie, że wartość xt. jest policzona? Otóż po prawej stronie k-tego przypisania występuje stała. Gdybyśmy chcieli w jednym równoległym kroku algorytmu obliczać jedynie wyrażenia U7,., w których wszystkie zmienne są. policzone, to w niektórych wypadkach laki algorytm równoległy byłby nie wiele lepszy od sekwencyjnego, na przykład w sytuacji, kiedy drzewo wyrażenia byłoby bardzo ,,ci aide” i miało wysokość liniową.. Przyjmijmy, że będziemy obliczać wyrażenia IV; w inny sposób. Zmienną ,v, nazywamy bezpieczną wtedy, kiedy wyrażenie W, zawiera co najwyżej jedną zmienną. Nasz al gorytm równoległy będzie polegać na „częściowym” obliczaniu wyrażeń IV,. Wszystkie zmienne bezpieczne, występujące, w VI7, będą zastąpione przez odpowiadające im wyra żenia, a następnie otrzymane w ten sposób wyrażenie będzie uproszczone jak to tylko możliwe. Operację laką nazywamy redukcją wyrażenia Wj. W naszym programie P zmiennymi bezpiecznymi są wszystkie zmienne oprócz ,v7, x8 i x9. Jeżeli operację redukcji zastosujemy do wyrażenia odpowiadającego x8, lo otrzymamy x8 := 3 * x7 -I- 2 z x5 + 4 Teraz, wyrażenie, odpowiadające xS bierze, się stąd, że w wyrażeniu 3 * *7 + 2 * xó zmienną bezpieczną x6 zastąpiliśmy wyrażeniem x 5 + 2. Zauważmy, że zmienna x6 jest „bezpieczna” w tym sensie, że liczba zmiennych w wyrażeniu dla x8 się nie zwiększa. Rozmiar wyrażeń stojących po prawej stronie instrukcji przypisania jest ograniczony przez stalą. W każdym wyrażeniu występują co najwyżej dwie zmienne, a ta sama zmienna występuje co najwyżej raz w jednym wyrażeniu. Wartość danej zmiennej *. zostanie w pełni obliczona, gdy wyrażenie W7, będzie stalą. {Alg o:i:ytm jednoczesnych podstawień; równoległe obliczanie wyrażeń zapisanych jako proste programy sekwencyjne] begin repeat for each i e [1 . .n | do in parallel dokonaj redukcji wyrażenia hj { castą);) w s z y s t k i e z m i e n n e b e z p i e c z n e w W, p r z e z i c h wyra żenią} until x n obliczone (wyrażenie wn jest. stałą) end JaIgorytm jednoczesnych pods bawień] ;
210
(i. A lgorytm y rów n olegle
Przedstawimy teraz działanie lego algorytmu dla naszego przykładowego prostego pro gramu sekwencyjnego P . Po pierwszym wykonaniu instrukcji iloracyjnej (iteracji.>otrzy mujemy program PI. PI: x5 := 6; x7 := 3; xS
3 * x7 l 2 * .O + 4; x9
2 * xS + 4;
Wypisujemy tylko wyrażenia dla zmiennych aktywnych (odpowiadające im węzły należ;) do grafu graf(PI)).. Po drugim wykonaniu instrukcji iteracyjnej otrzymujemy: P2: xł> ~ 25; x9 := 2 * .v.S' + 4; Po następnym wykonaniu mamy: P3: x9 - 54 Wartość x9 zostaje obliczona i kończy się działanie algoryinut. W naszym przykładzie były potrzebne tylko trzy kroki na obliczenie x9. Kilka proeesorpjv znacznie przyspieszyło obliczenia. Ciąg grafów kolejnych programów GO = grąf(P), Gl = grąf(PI), G2 = grąf(P2) oraz G3 = grąf(P3) jest przedstawiony na rysunku 6.1.
Okazuje się, że rozmiar tych grafów sukcesywnie maleje; w ogólnym przypadku liczba iteracji jest logarytmiczna. Żeby to udowodnić, przejdziemy do języka teorii grafów (grafy, które tu występują, są w istocie drzewami).
Rys. <>.l. Gniły kolejnych programów
21 I
(i.l, R ó w n o leg le o b lic z a n ie w y ra ż e ń i p ro s ty c h p ro g ra m ó w
P iy.cz, redu<:e{'P) będziemy oznaczać drzewo binarne T' powstałe z '/'w wyniku następu jącej operacji: każda krawędź prowadząca do liścia jest usunięta; każda kraw ęd ź prowa
dząca od węzła v do węzła w jest zastąpiona przez krawędź od v do nasięimikiw), jeść tv ma tylko jeden następnik. Drzewo T ' jest podgrafem otrzymanego grain, zawierając) m tylko węzły osiągalne z korzenia (połączone z korzenieni ścieżką). Niecił teraz 7' = grafit*) i niech P ' będzie programem sekwencyjnym (ciągiem przypi sań), otrzymanym z / ’ za pomocą jednego wykonania instrukcji iteracyjnej algorytmu jednoczesnych podstawień. Mamy rediice(T) = graJ{P'), ponieważ podstawieniu stałej mi zmienną odpowiada usunięcie krawędzi prowadzącej do właściwego dla niej liścia, a wymianie, krawędzi od v do w na krawędź od v do iia.siępnik(w) odpowiada zastąpienie xk przez IV,, gdy IV, zawiera dokładnie jedną zmienną. Niech 17*| oznacza liczbę węzłów (rozmiar) drzewa T. Prawdziwa jest następująca nierówność: | irdiii iiT) | < 2/317j , jeżeli |7 j > I W dowodzie posłużymy się rysunkiem 6.2. Niektóre węzły drzewa 7',,znikają” w drze wie T = reduce(T), ponieważ nie, są osiągalne z korzenia po usunięciu/wyniianie pew nych krawędzi. Oszacujemy, jak wiele, węzłów pozostaje w 1". Przez łańcuch rozu miemy maksymalmt ścieżkę (w kierunku liści drzewa 7), zawierającą co najmniej dwa węzły, na której każdy węzeł ma w T dokładnie jeden następnik. (Liście nie są zatem elementami łańcuchów). Dodatkowo żądamy, żeby ostatni węzeł v takiej ścieżki nic należał do łańcucha wtedy, kiedy ścieżka ma nieparzystą liczbę wę/.lów, a następnikiem węzła v jest liść (zob. zaznaczone łańcuchy na rysunku 6.2). Podzielmy zbiór V wszyst kich węzlc'".v drzewa T na dwa podzbiory Vj i K„ gdzie V, składa się. /. węzłów zawartych P); / reduce
s ,<
V, JS
5
14
3<
13 I1
Rys. 6.2. Opernejn n u llin '
o 16
212
6. A lgorytm y równoległe
w łańcuchach, a V, z pozostałych węzłów. Wystarczy udowodnić, że. z każdego ze zbiorów V, i V', pozostaje (po redukcji) co najwyżej 2/3 węzłów. Jest to dosyć oczywiste dla zbioru Vt (w rzeczywistości z każdego łańcucha „zniknie” co najmniej połowa węz łów, a więc w V, zostanie jedynie co najwyżej połowa węzłów). Jeżeli chodzi o zbiór K,, to zauważmy, że ma on co najwyżej 3m węzłów, gdzie /u jest liczb:) liści drzewa V. Do zbioru V., należ:) liście oraz co najwyżej m węzłów, których następnikami sg liście. Na pewno leż iiale.żi) przodkowie liści z dwoma następnikami. Takich przodków może być co najwyżej m .. I. Jeżeli do V., należ:) jednocześnie liść v i jego poprzednik, którego u jest jedynym następnikiem, to poprzednik liścia v jest usuwany. W przeciwnym razie jest usuwany sam liść \>. Stąd wynika, że w V2 pozostanie co najwyżej 2/31K, | węzłów. W sumie pozostanie w T' co najwyżej 2/3 węzłów początkowego zbioru V wszystkich węzłów. Kończy to dowód poprawności naszego oszacowania. Jeśli 7jest drzewem składającym się z trzech węzłów r,, u,, v„ gdzie vs jest następnikiem e,, a v’:, jest następnikiem v2, to | w //«r(7) | ■•=2/317j, a zatem nasze oszacowanie jest dokładne. Z faktu, że \redncc(T)\ < 2/3|7 j , wynika, że liczba iteracji jest logarytmiczna. Algo rytm obliczania równoległego prostych programów sekwencyjnych o strukturze drzewia stej działa więc w czasie 0(iog/i) przy użyciu n procesorów równoległego komputera PRAM. W ten sposób doszliśmy do istotnego wniosku, a mianowicie, że wyrażenia arytmetyczne (rozpisane jako ciąg n instrukcji przypisania) można obliczać w czasie. CAlug//), używając n procesorów. Liczbę, procesorów można zmniejszyć do 0(n/k)gn) przy użyciu innego (choć o podob nej strukturze) algorytmu. Żeby się.p> tym przekonać, sięgnij do )GR|. Przedstawiony tu algorytm możemy również stosować wtedy, kiedy mamy do czynienia z operacjami w algebrze o nośniku (zbiorze wszystkich argumentów i wartości) rozmiaru 0(.I). Rząd złożoności nie ulegnie zmianie. Trzeba jedynie zmodyfikować procedurę zastępowania zmiennych przez odpowiadające im wyrażeni:!. Zamiast wyrażeń możemy przechowywać tablice, na podstawie, których możemy się dowiedzieć, jaka będzie war tość wyrażenia dla zadanych wartości zmiennych. Wartości te przebiegają tu cały nośnik algebry; ponieważ jest on rozmiaru 0(1), rozmiary tablic i koszt operacji na nich są og rtuiie zo nc |>rzez s I:a Ią. Rozważmy na przykład problem obliczania najliczniejszego zbioru niezależnego Z w drzewie binarnym. Zbiór Z ma następującą własność: żadne dwa różne jego wę/.ly nic mogą być połączone krawędzią. Naszymi operacjami są teraz operacje logiczne and, sito; i nr. Wartościami zmiennych odpowiadających węzłom drzewa są wartości logiczne. Zmienne o wartości truć zaliczamy do zbioru Z, a liściom przypisujemy wartość lnie. .u-żi-li węzeł x, ma następniki xt i x,, to piszemy x, := noi(.v); or x,). Wykonujemy prosty program sekwencyjny i w ten sposób obliczamy również najliczniejszy zbiór niezależ ny Z. Poprawność powyższego algorytmu wynika z faktu, -że wśród najliczniejszych zbiorów niezależnych istnieje zbiór zawierający wszystkie liście.
6.1. R ów nolegle o b lic z a n ie w y ra żeń i p ro s ty c h program ów
213
Innym przykładem jest obliczanie minimalnego (co do liczności) zbioru dominującego w drzewie binarnym. Zbiór /') jest dominujący, jeżeli każdy węzeł drzewa należy do I) lub sąsiaduje, z elementem ze zbioru D. Teraz możliwymi wartościami węzłów są 0, I lub 2. Do zbioru D zaliczamy węzły o wartości 2, a liściom przypisujemy wartość 0. Pozostałym węzłom i> przypisujemy: 0, gdy wartości następników r są równe I; I, gdy jeden z następników ma wartość 2, a drugi nic ma wartości 0; 2, w pozostałych przypad kach. Jeżeli po wykonaniu powyższych obliczeń wartość korzenia jest 0, to zamieniamy ja na 2. Prosta indukcja ze względu na wysokość drzewa dowodzi poprawności algoryt mu. Wynika stąd, że zarówno problem obliczania maksymalnego zbioru niezależnego, jak i problem obliczania minimalnego zbioru dominującego można rozwiązać dla drzew w czasie. O(logu), używając 0(n) procesorów. Przypuśćmy, że. mamy danych // liczb a t, n2, ..., an i chcemy obliczyć wszystkie sumy częściowe Xj = «| + ... -I- a,. Problem ten można łatwo sprowadzić do obliczania pros tego programu sekwencyjnego: .v, := a t; x2 := .v, + a2; ...; x„ := x„ _ , + a„. Można zatem zastosować algorytm równoległego obliczania wyrażeń i rozwiązać problem sum częś ciowych w czasie 0(log n) za pomocą n procesorów. Liczbę procesorów można łatwo zmniejszyć do <9(///logn). Operację + można zastąpić przez *, max lub min. Można zatem obliczyć maksimum z każdych k początkowych elementów, dla k = l..a, korzys tając z algorytmu o takiej samej złożoności. Problem obliczenia sum częściowych można rozwiązać również prostym algorytmem rekurenoyjnym. Obliczamy równolegle sumy częściowe dla ciągów («,, .... «|/,/,. |) orMZ ,, .... a,). Mając policzone te sumy i manipulując nimi, możemy w jednym kroku równoległym obliczyć sumy częściowe dla całego ciągu. Opracowanie pełnego schematu tego typu algorytmu pozostawimy Ci jako zadanie. Założenie, że program sekwencyjny ma strukturę drzewiastą, jest istotne. Gdybyśmy na przykład chcieli obliczyć wartość //-lej liczby Fibonacciego ,v„, stosując nasz algorytm do programu .sekwencyjnego ■L := I; .v, := I; ,v:1 := x + .v,; ...; x„ := x„ _ , + x„ _ to liczba iteracji nie będzie logarytmiczna. Można jednakże obliczać wartości liczb Fibo nacciego innym algorytmem w czasie logarytmicznym, używając n procesorów (przy założeniu, żc dodawanie i mnożenie dużych liczb jest wykonywalne w czasie stałym). Taki algorytm otrzymamy wtedy, kiedy zamiast xn będziemy od razu obliczać wektory (ar,,, xn _ ,). Wektor z.„ powslaje z wektora z„ _ , przez pomnożenie przez pewną ma cierz o rozmiarze 2 x 2. Stosujemy teraz taki algorytm jak w wypadku obliczania sum częściowych - z tą tylko różnicą, że liczby zastępujemy (małymi) macierzami i zamiast operacji + mamy mnożenie tych macierzy.i i .<>/ważmy wraz bardziej ogólny przypadek, a mianowicie, gdy grat programu niekonie cznie jest: drzewem. Przyjmijmy jednak pewne ograniczenia. Jedynymi operacjami niech
214
6. A lgorytm y równolegle
będą -l- i 'i1. Jeżeli założymy, że liście grafu obliczenia odpowiadają zmiennym wejścio wym, to prosty program sekwencyjny P obliczy wartość pewnego wielomianu, z reguły wielu zmiennych, (Zmienne wejściowe są tymi zmiennymi .v,, dla których Uć zawiera same stale). Przez deg(P) oznaczmy stopień tego wielomianu i nazwijmy go stopniem programu P. Rozważmy na przykład taki oto program P: xl := cl\ x2 := c2; x3 c3\ x4 := x l * x 2 ; x5 := x2 + x4\ x 3 * „v5; x 7 . := x 4 + xó\
j-6 :=
Zmiennymi wejściowymi są x l , x 2 , x 3 . Program P oblicza wartość wielomianu x l * x 2 -1- x 3 * x 2 -1- x.l * x 2 * x 3 dla x , = c,, jc2 = c2 i x, — c,, a zatem deg(P) = 3. Inna definicja stopnia programu może być podana w terminach związanych z grafem obliczeń. Stopień każdego liścia jest równy jeden. Stopień węzła typu + jest równy maximum ze stopni jego następników, a stopień węzła typu * jest równy sumie stopni lewego i prawego następnika. (Dokładną definicję grafu programu sekwencyjnego poda my później). Stopień całego programu jest równy stopniowi korzenia grafu obliczeń. Do wykonania programu P stosujemy ten sam algorytm jednoczesnych podstawień. Zmieniamy jedynie definicję zmiennej bezpiecznej. Mówimy, żc zmienna .r; jest bez pieczna, jeżeli wyrażenie W, jesl slopnia co najwyżej jeden. Przez redukcję wyrażenia rozumiemy doprowadzenie wyrażenia do postaci sumy jednomianów slopnia jeden lub iloczynu dwóch takich sum plus pewna stała. Załóżmy, że początkowy program zawiera jedynie wyrażenia tej postaci. Przedstawmy najpierw działanie algorytmu dla prostego programu sekwencyjnego stop nia jeden. Weźmy program obliczania wartości piątej liczby Fibonacciego: xl
I; x2 := I; x3 := x2 -i- xl\
x4
x3 + x2; x5
x4 + x3\
Po pierwszym wykonaniu instrukcji iteracji otrzymujemy: ,v/: = 1; x2
I; x 3 := 2; x4 := x 2 + x l + 1; x 5
x3 + 2 *
x2
+
xJ;
a po następnej (ostatniej): xl
:= I; x2 := 1; x3 := 2; x4 := 3; x5 := 5;
W powyższym przykładzie mamy dwa równolegle kroki, podczas gdy wykonując kolej no instrukcje przypisania za pomocą jednego procesom, potrzebujemy trzech kroków (obliczenie x3, x4, x5). Przykład len jest jednak trochę mylący; sugeruje bowiem, że jedną iterację łatwo wykonać w czasie stałym, mając po jednym procesorze dla każdej zmiennej. Wykażemy, że w ogólnym przypadku wykonanie jednej iteracji jesl bardziej skomplikowane. Sprowadza się ono do mnożenia macierzy (operacja ta jesl łatwo wyko-
0.1. R ó w n o leg le o b lic z a n ie w y ra ż e ń i pro.si.ych programów
215
Maina w czasie ć>(Iog/i) za pomocą «•' procesorów). W tym celu działanie algorytmu przedstawimy jeszcze raz na następującym, bardziej skomplikowanym przykładzie: P: xJ I; ,v2 := 2 * x l + 2; xJ := 3 * ,v/ -I- 2 * ,v2 -I- 3; x4 := x l + 3 * ,v2 -1- 2 * .w! -1-2; x5 (xJ + 2 * ,v2 !- \ ! ! * (2 * x l -I- 3); Początkowo zmiennymi bezpiecznymi są wszystkie zmienne oprócz x5. Po pierwszej iteracji otrzymujemy program /•’/: x l := I; x2 := 4; xJ := 4 * x l + 10; x4 := 12 * x5 := 25 * XJ + 15 * x2 + W * xx' -l- 35;
+ 4 * ,v2 + 15;
Wyrażenie na x5 powstało w len sposób, że podstawiliśmy wyrażenie odpowiadające x l , x2, x4 w dwóch wyrażeniach: (,v7 -I- 2 * x2 -I- x4) i (2 * x! I- 3). Ponieważ po pod stawieniu i redukcji drugie z wyrażeń zmieniło się w stalą 5, to pierwsze wyrażenie (po zrobieniu w nim podstawień) pomnożyliśmy przez 5. Kluczowym problemem przy zrównoleglaniu takich obliczeń jest więc efektywność transformacji jednego wyrażenia przez równolegle wykonanie w nim wszystkich pod stawień. Sprowadzimy ten problem do mnożenia wektora przez macierz. Reprezentacją wektorową wyrażenia a,x, -I- r/aą + ... + oilxn + c jest wektor [u,, a,, .... a„, t.j. Reprezen tacją wyrażenia x l I 3 * x2 T 2 * xJ -l' 2 odpowiadającego x f w początkowym pro gramie /' jest wektor [I, 3, 2, 0, 0, 2|. Rozważmy macierz A , której i-ty wiersz jest reprezentacją wektorową wyrażenia W,, gdy zmienna a,.jest bezpieczna; w przeciwnym wypadku /-ty wiersz jest /-tym wersorem (na i-tym miejscu jest I, a na pozostałych zera). Jako ostatni wiersz dodajemy wersor z 1 na końcu. Dla naszego początkowego programu macierz A jest następująca: 000o0 I 200002 320003 13 2 0 0 2 00001 o 00000 I Teraz transformacja wyrażenia x l -1 3 '■■■x2 + 2 * x3 + 2 polega na pomnożeniu repre zentacji wektorowej lego wyrażenia przez (i, 3, 2, 0, 0, 2| * A ~ [12, 4, 0, 0, 0, 15 1. Otrzymany wektor jest reprezentacją wyrażenia 12 * x l + 4 * .r.2 -I 15. Jest to więc zgodne z tym, co poprzednio dostaliśmy w programie / ’/, uzyskanym za pomocą jednej iteracji algorytmu z programu / ’. Po każdym wykonaniu instrukcji iteracyjnej macierz. A może się zmienić, gdyż pewne zmienne .p stają się bezpieczne, a następnie /-ty wiersz macierzy się zmienia (/. /-tego wersom na reprezentacji,' wektorową W,).
216
«. A lg o ry tm y i-ównoUigle
jako zadanie pozostawiamy Ci opracowanie algorytmu mnożenia wektora przez, macierz w czasie Ctlog/;) przy użyciu /r procesorów. W jednej iteracji mamy O(n) takich mno żeń. Algorytm jednoczesnych podstawień (przy ostatniej definicji zmiennych bezpiecznych) działa w czasie (log//) • (liczba iteracji) i wykorzystuje 0(nx) procesorów. Założyliśmy, że w każdym z wyrażeń W, jest co najwyżej jedna operacja * (mnożenie wielomianów) i że jeśli operacja * występuje, to Wi ma postać: (/'i A, + a 2xi,
••• + atx,t + nt , i)* (b\xj, + lh-% + ••• + l’ixii + l>i i i) + c
g
+ “r. I i ) :|: (V .,, + l’ix h +
+ ,}ixj, ■!' /’/ I |)
r
gdzie cij, bj, c są stałymi, to lewymi następnikami są węzły odpowiadające zmiennym .v,, . . . . xt , a prawymi ,v/( .ty......... x/r Zauważmy, że pewien węzeł może być jednocześ ni'.-. lewym i prawym następnikiem lego samego węzła.
..,r
Jedna iteracja w algorytmie odpowiada transformacji reduce! grafu programu, poi.gają cej Mii jednoczesnym wykonaniu opisanych poniżej operacji (a) i (b), a następnie wyko naniu operacji (c): ta) usunięcie krawędzi prowadzących do liści; (b) każda krawędź prowadząca od węzła v do węz lit typu -l- w jest usunięta i są tworzo ne krawędzie prowadzące, od r do ktiżdego następnika w; jeżeli wjest węzłem typu *, to nowe krawędzie są tego samego typu co krawędź, którą zastępują (w sensie lewy i prawy następnik); (ej jeśli po wykonaniu wszystkich operacji (a) i (b) węz,et typu * e ma tylko lewe lub tylko prawe następniki (z powodu usunięcia pewnych krawędzi), to v staje się węz łem typu -I-. viral' reduce!(G) dla grafu G z rysunku 6.la jest przedstawiony na rysunku 6.da. Zauważ my, że .v, jest zarówno lewym, jak i prawym następnikiem ,vv
R ów nolegle o b lic z a n ie wyrażeń i p r o sty c h programów
Rys.
6.3.
(a) Ciriil' G'; (l>) tlr/.ewo »•<■«( G); (c) drzewo ze zbioru
, 217 4~l-----
xub('l)
Wprowadzimy leraz operację „udrzewiania” grafu G, zapisywaną jako irce(G). Z definicji drzewo y, .:((./) ma węzły, które odpowiadają z mioimym, ale lej samej zmiennej może od powiadać i;ilka węzłów. Nieci) paths(G) będzie zbiorem ciągów zmiennych na ścieżkach od korzenia do liści grafu G. Drzewo lrec(G) jest takim drzewem T, że palhsiG) ~ pathĄT). Inaczej mówiąc, 7'jest drzewem wyrażenia IV, które powstałoby, gdyby program sekwen cyjny / ’ zapisać jako jedno duże wyrażenie. Zauważmy, że jeżeli G jest podanym wcześniej grafem programu liczącego liczby Fibonaeciogo, to ircc(G) ma wykładniczą liczbę węzłów, chociaż stopień programu jest bar dzo mały (równy !). Niech T będzie drzewem odpowiadającym grafowi programu P. Przez sulĄT) oznaczmy zbiór wszystkich poddrzew T’ spełniających warunki: (a) korzeń T' jest laki sam jak korzeń 7’; (b) jeśli węzeł wewnętrzny v drzewa T' jest typu -I-, to dokładnie jeden z jego naslępników w 7" należy do T'\
218
6. A lgorytm y równolegle
Kys. 6.4.
( ii)
Oral’ r e d m
:e J(G y ,
(l>)
lie d r e d u c e I(G ))\
(e) drzewo ze zbioru
s iii< o r r .r \n '< h ie e l( G ) ) )
(c) jeśli V jest węzłem typu * z T , lo do 7" należy tylko jeden lewy i jeden prawy następnik u w 7’. Niech runk(G) będzie maksymalnym rozmiarem (liczbą węzłów) drzewa ze zbioru sub(tree(G)). Łatwo zauważyć, że stopień programu P jest równy maksymalnej liczbie liści w drzewie ze zbioru sub(tree(G))y gdzie G = graf{P). Jednocześnie długość każdej ścieżki w drzewie nie przekracza rozmiaru n grafu G (liczby zmiennych), a więc nmk{G) < nd, gdzie d jest stopniem / ’. Wystarczy teraz udowodnić, że jeżeli |G | > 1, to nmk(rcducel(G)) < 2/?i(nink{G)). Dowód przeprowadzimy, korzystając z redukcji drzew za pomocą operacji reduce. Niech reduce 1(G) = GJ i niech 77 = tree(GJ) oraz 7’ = tree(G). Dowód opiera się na następu jącej strukturalnej własności operacji reduce i reduce 1: dla każdego drzewa 7'!' ze zbioru sub(77) istnieje drzewo 7" w zbiorze xub(7) takie, że 7T = rcduceiT'). Możemy ten fakt zapisać bardziej formalnie: sub{trec(reducel{G))) ę reduce(.sub(tree(G))) (zauważmy jednak, że może się zdarzyć, iż dla pewnego 7" z sub(T) drzewo reduce(T') nie będzie cienieniem sub(7'l), a więc odwrotna inkluzja nie będzie zachodzić). Powyższa własność jest: intuicyjnie oczywista; trzeba tylko przypomnieć sobie definicje operacji sub, tree, reduce i reduce!. W tym momencie nasze oszacowanie:
21 s;
R ó w n o leg le o b lic z a n ie w y ra ż e ń i p ro s ty c h p ro g ra m ó w
nm k(nuhicc 1(0)) < 2/3 rn:d;{0)
wynika bezpośrednio z faktu, że | rcduce(T') | < 2/3 | T' \ co udowodniliśmy wcześniej, analizując równolegle, obliczanie wyrażeń. Kończy to dowód, że liczba iteracji w algorytmie jednoczesnych podstawień z drugą definicja zmien nych bezpiecznych nic przekracza I logir,(nd) j. Omówimy teraz zastosowanie algorytmu jednoczesnych podstawień do programowania dynamicznego. Programowanie dynamiczne jest ogólna metoda rozwiązywania zadań optymalizacyjnych. .Metoda ta polega na rozbijaniu danego zadania na pod/.adania, a jej istot;) jest to, że rozwiązania pod/.adań również maja być optymalne, co daje możliwość zapisu rekurencyjncgo. Korzystając z, takiego zapisu, obliczamy tablice optymalnych rozwiązań dla wszystkich pod/.adań; dany element tablicy wyraża się prostym wzorem zależnym od innych elementów. Istotne jest również to, że liczba podzadnń jest wielo mianowa. Taki opis programowania dynamicznego nie jest zbyt formalny; dokładniejszy opis przedstawimy dla trzech wybranych problemów. Proiu,ism 1.
Obliczanie optymalnej kolejności mnożenia macierzy
Przypuśćmy, że mamy dany ciąg rozmiarów macierzy /)•/,, M-,....... M„. Macierz /••ta ma i) _ , wierszy i r, kolumn. Zakładamy, że koszt mnożenia macierzy o rozmiarach p x q i ą x r wynosi pqr. Chcemy wyznaczyć minimalny koszt obliczenia macierzy M — M , x M-, x ... x Mn. Koszt ten zależy od kolejności mnożenia. Niech ni, j będzie minimalnym kosztem obliczenia macierzy Mf- • = M, 4 , x /W. , , x M r Przyjmujemy /n- + , 0. Otrzymujemy system równali:
x ... x
| "3, ; i i = ^ dlii i = 0, .... n - I | ///,. ; = min,1/;/, -I mt j + : i < k
iii,
(. I- rlrt rj : i < k ■ j j
220
0. A l g o r y t m y
równoległe
w postaci takiego programu. Można lo 7,robić, wprowadzając nowe z-umi dla / < A <./: i i I ■— I T I i, , i
»{i. i. i *
2
•=
I
'■ / / / ,
>. , , i-
, - 1 := min('", , , "U. i
, ,
j +
//y
i' , i ,
"A /
i- i +
,2.7 + >V,
,•
■■1, , + '/') .. ,
">t. i. j - ó
Wartości r, rt r- można obliczyć równolegle, wcześniej i przyjąć, że są one stałymi. W ten sposób otrzymujemy prosty program sekwencyjny. 1’rzyjmijmy, że wcześnie: ■•'■et opera cji H- odpowiada teraz operacja min, ;t operneji * - operacja +. 1salwo możc.my wykazać, że stopień programu jest liniowy (.< «). Możemy zastosować algorytm jednoczesnych podstawień (z operaejttmi -I- i * zastąpionymi odpowiednio przez min i +). Tak jak porzodnio, korzystamy z reprezentacji wektorowej wyrażeń. Długość naszego programu sekwencyjnego jest (Mu'), a zatem otrzymujemy równoległy algorytm obliczania minimalnego kosztu mnożenia ciągu macierzy na maszynie PRAM w czasie Otlogół) przy użyciu (Hu'’) procesorów. Podamy jeszcze dwa analogiczne problemy związane z programowaniem dynamicznym. PitOiti.,;żvs 2.
Obliczanie minimalnego kosztu drzewa poszukiwań binarnych (BST)
Mamy danych n kluczy Aj, A',, ..., Aj, w porządku rosnącym. Niech pt będzie częstoś cią dostępu do elementu Aj. Chcemy umieścić nasze elementy w liściach drzewa binar nego tak, żeby średni koszt (długość ścieżki od korzenia.) do danych elemeii.ów by i minimalny. Podobnie jak poprzednio, możemy zdefiniować tu. j jako minimalny koszt BST dla ciągu elementów Aj , ,, ..., Aj, podać odpowiedni system równań, a następnie wypisać prosty program sekwencyjny obliczania ///„ „. Tak zdefiniowany problem op tymalnego BST możemy zatem rozwiązać równolegle w czasie 0(log2«), używając Oin") pr
Minimalna triangukicja wielokąta wypukłego
! siny jest. ciąg wierzchołków t>„, r,, .... r„ (w kolejności zgodnej z niebem wskazó wek zegara) wielokąta wypukłego na płaszczyźnie. Należy tak wybrać u - 2 cięciw lączącydi dane punkty, aby podzieliły wielokąt na trójkąty i aby koszt takiej triangulacji byl minimalny. Przez koszt rozumiemy sumę długości wszystkich cięciw. Znowu możemy zdefiniować wielkości / n i jako koszt triangulacji wielokąta r,, r, ,, .... ij i wypisać - podobnie jak poprzednio - prosty program sekwencyjny. 'Złożoność tego problemu jest zatem tego samego rzędu co poprzednjch dwóch problemów.
(i.2. S o rto w a n ie ró w n o le g le
221
Rozważaliśmy jedynie obliczenie minimalnego kosztu. Celowejest jednak obliczenie danych do realizacji takiego minimum (drzewo reprezentujące kolejność mnożenia macierzy, opty malne RST lub leż., w ostatnim problemie, minimalny zbiór cięciw). Zanalizowanie sekwen cyjnej i równoległej złożoności tak rozszerzonego problemu pozostawiamy Ci jako zadanie. W algorytmie należy korzystać z uprzednio wyliczonych wartości ///,- ..
6.2. S o r to w a n ie r ó w n o le g le Pokażemy teraz, w jaki sposób można szybko sortować ciąg n liczb, używając tylko n procesorów. Przez „szybko” rozumiemy tutaj - w czasie 0(1o£2h). ‘i Zaczniemy od stwierdzenia, że łatwo można posortować // liczb w czasie log n za pomo cą i r procesorów. Mianowicie przydzielamy każdemu elementowi ii procesorów, które w czasie log// obliczają liczbę elementów mniejszych od tego elementu (bez straty ogólności możemy założyć, że sortowane elementy są parami różne). Po obliczeniu w ten sposób pozycji każdego elementu w posortowanym ciągu wynikowym wystarczy w jed nym równoległym kroku wstawić każdy element na jego miejsce wynikowe. Redukcja liczby procesorów do /r/logn jest trywialna, natomiast redukcja do liniowej liczby procesorów (przy zachowaniu czasu O(logn)) jest zadaniem trudnym. Zadowoli my się więc dalej czasem T(n) = log"//. Zaczniemy od algorytmu, w którym jest używana liniowa liczba procesorów i który działa w czasie log"// na maszynie PRAM (później rozważymy znacznie uboższy, chociaż dosyć tradycyjny, model dla problemu sortowania, a mianowicie sieci sortujące). Algorytm ten jest równoległą \ \ v r i irytmu sortowania przez scalanie (mergesort). Wystarczy jedy nie umieć scalać dw i posortowane ciągi długości n w czasie log// przy użyciu // proceso rów. Projekt algorytmu mającego takie parametry zlożonościowe nie nastręcza większych trudności na maszynie PRAM. W celu posortowania ciągów x i y wystarczy przypisać procesor każdemu elementowi ciągu x Procesor len znajdzie, metodą wyszukiwania binarnego, miejsce pos(el) odpowiadające danemu elementowi eł w,ciągu y - przy założe niu, że tylko len element ma być wstawiony do ciągu y. Rzeczywiste miejsce elementu d będzie równe posiet) -l- rank(el), gdzie rank(el) jest liczbą elementów poprzedzających el na liście ,v. Szczegółowy zapis algorytmu pozostawiamy Tobie, Drogi Czytelniku. Podobnie możemy obliczyć miejsce każdego elementu ciągu y w posortowanym ciągu wynikowym. W ten sposób udowodniliśmy, że scalenie dwóch posortowanych ciągów o łącznej długości // można wykonać w czasie log// przy użyciu // procesorów. Jeśli korzysta się z równoległej wersji algorytmu mergesort, to sortowanie n elementów jest wykonalne w czasie log'’/; przy użyciu n procesorów.
222
G. A lgorytm y równolegle
Jednym z, najważniejszych wyników dotyczących sortowania jest. równoległa imple mentacja algorytmu mergesort w czasie 0(Iog»). W implementacji tej operacje sca lania ciągów rozmiaru liniowego są wykonywane w czasie stałym dzięki pomocniczym strukturom danych. Algorytm taki, o nazwie parallel mergesort, został podany przez, R. Cole’a [GRj. Tradycyjnym modelem obliczeń równoległych dla algorytmów sortowania są sieci sor tujące. Sieć sortująca jest algorytmem sortowania, w którym jedynym sposobem zdo bywania intormacjr o danych wejściowych są porównania dwóch elementów, a elemen tarną operacją jest zamiana dwóch elementów: compare-exchange. Jeśli zastosujemy tę operację względem pary elementów (x, y), to otrzymamy parę. (min(,v, y), max(.v, y)). Inaczej mówiąc, operacja compare-exchange jest elementarną operacją sortowania cią gów długości 2. Najważniejszą własnością, jakiej wymaga się od sieci sortujących, jest pewna „sztywność” obliczeń. Dla ustalonego n (liczby elementów do posoriowania) ciąg wykonanych porównali i operacji compare-exchange jest „sztywny” , to znaczy nie zależy od danych wejściowych (jest laki sam dla wszystkich ciągów wejściowych długości n). Inaczej (bardziej formalnie) mówiąc, sieć sortująca jest przyporządkowa niem n —>S„, gdzie jest ciągiem operacji compare-exchange sortowania wszystkich ciągów długości n. Podobnie można zdefiniować sieć scalającą (zamiast sortowania mamy teraz ciągi ope racji compare-exchange scalania dwóch danych posortowanych ciągów). W jednym momencie możemy wykonać pewną liczbę kolejnych, niezależnych operacji compare-exchange z ciągu Su. Niezależność polega na tym, że pary elementów, biorące udział w dwóch różnych operacjach compare-exchange, są rozłączne. Liczba procesorów jest zatem liniowa (liniowa liczba par rozłącznych). Podciąg kolejnych niezależnych operacji nazywamy fazą. Czas równoległy odpowiada minimalnej liczbie faz, na jakie można podzielić SH. Opiszemy teraz dwie siec; sortujące, oparte na równoległym zaimplemenlowaniti algo rytmu mergesort. Wystarczy jedynie opisać sieci scalające. Przedstawimy dwie takie sieci: scalającą metodą odd-even i scalającą metodą bitoniczną. Niech odd(L) będzie podciągiem ciągu L składającym się z elementów o indeksach nieparzystych, a even(L) podciągiem składającym się. z pozostałych elementów. Dla dwóch ciągów LI = (a,, ..., a j, L2 = (bt, b j definiujemy: interlcave(Ll, L2) — (at, b t, a2, b2, ...., an, b j Zdefiniujmy ponadto operację odd-cven(L). Operacja ta polega na jednoczesnym zasto sowaniu operacji compare-exchange do każdej pary elementów , ciągu L dla parzystych indeksów i. Niech LJ&L2 będzie operacją dopisania (konkatenacji) listy 1,2 do LI.
ii,'i. Sortowaniu równolegle
223
function o c l a- e v e n - m o r g ę ( L I , L 2 ) ; {1,1, L 2 s;,i posortowanymi ci ągami długości n , gdzie n jest potęgą dwó j ki j begin if n == i then zastosuj jedną operację c o m p e r e - e x c h a n g e else begin do in parallel begin l,.Kld := o d d - e v e n - m e r g e ( o d d ( L I ) , o d d ( L 2 ) ) ; Tj,,vf,n o d d - e v e n - m e r g e ( even ( L I ) , even ( L 2 ) ) ; end ; L := i n t e r l e a v e ( L tnkl, i:W(]) ; o d d - e v e n - m e r g e : = o d d - e v e n (L) end end ;
Przejdziemy teraz do opisu algorytmu bilonie merge. Podstawowa operacja będzie, lulaj operacja przeplotu. proceduro p r z e p l o t (L) ; begin L I : ■ ciąg pierwszych n / 2 elementów ciągu L; L2 ciąg os ta tn i.c!i n / 2 elementów ciągu L; L : ■■■-: i n t e r l e a w ; ( i ,1 , 1,2)
end;
Zakładamy, że mamy dwa posortowane ciągli LI i 1,2. Tworzymy ciąg l. U&L2*, gdzie L2r jest cii|gieni 1.2 z odwrócona kolejnością elernenlów. Ciąg L jest lz\v. ciągiem bilonicznyni; jego elementy najpierw rosną, a następnie maleją. Scalanie foitoniczne polega na wielokrotnym stosowaniu operacji przeplotu razem z operacjami compare-exchange na sąsiednich elementach ciągu. procedure h i t o n i c - m e r g e ( L I , L 2 ) ; { L I i L 2 są posortowanymi ciągami długości n , gdzie n jest; potęgą dwójki| begin !■' . b i t
|ie
(;•:.....................•■•,„) j
repeat logu; -i 1 times begin p r z e p l o t (I.,) ;
for each i do in ps.ra.llel if o d d ( i ) then c o m p a r e - e x c h a n g e ( x , , x, , ,) ; end end;
224
AJgorytisjy równolegle
Złożoność obu algorytmów scalania jest oczywista. Mniej oczywista jest poprawność tych algorytmów.. Przy dowodzie poprawności bardzo pomocna jest izw. zasada ze t -jedynko wsa Oto ona: sieć sortująca (scalająca) jest poprawna dla wszystkich ciągów wejściowych wtedy i tylko wtedy, gdy jest poprawna dla ciągów zero-jedynkowych (składających się jedynie z zer i jedynek). Wystarczy jedynie udowodnić, że jeżeli sieć nie .sortuje pewnego ciągu A, to równie/, nie sortuje pewnego ciągu zero-jedynkowe-go U. Przypuśćmy, że a i b, gdzie u < b. są dwoma elementami /„ klitre po posortowania są „źle” położone względem siebie (b znajduje się wcześniej niż u). Ciąg / / otrzymujemy z ciągu L w len sposób, że każdy element ,r zastępujemy przez J(x), gdzie fix)
-
f0 , I I
,v c b w przeciwnym razie
Łatwo zauważyć, że za pomocą tego algorytmu nie zostanie również posortowany ciąg U. W rzeczywistości, jeśli wynikiem działania operacji coiitpcire-ewlutniy względem pary ,r, y jest (tara x \ y \ to wynikiem działania coMpare- exchange względów pary /ty) jest para/i,v'), ./(>’')• Kończy lo dowód zasady zero-jetlynkowej. .Stosując zasadę zero-jedynkową, można pokazać, że scalanie metodą bitonic/.ną oraz metodą odd-even jest poprawne. Wystarczy jedynie zanalizować działanie algorytmów scalania dla ciągów postaci 0,;Ir (posortowanych ciągów zero-jedynkowych). Pozostawia my Ci taką analizę jako ćwiczenie.
.'Z adariia 6.1. Skonstruuj algorytm obliczania sumy i iloczynu pierwszych k liczb spośród danych n liczb, dla każdego k ~ I,.//, na maszynie PIŻAM w czasie <9(log/i), korzystając z M(///log//) procesorów. 6.2. Skonstruuj algorytm rozwiązywania tego samego problemu (z tym samym i.-ędem złożoności) na binarnym drzewie procesorów. Komunikacja odbywa się jedynie między poprzednikiem (ojcem) i następnikiem (synem) w drzewie. 6.3. Podaj pędny zapis algorytmu mnożenia dwóch macierzy na maszynie PRAM. 6.3. Udowodnij, że dla nieskończenie wielu wartości | reduced],) | = I. 2/3 | 7j, | J .
ii
istnieją takie drzewa Tn, że
w.5. Skonstruuj algorytm obliczania najliczniejsze; > zbioru niezależnego w dowolnym drzewie w czasie 0(log//j przy użyciu u procesorów (w rozdziale tym podaliśmy algorytm dla (drzew binarnych).
Z a d a n ia
225
b.h. Rozwiąż /.udanie 6.5 dla problemu minimalnego zbioru dominującego. 6.7. Skonstruuj podobny (jak w zadaniach 6.5 i 6.6) algorytm kolorowania krawędzi drzewa minimalną liczbą kolorów. Krawędzie o tym samym kolorze nie mogą mice wspólnego węzła. 6.8. Oszacuj rozmiar grafu gniĄP). gdzie P jest prostym programem .sekwencyjnym obliczającym liczby Fibonaeciego (zgodnie z równaniem rekurencyjnym definiują cym te liczby). 6.9. Oszacuj liczbę iteracji w algorytmie jednoczesnych podstawień (z pierwszą de finicją zmiennych bezpiecznych), zastosowanym do programu P z poprzedniego zadania. 6.10. Podaj algorytm liczenia /i-tej liczby Fibonaeciego w czasie O(logn) przy użyciu n procesorów, przy założeniu, że operacje arytmetyczne mają koszt jednostkowy (pomimo tego, że liczby Fibonaeciego są bardzo duże). 6.11. Skonstruuj algorytm sprawdzania w czasie 0(log«) i przy użyciu n procesorów, czy dane słowo jest słowem Fibonaeciego (zob. rozdział 5). 6.12. Opisz pełną implementację jednej iteracji w algorytmie jednoczesnych podstawień z drugą definicją zmiennych bezpiecznych. Pokaż, że koszt jednej iteracji jest proporcjonalny do mnożenia macierzy liczbowych o rozmiarze n x n. 6.13. Udowodni j, że dla nieskończenie wielu wartości n istnieje graf obliczeń G„, taki że j rahiccl(Gn) | = L2/3 | C(I | J . ' i( 6.14. Udowodnij, że rozmiar prostego programu sekwencyjnego P dla problemów pro gramowania dynamicznego jest O(rP) i że cleg(P) = O(n). 6.15. Napisz dokładnie wzory rekurencyjne na obliczanie optymalnego UST metodą programowania dynamicznego. 6.16. Zrób to samo dla problemu minimalnej triangulacji wielokąta wypukłego. 6.17. Podaj dokładny zapis algorytmu scalania dwóch posortowanych ciągów długości n w czasie (7(log//) na PRAM. Zgrubny opis takiego algorytmu został podany w podrozdziale o sortowaniu, przed opisami sieci sortujących. 6.18. Korzystając z zasady zero-jedynkowej, udowodnij poprawność sieci scalających metodą odd- even. 6.1‘>. Wykaż poprawność sieci scalających metodą biloniczną.
226
(i. A lgorytm y rów n oległo
6.20. Maszyna przeplotowa jest równoległą maszyną składającą si ę z // procesorów (RAM). Procesor «'-ty (i < n) jest połączony z procesorem (/ i I) dla każdego parzystego /. Oprócz tego procesor /-ty jest połączony z procesorem /,, gdzie Oj, ./-,...... ,./„) = prz.eptol( I, 2, .... //). Podaj implementację scalania metodą hiloniczną na maszynie przeplotowej. 6.21. Skonstruuj algorytm sumowania// liczb w czasie log// na maszynie przenlotowej. 6.22. Korzystając z tego, że sortowanie można wykonać w czasie <9(log’//) za pomocą n procesorów, skonstruuj algorytm rozwiązywania problemu WW i obliczania naj dłuższego powtarzającego się podsłowa w czasie ć>(log-'/i) przy użyciu // proceso rów (zaprojektuj równoległą wersję algorytmu KMR). 6.23. Rozwiąż zadanie 6.22 dla przypadku dwuwymiarowego (zamiast najdłuższego po
wtarzającego się podsłowa szukaj maksymalnej powtarzającej się podtablicy). 6.24. Zdefiniujmy drzewa Fibonacciego T„ w następujący sposób. Drzewa 7j i 7j składa ją się. tylko z korzeni. Drzewo 7’„ 2 składa się z korzenitt,-którego jedynym synem jest sklejony korzeń drzew Tn i T„ Wtedy 7j składa się z dwóch wierzchołków, a 7j z. trzech. Jaki jest związek liczby wierzchołków z liczbami Fibonacciego? Zastanów się, jak działa operacja reduce na drzewach Fibonacciego. Udowodnij, że potrzeba co najmniej (logjii - c) iteracji operacji reduce dla drzew Fibonac ciego, gdzie n jest liczbą wierzchołków,/ = ('IH + !)/2, a c pewną stalą. 6.25. Udowodnij, że dla każdego drzewa mającego n wierzchołków wystarczy log,-// -i- c operacji reduce, żeby zredukować drzewo do pojedynczego wierzchołka, gdzie c jest pewną stalą (niekoniecznie taką sarną jak w poprzednim zadaniu).
lgorytmiczna teoria grafów jest dziedziną informatyki teoretycznej, w której w ostatnich latach nowe algorytmy pojawiały się najczęściej. Algorytmy grafowe, można spotkać w wielu .różnych działach informatyki, na przykład w złożoności obliczeniowej, geometrii obliczeniowej, grafice komputerowej, metodach translacji sie ciach komputerowych, obliczeniach równoległych i rozproszonych. W tym rozdziale zajmiemy się, podstawowymi metodami konstrukcji efektywnych algorytmów grafo wych. Nie jest naszym zamiarem podanie pełnego przeglądu najważniejszych proble mów i algorytmów grafowych. Skoncentrujemy się raczej na przedstawieniu pewnych ogólnych metod przetwarzania grafów, w tym tak ważnych jak metody systematycznego przeszukiwania grafu i syntezy informacji o grafie z jego drzewa rozpinającego. Zwróci my także uwagę na możliwość sprowadzania jednego problemu do drugiego, a tym samym wykorzystywania istniejących algorytmów do rozwiązywania nowych zadań. Na sze rozważania zilustrujemy, rozwiązując następujące problemy:
A
® spójnych, dwuspójnyclt i silnie spójnych składowych; 'f silnej orientacji grafów niezorientowanych; ® cyklu Hulera; • najkrótszych ścieżek i minimalnego drzewa rozpinającego; ® 5-koIorowania grafów planarnych. Zrozumienie materiału z lego rozdziału ułatwi Ci znajomość podstawowego kursu z teorii grafów. W naszych rozważaniach skupimy się głównie na algorytmicznej stronie omawia nych zagadnień, pozostawiając Ci często udowodnienie prostszych faktów teoriografowych. Niech G ■ — (V, E) będzie //-wierzchołkowym grafem niezorientowanym. Pisząc „graf” , będziemy zawsze mieli na myśli graf niezorientowany. Przypomnijmy, żc liczbę kra wędzi grafu G oznaczamy przez m. Będziemy zakładać, że V = {1, 2, .... «}, chyba że. zaznaczymy, że jest inaczej. Najprościej jest reprezentować graf na płaszczyźnie.
228
7. A lg o ry tm y g ra fo w e
Wierzchołkom grain odpowiadają wiedy punkty płaszczyzny, a krawędziom - odcinki łączące punkty reprezentujące ich końce. Niestety, sposób ten nie może, być wykorzys tany do jimplenientaeji grain w komputerze. W tym wypadku grafy reprezentujemy zazwyczajj za pomoc;) list lub macierzy sąsiedztwa (zob. rozdział I). Należy podkreślić różnice iw złożoności konstruowania obu reprezentacji. O ile listy sąsiedztwa można zbudować w czasie ()(n + w), o tyle budowa macierzy sąsiedztwa.wymaga czasu LUif ). Jeśli liczba krawędzi w grafie jest istotnie mniejsza od ir, to listy sąsiedztwa są oszczęd niejsze - zarówno jeśli chodzi o czas budowy, jak i o zużycie, pamięci. Zaletą macierzy sąsiedztwa jest jednak możliwość sprawdzania w czasie stałym, czy istnieje krawędź między dowolną parą wierzchołków. W prezentowanych algorytmach przyjmujemy, że graf jest dany przez listy sąsiedztwa. Na rysunku 7.1 przedstawiamy przykładowy graf i różne jego reprezentacje. 00 0 '= ( ! !, 2, 3, 4, 5, 6, 7), I I ...2, 2—3, 3....4, 4.. -I, 1—3, 5— 6, 6—7, 5—7))
Wierzchołki 1 u 3 t ;> 0 7
Sąsiedzi 2, 3, 4 1, 3 4, 1.2 3. 1 7, 6 5, 7 6, 5
(cl.) 1 2 3 4 5 6 7
1 0 I 1 1 0 0 0
o 1 0 1 0 0 0 0
3 1 1 0 1 0 0 0
4 1 0 1 0 0 0 0
5 0 0 0 n it ł I
6 0 0 0 0 i 0 t
7 0 0 0 0 1 1 0
Rys. 7.1. ;i) (inil'CV: b) rcprcuMilncju graficznii; c) listy sąsiedztwa; 4) macierz sąsiedztwa
Przypomnimy teraz podstawowe pojęcia dotyczące grafów, którymi będziemy się po sługiwać w dalszej części rozdziału. Drogą (albo .ścieżką) w grafie. G nazywamy każdy skończony ciąg wierzchu:, mv i’,, ..., \>t, laki że dla każdego i = 0, ..., k - I, tę— r, , (v,—>v, , w wypadku grain zorienlowanegt>) jest krawędzią grain. I ,’iczba k jest długością ścieżki. O ścieżce v0, tą,.... vt mówimy, że łączy wierzchołki v„ i vk (lub prowadzi od v„ do tg, gdy G jest zorientowany). Wierzchołki t>„ i vPnazywamy—odpowiednio —początkiem i końcem ścieżki. O krawędzi łączącej dwa kolejne wierzchołki na ścieżce mówimy, że należy do tej ścieżki. Długość najkrótszej ścieżki łączącej dwa wierzchołki v i w nazywamy ich odległością w grafie (lub
7.1, Spójne .składowe
229
odległości:) od i’ do w w wypadku grafu zorientowanego). Ścieżkę nazywamy prostą, jeżeli żadne dwa wierzchołki na ścieżce się nie powtarzają. O ścieżce mówimy, że jest cyklem, jeżeli pierwszy wierzchołek i ostatni są takie same. Jeśli wszystkie wierzchołki, z wyjątkiem t>„ i ty., są parami różne oraz. k > 2 (k > I dla grafów zorientowanych), to cykl nazywamy prostym. Graf G jest spójny, jeżeli każcie dwa jego wierzchołki można połączyć ścieżką. Graf G, = (l7,, /?,), w którym V, jest podzbiorem zbioru wierzchołków' V, a fij jest podzbiorem zbioru krawędzi grafu G łączących tylko wierzchołki z Vj, nazywamy podgrafem G. Graf G, jest. podgrafem rozpinającym, jeżeli V, = V. W sytuacji, kiedy E, zawiera wszystkie krawędzie grafu G o końcach w wierzchołkach z Vt, mówimy o G„ że jest indukowany przez, ł7,. Graf G, jest spójną składową grafu G, jeżeli jest jego maksy malnym (w sensie zawierania zbiorów wierzchołków i krawędzi) spójnym podgrafem. O PU7.YKl.aVlV.
Graf z rysunku 7.1 nie jest grafem spójnym. Ma dwie spójne składowe: jednej o wierz chołkach 1, 2, 3, 4 i drugą o wierzchołkach 5, 6, 7.
W bardzo wielu zadaniach obliczeniowych dla grafów zakłada się, że przetwarzany graf jest spójny. Stąd jednym z podstawowych problemów napotykanych przy rozwiązy waniu problemów grafowych jest problem znajdowania spójnych składowych danego grafu G. Można go sformułować następująco: P k O l t l .E M
7.1 .
Dla danego grafu G = (\7, E) mamy obliczyć (znaleźć) funkcję C: l7—> (I, 2, ..., /;}, taką że dwa wierzchołki v i w należą do tej samej spójnej składowej G wtedy i tylko wtedy, gdy C(v) = C(w). Rozwiązanie powyższego problemu opiera się na spostrzeżeniu, że dwa wierzchołki są w tej samej spójnej składowej wtedy i tylko wtedy, gdy można je połączyć ścieżkami z tym samym wierzchołkiem w grafie. Do, znajdowania spójnych składowych wykorzys tamy metodę przechodzenia grafu w głąb (DFS), o której była już mowa w rozdziale. I. Przechodzenie w głąb zrealizujemy za pomocą procedury rekurencyjnej search, opisanej nieco dalej. Tak jak w rozdziale 1, zakładamy, że z każdym wierzchołkiem i>są zwtfązane dwa pola: visiicd(v) i enrrentiv). Pierwsze pole służy do zaznaczania stanu wierzchołka: odwiedzony (true) lub nie odwiedzony (false). Drugie pole - current(v) - jest wskaź nikiem do pierwszego wierzchołka vi’ na liście sąsiedztwa v, takiego że krawędź v— w nie była jeszcze odwiedzona z v. Listę sąsiedztwa v oznaczamy przez L{v), W opisie procedury search opuściliśmy pewien fragment, który zaznaczyliśmy trzema kropkami.
230
7. A lgorytm y gra fo w e
Fragment ten zależy od rozwiązywanego problemu. Za chwilę pokażemy, jak on wy gląda w wypadku algorytmu obliczania spójnych składowych. procedure search(v : wierzchołek) ; var w : wierzchołek; begin visited(v) := true; [zamarkuj wierzchołek v jako odwiedzony} while current; (v) nil do begin w := wierzchołek na liście L(v) wskazywany przez curr ent; (v) ; if not visi ted(w) then search (w) {*»*}; current (v) := wskaźnik do następnego wierzchołka na liście -L(v) end end ;
Jako zadanie pozostawiamy Ci udowodnienie, że wywołanie scarch(u) spowoduje odwie dzenie wszystkich wierzchołków połączonych w grafie ścieżką z u. Mając do dyspozycji procedurę search, łatwo już rozwiązać problem spójnych składowych. Funkcję <-■okieślumy w ten sposób, że C{v) jest równe najmniejszemu wierzchołkowi w spójnej składowej zawierającej r. Identyfikator wykrywanej właśnie spójnej składowej (jej najimiiejsZ> wierzchołek) jest pamiętany w zmiennej globalnej id. Teraz, żeby obliczyć Ć7(V), VVTS wpisać w opuszczone miejsce w opisie procedury search instrukcję C(v) := id Pełny algorytm obliczania spójnych składowych został zapisany za pomocą P,l)Ł ' c-components. procedure c-camponent begin for v := 1 to n do begin curren t; ( v) ; wskaźnik do pierwszego wierzcholk
L (v) ; I;
v i s i t e d {v) : f a l s e end; fo r v : = 1 to n do begin i f not v is ite d ( v) then begin
id := v; s e a r c h (v) end end end;
1. :i Pc:iW
7.1. Spójne sk ład o w e
231
W procedurze c - c o m p o n e n i s listu sąsiedztwa każdego wierzchołka jest przeglądana jeden raz. Ponieważ łączna długość wszystkich list sąsiedztwa wynosi 2//i, czas działania pro cedury c-components jest liniowy, a dokładniej 0 { n + iii). Przeglądanie listy sąsiedztwa każdego wierzchołka (instrukcja widie w procedurze search) może być także rozumiane jako przeglądanie listy krawędzi grafu o jednym z końców właśnie w tym wierzchołku. Jak już wspominaliśmy, metoda przechodzenia grafu, realizowana za pomocą procedury search, nosi nazwę metody przechodzenia grafu w głąb. Kierujemy się w głąb grafu lak długo, j.ik długo można. Metoda ta jest podstawą konstrukcji bardzo wielu efektywnych algorytmów grafowych, działających często w czasie liniowym. Umożliwia ona zebranie potrzebnych informacji dotyczących struktury grafu przy tylko jednokrotnym przejrzeniu go. Zdobyte informacje wykorzystuje się następnie do obliczania właściwego rozwiąza nia rozpatrywanego problemu. Załóżmy, że. badany graf jest spójny. W trakcie wykonywania procedury search kim zaznaczony trzema gwiazdkami odpowiada przejściu po krawędzi od zamarkowanego wierzchołka v do nic zamarkowanego wierzchołka w. Wyróżnijmy wszystkie takie krawędzie i niech F będzie zbiorem tych krawędzi. Nietrudno zauważyć, że podgraf (K /') jest drzewem rozpinającym badanego grafu (spójnym podgniłem bez cykli pro stych, zawierającym wszystkie wierzchołki). Drzewo to będziemy traktować jak drzewo z korzeniem w wierzchołku, od którego rozpoczynamy przechodzenie. Jeżeli w kroku i.***] procedury search przechodzimy od wierzchołka i> do w, to w tak otrzymanym drzewie u jest. poprzednikiem n», a u-1jest następnikiem u. Omawiane, drzewo będziemy nazywali drzewem przechodzenia w głąb. Przechodząc graf w głąb, można dodatkowo numerować wierzchołki 'w kolejności odwiedzania ich. Numer wierzchołka u będziemy oznaczać przez nr(\>). Przykładowy graf z ponumerowanymi w głąb wierzchołkami i z zaznaczonymi krawędziami drzewa przechodzenia (linie ciągle) jest przedstawiony na rysunku 7.2. Liczby przy każdym wierzchołku oznaczają, odpowiednio, jego iden tyfikator oraz numer w głąb. Dla przejrzystości przykładu przyjmujemy, że wierzchołki na listach sąsiedztwa są uporządkowane rosnąco. Należy tu jednak 'podkreślić, że ko lejność wierzchołków na listach sąsiedztwa może być dowolna. Przechodzenie grafu rozpoczyna się. od wierzchołka I.
I, I
2, 2
4, I I 5, 5
10, 9
3, 3 7, 4 it, 7
9,
6, 6
Rys. 7.2. Graf ponumerowany w yh|b z wyróżnionymi krawędziami drzewa mzpmajar.eyu (linie uiayie)
2fJ2
7. Algorytmy grafowe
7.2.' Dwiispójne s k ła d o w e Pokażemy lora/., jak można wykorzystać zdobyła informację (numerację i drzewo) (Jo wyznaczenia dwuspójnych składowych grafu G. Przypomnijmy najpierw definicję dwuspójnych składowych. Niech R będzie relacja na krawędziach grafu G zdefiniowana następująco: e tRe., wtedy i tylko wtedy, gdy e, — o, lub istnieje cykl prosty zawierający jednocześnie e, oraz ev Można udowodnić, że R jest relacja równoważności (zadanie 7.8). Podgrafy grafu G, indukowane przez zbiory wierzchołków incydcnlnych z krawę dziami z klas abstrakcji relacji R, nazywamy dwuspójnymi sk ła d o w y m i, a wierzchołki należące do dwóch lub więcej różnych dwuspójnych składowych - w ierzch o łk a m i roz dzielającymi. Wierzchołki rozdzielające charakteryzują się tym, że usunięcie ich „rozspójnia” graf. O krawędziach należących do jednoelemenlowych klas abstrakcji relacji R mówimy, że są mostami. Usunięcie mostu także „rozspójnia” graf. Zaobserwujmy, że jeśli dwuspójna składowa zawiera więcej niż 2 wierzchołki (nie jest mostem), to każde jej dwa, różne wierzchołki leżą na cyklu prostym złożonym tylko z. wierzchołków lej dwiispójnej składowej. Graf zawierający tylko jedną dwuspójną składową nazywamy grałem dwuspójnym. □
IPu z y k l a d :
Dwuspójnymi składowymi grafu z, rysunku 7.2 są podgrafy indukowane przez, zbiory wierzchołków (1 ,2 , 3, 4}, (3, 7, 8}, [5, 7], (6, 7), (8, 9}, {9, 10, I I }. Wierzchołkami rozdzielającymi są 3, 7, 8 i 9. Mostami są krawędzie 5 ... 7, 6 — 7 oraz 8 — 9.
Formalnie problem znajdowania dwuspójnych składowych definiuje się następująco: Pitoiu.KM 7.2.
Dany jest spójny graf G = ( V, R). Mamy obliczyć funkcję />’: { I.......taką żc. />(tą) ■- B(e?) wtedy i tylko wtedy, gdy c, i e, należą do lej samej dwiispójnej składowej. W cel u wykorzystania drzewa przechodzenia w głąb do znajdowania dwuspójnych skła dowych dokonajmy pewnych obserwacji. O isskrwacia 1:
Każda krawędź grafu łączy zawsze potomka z. przodkiem w drzewie przechodzenia w głąb. Krawędź drzewowa łączy poprzednika z następnikiem. O kskkwaoja 2:
Numer w głąb wierzchołka u jest mniejszy od numeru każdego jego właściwego (róż nego od e) potomka.
7.2. D w uspójn e sld ad ow e
233
Oit.siiuwAC.iA 3: Korzeń drzewa przechodzenia w głąb jest wierzchołkiem rozdzielającym wtedy i tylko wtedy, gdy ma co najmniej dwa następniki. OllSKKWAC.JA 4:
Niech low: V—>V będzie funkcją zdefiniowaną rekurencyjnie w następujący sposób: low(v) — min( (nr(v) } u {nr(w):v — w jest krawędzią niedrzewową w G} u { l o w ( u ) : i i jest następnikiem w w drzewie przechodzenia}). Mniej formalnie, l o w ( v ) jest najmniejszym numerem w głąb n r ( w ) , takim że wierzchołek iv jest przodkiem v w drzewie przechodzenia w głąb i istnieje ścieżka o początku w v i koń cu w tv, riti której wszystkie wierzchołki, poza w, łeżą w poddrzowic o korzeniu w v. U P rz y k ła d :
Dla grafu z rysunku 7.2 low( I) = I, /ó'iv(2) = I, l/m(3) = I, low(4)-- I, /oiv(5) = 5, low(6) = 6, low(l) ~ 3, /oh'(S) = 3, /oiv(9) = 8, /env(l(.)) = 8, l.ow( 11) = 8. _______U 5: Wicrzcliolek v, różny od korzenia, jest rozdzielający wtedy i tylko wtedy, gdy dla jego pewnego następnika u w drzewie przechodzenia w głąb low(u) > niiv). (Jeśli nierówność zachodzi, to każda ścieżka łącząca u z korzeniem drzewa zawiera u. Stąd wynika, żc v jest wierzchołkiem rozdzielającym). OlISUKWACJA
□ P rzykład : W grafie z rysunku 7.2 następujące wierzchołki są rozdzielające: 3, ponieważ lnw(7) = 3; 7, ponieważ low(5) = 5 (i low(6) = 6); 8, ponieważ low(9) = 8; oraz 9, ponieważ /mr(IO) = 8. O
bserw acja
6:
Załóżmy, że badany graf ma co najmniej dwie dwuspójne składowe. Zauważmy, że w takim grafie eo najmniej jedna (faktycznie co najmniej dwie) dwuspójna składowa zawiera tylko jeden wierzchołek rozdzielający. Niech B będzie taką dwuspójną składową, b jedynym wierzchołkiem rozdzielającym w B, a .v jedynym następnikiem b w drzewie przechodzenia w głąb, należącym do B. W trakcie przechodzenia grafa metodą w głąb, po przejściu z b do.r, ponowny powrót do wierzchołka b następuje dopiero po przejrzeniu list sąsiedztwa wszystkich wierzchołków składowej B, różnych od b. Jeśli na przeglądanie listy sąsiedztwa dowolnego wierzchołka i> patrzymy jak na odwiedzanie kolejnych krawędzi o jednym z końców w tym wierzchołku, to krawędzie z list. sąsiedztwa wierz chołków dwuspójnej składowej B, różnych od b, są przeglądane kolejno zaraz po odwie dzeniu krawędzi h — x na liście /;. Więcej, jeśli usuniemy z grafu G krawędzie składowej />', lo kolejność przeglądania krawędzi w pozostałej części grain się nic zmieni.
234
7. A lgorytm y grafow e
Przedstawimy teraz zmodyfikowaną wersję procedury search, w której ■■ jednocześnie - będziemy numerowali wierzchołki w kolejności odwiedzania w głąb, obliczali funkcję Iow oraz wyznaczali dwuspójne składowe. Do tego celu wykorzystamy ir/.y struktury pomocnicze: stos S, na który będziemy odkładali kolejno przeglądane krawędzie, oraz zmienne id i /, na których będziemy pamiętali liczby, odpowiednio, już wygenerowanych dwuspójnych składowych i już odwiedzonych (zamarkowanych) wierzchołków. Każda krawędź v — w znajduje się zarówno na liście sąsiedztwa v (reprezentowana przez iv), jak i na liście sąsiedztwa w (reprezentowana przez v). Niech v będzie przodkiem w w drzewie przechodzenia. Jeśli w jest poprzednikiem w, to krawędź v — ii' będzie odkładana na stos, gdy napotkamy ją na liście sąsiedztwa w Jeśli r nie jest poprzed nikiem vw, to krawędź tę odkładamy na stos, gdy napotykamy ją na liście sąsiedztwa tr. Żeby łatwo sprawdzić, czy \>jest poprzednikiem iv, przyjmiemy, że jest znany poprzed nik odwiedzanego właśnie wierzchołka. Początkowo id = 0 i / = 0. Procedure s e a r c h - h ( v , u : w i e r z c h o ł e k ) ;
{ v - odw iedzany w ła ś n ie w ie rz c h o łe k ; u - jego p o p rz e d n ik w d rzew ie p rz e c h o d z e n ia ; j e ś l i v jest korzeniem , to u = 0) var w : w ierzchołek; e : krawędź;
begin visited (v) 1 ;= 1
-I- 1 ;
;= t r u e ; Itr(v )
:= 1 ; io w {v )
1;
{przeglądanie listy .sąsiedztw a w ie rz c h o łk a v[ I i while c u r r e n t ; ( v ) -z nil do
begin
w := w ie rz c h o łe k na l i ś c i e L(v) , wskazywany p rz e z c u r r e n t ( v ) ; if not v i s . i t : e d ( w ) then begin
[w j e s t n a stę p n ik ie m v w d rzew ie p rzech o d zen ia} push(S,
v — w) ;
search-b(w,
v) ;
if low(w) > n r ( v ) then begin
(w ykryta z o s t a ł a dwuspójna składow a z a w ie ra ją c a krawędź v — w; w sz y stk ie j e j kraw ędzie zn ajd u ją s i ę k o le jn o na s z c z y c ie s to s u , p rz y czym v — w j e s t po ło żo n a n a jg łę b ie j} i d := i d + 1; repeat e := f r o n t ( S) ; 13(e) :=id; p o p (.9) until e = w — w
end
7.2. Dwu-spójne skłaclowci
235
e lse If Ic'n'(iv') < .l . ow( \ ) then l . o w ( v )
:= l o w ( w ) end e lse i f ( n r ( i v ) < m ( v ) ) y.ncl { w * u) t h e n b e g in |u';je:;l: przodkiem v w drzewie różnym od poprzeihiika]
piishiS, v-— w) ; if n r (w) < l o w ( v ) then l o w [ v )
:~nr(w)
end; c u r r e n t ( v) := w s k a ź n i k ćto n a s t ę p n e g o w i e r z c h n i k:a n a l i ś c i e U v) end
end; Procedurę semch-b można wywołać dla dowolnego wierzchołka v i n = 0. Jej popraw ność wynika bezpośrednio z. obserwacji 1-6 (zad. 7.9). Podobnie jak poprzednio, każda lista sąsiedztwa jest przeglądana lylko raz. Dodatkowo każdą krawędź tylko nr/, wstawia my na stos i tylko raz /. niego usuwamy. Stąd wynika, że złożoność powyższego algoryt mu jest liniowa (Oiu -! ///)). W ostatnich lalach nastąpił szybki rozwój badań nad algorytmami równoległymi. Oka zało się, że stosowane w tej dziedzinie metody umożliwiają konstruowanie efektyw nych, alternatywnych do istniejących, algorytmów sekwencyjnych. Poszukiwanie, ta kich metod do rozwiązywania problemów grafowych było konieczne, ponieważ prze chodzenie w głąb okazało *i • b mine do zrównoleglenia. Pokażemy, że dwuspóine składowe daje się wyznacz, , czasie liniowym, nie przechodząc grafu w głąb. W tym celu sprowadzimy [Miotem dwuspójnych składowych do problemu spójnych składowych. Dla danego spójnego grafu G zbudujemy pomocniczy graf którego spójne składowe będą odpowiadały dwuspójnym składowym G. Zbiorem wierzchoł ków grafu € / będzie zbiór krawędzi G. Dwie krawędzie e, i e, będą należały dn tej samej spójnej składowej G' wtedy i tylko wtedy, gdy należą do tej samej dwuspójnej składowej G. Omawiany algorytm jest przykładem zastosowania ważnej me tody używanej w projektowaniu algorytmów, a polegającej na sprowadzeniu rozwią zywanego problemu do problemu, dla którego rozwiązanie (algorytm) jest już znane. Oczywiście, żeby otrzymany w len sposób algorytm był efektywny, zarówno spro wadzenie go clo znanego rozwiązania, jak i znane rozwiązanie muszą być efektywne. Niech T będzie dowolnym drzewem rozpinającym grafu G o korzeniu w dowolnie wybranym wierzchołku r. W przeciwieństwie do poprzedniego algorytmu, T nie musi być drzewem przechodzenia w głąb. Załóżmy także, że wierzchołki V są ponumero wane w kolejności odwiedzania ich metodą preorder. W rozdziale I pokazaliśmy, w jaki sposób obliczyć taką numerację w czasie liniowym. Od lej chwili będziemy, utożsamiać wierzchołki z ich numerami. Korzeń ma zawsze numer I. Miech j>(v) bę dzie poprzednikiem a w drzewie T dla każdego wierzchołka a, różnego od korzenia.
236
7. A lgorytm y grafow o
jak już powiedzieliśmy wcześniej, wierzchołkami pomocniczego grafu G' są krawędzie grafu G. a krawędzie w (,!' są tworzone według następujących reguł (zob. rys. 7.3): (a) (ii-.- w)..(\>— w) jest krawędzią w G \ jeżeli n jest poprzednikiem u w drzewie 7", krawędź r — w nic jest krawędzią drzewowg i i> > w, (1)) (u -..i j .....(,v..-w ) jesl krawędzią, w G', jeżeli u jest poprzednikiem v, ,vjesl poprzed nikiem ii' (u....v i x .....w są krawędziami drzewowymi), v ....ir jesl krawędzią niedrzewową oraz w i w nie sg w relacji potomek-przodek w drzewie 7"; (e) (u.- 11) — (u -...vr) jesl: krawędzią, w G', jeżeli u jest poprzednikiem v w drzewie, i' jest poprzednikiem w oraz pewna krawędź grafu G jgezy polonika w w drzewie T z. wierzchołkiem nie będącym potomkiem w
Rys. 7.3. n) Grał z zaznaczonymi krawędziami drzewa rozpinajijccgo (linie chicle). w którym wierzchołki s;j ponumerowane me(od;t preorder; b) grał’ O '
Zauważmy, że jeśli dwie krawędzie z G sg połączone bezpośrednio w G', to leżą na tym samym cyklu prostym w G, a więc należą do tej samej dwuspójnej składowej. Co więcej, zachodzi twierdzenie Tarjana-Vishkina [TV]. T w i e r d z e n i e 7.1. Dwie krawędzie należą, do tej samej dwuspójnej składowej grafu G wtedy i tyl ko wtedy, gdy jako wierzchołki należą do tej samej spójnej składowej grafu G'. V o w O iKażda niedrzewowa krawędź ,v— y wyznacza w G cykl prosty składający się z tej krawędzi i jedynej ścieżki prostej w 7’, łączącej x i y. Dla ustalonego drzewa 7 'cykle takiej postaci nazywają się cyklami bazowymi. Cykle bazowe charakteryzują się, tym, że zbiór krawędzi dowolnego cyklu prostego w grade można wyrazie jako różnicę symetryczną” zbiorów krawędzi pewnych cykli1 11
ki>/,uii:;i symetryczni] skończonych zbiorów A',. .... A', nazywamy zbiór zawierający dokładnie Ic eJcmcniy, któro należn Jo nieparzystej liczby zbiorów spośród .V....... Aj.
7.:!. O w uspójne sk ła d o w e
237
bazowych. Niech Q będzie relacją na krawędziach grafu G zdefiniowaną na stępująco: exQe2 wtedy i tylko wtedy, gdy e, i e2 należą do tego samego cyklu bazowego. Przez ()' oznaczmy zwrotne i przechodnie domknięcie relacji (J. Mówimy, że dwie krawędzie e i / s ą w relacji Q wtedy i tylko wtedy, gdy c =■■f lub istnieje taki ciąg e5, ..., ek krawędzi grafu G’, gdzie k> I, że c,Qi\. U'xQe;,, ak ...j Qek oraz e, = c i ck ~ f Pokażemy, że Q - R, gdzie A’ jest relacją równoważności /.definiowaną wcześniej. Przypomnijmy, że klasy abs trakcji relacji A’ wyznaczają dwuspójne składowe grafu G. Dwie krawędzie są w relacji R wtedy i tylko wtedy, gdy są identyczne lub należą do tego samego cyklu prostego. Wynika stąd, że Q’ c R, Pozostaje nam zatem pokazać, że R ci Q \ Niech c2 będą różnymi krawędziami w G, należącymi do pewne go cyklu prostego C. Z tego co powiedzieliśmy wcześniej, C można przedsta wić jako różnicę .symetryczną pewnych, parami różnych, cykli bazowych Cj, C2, .... C\, dla pewnego k > 0. Bez straty ogólności możemy założyć, że kolej ność Cj, C2, .... Cj jest taka, że dla każdego I < i < k cykl C, ma wspólną krawędź z pewnym cyklem Cj, gdzie 1 < / < i. (W przeciwnym razie krawędzie ze zbioru będącego różnicą symetryczną Cj, Cj, ..., Ck nic mogłyby należeć do t.ego samego cyklu prostego). Teraz, przez indukcję, względem k możemy udo wodnić, że każda para krawędzi należących do cykli Cj, Cj, .... Cj jest w relacji <2*. W szczególności co implikuje R c Q \ W ten sposób wykazaliśmy, że Q ' - R. Żeby udowodnić twierdzenie, wystarczy tylko pokazać, że każde dwie krawędzie sąsiadujące w G' należą do wspólnego cyklu bazowego w gra fie G oraz że krawędzie każdego cyklu bazowego z. grafu G są wierzchołkami tej samej spójnej składowej grafu Gk Niech ii •— v oraz x — w będą krawędziami, które sąsiadują w G'. Rozważmy trzy przypadki konstrukcji krawędzi grafu G'. (1) Krawędź u — u jest krawędzią drzewową należącą do cyklu bazowego wy znaczanego przez krawędź x — w (lulaj .v = v). (2) Krawędzie drzewowe u — v i x — ti> należą do cyklu bazowego wyznacza nego przez krawędź v— w. (3) Wierzchołki x i u, są takie same (,v.= v), u — u, r — u> są krawędziami drzewowymi, u jest poprzednikiem u, u jest poprzednikiem w oraz istnieje krawędź y— z, laka że y jest potomkiem w, a z nie jest potomkiem u. Krawędzie u — u i r>— w są krawędziami cyklu bazowego wyznaczanego przez krawędź y — z. Rozważmy teraz cykl bazowy wyznaczony przez krawędź niedrzewową x — y. Bez straty ogólności możemy założyć .v < y. Niech z będzie najbliższym wspól nym przodkiem wierzchołków x i y w drzewie T, tzn. niech będzie pierwszym wspólnym wierzchołkiem na ścieżkach łączących x i y z korzeniem drzewa. Rozpatrując przypadki (1), (2), (3), otrzymujemy, że w grafie G' są następujące krawędzie: (,:v— v)-.-(y— p(y)), gdzie />(y) jest: poprzednikiem y w drzewie
238
7. A lgorytm y gra fo w e
7 '(przypadek (1)); (i>— u) — (u — w), jeżeli tylko ( v ~ u ) i (// — w) sit dwiema kolejnymi krawędziami dr/.ewowymi na ścieżce od z do y lub od z do ,v (przypa dek (3)); ( x — p(.x)):— ( y — p(y)), jeżeli tylko x i y nie su w relacji przodek-polomek, tzn. x ■£ z (przypadek (2)). Łatwo teraz zauważyć, że między dwiema dowolnymi krawędziami każdego cyklu bazowego w G' istnieje ścieżka, która implikuje ich przynależność do tej samej spójnej składowej. cbdoj Dzięki poczynionym obserwacjom możemy sformułować następujący algorytm oblicza nia dwuspójnycli składowych. Kkok 1 il: 1.1. Znaleźć w grafie G dowolne drzewo rozpinające T. 1.2. Ponumerować wierzchołki drzewa metodą preorder i utożsamić je z tymi numerami. 1.3. Dla każdego wierzchołka v obliczyć nd(\>), tzn. liczbę potomków v w drzewie. Zadanie to można wykonać, obchodząc drzewo metodą postorder i korzystając z na stępującego równania rekurencyjnego: nd{v) ~ I -I- Xn+ nd(v) ~ I. K kok
2:
Dla każdego wierzchołka obliczyć low(v) i high(v), gdzie [ow(v) (hig/i(v)) jest naj mniejszym (największym) wierzchołkiem w, takim że w = v lub w jest połączony z po tomkiem v krawędzią niedrzewową. Formalnie: low(v) = min({v( u {w. v — w jest krawędzią niedrzewową) u {low(u): u jest następ nikiem v w 7’}) high(y) = max({v) u {w: y — w jest krawędzią niedrzewową) u {high(u): u jess następnilciem v w 7'}). Funkcje Iow i high oblicza się podobnie jak nd - obchodząc 7 'metodą postorder. K kok
3.
Skonstruować krawędzie grafu G' według podanych reguł 1-3. Reguła 1: Dla każdej niedrzewowej krawędzi v — w, takiej że v < w, dodać do G' krawędź. (l>(w)— w )— (v— w). Reguła 2: Dla każdej niedrzewowej krawędzi v — n>, takiej że i>+ nd(v) < w (gdzie v i w nie są w relacji przodek-polomek), dodać do G' krawędź (p(v)— v) — QĄw)-—w).
7.:!. S iln io sp ó jn e s k ła d o w e i s iln a o rie n ta c ja
239
Ri‘|>ula 3:
Dla każdej drzewowej krawędzi v — h\ takiej że e jest poprzednikiem n> w T i r nie jest korzeniem (>’ ż- I), dodać do G' krawędź (p(v) — v)— (V— ti>), jeżeli tylko low(w) < i> lub hivji(w) > v -l- iul(v). K ro k
4:
Obliczj'e spójne składowe grafu O''. Zauważmy, że ponieważ każda krawędź z G wnosi co najwyżej dwa poleczenia do grain G \ to grał ten ma ni wierzchołków i co najwyżej 2ni krawędzi. Siad wynika, że jeśli każdy z. czterech kroków można wykonać w czasie liniowym, to cały algorytm działa w czasie 0(n + m). Działania 1.2 i 1.3 oraz kroki 2 i 3 w oczywisty sposób wykonuje się. w czasie 0(n -i- ///). Złożoność czynności 1.1 i kroku 4 zależy od doboru odpowiednich algorytmów. Jeżeli drzewo rozpinające jest dane, to czynność 1.1 można pominąć. Spój ne składowe i drzewo rozpinające grafu można wyznaczyć w czasie liniowym w sposób alternatywny do przechodzenia w głąb, stosując na pr/.yklad przechodzenie wszerz (zob. rozdział I). Generalnie, jeśli zapewnimy wykonanie czynności 1.1 i kroku 4 w czasie liniowym, to prezentowany algorytm obliczania dwuspójnych składowych będzie działał w czasie 0(7/ -i- ni). Wynika z powyższego, że informację o strukturze grafu można czasami uzyskać z jego dowolnego drzewa, rozpinającego, niekoniecznie drzewa przechodzenia w głąb. Przedsta wiamy jeszcze, jeden algorytm przetwarzający dowolne drzewo rozpinające danego grain.
7.3 . S iln ie s p ó jn e sk ład ow e i s iln a o r ie n ta c ja Kolejnym problemem, który rozważymy, jest problem silnej .spójności grafów zo rientowanych. Powiemy, że graf zorientowany G jest silnie spójny, jeżeli dla każdych dwóch wierzchołków v i w grafu G istnieją ścieżki zorientowane z t>do te oraz z n> do i>. Jeśli G nie jest silnie spójny, to możemy podzielić go na co najmniej dwie silnie spójne składowe, tzn. maksymalne (w sensie zawierania wierzchołków i krawędzi) podgrafy silnie spójne. Na rysunku 7.4 widać przykładowy grał' i jego silnie spójne składowe.
Kys. 7.4. ( iiii! zorientowany i jego .silnie spójne składowe
240
'i. A lgorytm y grafow e
Zanim przystąpimy do rozwiązywania problemu znajdowania silnie spójnych skła dowych zastanówmy się, czy krawędzie każdego grafu niezorientowanego można tak zorientować (tzn. zamienić każdą krawędź niezorientowaną u~ .r na jedną z dwóch zorientowanych u->v lub v~>it), żeby otrzymać graf silnie spójny. Odpowiedź oczy wiście jest negatywna. Grafu niespójnego lub z mostami nie można zorientować w len sposób. Okazuje się jednak, że jeśli graf niezorientowany jest spójny i bez mostów, to laka orientacja jest możliwa. Można ją znaleźć w czasie liniowym, używając bar dzo podobnych metod do tych z poprzedniego algorytmu dla problemu dwuspójnych składowych. P roblem 7 .3 . Dany jest spójny graf niezorientowany bez. mostów G = (V, E) i jego drzewo roz pinające '/'. Nasźym zadaniem jest zorientować krawędzie grafit G w ten sposób, żeby otrzymany graf by 1 silnie spójny. Jak poprzednio załóżmy, że wierzchołki drzewa 7" zostały ponumerowane metodą preortler i utożsamione z tymi numerami oraz że obliczyliśmy ltnv(v) dla każdego wierz chołka v. Wszystkie te czynności można wykonać w czasie liniowym. Niech v — w będzie krawędzią w grafie G. Bez straty ogólności będziemy zakładać, że v < w. 0 v....w powiemy, że jest krawędzią w tył, jeżeli r i ir są w relacji przodok-polomek 1 v nie jest poprzednikiem w. Jeżeli v i w nic są ze sobą w relacji przodek-potomek, to v-... i i 1nazywamy krawędzią poprzeczną. Zauważmy, że krawędź r — ir jest krawędzią w ty i wtedy i tylko wtedy, gdy jest niedrzewowa i ir < r + iid(r) - I. Zarowno krawę dzie w tyl, jak i krawędzie poprzeczne są niedrewowe. Z kolei każda krawędź uiedr/.e.wowa jest albo krawętlzią w tyl, albo i-eev ■ ' poprzeczną. !’owyższe rozważania wskazują, że krawędzie niedrzewowe można skluv/iikowae' w cz.ttsic liniowym. Dla każdego wierzchołka a /.definiujemy funkcję lowbiiek(y) analogicznie do foirfij, lecz z użyciem tylko krawędzi w tyl: ltnvback(v) = niin(fr} u Jir: r — w jest krawędzią w tyl] U {lowbackiu): u jest następnikiem r w 7'].). Funkcję borlwck można liczyć jednocześnie z Iow. Żeby przekształcić G w graf silnie spójny orientujemy krawędzie w następujący sposób: (u) jeśli r — ir jcsl krawędzią w tyl (\> < w), to i>— ir zamieniamy na ir :>r; (li) jeśli w—w jest krawędzią po|'>r/.c.czną (r < \rl. to r ... ir zarweniamy na r ->ir: (c) jeśli i- — u*jest krawędzią dr/.ewową (ir jest następnikiem v), to r w zamieniamy na r —:• ir, o ile tylko lowback(w) < w lub low(w) > iią a w przeciwnym razie na ir • .• r. Na rysunku 7.5 widać graf z obliczonymi funkcjami Iow i Inwback oraz jego orientację. p. Miirważ funkcje Iow i lowback można obliczyć w czasie liniowym ((>(/«)), graf G moż na następnie zorientować w czasie liniowym. Poniżej udowodnimy, że otrzymany w ten sposob graf jest silnie spójny, a zatem, że zachodzi twierdzenie Tarjana-Vishkina |TV|.
7.3. S iln ie sp ójn e sk ła d o w e i siln a orien tacja
i (I, I)
4 (2, 4);*/
I
7 <3, 3) 3
2 (2
241
/
; 3)1
>8 (3,3)
H ' <) (3,3)
Rys. 7.5. ;0 Grał spójny bez mostów, drzewo roy.pinającc, obliczone funkcje wiasach); b) graf silnie spójny
Io w
i
lo w b u c k
(para liczb w na
fjjPj
Twierdzenie 7.2.
i
Graf
D ow ód:
Żeby udowodnić, że otrzymany graf zorientowany jest rzeczywiście, silnie spój ny, wystarczy wykazać istnienie ścieżek (zorientowanych) z wierzchołka I do każdego innego wierzchołka grafu oraz z. każdego wierzchołka do I. Wykażemy tylko, że do każdego wierzchołka prowadzi ścieżka z I. Dowód drugiej części jest analogiczny do dowodu pierwszej i pozosiawiamy go Tobie, jako zadanie. Przypomnijmy, że wierzchołki są utożsamiane z ich numerami otrzymanymi metodą preorder. Dowód przeprowadzimy metodą indukcji względem numerów wierzchołków. (Oczywiście I jest osiągalny ścieżką o długości 0. Rozważmy wierzchołek v > 1 i załóżmy, że do wierzchołków I, 2, ..., v - 1 prowadzą ścieżki z !. Niech u będzie poprzednikiem v w drzewie (« < y). Jeśli krawędź u ...- v jest zorientowana od u do v, to ścieżka z I do ii rozszerzona o krawędź u ....v prowadzi do i>. Załóżmy zatem, że u — i- jest zorientowana od u do u. Na mocy reguły (e) lowbackiy) > v (*) oraz lmv(v) < u. Stąd wynika, że musi ist nieć krawędź poprzeczna łącząca pewnego potomka x wierzchołka v z low[v). Krawędź la jest zorientowana nó.low(v) do x (reguła (b)). Ponieważ lo\v{v) < u, na mocy założenia indukcyjnego do low{\>) prowadzi ścieżka z I i dalej krawę dzią lo\\'(v) —>,v można dotrzeć do .v. Pokażemy teraz, że istnieje ścieżka zorien towana z .v do v złożona z krawędzi drzewowych lub z krawędzi w tyl. Załóżmy przeciwnie. .Nieclt y, gdzie y ^ v, będzie najmniejszym przodkiem x i niech jednocześnie będzie potomkiem v, do którego prowadzi taka ścieżka. W takim wypadku krawędź />(>’) — >’ musi być zorientowana od piy) do y. Ponieważ low(y) = /mr(.v) - lowiy) < v < y, to zgodnie z regułą (e) lowbackiy) < y. Niech z będzie takim potomkiem y, że, — lowbackiy) jest krawędzią w lyf. Zgodnie
O ",
otrzymany /. G zgodnie z regułami a-c, jest grafem silnie spójnym.
242
7. A lgorytm y grafow e
z regularni (c) i (a), wszystkie krawędzie drzewo we na ścieżce od y do z są /.orientowane od poprzednika do następnika, a krawędź w tyI z — lowback(v) - od z do lowbackiy). Zauważmy teraz, że y jest tak dobrany, że lowhuck(y) < i>. Stąd wynika, że lowback(v) < u, wbrew założeniu (*), czyli istnieje ścieżka zorientowana z ,v do v, co implikuje istnienie ścieżki z I do r. cbdo j Jeśli drzewo rozpinające jest drzewem przechodzenia w głąb, to krawędzie niedrzewowo są tylko krawędziami w tył. W takim wypadku lowback(w) = low(w) dla każdego wierz chołka w. Stąd każdą krawędź drzewową v— w, gdzie v < w, zamieniamy zawsze na v —>w, a każdą krawędź v— w w tyI tak jak poprzednio, tzn. na w—i>v. Przystąpimy teraz do rozwiązania problemu znajdowania silnie spójnych składowych. P k o k l e m 7.4.
Dany jest: graf zorientowany G = (V, E). Mamy obliczyć funkcję S: V—>{ i ,2 ,.... //}, taką że dwa wierzchołki \>i w należą do tej samej silnie spójnej składowej wtedy i tylko wtedy, gdy S(v) = S(w). Graf G będziemy reprezentować za pomocą list sąsiedztwa. Z każdym wierzchołkiem i> wiążemy dwie listy L'(v) i L~(v). Są to - odpowiednio - lista wierzchołków, do których prowadzą krawędzie o początku w v, oraz lista wierzchołków będących początkami krawędzi o końcach w v. Grafy zorientowane można przechodzić w głąb podobnie jak grafy niezorientowane. Przechodzenie nazywamy przechodzeniem w przód, gdy po krawędziach przechodzi się zgodnie z ich zwrotami, a przechodzeniem w tył, gdy po krawędziach przechodzi się przeciwnie do ich zwrotów. Łatwiejszym zadaniem do rozwiązania od znajdowania silnie spójnych składowych jest sprawdzanie, czy dany graf jest silnie spójny. W tym celu wystarczy dla dowolnego wierzchołka v sprawdzić, czy każdy wierzchołek u w grafie jest osiągalny z i> (tzn. czy istnieje ścieżka zorientowana od.z do u ) i czy z każdego wierzchołka u można osiąg nąć r. W pierwszym przypadku można to zrobić, przechodząc graf w przód, a w drugim - przechodząc go w tyl, za każdym razem rozpoczynając przechodzenie od w Jeśli w obu tych przypadkach wszystkie wierzchołki grafu zostaną odwiedzone, to graf jest silnie spójny. Złożoność tego algorytmu jest oczywiście liniowa. Powyższą ideę wykorzystamy teraz do znajdowania silnie spójnych składowych. Posłu żymy się dwiema procedurami rekurencyjnymi search-forward oraz search-back. Pierw sza z nich powoduje przechodzenie grafu G w przód, z jednoczesnym obliczaniem dla każdego wierzchołka v numeru w kolejności odwiedzania, czyli nr(v), i liczby potomków w poddrzewie przechodzenia w przód o korzeniu w i>, czyli nd(v). W procedurze wyko rzystujemy zmienną globalną /, na której jest pamiętana liczba dotychczas odwiedzonych wierzchołków. Początkowo / = 0. Podobnie jak przy przechodzeniu w głąb grafu uie/.o-
7.3. S iln ie spójn e sk ła d o w e i siln a orien ta cja
243
rieiilowancgo zakładamy, że z każdym wierzchołkiem v są związane dwa pola: visited-J{v) i curreiU-J{v). Pierwsze pole służy do zaznaczania Klanu wierzchołka przy prze chodzeniu w przód. Drugie pole jest wskaźnikiem do takiego pierwszego wierzchołka ir na liście /,'(v), w wypadku którego krawędź w—»h> nie była jeszcze odwiedzona. p ro c e d u re s e a r c h - f o r w a r d {v : w ie r z c h o łe k ) ; var w : w ierzch o łek; b eg in vis: ved -fiv) tru e; [zam arkuj v ja k o w ie rz c h o łe k odwiedzony p ray p rz ech o d z en iu w przód) i := 1 I i; nr(v| :--i; n d ( v )
;= 1;
w h ile c u r r e n t - 1: (v) “Z n i l do b e g in w := w ie rz c h o łe k na l i ś c i e L* (v) , na k tó ry w sk azuje c u r r e n t - S: (v) ; i f n o t v t s i z e d - fiw ) {w nie b y ł odwiedzony p rz y p rzechodzeń i.u w p rzó d ) th e n b e g in ( v —:■w zaliczam y do kraw ędzi drzewa p rz e c h o d z e n ia w p rzó d o k o rz e n iu w w ie rz c h o łk u , od k tó re g o ro z p o czę liśrn y poszukiw anie) s e a r c h -fo r w a r d Iw) ; nd(v) nd(v) -indlw ) end; p rz e s u ń w skaźnik c u r r e n t - f i v) do n astęp n e g o w ie rz c h o łk a na l i ś c i e Ii'(v) end end; Jeśli graf nie jest silnie spójny, to wywołanie seardi-forwiini(\) może nie spowodować odwiedzenia wszystkich wierzchołków grafu. Żeby zagwarantować poprawne przejście całego grafu, wystarczy wykonać następujące instrukcje: 1 := 0 ; f o r v' : = 1 to n do b e g in v i s i t e d - f i v ) := fa ls e ; c u r r e n t - f i v) := p ierw szy w ie rz c h o łe k na l i ś c i e L* ( v) end; f o r v : = i t o n do if n o t v i s i t e d - f i v ) then s e a r c h -fo r w a r d (v) ; Bez straty ogólności będziemy teraz utożsamiać każdy wierzchołek w z jego •numerem nr(v), tzn. przyjmiemy, że v = «/•(>'). Funkcje S obliczymy w ten sposób, że wartość .V(rj
244
'I-
A Jgorylm y g ra fo w e
będzie równa nąjmnicjszymu wierzchołkowi w silnie spójnej składowej zawierającej c. Zauważmy, że przy przechodzeniu grafu w przód wierzchołek najmniejszy ze wszystkich wierzchołków tej samej silnie spójnej składowej jest odwiedzany jako pierwszy. Niech ii będzie właśnie, takim wierzchołkiem, tzn. S(u) - u. Wszystkie wierzchołki silnie spój nej składowej zawierającej u leżą w pockJrzewie przechodzenia w przód o korzeniu w tym wierzchołku. Oczywiście, w tym poddrzewie mogą być także wierzchołki spoza silnie spójnej składowej, do której należy u. Żeby wyznaczyć tylko le wierzchołki p o j drzewa, z których w grafie G prowadzą ścieżki do u, wystarczy sprawdzić, które z nieh można odwiedzić z. u, przechodząc graf w tył. Zmianie lo można zrealizować za pomocą procedury search-back. W jej opisie znaczeni;! pól visiied-h(v) oraz current-My) są po dobne do, odpowiednio, visite
te : w ie r z c h o łe k ; begin ,S'( v )
: = u;
visited -b {v)
t r u e {v markujemy jako wierzchołek odwiedzony przy przechodzeniu w tył) wh:Li e c u r r e n t - b i w ) yi nil do begin w := w i e r z c h o ł e k na liście il"(v) , na który w skazuje c u r r e n t - b (v ) ; if i.i< w < u -I- rid(u) {w jest w poddrzewie przechodzenia w przód o korzeniu u) and not v i s i Ced-h(w) j w nie byl odwiedzony przy przechodzeniu w tył] then, search-back(w ) ; p rz e su ń w skaźnik a u r r e n t - b ( v) do następnego wierzchołka na liście L " { v ) end end ;
Silnie spójne składowe możemy teraz wyznaczyć w następujący sposób: for u := 1 to n do begin visited -b (u )
: = false;
ciw ren t-b (u )
pierwszy wierzchołek na liście L (u) end ; for u :-- 1 to n do if not v i f j . i t e d ~ b { u ) (u nie był odwiedzony przy Pr zechod zen:i.u w tyli] then. s e a r c h - h a c k {i i) ;
7.4. Cykle. E u le ra
245
Rys. 7 . 6 . ;ii < irnC /.orienlousiiiy /. wierzchołkami ponumerowanymi (pierwsza liczba w każdej parze) z.a pomoea h - j a r w a r t l ; dni;.',a liczba w parze jest równa liczbie wierzchołków w podclrzewie pfzpchoilzevia przód o korzeniu w danym wierzchołku; b) silnie spójne składowe
Złożoność tego algorytmu, podobnie jak poprzednich, wynosi 0( n + m). Wynika to z, faktu, że w opisanym algorytmie po każdej krawędzi przechodzi się dwa razy: raz w procedurze search-forward i raz w procedurze search-back. Działanie powyższego algorytmu przed stawiamy na rysunku 7.6.
7.4. Cykle Eulera Rozwiążemy teraz problem znajdowania cykli Eulera w grafach. Powiemy, że cykl C w grafie. G jest cyklem Eulera, jeśli każda krawędź grafu występuje w nim tylko raz. Przedstawimy najpierw algorytm znajdowania cykli Eulera w grafach zorientowa nych, a następnie sprowadzimy zadanie dla grafów niezorientowanych do rozwiązanego zadania dla grafów zorientowanych. Niech G ~ (P, E) będzie grafem zorientowanym. Istnieje bardzo proste kryterium, na podstawie którego można stwierdzić, czy graf G ma cykl Eulera. Graf G ina cykl Eulera wtedy i tylko wtedy, gdy jest .spójny (każdą krawędź z E traktujemy jako niezorientowaną)- i liczba krawędzi wchodzących do każdego wierzchołka u (o końcu w r) jest równa liczbie krawędzi go opuszczających (o po czątku w v). Jeśli chodzi o grafy niezorientowane, to oprócz spójności żąda się, żeby liczba sąsiadów każdego wierzchołka była parzysta. Dowody powyższych faktów nie są specjalnie skom plikowane, a poza tym można je znaleźć w prawie każdym podręcznika z teorii grafów. Więcej, wynikają one, także bezpośrednio z algorytmu, który za chwilę przedstawimy.
246
7. A lgorytm y grafow e
Ponieważ warunki na istnienie cyklu Eulera łatwo sprawdza się w czasie liniowym, w dalszych rozważaniach założymy, że su spełnione. P r o b l e m 7.5.
Dany jest spójny graf zorientowany G, taki żc dla każdego wierzchołka r liczba krawędzi wchodzących do v jest równa liczbie krawędzi z niego wychodzących. Dla każdej krawędzi e mamy obliczyć next(e) - następną po e krawędź w pewnym cyklu Eulera C. Na rysunku 7.7 widać graf mający cykl Eulera.
6
Rys. 7.7. Grał' majqcy cykl Eulera, na przykład I, 2, 3, 5, ft, ‘I, 2, ś, -I, 3, I
Zanim przystąpimy do opisu algorytmu znajdowania cyklu Eulera, zdefiniujmy opera cję switch, która umożliwi łączenie dwóch rozłącznych krawędziowe cykli w jeden. Niecli Cj i Cj będą rozłącznymi krawędziowe cyklami o wspólnym wierzchołku v (Cj i C, mogą mieć więcej niż jeden wspólny wierzchołek). Załóżmy, że dla każdej krawędzi e z Cj (Cj,) krawędź ne.xt{e) występuje po e w Cj (Cj). Niech e i / będą. krawędziami, odpowiednio, wchodzącą do v w cyklu Cj i wchodzącą do v w Cj. Procedu ra switch{e, /) umożliwia łączenie cykli Cj i Cj w jeden z ustanowieniem iw.xt(e) jako kolejnej krawędzi po./j u ncxt(j) jako kolejnej krawędzi po e w nowo powstającym cyklu (rys. 7.8).
switch(e, f )
e
>' /
Rys. 7.8. Łączenie cykli
247
7.1. C ykle E ulera
'Formalnie procedurę
sw itch
definiuje się następująco:
procedure sivitdi(e, f : k r a w ę d ź ); var
e : kr awędź ; begin
o' ■- uext:(e); next:(e) := n e x t (f) ; next: (if) :=e' end ;
Operację Krok
sw itch
wykorzystamy w następującym algorytmie obliczania cyklu Eulera.
1:
Znaleźć w G dowolny cykl Cj, be.z powtarzających się krawędzi. K rok
2:
Usunąć z G krawędzie cyklu Cj,. Niech G|, .... 6j. będą spójnymi składowymi tak otrzymanego grafu, o co najmniej 2 wierzchołkach, a ią.... i'; wierzchołkami C„ nidcżącymi, odpowiednio, do Gj, .... Gt. Zauważmy, że każdy z otrzymanych grafów ma cykl Eulera, ponieważ, usuwanej krawę dzi wchodzącej do wierzchołka odpowiada zawsze usuwana krawędź opuszczająca go. K rok
3:
Znaleźć rekurcncyjnie cykle Fulcra Cj, .... Ck w grafach G,, .... Gt. K rok
4:
Za pomocą operacji
sw itch
połączyć Cj, z Cj, .... Cj w jeden wspólny cykl.
Żeby efektywnie zrealizować powyższy algorytm, potrzebujemy właściwych' struktur danych. Zakładamy, żc z każdym wierzchołkiem t> jest związana lisia L'(v) wierz chołków, do których prowadzą krawędzie, o początku w u. W trakcie wykonywania algorytmu sąsiadzi w są przeglądani kolejno, od pierwszego do ostatniego na liście. Zmienna airrcnt{v) wskazuje na pierwszy wierzchołek na liście sąsiedztwa v, który nie by 1 jeszcze odwiedzony z v. Początkowo cuirent(v) wskazuje na pierwszy wierz chołek na liście L'(v). Będziemy uważali, że rozważany właśnie graf (na danym po.ziomie rekursji) zawiera tylko te krawędzie, które prowadzą od każdego wierzchołka v do wierzchołków z jego listy sąsiedztwa, położonych między currcnt(v) a końcem listy. Cykl Eulera konstruujemy za pomocą procedury rekurencyjnej liuler-cycli:(v). Dzięki niej znajdziemy cykl Eulera w spójnej składowej bieżącego grafu, która zawiera v. Dla każdej krawędzi c pamiętamy nie tylko next(e), ale i krawędź prctl{e), poprze dzającą e w konstruowanym cyklu. procedure E u l e r - c y c l e (v : w i e r z c h o ł e k ) ; var x, w : w i e r z c h o ł e k :
e, e , 1:, 1:' : k r a w ę d ź ;
248
7. A lgorytm y grafow e begin {znajdowanie cyklu iv := wierzchołek, na który wskazuje c u i ' r e n t : (v ) ; {usuniecie krawędzi y-->i/z gru l:u] c u r r e n t : (v) :=■ wskaźnik do następnego wierzchołka nu l-iśc.i.e C' ( v) ; e :■= y .> w; f := o?; repeat x w; w := wierzchołek, na który wskazuje c u r r e n t ( x j ; curreji!: (x) := wskaźnik do następnego wierzchołek na liście I,1(x) ; n e x l : ( f ) :=.x-~imv; p r e d ( x - > w ) :=.f; f r - = x - > w until w ~ v , nexl:(f) :=e; p recl(e) := f; Iznajdowanie cykli w spójnych składowych i łączenie ich z C0J
X :==V; e
i7;
repeat e :=■ n e x t ( e ) ; je' - kolejna k r a w ę d ź po e w if c u r r e n t ( x) i- nil than foagin {x należy do spójnego podgrafu zawierającego w:iogee j niż jeden wierzchołek) y := wierzchołek, na który wskazuje c u r r e n t ( x ) ; /:' : - x ~ r > y ; E u l e r - c y c l e ( x ) ; ./' : = p r e d ( £') ; s w i t c h (e,
f)
end; e : e'; x końcowy wierzchołek krawędzi e uafcil x v ond ;
Łatwo zauważyć, żc złożoność algorytmu zależy od łącznej sumy długości wszystkich cykli generowanych w czasie pierwszego wykonania instrukcji „powtarzaj” (repeat;) w procedurze Euter-cych-. Ponieważ cykle te są krawędziowe rozłączne, ich łączna dłu gość wynosi ni. Wynika stad, że za pomocą tego algorytmu możemy znaleźć cykl Fulcra w grafie zorientowanym w czasie liniowym 0(n + i i i ) . Powstaje naturalne pytanie, czy ten algorytm można wykorzystać do znajdowania cykli Eulera w grafach niezorientowanych. Graf niezorientowany ma cykl Eulera wtedy i tylko wtedy, gdy jest spójny i liczba sąsiadów każdego wierzchołka jest parzysta. Pokażemy, żc krawędzie takiego grafu można zorientować w len sposób, żeby liczba krawędzi wchodzących do każdego wierzchołka była równa liczbie krawędzi z niego wychodzą cych. W tym celu zamieniamy każdą krawędź niezorientowaną u — v na dwie zorien towane: u —>v i i’—>h, po czym na krawędziach zorientowanych budujemy cykle w laki
7.5. 5-koiorow axuc grafów planarnych
2d 9
sposób. żeby dla każdego cyklu C istniał odpowiadający mu cykl C , zawierający te same krawędzie, lecz zorientowane przeciwnie. Żeby znaleźć cykl Kuleni, wystarczy pozostawić tylko krawędzie zorientowane jednego cyklu z każdej pary odpowiadających sobie cykli. W celu zbudowania takich cykli postępujemy w następujący sposób. Niech u ..... . uk będą sąsiadami wierzchołka v w G (gdzie h, jest /-tym wierzchołkiem na liście sąsiadów i>). Dla każdego nieparzystego / następną krawędzią w cyklu po «f—>v będzie krawędź i a następnti po u, + ,-» v krawędź t>—>»,. Otrzymane w ten sposób cykle spełniają żądane warunki. Powyższą operację, można w oczywisty sposób prze prowadzić w czasie liniowym, Do otrzymanego grafu stosujemy wcześniej omówiony algorytm obliczania cyklu Hulero w grafach zorientowanych.
7 .5 . 5“koIorowanie grafów planarnych . Graf G = (K, E) jest grafem planarnym, gdy można go przedstawić na płaszczyźnie w taki sposób, że dwóm różnym wierzchołkom odpowiadają dwa różne punkty płasz czyzny, a każdej krawędzi e - krzywa zwyczajna o końcach w punktach odpowiadają cych wierzchołkom incydentnym z e. Ponadto dla pary różnych krawędzi c i /, jeśli e i / nie sąsiadują ze sobą, to przyporządkowane im krzywe, nie mają punktów wspól nych. Jeśli są to krawędzie sąsiednie, to mają tylko jeden punkt wspólny, który jest jednocześnie punktem odpowiadającym wspólnemu wierzchołkowi e i f ( Wiadomo, że liczba krawędzi iii w każdym grafie planarnym zależy liniowo od liczby jego wierzchołków ii. Dokładniej, jeśli n > 3, tom < 3n - 6. Z powyższego faktu wynika natych miast, że pełny graf 5-wierzcliolkowy nie jest planarny. Prawdziwy jest podany tu lemat. Im
Lemat 7.1. Każdy graf planarny ma wierzchołek o stopniu co najwyżej 5.
D owód :
Bez straty ogólności możemy rozważać tylko grafy spójne i o co najmniej 6 wierzchołkach. Jeżeli każdy wierzchołek w takim grafie miałby stopień co najmniej 6, to zawierałby co najmniej 3n krawędzi, a przeczyłoby temu, co powiedzieliśmy wcześniej o grafach planarnych. cbdo
Mówimy, że graf G jest k-kolorowalny, jeżeli każdemu wierzchołkowi możemy przy dzielić jeden z k różnych kolorów w laki sposób, iż żadne dwa sąsiednie wierzchołki nie otrzymują lego samego koloru. Dosyć łatwo można udowodnić, że każdy graf planarny jest 5-kolorownlny. Przez ponad sto lat wielu badaczy starało się dociec, czy każdy graf planarny (a dokładniej - każdą
250
7. A lgorytm y grafowi)
mapo narysowana na karl.ce papieru) można pokolorować 4 kolorami. W 1976 r. Kenneth Appel i Wolfgang Haken podali pozytywną odpowiedź na to pytanie. Ponieważ jednak dowód tego /aktu i wynikający z niego algorytm 4-kołorowania wykracza poza zakres lej książki, wróćmy do problemu 5-kolorowania. T w ie r d z e n ie 7.4. Każdy graf planarny jest 5-kolorowalny. D ow ód :
Dowód przeprowadzimy metodą indukcji względem liczby wierzchołków grafu planarnego Cl. Jeżeli n < 5, to twierdzenie jest oczywiście prawdziwe. Załóżmy, że ii > 5 i twierdzenie jest prawdziwe dla każdego grafu planarnego z mniejszą niż n liczbą wierzchołków. Na mocy lematu 7.1 istnieje w G wierzchołek w o stopniu co najwyżej 5. Rozważmy dwa przypadki. Rkzyi-aduk I:
Stopień wierzchołka v jest mniejszy niż 5.
Niech G' będzie grafem otrzymanym z G przez usunięcie wierzchołka r i krawędzi z nim incydentnych. Graf C ' jest oczywiście planarny i z założenia indukcyjnego 5-kolorowalny. Rozważmy 5-pokołorowanie G'. Na pokolorowa nie sąsiadów wierzchołka v używa się w G' co najwyżej 4 kolorów. Stąd wyni ka, że można rozszerzyć pokolorowanie G' do G, kolorując v kolorem nie wykorzystanym do pokolorowania jego sąsiadów. P rzypadek 2: -Stopień'wierzchołka u jest równy 5.
Zauważmy, że w takim przypadku istnieją dwaj sąsiedzi u, powiedzmy u i w, nie połączeni krawędzią w G. W przeciwnym bowiem razie graf induko wany przez zbiór sąsiadów v byłby pełnym grafem 5-wicrzcholkowym i stąd - wbrew założeniu - G nie byłby planarny. Niech G' będzie grafem otrzymanym z G przez usunięcie v i utożsamienie u z w. Utożsamienie polega na usunięciu wierzchołków u i w z grafu i zastąpieniu ich przez wierzchołek ,v, którego sąsiadami stają się wszyscy sąsiedzi usuniętych wierzchołków. Otrzymany graf jest oczywiście, planarny i z założenia indukcyjnego 5-kolorowalny. Rozważmy dowolne 5-pokolorowanie G'. Można je rozszerzyć do 5-pokoIorowania (7 w na stępujący sposób: wierzchołki u i w dziedziczą kolor wierzchołka x (pamiętajmy, że nie są sąsiadami w G). Zauważmy, że na pokolorowanie sąsiadów v ponownie używa się co najwyżej 4 kolorów. Wolnym kolorem kolorujemy v. cbdo
Na podstawie dowodu ostatniego twierdzenia otrzymujemy już pewien algorytm koloro wania grafów planttrnych 5 kolorami. Pokażemy, w jaki sposób można zrealizować ten algorytm, żeby działał w czasie liniowym. Załóżmy, że wejściowy graf planarny jest dany przez listy sąsiedztwa (z pewnymi modyfikacjami, ó których piszemy dalej). Naj-
7.5. 5-k olorow an ic grafów planarnych
251
hardziej pracochłonny operacji) opisani) w dowodzie-algorytmie jest utożsamianie wierz chołków. W operacji lej trzeba z dwóch list siisiedzlwa wierzchołków u i ir stworzyć jedną lisię sąsiadów .r. Ponieważ na takiej liście wierzchołki nie mogą się powtarzać, tuczenie dwóch lisi wymaga co najmniej czasu proporcjonalnego do długości krótszej z nich, która w pesymistycznym przypadku może być rzędu liczby wszystkich wierzchoł ków w grafie. Wynika stąd. żc. bezpośrednia realizacja dowodu twierdzenia mogłaby dać algorytm o koszcie kwadratowym. Pokażemy jednak, że zawsze można tylko utożsamiać wierzchołki z krótkimi listami sąsiedztwa. W tym celu wykorzystamy taki oto lemat. ;-L:!
L e m a t 7.2.
|
r
Niech G będzie grafem planarnym, w którym każdy wierzchołek ma stopień co najmniej 5. Istnieje wówczas wierzchołek o stopniu fi, mający dwóch nie połą czonych ze sobą sąsiadów o stopniach < 11,
D owód:
Wystarczy pokazać, że w G istnieje wierzchołek r o stopniu 5 , mający co najwyżej jednego sąsiada o stopniu większym niż I I, ponieważ graf indukowa ny przez v i dowolnych jego 4 sąsiadów nie może być pełnym grafem 5-wierzchołkowym. Oznaczmy przez
liczbę wierzchołków w grafie o stopniu <7. Ponieważ
2iii = X chi'! i m < 3n - - 6 - 3 ,/::: 5
X »(/ -- 6 \il > 5
J
mamy /i5 >
X
(d -
6)
iij
+ 12
,{ 1: 7
Załóżmy, że każdy wierzchołek stopnia fi ma co najmniej dwóch sąsiadów o stopniu większym niż I I. Wówczas 2n,; < X< dnd i/■■• i:1 Z powyższych nierówności wynika, że. Z (2d - 12) /;„ -l- 24 < I , dnlt ,1 Z 7
X
ii
I'.’.
(2d - 12) nd -i- X (d - 12)
7 : .
a to jest sprzeczność.
r/S*. 12
-l- 24 < 0
252
7. A lg o ry tm y g ra fo w e
... i" Algorytm 5-kolorowaniu działa w dwóch lazach. F aza
'
I:
Grał' wejściowy jest ileracyjnic redukowany do grafu planarnego o co najwyżej 5 wierzcboTncb, klóry następnie w oczywisty sposób jest kolorowany ó kolorami. W każdej żcracji należy wykonać następujące działania: (1) jeśli w grafie bież:|cym jest wierz chołek v o stopniu < 4, lo należy zredukować graf w wierzchołku >.-. wywołując procedu rę rediiee(v): (2) w przeciwnym razie należy znaleźć wierzchołek c o stopniu ń z dwoma nie połączonymi .sąsiadami ,v i y o stopniu < I 1, a następnie wykonać reduee[v) i wywo łać procedurę. idenlify(x, y) utożsamiania ,v z y. Każdy wierzchołek spelnittjący (I) lub (2) nazywamy rcdukowalnym. Graf wejścio wy G jest reprezentowany za pomocą dwukierunkowyclt list sąsiedztwa. Dodałkowo zakłada się, że dwa wysląpieniti lej samej krawędzi u — te na listach sąsiedztwa u (re prezentowanej przez h’) i w (reprezentowanej przez u) są połączone, tzn. z wystąpie niem u1 iw liście; u jest dane dowiązanie do wystąpienia // na liście w i odwrotnie z wystąpieniem u na liście tv jest dane dowiązanie do wystąpienia w na liście u. Te dodatkowe dowiązania nazywamy krzyżowymi. Zakłada się także, że każdy element listy sąsiedztwa ma wskaźnik do jej początku. Niech L(y) będzie listą sąsiedztwa v. Do realizacji fazy I używa się dwóch kolejek Q i R (realizowanych także przez listy dwukierunkowe), w których pamięta się wierzchołki redukowalne. W kolejce Q są przechowywane wszystkie wierzchołki o stopniu < 4, ii w kolejce R- - wszystkie wierzchołki redukowalne o stopniu równym 5, Z lematu 7.2 wynika, że zawsze któraś z kolejek, Q lub R, jesl niepusla, W realizacji algorytmu wykorzystani)' także slos S do pamiętania wierzchołków usuwanych z grafu wraz z ich bieżącymi listami sąsiedztwa i do pamiętania piątek [/, .v, /,(,\j, y, /„,(y)|, gdzie / jest wierzchołkiem powstałym przez utożsamienie x z ,y. Jeśli kolejka Q jest niepusla, to wierzchołek, w którym nastąpi redukcja grafu, pobiera się z. Q. W przeciwnym razie pobiera go się. z R. Pozostaje jeszcze opisać procedury reduce oraz identify. Najpierw jednak zdefiniujemy wykorzystywane w nich operacje na kolejkach: (a) inscn(it, P):: . if u <2 P Hien wstaw u do /’; (b) deleieiii, P):: ii' u e P then usuń i i z P; Jeśli przyjmiemy, że. dany jest wskaźnik do wystąpienia wierzchołka obie. operacje - insert i delete.można wykonać w czasie stałym.
ii
w kolejce P, to
Wykorzystana zostanie- także pomocnicza procedura consider, umożliwiająca właściwe, przetwarzanie. wierzchołków redukowalnych (deyfu) oznacza stopień wierzchołka u). Oto ta procedura:
7,5. 5-lcolormx'iimf) grafów planarnych
253
';roi:::s,:iure c o n s i d e r ( u) ;
cnee, deg(u) <4
: d e l e t e ( u , R) ; insert (u, Q) ; degr(u) = !"> : if u jest redulcowalny then i n s e r t ( u , R ) else d e l e t e {u, R ) ; d e g ( u ) = .1.1 : for each x - sąsiad u do
if d e g ( x )
5 and x jest redukowalny then
i n s e r t : ( r , R)
endcase end;
Procedura consider działu w czasie, stałym. Zdefiniujemy teraz procedury reduce i identify. procedure r e d u c e ( v ) ; p u s h
( { v ,
L, ( v ) ) ,
S ) ;
usuń v z grafu; for each u e L (v) do c o n s i d e r ( u ) end ; procedure i d e n t i f y ( x , y ) ; utwórz nowy wierzchołek t, którego sąsiadami są sąsiedzi x i y ; p u s h (11;, x , , L( x) , y , L ( y ) \ , S) ; usuń x, y z grafu; c o n s i d e r (t) ; for each s e L ( t) do c o n s i d e r ( z ) end ;
Procedury „usuń”-i ,,utwórz” można łatwo zapisać w laki sposób, żeby listy sąsiedztwa, stopnie wierzchołków oraz kolejki Q i R w czasie stałym były właściwie aktualizowane. Ponieważ obie procedury reduceiy) i idenlify(x, y) sg wywoływane tylko dla wierzchoł ków o stopniu co najwyżej I I, czas ich działania wynosi 0(1). Wynika stąd, że faza I może być zrealizowana w czasie liniowym (0(n)). F aza
2:
W (ej fazie zdejmuje się kolejne elementy ze stosu S. Jeśli jest zdejmowany wierz chołek v (i jego lista sąsiedztwa), to jest on kolorowany kolorem różnym od1kolorów jego sąsiadów. Jeśli ze stosu jest zdejmowana piątka |/, x, L(x), y, L(y)], to są odtwarzane wierzchołki ,v i y, a następnie jest im przypisywany kolor wierzchołka /. Ponieważ laza 2 jest właściwie odwrotnością fazy 1, czas jej działania jest liniowy. W len sposób pokazaliśmy, że każdy graf planarny można pokolorować 5 kolorami w czasie liniowym. Zwróćmy jeszcze -uwagę, że jeśli chcemy efektywnie manipulować grafem, na przykład usuwać jego wierzchołki i/lub krawędzie, to dowiązania krzyżowe są niezbędne.
254
7. Algorytmy grafowe
7. 6. Najkrótsze ścieżki i minimalne drzewo rozpinające Niech G - (V, E) będzie spójnym grałem .niezorientowanym, w którym każdej kra wędzi e jest przypisana dodatnia liczba rzeczywista w(e), zwana dalej jej wagą. W takim grafie długość ścieżki mierzy się suma wag krawędzi do niej należących. Podobnie definiuje się koszt drzewa rozpinającego, a mianowicie jako sumę wag jego krawędzi. W tym podrozdziale podamy schemat algorytmu, który zastosujemy .dalej do konstruo wania algorytmów obliczania • najkrótszych ścieżek łączących wierzchołki grafu z jednym, wcześniej ustalonym wierzchołkiem; • drzewa rozpinającego o najmniejszym koszcie. Algorytm ten służy do tworzenia drzewa rozpinającego danego grafu G, zakorzenionego w wybranym wcześniej wierzchołku r (danym także jako parametr wejściowy). Działa w n — I fazach i ma podane tu własności. (1) Przed wykonaniem każdej fazy zbiór wierzchołków grafu jest podzielony na dwa zbiory L i R. Zbiór L zawiera zawsze wierzchołek r. (2) Przed wykonaniem /-tej fazy zbiór L zawiera / wierzchołków. (3) Dla każdego wierzchołka v e L, różnego od /-Jest w konstruowanym drzewie określony poprzednik p(v) wierzchołka v oraz liczba dist(v), zależna od tego poprzednika. Dla korzenia r mamy //(/•) = nil, a dist(r) = 0. Jeśli dla każdego wierzchołka v e R istnieją krawędzie w G łączące v z wierzchołkami z /,, to p(v) jest jednym z tych wierzchołków, a di\t(v) jest liczbą związaną z v, zależną od dist(p(y)) i w(v— p(v)). Jeżeli v nie jest połączony z żadnym z wierzchołków z L, to p(v) -- nil,ar/«7[i>] = W opisie algorytmu symbol « oznacza wartość, co do której zakłada się, że jest większa od każdej liczby rzeczywistej. Jeśli ujest połączony z wyróżnionym wierzchołkiem r, to przed pierwszą fazą dixl(v) = n>(v — r). (4) Każda l aza składa się z usunięcia z R wierzchołka o najmniejszej wartości dist, wstawie nia go do L oraz (właściwej) modyfikacji dist i p dla pewnych wierzchołków z R. Oto formalny zapis opisanego schematu tego algorytmu: beg in L := {rj; R := V \ {r}; p(.r) := nil; ćlist(r) := 0 ; feor v e R do if r — v e E then begin p(v) := r; dist(v) :~w( r — v) end else begin p (v) :=nil; dis t(v) := ~ end; for fasa := 1 to li - 1 do
7.6. N ajkrótsze ścieżki i minimalne drzewo rozpinające
255
begin u := wierzchołek z R z minimalną wartością d i s t ;
R
R \ |u] ; L :=- L U fuj; (*) dla każdego sąsiada u, należącego do R, zmodyfikuj w e dług przyjętej reguły d i s t oraz p
and and
Rodzaj drzewa, jakie otrzymamy, zależy od sprecyzowania reguły (*). Rozważmy dwa różne warianty. W ariant
1:
a.
Przyjmujemy, że dla każdego wierzchołka r e L liczba dist[v\ jest równa długości naj krótszej ścieżki łączącej v z wyróżnionym wierzchołkiem r. Zakładamy także, że jeśli v ^ /•, to dist(v) = dist(p(v)) + n>(v— p(v))- Jeśli v e A’ i dist(v) < ■*>, to disl(y) jest długo ści;! najkrótszej ścieżki łączącej v z r i takiej, że wszystkie wierzchołki na tej ścieżce, z wyjątkiem w, należą do L. Krok (») ma w tym wypadku następującą postać: f o r y g {x : x — u e E o ra z x G R| do i f d i s t ( u) + w(ii— y) < d i s t (y) th e n b e g in p i y ) : = u ; d i s t (y) d i s t ( u) + w(u — y) end; Po wykonaniu całego algorytmu dla każdego wierzchołka v liczba dixt(v) jest długością najkrótszej ścieżki łączącej t z wyróżnionym wierzchołkiem i\ a ścieżka v,p(v),p(jĄv)),.... r jest właśnie taką ścieżką. Algorytm w tej postaci jest znany jako algorytm Dijkstry. Jeśli wszystkie, wagi są równe I, to otrzymane drzewo ma następującą własność: w odległości k w drzewie są wszystkie wierzchołki, których odległość w całym grafie od r wynosi k. Takie drzewo nazywa się drzewem przechodzenia wszerz. Jak mogliśmy się już przekonać w roz dziale I, drzewo przechodzenia wszerz można skonstruować za pomocą znacznie prostszego algorytmu. W
ariant
2:
W tym wypadku przyjmujemy, żc dla każdego wierzchołka v e A’ liczba disl(v) jest równa minimalnej wadze ze wszystkich wag krawędzi łączących r z wierzchołkami z L. Jeśli dixi(v) < co, to p(\>) jest tym wierzchołkiem z A, dla którego disdy) = w(p(v)—-v). Krok (*) ma teraz następującą postać: for y e ( : x- .u £ E oraz :: e /:; do i f w(y u) < d i s t (y) then b e g in p(y) u; d i s t iy) := w iy — u) and;
Drzewo, które znajdujemy w tym wypadku jest minimalnym drzewem rozpinającym, tzn. drzewem, w którym suma wag krawędzi jest najmniejsza. Algorytm w powyższej postaci nosi nazwę algorytmu Prima.
!
;! S , J
■;! .j .:/! :1 ' i1' :.t a
256
7. Algorytmy grafowe
Realizacja lego algorytmu w czasie O(ir) nie powinna Ci przysporzyć klopom. W zada niu 7.15 proponujemy rozwiązanie efektywniejsze.
Z a d a m i a. 7.?, Niccli będą dane liczby naturalne n i /// oraz ciąg krawędzi r e; E numer-wierzchołka u byI mniejszy niż numer wierzchołka r. 7.7. Niech G = (V, E) będzie //-wierzchołkowym grafem dwuspójnym. Funkcję równo wartościową / : V —>i I, .... //) nazywamy s-l num eracją grafu G, jeżeli wierz ch o łek /"'(I) jest połączony w G krawędzią z wierzchołkiem / “'(//) oraz dla każ dego I < i < n wierzchołek /"'(O jest połączony krawędziami z wierzchołkami / ' ( / ) i /"'(/>) dla pewnych j i k, takich że./ < i < k. Zaproponuj liniowy algorytm obliczania s-t numeracji danego grafu dwuspójnego G. 7.15. Miec.li R będzie relacją na krawędziach grafu G, zdefiniowaną następująco: tąAV:, wtedy i tylko wtedy, gdy tą tą, lub istnieje cykl prosty zawierający jednocześnie tą i i1,. Udowodnij, że R jest relacją równoważności. 7.9. Uzasadnij poprawność algorytmu znajdowania dwnspójnych składowych, działają cego na .podstawie melody przechodzenia w głąb. Wskazówka: dowiedź słuszności obserwacji 1-6.
Z adania
257
7.10. Zaproponuj liniowy algorytm obliczania mostów w grafach. 7.11. Udowodnij poprawność algorytmu rozwiązywania problemu 7.3. 7.12. Dany jest spójny graf G i jego drzewo rozpinające 7’ z wyróżnionym korzeniem r. Zaproponuj liniowy algorytm sprawdzania, czy można tak uporządkować listy są siedztwa (7. żeby 7 'było dla grafu G drzewem przechodzenia w głąb. 7.13. Zaprojektuj algorytm liniowy, w którym drzewo przechodzenia wszerz jest wyko rzystywane do znajdowania mostów w grafie. 7.14. Ułóż efektywny algorytm znajdowania spójnych składowych w grafie, ale w któ rym nie wykorzystuje się ani przechodzenia grafit w głąb, ani przechodzenia grafu wszerz. 7.15. Niech d > 2 będzie liczbą całkowitą. Prze/, r/ kopiec zupełny rozumiemy kopiec będący zupełnym drzewem d-arnym, w którym wszystkie, poziomy są wypełnione ca'kowicie. z wy jątkiem co najwyżej ostatniego poziomu, który jest spójnie wypeł niony od strony lewej. W rozdziale 2 rozważaliśmy 2-kopce. Zaprojektuj imple mentację z użyciem d-kopców algorytmu rozwiązywania problemu najkrótszych ścieżek (minimalnego drzewa rozpinającego) w czasie 0(d/tlog(//t + młog^/t). Za nalizuj koszt algorytmu dla d = [2 + w/n |. 7.16. Udowodnij, że krawędzie każdego grafu planarnego można zorientować w ten sposób, żeby stopień wyjściowy każdego wierzchołka był < 5. Wykorzystując ten laki, zaproponuj sposób reprezentacji grafów planarnych w pamięci liniowej, umo żliwiający sprawdzanie w czasie stałym, czy para wierzchołków jest połączona krawędzią. 7.17. Zaproponuj liniowy algorytm sprawdzania, czy dany graf planarny ma cykl prosty długości 3. 7.18. Zaproponuj liniowy algorytm znajdowania minimalnego drzewa rozpinającego w grafach planarnych. 7.19. Grafem zewnętrznie planarnym nazywamy graf planarny, który można przed stawić na płaszczyźnie w taki sposób, że wierzchołki są wierzchołkami pewnego wielokąta wypukłego, a krawędzie jego bokami (niekoniecznie wszystkimi) lub nie przecinającymi się (parami) przekątnymi. Zaproponuj liniowy algorytm sprawdza nia, czy dany graf jest zewnętrznie plamimy. 7.20. Zaproponuj liniowy algorytm kolorowania wierzchołków grafu zewnętrznie pla narnego minimalną liczbą kolorów.
g
Algorytmy geom etryczne
ozdział ten poświęcimy algorytmom geometrycznym. Dział informatyki zajmujący się tą problematyką jest nazywany geometrią obliczeniową. Przedstawimy najprostsze metody używane, w konstruowaniu algorytmów geometrycznych. Elementarnymi ope racjami arytmetycznymi, jakie dopuścimy w naszych algorytmach, będą dodawanie, odejmo wanie, mnożenie i dzielenie. W szczególności nie będziemy używali funkcji trygonometrycz nych. Więcej, założymy, że wszelkie obliczenia są wykonywane w arytmetyce nieskończonej precyzji, a zatem są zawsze dokładne. Nasze rozważania ograniczymy do problemów na płaszczyźnie. Rozpoczniemy od zdefiniowaniu elementarnych operacji, które potem wyko. rzystamy w bardziej skomplikowanych algorytmach dla następujących problemów:
R
® przynależności punktu do wielokąta; ® wypitklej otoczki; ® najmniej odległej pary punktów; ® przecinających się par odcinków. Na przykładzie powyższych zagadnień przedstawimy kilka ważnych metod używanych w projektowaniu efektywnych algorytmów geometrycznych. Będą to metody: „dziel i zwyciężaj” , przyrostowa i zamiatania. Podstawowymi obiektami geometrycznymi, o których będziemy tu mówić, są punkt, odcinek, wektor oraz prosta. Każdy punkt p będziemy reprezentować przez parę jego współrzędnych (x(p), y(p)) w ustalonym wcześniej układzie współrzędnych kartezjańskich. Odcinek o końcach w punktach p i q będziemy oznaczać przez p — q, a wektor o początku w p i końcu w ą przez p —>ą. Każdą prostą będziemy reprezentować przez dowolny zawarty w niej odcinek (wektor) o różnych końcach. Mówiąc prosta p ... q (/.»—><■/), będziemy mieć na myśli prostą zawierającą odcinek (wektor) p — q (p—tq). Zanim przystąpimy do omawiania algorytmów geometrycznych, przypomnijmy kilka prostych faktów z. geometrii analitycznej na płaszczyźnie.
259
8.1. E le m e n ta rn e a l g o r y t m y g e o m e try c z n e
Jeżeli x jest liczbą rzeczywistą, to znak liczby x, oznaczany przez sgn(x), definiuje sic w następujący sposób: -i- 1 dla ,v > 0 0 dla ,v:=0 - 1 dla ,v > 0
•V£;n(.v) =
Niech p, q i r będą różnymi punktami: p = (,v, y), q - (x', y'), r = (,v", y"), a det(p, tp r) ,v y wyznac/.nikicm niacierzy •vt .V/ . Znak dcl(p, q, r) jest równy znakowi sinusa kąta tr fł x y nachylenia wektora /.>—>/• do wektora p —>q. Powiemy, że punkt r leży po lewej (pra wej) stronie wektora p —>q, jeżeli dei(p, 0 (dei(p,
del(p, q, /•)>() a sinę>()
d e l(p , q ,
/■)= () a
s in < p = 0
det(p, q, r)<() a siinp
r ■
Rys. 8.1. Możliwe położenia puiikiu
r
wzglądem wckluin
p — >q
8. 1. E le m e n ta rn e algorytmy geometryczno Przedstawianie algorytmów geometrycznych rozpoczniemy od prezentacji algoryt mów dlii trzech bardzo prostych problemów, które są podstawowe dla naszych dalszych rozważań. Algorytmy te będą dalej wykorzystywane jako podprogramy w algorytmach bardziej skomplikowanych.
260
8. A lgorytm y g eo m etry czn e P k o h l e m
8.1.
Naszym zadaniem jest stwierdzić, czy punkty p, i p> leżą po lej samej stronie prostej p ..- q. PuonuiiYi 8.2.
Mamy zbadać, czy punki r należy do odcinka p ....q. PuoiujcM 8.3.
Mamy zbadać, czy dwa odcinki p — q i r — .v się przecinają. Żeby rozwiązać problem 8.1, wystarczy sprawdzić, czy sgn{ilei{p, q, />,)) = sgn(det(p, q, p)). W celu rozwiązania problemu 8.2 zauważmy, że jeśli punkt r należy do odcinka p — q, to rzuty prostokątne r na osie (OX i 0)0 układu współrzędnych wpadają do rzutów prostokątnych odcinka p — ą (rys. 8.2). Wynika stąd, że /• należy do odcinka p — q wtedy i tylko wtedy, gdy min (x(p), x(q)) < x{r) < max (x(p), ,v(r/», min (y(p), y(q)) < y(ń < < max (>’(/>), y(q)) oraz p, q i r są wspólliniowe (xgn(del(p, q, r) = 0).
Rozwiązanie problemu 8.3 opiera się na spostrzeżeniu, że dwa odcinki przecinają się wtedy i tylko wtedy, gdy punkty p i q leżą po przeciwnych stronach prostej r — ,v (wektora r —>.v), a punkty r i ,y po przeciwnych stronach prostej p — ‘j lub któryś z koń ców jednego z odcinków należy do drugiego odcinka. Jak łatwo zauważyć, każdy z problemów 8.1, 8.2 i 8.3 można rozwiązać w czasie stałym, tylko z użyciem operacji arytmetycznych.
8. 2. P r o b le m p r z y n a le ż n o ś c i Jednym z najczęściej spotykanych problemów w geometrii obliczeniowej jest prob lem przynależności.
8.2. P roblem p rzy n a leżn o ści
261
P roiilicm 8.4.
Dany jest obiekt geometryczny W (zbiór punktów) oraz punkt /;. Mamy zbadać, czy p e IV. Problem len rozwiążemy dla przypadku, gdy W jest wielokątem. Odpowiedzmy sobie najpierw na pytanie, co to znaczy, że dany jest wielokąt: IV. Każdy //-wierzchołkowy wielokąt IV będzie, reprezentowany przez ciąg punktów wu, .... wn _ , będących jego wierzchołkami i laki, że dla każdego i = 0, .... n - 1, w,— w, , jest bokiem w IV (/ + 1 jest wyliczane modulo n). Jest to przykład reprezentacji nieskończonego obiektu geomet rycznego w sposób'skończony (podobnie reprezentujemy odcinek i prostą). Przystąpmy teraz do rozwiązania problemu 8.4. Wiemy, że w czasie liniowym (0(n)) można spraw dzić, czy p należy do któregoś z boków wielokąta. Przyjmijmy zatem, że to nie zachodzi. Idea rozwiązania opiera się na następującym spostrzeżeniu. Niech / będzie pólprostą o początku w p, taką że żaden z wierzchołków wielokąta IV nie leży na tej półprostej. Punkt p leży wewnątrz wielokąta W wtedy i tylko wtedy, gdy / przecina brzeg W niepa rzystą liczbę razy (rys. 8.3).
Rys. 8.3. Punkt liczbę razy
p
leży wewnątrz wielokąta. Pólprostą / o początku w p przecina brzeg wielokąta nieparzystą
Wynika sląd, że w celu rozwiązania problemu przynależności punktu p do wielokąta wystarczy wybrać dowolną pólprostą / o początku w /;/, na przykład równoległą do osi O.V, i stwierdzić, ile razy przecina ona boki wielokąta. Niestety, może się zdarzyć, że / przecina brzeg wielokąta w wierzchołkach. Rozważmy dwa przypadki.1 (1) Prosl.ii / zawiera bok wielokąta /;. Niech c i d będą bokami sąsiadującymi z //, a ,v i y ich końcami nie należącymi do b. Jeśli punkty x i y leżą po przeciwnych stronach półprostej / (dokładniej prostej zawierającej /), lo przyjmujemy, że liczba punktów pt/.odęcia / z. bokami b, c i d wynosi I. W przeciwnym razie przyjmiemy, że liczba la wynosi 0. (2) Prosta / przecina brzeg wielokąta w wierzchołku t> i nie zawiera żadnego z boków z nim sąsiadujących. Niech b i c będą tymi bokami, a.v i y ich końcami różnymi od v.
262
8. A lgorytm y geom etryczn e
Jeśli a- i .V leżu po przeciwnych stronach /, to przyjmujemy, że liczba punktów prze cięcia z bokami b i c wynosi I. W przeciwnym razie przyjmujemy, że liczba ta wynosi 0. Powyższe zasady st| oparte na obserwacji, że każda półprostą o początku w p można tak obrócić dookoła p o niewielki kąt e, żeby otrzymać półprostą nie przecinającą IV w wierz chołkach (rys. 8.4).
ltys. 8.4. Liczba punktów przecięć pólproslcj I z wielokątem, liczona zgodnie z regułami podanymi w przypa dkach (I) i (2), wynosi 3. Punkt p leży wewnątrz wielokąta
Ponieważ koszt obliczeń związanych z każdym bokiem i wierzchołkiem //-kąta jest stały, złożoność sprawdzenia, czy punkt leży w jego wnętrzu, jest (?(/»). Problem ten możemy rozwiązać dużo szybciej, jeżeli wiemy coś więcej o wielokącie IV. Rozważmy przypa dek, gdy IV jest wypukły. Przypomnijmy, że wielokąt jest wypukły wtedy, kiedy każdy odcinek o końcach należących do wielokąta jest całkowicie w nim zawarty. W celu sprawdzenia, czy p <= IV, posłużymy się metodą wyszukiwania binarnego. Bez straty ogólności załóżmy, że wierzchołki IV są dane w kolejności ich występowania na ob wodzie, zgodnej z ruchem wskazówek zegara. Jest oczywiste, że w czasie stałym można stwierdzić, czy p leży wewnątrz trójkąta. Załóżmy, że W ma więcej niż trzy wierzchołki. Poprowadźmy przekątną łączącą it>(l z i)/2 |. Są trzy możliwości. (1) Punkt p leży na prostej u?„— ")■<„~ i,/2 l- W tym wypadku łatwo stwierdzić, czy p na leży do odcinka u>„— vpp(„_. I)/2 |, czy leży poza nim. (2) Punkt p leży po lewej stronie wektora n>()— l)/2-|. Naieży sprawdzić rekureneyjnie, czy p leży wewnątrz wielokąta o wierzchołkach w(„ w....... n>|-((1_ (3) Punkt:/? leży po prawej stronie wektora h'„—>ivp(„ _ t)/31 . Należy sprawdzić rekurencyjnie, czy p leży wewnątrz wielokąta o wierzchołkach u?,„ „ .... ug Ponieważ na każdym poziomie rekursji wykonujemy stalą liczbę operacji o stałym kosz cie, a rozmiar (liczba wierzchołków) badanego wielokąta zmniejsza się blisko o połową (wynosi co najwyżej [ ( ; / - i )/2 I + 1), koszt sprawdzenia, czy dany punkt leży we wnątrz wielokąta wypukłego, jest 0 (lo g n). Podkreślmy na koniec, że chociaż nasz al gorytm został opisany z użyciem rekursji, to łatwo go zaprogramować iteracyjnie. Wielokąty wypukłe odgrywają ważną rolę w geometrii obliczeniowej. Jak mogliśmy zauważyć, obliczenia dla nich są zdecydowanie efektywniejsze. Dlatego kolejnym prób-
8.3. W ypukła otoczk a
263
leniem klórym się zajmiemy, jest problem dotyczący znajdowania wypukłej otoczki skończonego zbioru punktów.
8 .3 . Wypukła otoczka Wypukłą otoczką dowolnego niepustego zbioru punktów S nazywamy najmniejszy zbiór wypukły zawierający S. Można udowodnić, że jeśli Sjest zbiorem skończonym, to jego wypukła otoczka jest wielokątem wypukłym o wierzchołkach ze zbioru S (czasami zredukowanym do odcinka lub punktu). Pitoiu.iiM 8.5. Dany jest. zbiór skończony n punktów S = Mamy znaleźć najmniejszy wie lokąt wypukły ztiwierajtiey S, a dokładniej - wyznaczyć wierzchołki lego wieloktiia w ko lejności ich występowania na obwodzie. Dla każdego wierzchołka p wypukłej otoczki ko lejność ta będzie zadami przez dwa wskaźniki next(p) i pred(p), wyznaczające - odpowied nio - wierzchołki następny i poprzedni względem p przy poruszaniu się po obwodzie otocz ki w kierunku przeciwnym do ruchu wskazówek zegara. Zanim przystąpimy do układania efektywnych algorytmów obliczania wypukłej otoczki, zastanówmy się, jak szybkich algorytmów możemy się spodziewać. Okazuje się, że problemu wypukłej otoczki nie można rozwiązać szybciej niż problemu sortowania. Aby to uzasadnić, pokażemy, że problem sortowania liczb rzeczywistych jest liniowo sprowa dzał ny do problemu wypukłej otoczki. Niech a ,, ..., ,v„ będą liczbami rzeczywistymi, które chcemy uporządkować. Bez straty ogólności możemy założyć, że liczby te są parami różne i większe od zera. Rozważmy n punktów na płaszczyźnie (.v|t a,)...... („v , Punkty te leżą na prawym ramieniu paraboli y - x2 w kolejności wzrastania pierwszej współrzędnej. Kolejność ta wyznacza porządek między elementami ciągu ,vp ..., ,v„. Wierzchołkami wypukłej otoczki utworzonego zbioru punktów są właśnie te punkty. Ich kolejność (cyklieznie) na obwodzie wielokąta jest taka sama jak na ramieniu paraboli. Znając kolejność występowania punktów .x-,')....... ( a „ , v„j na obwodzie wypukU i '.loczki. można już w czasie liniowym odtworzyć porządek między liczbami a , , ..., ,v„. Wystarczy przejrzeć te punkty .kolejno w kierunku przeciwnym do ruchu wska zówek zegara, poczynając od punktu z najmniejszą współrzędną .v. Wynika stąd, że obliczenie wypukłej otoczki dla n punktów nie może kosztować mniej niż sortowanie ii liczb, czyli .Q(//log//), Najprościej wypukłą otoczkę można obliczyć w podany tu sposób. K rok
I:
Znaleźć wszystkie wierzchołki wypukłej otoczki zbioru punktów ó.
264 K rok
8. Algorytmy geometryczne 2:
Uporządkować znalezione punkty w kolejności ich występowania na obwodzie wypukłej otoczki. W celu znalezienia wierzchołków wypukłej otoczki możemy oprzeć się na takim oto spostrzeżeniu: punkt p nie jest wierzchołkiem wypukłej otoczki wtedy i tylko wtedy, gdy leży wewnątrz pewnego trójkąta o wierzchołkach z .V, różnych od p, lub należy do odcinka łączącego dwa punkty z ,V, różne od p. Trójkąt o wierzchołkach qv q2 i ,, ..., pn pewnego wielokąta wypukłego P\ trzeba wyznaczyć kolejność ich występowania na obwodzie lego wielokąta. W celu rozwiązania tego problemu wybierzmy dowolny punkt O wewnątrz wielokąta P. Tym punktem może być na przykład centroid, tzn. punkt ((.*(/>,) -I- ... + .v(/>,,))/»,
(y(pt) -i- ... + y(p„))/n).
5iys. K.5. Wierzchołki wielokąta wypukłego uporządkowane rosnąco względem kątów nachyleniu ich wek torów wodzących do osi O .V
;
Bez strntyj ogólności możemy przyjąć, żc środek układu współrzędnych jest w O. Upo rządkujmy wierzchołki rosnąco względem kątów nachylenia ich wektorów wodzących do osi X (rys. 8.5). Sortowanie, punktów możemy wykonać w czasie 0(//logn), ale tylko pod warunkiem, że umiemy porównywać kąty nachylenia wektorów wodzących. Pokażemy, w jaki sposób
iI
265
Wypukła otoczka
lego dokonać buz obliczania łych katów. Niech alfa będzie funkcjn określona dla punk tów płaszczyzny różnych od O, zdefiniowaną następująco: r y(p)/t/(p), ■ utfuU>) ----- J 2 “ y(p)!d(p), 1 2 -l- 1v(/->) | /r/(p), v 4 - |y(p) |/r/(p),
gdy .v(p) > 0 a gdy ..v(p) < 0 a gdy x(p) < 0 a gdy x(p) > 0 a
y(p) > 0 y{p) > 0 y(p) < 0 y(p) < 0
gdzie i l ( p ) = ],v (p )j + ]y(p)|. Jako zadanie, pozostawiamy Ci udowodnienie, że kąt na chylenia wektora wodzącego punktu p t jest mniejszy (równy) niż kąt nachylenia wektora wodzącego punktu p2 wtedy i tylko wtedy, gdy alfa(pt) < alfa(p2). Funkcja alfa umoż liwia wyznaczenie kolejności wierzchołków na obwodzie wielokąta wypukłego w czasie O(uiogn).
Powstaje pytanie, czy żeby wyznaczyć wierzchołki wypukłej otoczki, musimy sprawdzać wszystkie
trójkąty. Bez straty ogólności możemy przyjąć, że znamy pewien punkt: O
leżący wewnątrz wypukłej otoczki, na przykład centroid. Łatwo zauważyć, że każdy punkt: nie. będący wierzchołkiem wypukłej otoczki musi wpadać do wnętrza trójkąta wyznaczonego przez punkt O i pewne dwa kolejne wierzchołki otoczki (lub musi leżeć na jednym z boków takiego trójkąta). Spostrzeżenie to jest kluczowe w algorytmie Gra hama służącym do obliczania wypukłej otoczki n punktów w czasie O(nlogn). {Algorytm Grahama obliczania wypukłej otoczki}
K rok
1:
Wybierz dowolny punkt O wewnątrz wypukłej otoczki, na przykład centroid. Umieść w nim środek układu współrzędnych i oblicz współrzędne punktów wej ściowych w nowym układzie współrzędnych. K rok
2:
Uporządkuj punkty p,, .... pn leksykograficznie względem współrzędnych bieguno wych (a f, /•,), gdzie a f jest kątem nachylenia wektora wodzącego O —>pf do osi OX, a r, jego długości;!. (Uwaga: Żeby.nie liczyć pierwiastków, w sortowaniu porów nujemy alfa(p,) zamiast a, oraz rj zamiast /•,). Z uporządkowanych punktów utwórz dwukierunkową listę cykliczną, w której dla każdego punktu p, nexl(p) jest następnym (cyklicznie) punktem w wyżej zdefiniowa nym porządku, a prcd(p) poprzednim. Spośród punktów o najmniejszej współrzędnej y znajdź punkt ,v z najmniejszą współrzędną x.
266
8. A lgorytm y geom etry czn e K rok
3:
W kroku 3 przejrzyj punkty na liście, usuwając te, które nie są wierzchołkami wypukłej otoczki. Po zakończeniu działania algorytmu lista będzie zawierała tylko wierzchołki wypukłej otoczki w kolejności ich występowania na obwodzie. Listę przeglądaj, zaczynając od punktu s i kierując się w stronę przeciwną do ruchu wskazówek zegara (zgodnie ze wskaźnikami next). W celu wyeliminowania zbęd nych punktów zawsze sprawdzaj trzy kolejne punkty q x, q2 i q s z bieżącej listy. Jeśli
s;
while n e x t ( c j ) -A s do
{*}
if n e x t ( q ) leży wewnątrz A (O, g, n e x t (nex t (ty) ) ) t h e n begin
{usunięcie punktu next (g) z listy} n e x t (g ) := n e x t ( n e x t ( g ) ) ; p r e d (n e x t (q ) ) ; = g;
i f g A- s then q := pred (g)
[•*} end
{.*•} else g := n e x t ( g ) ;
Na złożoność powyższego algorytmu decydujący wpływ ma krok 2, który można wykonać w czasie 0(nlogn). Kroki I i 3 są wykonywane w czasie liniowym. Uzasadnienia wymaga tylko złożoność kroku 3. W analizie złożoności lego kroku stosujemy zasadę magazynu. W magazynie znajdują się wszystkie punkty na liście między punktem startowym .s u testowanym właśnie punktem next(q), włącznie z tym punktem. W każdym wykonaniu instrukcji while albo posuwamy się po liście do przodu zgodnie z next (krok oznaczony {***}), albo do tyłu zgodnie z pred (krok oznaczony {**}). Przejście zgodne z next odpowiada włożeniu do magazynu nowego punktu nex/(next(q)), a przejście zgodne z pred oznacza usunięcie z magazynu punktu ne.xt(q). Zwróćmy także uwagę, że jeśli wracamy do punktu startowego .v, to do magazynu jest wkładany bieżący punkt next(s). Każdy wierz chołek jest wkładany do magazynu raz i co najwyżej raz może być z niego usunięty. Stąd wynika liniowość kroku 3. W ten sposób udowodniliśmy, że problem wypukłej otoczki można rozwiązać w czasie O(tńogn). Na koniec zauważmy jeszcze, że zamiast sprawdzać, czy punkt next(q) leży wewnątrz A(O, q, next (next (q))) (warunek oznaczony {*}), prościej jest testować, czy next(q) leży po lewej stronie (lub należy do) wektora q —>next(next(q)). Złożoność algorytmu Grahama nie zależy od liczby wierzchołków obliczanej wypukłej otoczki. Przedstawimy teraz algorytm, który umożliwia znalezienie wypukłej otoczki w czasie 0(kn), gdzie k jest liczbą jej wierzchołków. Algorytm len jest szczególnie, przydatny wtedy, kiedy wiemy z góry, że liczba wierzchołków wypukłej otoczki jest niewielka, na przykład ograniczona przez stalą niezależną od n. Pomysł takiego roz wiązania pochodzi od R. Jarvisa i jest oparty na podanych tu dwóch spostrzeżeniach.
8.3. Wypukła otoczka
267
(1) Odcinek p — ). j AIg o ry tm J a r v is a o b l i c za n ia wypu k łe j o to c z k i } K rok
1:
Spośród punktów z najmniejszą współrzędną y znajdź punki: cl położony najbardziej na lewo (czyli z najmniejszą współrzędną .r), a spośród punktów z największą współrzęd ną y punkt g położony najbardziej na prawo (z największą współrzędną ,\j. Łatwo za uważyć, że oba punkty d i g są wierzchołkami wypukłej otoczki. K rok P
2:
d;
while p ■/-g do begin {p jest kolejnym wierzchołkiem wypukłej otoczki} umieść środek układu współrzędnych w punkcie p, a następnie spośród punktów o na jurniejszym kącie nachylenia wektora wodzącego do osi pX wybierz punkt r o największej odległości od p; {wszystkie punkty z b leżą w jednej półpłaszczyźnie wyznaczonej przez odcinek p-.r; odcinek ten jest kolejnym bokiem wypukłej otoczki} n e x t ( p ) :=r; pred(r) ':=p; p :=r
onct ; Krok 3: Powtórz krok 2, przyjmując za punkt startowy g, a za punkt końcowy d. Rozważaj tylko punkty o kątach nachyleni;! promieni wodzących > ISO”. Działanie algorytmu Jarvisa przedstawiamy na rysunku 8.6. Każda iteracja w krokach 2 i 3 jest wykonywana w czasie, O ( n ) . Ponieważ liczba iteracji jest równa liczbie wierzchołków wypukłej oloczki, złożoność algorytmu Jarvisa \ wynosi O(kn). Przedstawiliśmy dwa najbardziej znane algorytmy obliczania wypukłej otoczki;. Problem wypukłej otoczki jest tak podstawowym problemem w geometrii obliczeniowej jak prob lem sortowania w całej informatyce. Na jego przykładzie można zademonstrować wiele różnych metod stosowanych w projektowaniu efektywnych algorytmów geometrycznych.
2(>8
8. A lgorytm y geo m etry czn o
/ /
..Z d Rys. 8 .6 . Zasmln cl/.iahmia algorytmu Jarvisa
Przedstawimy torn/, jeszcze dwa inne, algorytmy obliczania wypukłej otoczki. Jeden z nich został zaprojektowany na podstawie metody przyrostowej, a drugi na podstawie metody „dziel i zwyciężaj” (o której była mowa już wcześniej). Dla prostoty opisu algorytmów zakładamy, że żadne trzy różne punkty zbioru wejściowego S nie sa. wspólliniuwe, a żadne dwa nie maj i} tej samej współrzędnej ,v. Jako zadanie pozostawimy Ci uwzględnienie braku powyższego założenia w algorytmach opisanych poniżej. Przyjmij my też, że liczba punktów w zbiorze $ jest większa niż 3. W obu algorytmach punkty zbioru wejściowego S są najpierw porządkowane rosnąco względem pierwszych współrzędnych. Ponieważ sortowanie można wykonać w czasie D(/;log//), załóżmy, że punkty />,, />,, .... pn są dane już w takim porządku. W algorytmie przyrostowym budujemy ciąg wypukłych otoczek WPj%tiki i = 3, 4, .... n. w którym IV /je st wypukłą otoczką punktów .... /z. Otoczka WP„ jest żądanym wynikiem a Ig o ry tni u. i a i ‘t- itytm przyrostowy obl i czania w ypukłej otoczki] K k o k
1:
Zbuduj wypukłą otoczkę IV/’,, Izn. dla każdego punktu gdzie / = 1,2, 3, wyznacz nc.xtip,) oraz pred(ph), czyli odpowiednio - następny i poprzedni punkt dla p, w wypukłej otoczce IVP., w kierunku przeciwnym do ruchu wskazówek zegara. K u o k
2:
for i := 4 to n do dodaj punkt p, do bieżącej wypuk łej otoczki W i \ dwukierunkową I i:U ę cykliczną no;; b - p r e d tak, 'wała otoczkę NPi ;
, i zaktiukl izu j by reprezento
Pozostiijc jeszcze tylko pokazać, jak należy aktualizować listę next-pred wierzchołków otoczki WPj , po dodaniu punktu />,. Niech /•*. będzie zbiorem wierzchołków WP- cl la
8..'!. Wypukła otoczka
269
/ = .3, 4, .... i i . W celu aktualizacji listy dla WP, _ , znajdujemy wśród wierzchołków z P, ..., takie punkty d i g, że wszystkie punkty z _, \{z/} leżą na prawo od wektora Pj—>d, a wszystkie punkty z Pt _,\{g} - na lewo od wektora p,—>g. Następnie w miejs ce punktów next(d), nexi(next(d)), ..., pred(g) wstawiamy punkt />,. Na rysunku 8.7 jest przedstawiony przebieg tej operacji.
Rys. K.7. Akluiilizaojn otoczki WP, .. , po dodaniu punktu />,■
Oto formalny opis aktualizacji otoczki WPj_t {znajdź punkt: cJ] d ■■■= P : _ while punkt pred(d) leży na lewo od wektora p, —> d d o d := p r e d (d ) ; {znajdź punkt r/} •l ■ Pi
while punkt n e x t ( g ) leży na prawo od wektora p, —;> g do g := next: (g) ;
{usuń punkty między d i g ; wstaw w to miejsce pj] n e x t : (d) := p , ; p r e d { ) :=:d; {;; ) :=g; p r e d ( g ) :=p,;
Slosujiic zasadę magazynu, łatwo jest pokazać, że jeśli ciąg wejściowy punktów jest uporządkowany rosnąco względem pierwszych współrzędnych, lo algorytm przyrostowy działa w czasie liniowym. Ostatni z prezentowanych algorytmów obliczania wypukłej otoczki działa na podstawie zasady „dziel i zwyciężaj” . {Algorytm typu „dziel i zwyciężaj" obliczania wypukłej otoczki] begin if n < 3 then zbuduj bezpośrednio dwukierunkową listę cykliczną next-pred dla punktów ze zbioru S e 1 se
270
8. A lgorytm y geom etry czn e
b e g in {d ziel} k ■.= I n /2 J ; Sj := {p,, . . ., p j ; S2 := {p* ,. ...... P„! ; { re k u rs ja ] o b lic z re k u re n c y jn ie wypukłe o to c z k i ( l i s t y jioxt ••/■>.>v:«J) d la zbiorów S, i S-,; . . {łącz} p o łą c z wypukłe o to c z k i d la zbiorów Sj i S2 w jed n ą wypukłą o to c z k ę d la zb io ru S end end ; W większości algorytmów typu „dziel i zwyciężaj” najtrudniejsze jest obliczenie rezul tatu końcowego z rezultatów otrzymanych w wynikli wywołań rekurencyjnych. Podobnie jest w tym wypadku. Pokażemy, jak obliczyć wypukłą otoczkę dwóch wielokątów wypu kłych W, i W2 (odpowiednio, wypukłych otoczek dla zbiorów S, i S2), takich że wielokąt W, leży na lewo od IV2, tzn. pierwsza współrzędna każdego wierzchołka w IV, jest mniejsza niż pierwsza współrzędna każdego wierzchołka z W2. Zauważmy, że najbar dziej wysuniętym na prawo wierzchołkiem wielokąta W, jest punkt pk, a najbardziej wysuniętym na lewo wierzchołkiem w 1V2 jest punkt pk + W celu policzenia wypukłej otoczki obu wielokątów wystarczy znaleźć dwie pary wierzchołków g,, g2 oraz d,, takich że g, i cl, są wierzchołkami w W,, a g, i cl2 wierzchołkami w W, oraz wszystkie wierzchołki z obu wielokątów, poza g, i g2, leżą na prawo od wektora g, —>g2, a wszyst kie wierzchołki, poza cl, i r/2, leżą na lewo od wektora cl, —>d2. Jeśli z listy next-pred dla wielokąta W, wytniemy wszystkie punkty leżące między cl, i g, w kierunku przeciwnym do ruchu wskazówek zegara, a z listy next-pred dla W2 wytniemy wszystkie punkty między d2 i g2 w kierunku zgodnym z ruchem wskazówek zegara, a następnie połączymy obie listy, to otrzymamy żądaną reprezentację wypukłej otoczki dla zbioru wejściowego S. Proces len przedstawiamy na rysunku 8.8. Oto algorytm znajdowania punktów g, i g2. Podobnie można znaleźć punkty d, i d2. (A lgorytm zn ajd o w ania ąąunktów g , i g,} b e g in 1 := p k; r : = p k + re p e a t w h ile p u n k t p .reu (r) le ż y na lewo od w ektora 1 —>r do r : = p r e d (r
)
;
w h ile p u n k t next: (.1) le ż y na lewo od w ektora 1 —■>r do 1 : = n ex t (1) u n t i l (p r e d (r) le ż y na prawo od l —>r) and ( n e x t( l) le ż y na prawo od I ~ > r ) ; g\ ■■= l ; g2 ■= r end;
271
8.4. M etoda za m ia ta n ia
Rys. 8.8. Obliczanie wypukłej oloc/.ki dwóch wieloki|lów wypukłych
Jeśli punkty d t, g,, d, i g2 s;ł już obliczone, to wycinanie, wierzchołków nie należących do wypukłej otoczki i obliczanie jej reprezentacji wykonuje'się następująco: pred(d,) := ci,; nexŁ(d,) :=d,; p r e d (cp) g :, ; n e x t : ( g 2) := g l ;
Jaka jest złożoność przedstawionego algorytmu? Zauważmy, że jeśli wierzchołek jest usuwany z listy nexl-prcd, to więcej na takiej liście już się nie znajdzie. Czas obliczania wierzchołków g,, r/:, g., i <1, (instrukcja repeat) liniowo zależy od liczby usuwanych wierzchołków. Wynika stąd, że jeśli ciąg wejściowy punktów jest uporządkowany rosnąco względem pierwszych współrzędnych, to powyższy algorytm działa w czasie liliowym.
8.4. Metoda zamiatania Na zakończenie tego rozdziału przedstawimy pewną metodę systematycznego prze szukiwania płaszczyzny, a następnie zastosujemy ją do rozwiązania dwóch podanych tu problemów. Pkohlkm 8.6.
Dany jest zbiór S zawierający n punktów płaszczyzny. Mamy znaleźć najmniej odległą parę punktów w ,V, t/.n. dwa różne punkty p i q w S, takie że ii(p, q) = rnin({<7(/\ ,v): r, ,v e S, r
,v})
gdzie d(i\ ,v) jest odległością między punktami r i ,v.
272
8. Algorytmy geometryczne
’ F ito m ,km 8.7.
Danych jest n odcinków /,, się odcinków.
ln. Mamy wyznaczyć wszystkie paty przecinających
Zauważmy, że każdy z, powyższych problemów łatwo rozwiązać w czasie OOr). W tym celu wystarczy obliczyć odległości między każdą parą punktów z problemu 8 . 6 i zbadać każdą parę odcinków z problemu 8.7. Za chwilę, pokażemy, jak rozwiązać oba problemy stosując metodę zamiatania. .Metoda la polega na przesuwaniu po płaszczyźnie („zamiataniu” ) prostej pionowej („miotły” ) od strony lewej do prawej i wykonywaniu obliczeń dla napotykanych obiektów (figur) geometrycznych. W każdym kroku obliczeń każdy obiekt jest albo przetworzony, albo aktywny, albo oczekujący. Obiekty przetworzone znajdują się zawsze z lewej strony miotły i wszystkie potrzebne obliczenia z nimi związane są już zakończone. Obiekty aktywne są aktualnie przetwarzane, natomiast obiekty oczekujące znajdują się z prawej silony miotły i obliczenia z nimi związane będą dopiero wykonywane. Miotła jest przesu wana po płaszczyźnie w sposób dyskretny. Punkty, między którymi się porusza, są ele mentami x-slruktury. Struktura ta jest zazwyczaj kolejką priorytetową, w której punkty są uporządkowane niemalejąco względem ich pierwszych współrzędnych. Miotła zawsze wędruje do punktu z x-struktury z najmniejszą współrzędną ,v, który jest: później z tej struktury usuwany. Obiekty aktywne natomiast są pamiętane w y-,strukturze. W każdym położeniu miotły obiekty aktywne są uporządkowane niemalejąco względem drugich współrzędnych pewnych wyróżnionych punktów, należących do tych obiektów. Punkty te, reprezentanci obiektów, mogą być różne w różnych położeniach miotły. Do reprezenta cji y-struktury używa się zazwyczaj zrównoważonych drzew poszukiwań binarnych, z pewnymi modyfikacjami zależnymi od rozwiązywanego problemu. Zamiatanie staramy ■i'- /.iws/.e lak zorganizować, żeby miotła wędrowała przez możliwie mało punktów - - oy w każdym punkcie była wykonywana tylko siała liczba operacji na y-siruklurze. Pr/./siąpimy teraz do opisu algorytmów dla problemów 8 . 6 i 8.7. Podobnie jak w wypad ku wypukłej otoczki, założymy, że punkty zbioru S w problemie 8 . 6 i końce odcinków wejściowych w problemie 8.7 spełniają następujące warunki: żadne trzy różne punkty zbioru wejściowego S nie są wspólliniowe i żadne dwa punkty nie mają lej samej współ rzędnej x. Więcej, założymy, że w jednym punkcie przecinają się co najwyżej dwa odcinki. Robimy to założenie tylko dla przejrzystości opisu prezentowanych algoryt mów. Nie powinieneś mieć żadnych trudności z taką zmianą przedstawionych Iu algoryt mów, żeby działały one poprawnie dla każdych (lanych wejściowych.
8.4.1.
N ajm niej o d leg ła para pu n k tów Poprawność algorytmu, który za chwilę opiszemy, wynika z następującej obserwa cji: jeśli .V jesi zbiorem n punktów, a 5 odległością między najmniej odległą parą punk tów z .V, lo każdy kwadrat o bokach o długości 8 zawiera co najwyżej 4 punkty z S.
8.4. M etoda zam ia ta n ia
273
Załóżmy przeciwnie. Niech K będzie kwadratem o bokach o długości 5, zawierającym więcej niż 4 punkty z S. Jeśli podzielimy K na cztery mniejsze kwadraty o długości boków 5/2 każdy, to jeden •/. nich musi zawierać co najmniej 2 punkty. Największa odległość międ/y punktami takiego kwadratu wynosi ( V2 5)/2. Jest to sprzeczne z zało żeniem. żc odległość 5 jest najmniejsza. Algorytm obliczania najmniej odległej pary punktów metodo zamiatania jest nastę pujący. W x-strukturze są. przechowywane punkty zbioru wejściowego .V, uporządko wane niemalejąco względem pierwszych współrzędnych. W tym algorytmie x-struktura jest listą. Wskaźnik do punktu z listy, do którego ma być przesunięta miotła, znajduje się na zmiennej current. Do reprezentacji y-slruktury używamy zrównoważonego drze wa poszukiwań (na przykład drzewa AVL), w liściach którego od strony lewej do prawej znajdują się punkty aktywne, uporządkowane niemalejąco względem współ rzędnej y. Liście drzewa AVL są połączone w listę dwukierunkową. Jeśli <7 jest pun kiem w y-strukturze, l:o pred{q) jest w tej strukturze punktem bezpośrednio poprze dzającym 3 , -<») jest pierwszym punktem w y-strukturze, a punkt ( - 0 0 , +<*>) ostatnim. Dodatkowo zakładamy, że każdy węzeł v w flpewie jest wyposażony w atrybut tnax(\'), którego wartością jest największa współrzędna y spośród drugich współrzędnych punktów znajdujących się w liściach poddrzewa 0 korzeniu w v. Umożliwi to szybkie wyszukiwanie punktów w liściach drzewa. Na y-strukturze definiujemy operacje jind(p), insert(p) i delete(p). W wyniku operacji find(p) zostaje zwrócony taki punkt q z y-slruktury, że y(p) < y{q) i druga współrzędna każdego punktu w y-strukturzc poprzedzającego <7 nie przekracza y(p). Operacja inse.rt(p) służy do wstawienia punktu p do y-stmkl.ury.YV wyniku operacji delete(p) zo staje usunięty punkt /; z y-sfruktury. Jako zadanie, pozostawiamy Ci opracowanie al gorytmu tych operacji w taki sposób, żeby przebiegały w czasie <9(log/ć), gdzie k jest liczbą punktów w y-strukturze. W każdym położeniu miotły jest pamiętana najmniejsza odległość 5 między punktami na lewo od miotły. Punktami aktywnymi są wszystkie punkt)' na lewo od miotły, których odległość od niej wynosi co najwyżej 5. Punkty aktywne znajdują się w x-slrukiurze (która jest; listą uporządkowaną) między punktami wskazywanymi przez first-active i current, bez tego ostatniego. Zakładamy też, że. każdy punkt aktywny w x-strukturze ma wskaźnik do swojego wystąpienia w y-struklurze. Rozważmy sytuację, kiedy miotła przesuwa się do punktu p ~ (x(p), y(p)) (punkt wskazywany przez current). Wśród punktów aktywnych znajdujemy takie dwa kolejne punkty / i r, że y (/) < y(p) < y(r), W tym celu wykorzystujemy operację find 1 lisię, na której znajdują się liście. W wyniku dołożenia nowego punktu p dotych czasowa minimalna odległość 5 między punktami może zmaleć. Które punkty mogą leżeć w odległości od p mniejszej niż 5? Łatwo zauważyć, że tymi punktami mogą być tylko punkty aktywne o drugiej współrzędnej z przedziału \y(p) - 8 , y(p) + 8 |. Z obserwacji wynika, żc takich punktów jest co najwyżej 8 - cztery poprzedzające
274
8. Algorytmy geom etry czn e
na liście / (włącznie z tym punktem) i cztery występujące po r (także włącznie z tym punkiem). Oto formalny opis algorytmu obliczania najmniej odległej pary punktów. begin {inicjowanie y-struktury i x-struktury} y-struktura --°°) ; (-“>, -H*>)}; x-struktura : - lista uporządkowana n punktów ze zbioru S; {pierwszym punktem ,,postoju" miotły jest pierwszy punkt wx-strukturze} c u r r e n t := wskaźnik do pierwszego punktu w x~strukturze; f i r s t ~ a c t i v e := c u r r e n t ;
8 ;= -H» ; while c u r r e n t begin
nil (nie wyczerpałiśray punktów z x-struktury} do
p := punkt na liście wskazywany przez c u r r e n t ;; { „usuń" p z x-struktury] c u r r e n t : - wskaźnik do następnego elementu w x-struktur z e ; r := f i n d ( p ) ; i := p r e c i ( r ) ; i := 0 ; {obliczanie odległości między p i co najwyżej 4 punktami występującymi po r(włącznie z tym punktem) w y-strukturze; aktualizacja 8 } while ( r - A (-<», i<*>) ) and (i < 4) do begin if d(p, r ) < 8 then begin (aktualizacja 5}
5 := c2 (p, r ) ; {P, (? jest parą punktów w odległości o] P :=p; Q := r end ; :r := succ(r) ; i := i + 1 end ; {obliczanie odległości między p i co najwyżej 4 punktami występującymi, przed I (włącznie z tym punktem) w y-strukturze; aktualizacja o) i := 0 ; while (1 k-°°) ) and (i < 4) do begin if d(p, 1 ) < 8 then begin {aktualizacja 5)
8 ; - d (p, 1 ); {P, O jest parą punktów o odległości 8 } P p ; Q := 1 end; 1 : = p r e d (1 ) ; i := i + 1 end;
, 8.4. Metoda zamiatania
275
[punkty aktywne, które znalazły się w odległości większo-! niż 5, są usuwane z y-struktury} g : = punkt w x~strukturze wskazywany przez f i r s t - a c t i v e ; while (x(p) - x(g) ) > 8 do begin f i r s t - a c t i v e : = wskaźnik do następnego punktu w x-strukturze po g; delete(g); [usunięcie q z y-struktury} g := punkt: wskazywany przez f i r s t - a c t i v e end; [wstawienie punktu p do y-struktury} i n s e r t (p) end [while c u r r e n t ] end; { P , Q jest najmniej odległą parą punktów}
Jaka jest złożoność przedstawionego algorytmu? Zainicjowanie x-slruklury może być wykonane w czasie 0(»log//), Każdy punkt trafia do y-struktury tylko raz (operacja insert) i co najwyżej raz jest z niej usuwany (operacja delete). Zarówno wsławienie punktu, jak i usunięcie zabierają czas 0(log//). W każdym wykonaniu instrukcji while current ■/- ... jest sprawdzanych tylko co najwyżej 8 punktów z y-struktury. Wynika stąd. że znalezienie za pomocą tego algorytmu najmniej odległej pary punktów zajmuje 0(«logn) czasu. Można udowodnić, że rozwiązanie to jest optymalne. Dalej na przy kładzie przedstawiamy zasady działania omówionego właśnie algorytmu (rys. 8.9). Roz wiązanie zostało po raz pierwszy podane w pracy 11INS (.
8 .4 .2 . P a r y p r z e c i n a j ą c y c h s ię o d c in k ó w .lak już wspominaliśmy, problem przecinających się odcinków można łatwo roz wiązać w czasie. 0{tr). W tym celu wystarczy sprawdzić oddzielnie każdą parę odcin ków. Rozwiązanie to jest optymalne, gdy liczba par przecinających się odcinków jest rzędu n1. W algorytmie, który za chwilę przedstawimy, jest użyta metoda zamiatania. Jego złożoność zależy od liczby przecinających się, par odcinków. Jeśli ,v jest liczbą takich par, to za pomocą naszego algorytmu można je znaleźć w czasie 0((n -!- ,v)log/i). W każdym położeniu miotły odcinkami przetworzonymi są wszystkie odcinki, których oba końce znajdują się na lewo od miotły. Odcinkami aktywnymi są te, które przecinają miotłę. Do odcinków oczekujących zaliczają się odcinki o obu końcach położonych na prawo od miotły. Będziemy żądać, żeby był spełniony taki oto niezmiennik. N iiozmiknnik i. Fary odcinków przecinających się na lewo od miotły są już wyznaczone. W y-struklurze są przechowywane odcinki aktywne, uporządkowane rosnąco .względem współrzędnych y ich punktów przecięć z miotłą. Punkty te dzielą miotłę na przedziały.
276
8. Algorytmy geom etryczn e
a
h ■
x-strukluru first-active
e
a, h, <:, d, e, /' g
T
cl
current y-sln.ik.lura -<*>), a, +<*>)
8
a
i)
x-siruktura first-active
c
a, b, c, d, v, f g current
cl /
-g
8
y-slruklura --<»), a, b, (—°°, -H») P =b, Q —ci
x-slmktura first-active i a, b, c. cl. e, f, g
T
. current /.
y-slniklora C, I), i . ' -X +<-<•)
P =h. <2=a
s\
a
b
x-slruktura first-active
e
a, b. c, cl. e, f, <; t' current
cl f ■ c 8
y-slniklura ~°»), (/, +°°) /' = />, Q =a
277
B.'l. M etoda zam iatan ia
a
b
x-sirukiura first-active
i
a, b. c, d, e, f, g f cu rren t
y-struktiira (—o°, - - ( / , (\ ( P =b, Q =a
-l-f-o)
x-slmkiura first-active
i
a, b, c, d, e, /, t; T
current y-struklura (—ooi —oo), (—«>, +«>) P =h\ Q=a
a
b
x-struktura first-active i a, l>, c, d, e.f, s> T a m ent
y-struklura (—°°> “ °°), /, t*’ (~°°i 'i'°° )
r=R.'Q=f
11
Hys. 8.9. Znajdowanie najmniej odległej pary punktów metodą zamiatania
Do reprezentacji y-struklury używamy zrównoważonego drzewa poszukiwań (na przy kład drzewa AVL) w sposób umożliwiający wykonywanie następujących operacji w cza sie 0 ( log/c), gdzie &jest liczbą odcinków w y-strukturze: szukanie odcinków aktywnych, których punkty przecięcia z mio (a) find(p):: tłą wyznaczają przedział zawierający punkt: p\ dodanie odcinka aktywnego / do y-struktury; (b) inserl(l):: usunięcie odcinka / z y-slruktury; (c) deleteU)" (d) predU), s n a i l V. szukanie poprzednika (następnika) odcinka / w y-strukturze;
278
8. A lgorytm y geom etry czn e
(e) interchange^ , /'):: zmiana kolejności występowania w y-,strukturze sąsiednich od cinków I i tzn. jeśli przed wykonaniem interchange I poprze dza /', to po jej wykonaniu /' poprzedza /. W każdym położeniu miotły x-struktura zawiera tylko punkty leżące z jej prawej strony, a w szczególności punkty końcowe, odcinków aktywnych oraz oczekujących. Dodatkowo żądamy, żeby był spełniony tak oto niezmiennik. N iezmiennik
2.
W x-strukturze znajdują się punkty przecięć, leżące na prawo od miotły, wszystkich par odcinków aktywnych, które kiedykolwiek były sąsiednimi w y-strukturze. Niezmiennik 2 gwarantuje, że jeśli punkt przecięcia p pary odcinków leży najbliżej miotły ze wszystkich punktów (punktów przecięć bądź końców) położonych na prawo od niej odcinków, to p występuje w x-struklurzc. Niezmiennik ten jest kluczowy dla po prawności przedstawianego algorytmu. Punkty w x-strukturze są uporządkowane rosnąco względem współrzędnej x. Do reprezentacji x-struktury używamy zrównoważonego drzewa poszukiwań, zaimplementowanego w taki sposób, żeby można było w czasie logarytmicznym wykonywać następujące operacje: (a) findtiiin:: wyznaczenie punktu z minimalną współrzędną a:; (b) cieletemin:: usunięcie punktu z minimalną współrzędną; (c) acld(p) :: sprawdzenie, czy punkt p jest w x-strukturze i dodanie, p do x-struk tury, gdy go tam nie ma. Oto bardziej formalny opis algorytmu obliczani.'! par przecinających się odcinków: begin y--struktura 0 ; {początkowo y-struktura nie zawiera żadnych odcinków} x-struktura := 2 n punktów końcowych odcinków I,, .... i„, uporządkowanych rosnąco względem współrzędnej x ; while x-struktura 1- 0 do begin {♦} P := f i n d m i n ; {punkt z minimalną współrzędną x w x-strukturze}; d e l e b e m i n ; {usuń p z x-struktury} if p jest lewym końcem pewnego odcinka 1 then begin {znajdź (sąsiednie) odcinki 1' i 1" w y-strukturze, które wyznaczają przedział zawierający p , i wstaw 1 do y-struktury] (!', 1") := f i n d ( p ) ; {.?.' poprzedza 1" w y-strukturze} { =} i n s e r t {1) ; {***) if odcinki 1 i ./.'przecinają się then begin {dodaj punkt przecięcia odcinków 1 i 1' d o
8.4. M etoda zam ia ta n ia
279
x-struktury] <7 := punkt przecięcia i i i'; add(g)
{«*}
{>*! {***!
{**} {....M
end ; if odcinki I i 1" przecinaj ć< się then begin (dodaj punkt przecięcia odcinków 1 i 1" do x-struktury( g := punkt przecięcia 1 i 1 "; add(g) end end; if p jest; prawym końcem pewnego odcinka i then begin I' := pred (i ) ; 1" := suce (1) ; { i 1 " są sąsiadami I w y-strukturze] [usuń i z y-;j truktury} (i.•.',...•1.1 •(i ) ; if odcinki. I' :i.1" przecina ją się na prawo od miot ty 'che;-, begin . (dddaj punkt przecięcia 1 ' i i" do x - struktury) g := punkt przecięcia 1' i I"; add(g) end end; if q jest punktem przecięcia odcinków 1' i 1" then begin wypisz parę ] ' i 1" jako parę'przecinających się odcinków; {zamień .7/ z. 1 " w y-strukturze) i n t e r c h a n g e (.7/, l ") ; k := p r e ć U i " ) ; { k jest sąsiadem 1" w y-.strukt.urze różnym od 1') if odcinki k i i" przecina ją się na prawo oc'1 miotły then begin {dodaj punkt przecięcia odcinków k i I" do x-struktury} g := punkt przecięcia k i 1 "; add(q) end ;
in := s u c c ( l ') ; fro j e s t sąsiadem i'w y - s t r u k t u r z e , {***)
różnym od l"j :if odcinki in i 1' przecinają się na prawo od miotły then begin (dodaj punkt przecięcia odcinków in i 1' do struktury) g :-= punkt przecięcia m i i'; add(g) end end (instrukcji while)
end ;
Na rysunku K.IO jcsl przedstawiony przykład działania algorytmu.
280
ii. Algorytmy geometryczne
b
9
x- s l n i k t u r a :
1, 2 , 3 , 6 , 8 , 9
y-slm klura:
0
Miolfa
3, 6, 7, 8, 9 i l , I.K C
3, 6, 7, 8, 9
b, o. .
Kys. 8.10. Przykhid dzuihiniii alyorylimi znajdowania p:ir pizecinajncydi się odcinków melodii zniniaumia
W celu uzasadnienia popmwności powyższego algorytmu wystarczy pokazać, że są za chowane niezmienniki. Mówimy, że punki jest krytyczny, jeżeli jest końcem odcinka lub punktem przecięcia dwóch odcinków. Niezmiennik 2 gwarantuje, że punkt p wybie lany w wierszu {*) jest punktem krytycznym położonym najbliżej miotły z prawej strony. Każdy taki punkt jest badany tylko raz, a następnie usuwany z x-struktury. Wyni ka stąd, że wszystkie punkty przecięć zostają znalezione. Wiersze oznaczone {**} gwa rantują, że y-struktura zawiera tylko odcinki aktywne i to w dobrym porządku. Gwaran cją zachowania niezmiennika 2 są wiersze oznaczone {***}. Liczba iteracji instrukcji while wynosi 2n -I- s. W każdej iteracji jest wykonywana pewna stała liczba operacji na y-struklurze i x-strukturze. Każda z tych operacji kosztuje co najwyżej <9(log (n -l- .v)). Ponieważ .v < ir, łączny koszt: wykonania algorytmu jest 0((n -i- ,v)log n). (Powyższe roz wiązanie zostało po raz pierwszy zaproponowane w pracy |BO|. Algorytm przedstawio ny w tym opracowaniu pochodzi z |M|).
282
8. A lgorytm y geom etry czn e
Zadania 8 .1 .
Ułóż nicrekurencyjną wersję, algorytmu sprawdzania przynależności punktu do wielokąta wypukłego.
8.2. Niech alfa będzie funkcją zdefiniowaną w tym rozdziale. Udowodnij, że dla punk tów p i ą różnych od środka układu współrzędnych O alfa(p) < alfafj) wtedy i tylko wtedy, gdy kąt nachylenia wektora wodzącego punktu p do osi OX jest: nie większy od kąta nachylenia wektora wodzącego punktu q. 8.3. Zmodyfikuj algorytmy „dziel i zwyciężaj” oraz przyrostowy obliczania wypukłej otoczki. Zrób to tak, żeby działały dla każdego zbioru wejściowego punktów. 8.4. Przeprowadź dokładną analizę algorytmów „dziel i zwyciężaj” oraz przyrostowe go do obliczania wypukłej otoczki. 8.5. Algorytm „dziel i zwyciężaj” dla problemu wypukłej otoczki jest podobny do algorytmu sortowania przez scalanie. Zaproponuj algorytm obliczania wypukłej otoczki analogiczny do algorytmu quicksort. 8 .6
. Zaproponuj strukturę danych umożliwiającą wykonywanie na dynamicznym zbio rze punktów S następujących operacji: (aj insert(S, p):: S S u {p} (czas wykonania 0(max(l, log;;))); (b) CH(.Sj:: podanie wypukłej otoczki dla S (czas wykonania 0(m ax(l, //)); gdzie
ii
jest liczbą elementów w zbiorze S.
8.7. Zaproponuj algorytm liniowy obliczania wypukłej otoczki dla wierzchołków wie lokąta danych w kolejności ich występowania na jego obwodzie. 8 .8
. Zaproponuj algorytm liniowy obliczania wielokąta W będącego sumą danych wie lokątów wypukłych IV, i IV,.
8.9. Zaproponuj algorytm liniowy obliczania wielokąta W będącego iloczynem danych Wielokątów wypukłych U-j i HĄ 8 .10. Ułóż algorytm sprawdzania w czasie liniowym, czy dane dwa wielokąty VK, i W2 są
podobne. (Wskazówka: Sprowadź problem do równości słów cyklicznych, rozwa żając kąty wielokątów). 8 .11. Zaproponuj algorytm typu „dziel i zwyciężaj” znajdowania najmniej odległej pary
punktów wśród danych n punktów /;,, ..., pn.
Zadania
283
8.12. Ułóż algorytm znajdowania w czasie 0(nlog//) najbardziej odległej pary punktów wśród danych n punktów /;„. 8.13. Podaj dokładną implementację x- i y-struklur z algorytmu obliczaniu najmniej odległej pary punktów. 8.14. Podaj dokładną implementację x- i y-struktur z algorytmu obliczania pttr przecina jących się odcinków. 8.15. Złożoność pamięciowa algorytmu obliczania przecinających się par odcinków jest (Ąs -l' //). Zmodyfikuj algorytm w taki sposób, żeby złożoność pamięciowa była O(u). (Zauważmy, że takie rozwiązanie jest znacznie oszczędniejsze pamięciowo wtedy, kiedy .y jest rzędu //’). 8
. Hi. Triangulacją wielokąta IV nazywamy podział IV przekątnymi na trójkąty. Zapropo nuj algorytm z wykorzystaniem metody zamiatania do znajdowania triangulaeji danego wielokąta IV.
8.17. Niech IV będzie wielokątem o bokach równoległych do osi układu współrzędnych. Zastosuj metodę zamiatania do wyznaczania długości najkrótszej łamanej zwy czajnej, łączącej dwa punkty z IV i całkowicie w nim zawartej.