kubale - wprowadzenie do algorytmow
Post on 25-Jun-2015
5.363 Views
Preview:
TRANSCRIPT
Łagodne wprowadzenie do analizy algorytmów
Marek Kubale
Gdańsk 2009
PRZEWODNICZĄCY KOMITETU REDAKCYJNEGO WYDAWNICTWA POLITECHNIKI GDAŃSKIEJ Romuald Szymkiewicz REDAKTOR Zdzisław Puhaczewski RECENZENT Krzysztof Goczyła
Wydanie V - 2008 poprawione i uzupełnione
Wydano za zgodą Rektora Politechniki Gdańskiej
Wydawnictwa PG można nabywać w Księgarni PG (Gmach Główny, I piętro) bądź zamówić pocztą elektroniczną (ksiegarnia@pg.gda.pl), faksem (058 347 16 18) lub listownie (Wydawnictwo Politechniki Gdańskiej, Księgarnia PG, ul. G. Narutowicza 11/12, 80-233 Gdańsk)
© Copyright by Wydawnictwo Politechniki Gdańskiej, Gdańsk 2009
Utwór nie może być powielany i rozpowszechniany, w jakiejkolwiek formie i w jakikolwiek sposób, bez pisemnej zgody wydawcy
ISBN 978-83-7348-265-4
WYDAWNICTWO POLITECHNIKI GDAŃSKIEJ
Wydanie VI. Ark. wyd. 5,5, ark. druku 6,0, 915/566
Druk i oprawa: EXPOL P. Rybiński, J. Dąbek, Sp. Jawna ul . Brzeska 4, 87-800 Włocławek, tel. 054 232 48 73
SPIS TREŚCI
::lRZEDMOWA .... . . . . . ....... ............................... ........................................ ............... ....... .......... 5
•. WPROWADZENIE.............................................................................................................. 7
1.1. Problemy algorytmiczne .. . ......................... . . .............. ........................................ ...... 7
1.2. Język PseudoPascal ... . . . . . . . . . . . . . . . . . ... . ........... .............. . . .............................................. 13
1.3. Podstawy matematyczne ....... ....................................................... . . ..................... ..... 15
1.3.1. Logarytmy i zaokrąglenia całkowite . ............................................................. 15
1.3.2. Sumy szeregów ........... ................. . ... . . . ........... . . .......................... . . . . .... . .. . . . ..... . 16
1.4. Symbole oszacowań asymptotycznych . . . ... . . . . . . . . .. .. . ... . . . . ..... . . ......... . .. . ...................... 18
1.4.1. Symbol 0(·) .. . ....... . ........... . ... . ................... . ................ . . . . . ... . . . . . . .. . .. . .......... . . . .... 18
1.4.2. Symbol 0(') .. . . . . . . . . ... . . ...... . . . . . . . . . ... . . ......... . ............................ ........................... 19
1.4.3. Symbol no . . .... . . . . . . . . .. . . . . . ....... . . . . . . .. .... . . . ....... . . . ... . . . . . . . . . . . ... . . . ... . . . . . . ..... . . .......... 20
1.4.4. Symbol ro(.) ............................ . . . . . . . .. . . . . . . . . . . . . . ... . .. . . . . ........................................ 20
1.4.5. Symbol 8(·) .............. ......... . . .... . .......................................... .............. . ............ 21
1.4.6. Symbol E> (.) .................................................................................................. 21
1.5. Równania rekurencyjne niejednorodne . . ....................................... ........................... 22
1.5.1. Równania typu "dziel i rządź" ....................................................................... 22
1.5.2. Równania typu ,jeden krok w tył" ................................................................. 25
Zadania . . . . .............. . . . . . . . . . . . . . . . . . . . . . ..... . . . . . . . . . . . . . . . . . ........ . . . . . .. . . . . .. . . . . . . . .... . . . . . . . . ............. . . . . . . . . . 29
2. PODSTAWY ANALIZY ALGORyTMÓW ... . . . . . . . ........ ....... . . . .. . .... . . . . .... . . . ... ......... . . . .......... . . ...... 35
2.1. Wstęp . . . . . .... . . . . .... ......... . . ...... . .......................... . ......................................................... 35
2.2. Poprawność algorytmów .......................................................................................... 37
2.3. Złożoność czasowa algorytmów .............................................................................. 40
2.3.1. Operacje podstawowe ........................ ............ . ... .............. .. .... . . .... . . . ... ..... . . ..... . 40
2.3.2. Rozmiar danych ............................................................................................. 41
2.3.3. Pesymistyczna złożoność obliczeniowa ....... ...................... .................... ...... . . 42
2.3.4. Oczekiwana złożoność obliczeniowa ............................................................. 42
2.4. Złożoność pamięciowa ............................................................................................. 45
2.5. Optymalność ............................................................................................................ 49
2.6. Dokładność numeryczna algorytmów ............................................. . ........................ 51
2.6.1. Zadania źle uwarunkowane ...... . . . .... . . ...................... . ............... ....................... 51
2.6.2. Stabilność numeryczna ................................................................................... 53
2.7. Prostota algorytmów ...................... ............. .............. ... ....... . ... . . ... . . . . ... . . . . . . . ....... . ...... 54
4 Spis treści
2.8. Wrażliwość algorytmów .......................................................................................... 56
2.9. Programowanie a złożoność obliczeniowa .............................................................. 58
2.9.1. Rząd złożoności obliczeniowej ...................................................................... 58
2.9.2. Stała proporcjonalności złożoności obliczeniowej ........................................ 61
2.9.3. Imperatyw złożoności obliczeniowej i odstępstwa ......................................... 63
2.10. Algorytmy probabilistyczne ................................................................................... 64
Zadania ........................... . . . ............................................................................................. 67
3. PODSTAWOWE STRUKTURY DANyCH .. . ................. . . . . . . . . ................................................. 74
3.1. Tablice ...................... ............................................................................................... 74
3.2. Listy ........................... . ............................................................................................. 76
3.3. Zbiory . . .................................................................................................................... 77
3.4. GrafY ....... ................................................. ................................................................ 78
3.4.1. Macierz sąsiedztwa wierzchołków ............................................ ................ ..... 83
3.4.2. Listy sąsiedztwa wierzchołków ...................................................................... 85
3.4.3. Pęki wyjściowe ........ .................. ............................... ..................................... 86
Zadania ........................................................ . .. . ............................................................... 87
SŁOWNIK POLSKO-ANGIELSKI ............................................................................................. 91
LITERATURA ........................................................................................................................ 96
PRZEDMOWA
Przekazywana do rąk czytelników książka jest szóstym wydaniem podręcznika akade
mickiego, opublikowanego nakładem Wydawnictwa Politechniki Gdańskiej pod tym samym
tytułem. Od momentu piątego wydania w roku 2008 wystąpiły nowe fakty w dziedzinie
dużych liczb pierwszych i sposobów stymulowania nowych odkryć w zakresie teorii algo
rytmów i teorii liczb. Stąd zrodziła się potrzeba kolejnego wydania, uzupełnionego o naj
nowsze informacje z tych dziedzin. Ponadto, ostatnio ukazały się inne podręczniki w tej
serii na temat algorytmów i struktur danych i fakt ten musiał być odnotowany w niniejszej
publikacji.
Oddawany do rąk czytelników podręcznik jest przeznaczony dla osób interesujących
się podstawami informatyki, w tym przede wszystkim dla studentów kierunku Informatyka
na Wydziale ET! Politechniki Gdańskiej. Formalnie rzecz biorąc, jego treść pokrywa
pierwszą część wykładu z przedmiotu "
Podstawy analizy algorytmów", tj. algorytmy i pro
blemy wielomianowe, ale stanowi też miejscami rozszerzenie programu tego przedmiotu,
który jest prowadzony na II roku kierunku Informatyka. W tym miejscu odnotujmy, że dru
gą część wykładu doskonale pokrywa książka K. Giary "
Złożoność obliczeniowa algoryt
mów w zadaniach" [5] oraz poprzedni skrypt autora [8]. W szczególności niniejszy pod
ręcznik może służyć jako wprowadzenie do wykładu ,,Algorytmy i struktury danych". Jego
fragmenty mogą być także wykorzystane w nauczaniu przedmiotu "Matematyka dyskretna".
Sądzę, że książka może ponadto zainteresować studentów kierunku Informatyka na Wydzia
le Matematyki, Fizyki i Informatyki Uniwersytetu Gdańskiego, oraz studentów kierunków
pokrewnych, np. Matematyka Stosowana.
Zakładam, że czytelnik ma pewne podstawowe przygotowanie z matematyki dyskretnej
i że umie układać algorytmy w Pascalu lub innym języku wysokiego poziomu. Znajomość
przedmiotów "Metody i techniki programowania",
"Praktyka programowania" oraz
"Mate
matyka dyskretna" jest pożądana przy studiowaniu podręcznika.
Niniejsza pozycja składa się z trzech rozdziałów. Rozdział l daje podstawy formalne,
niezbędne przy analizie algorytmów pod kątem złożoności obliczeniowej. Podajemy tutaj
klasyftkację problemów rozwiązywalnych za pomocą komputerów, przypominamy wybrane
pojęcia matematyczne, definiujemy symbole oszacowań asymptotycznych. Jednakże naj
więcej miejsca poświęcamy metodom najczęściej spotykanym przy analizie złożoności
obliczeniowej algorytmów rekurencyjnych.
Rozdział 2 wprowadza w zagadnienie analizy algorytmów z różnych punktów widze
nia. Algorytmy, które tutaj rozważamy, są najprostsze możliwe, tj. szeregowe, scentralizo
wane, statyczne i dokładne. Rozważamy tutaj takie zagadnienia, jak: poprawność, złożo
ność czasowa, złożoność pamięciowa, optymalność, stabilność numeryczna, prostota i
wrażliwość. Rozdział zamykamy przykładem algorytmu probabilistycznego.
Ostatni rozdział 3 przedstawia podstawowe struktury danych, gdyż są one niezbędnym
komponentem każdego rozwiązania algorytmicznego. W rozdziale tym rozważamy takie
struktury, jak: tablica, lista zbiór, a zwłaszcza graf. Strukturom grafowym poświęcamy
6 Przedmowa
szczególnie wiele miejsca, gdyż grafy są najczęściej spotykanym modelem matematycznym
w informatyce. Więcej informacji na ten temat można znaleźć w podręczniku K. Goczyły
"Struktury danych" [6].
Co ważne i cenne dla czytelników studiujących zagadnienia złożoności obliczeniowej
algorytmów, każdy z powyższych rozdziałów kończy się zestawem około 30 zadań nie
zbędnych do sprawdzenia nabytej wiedzy i umiejętności oraz umożliwiających jej pogłę
bienie. Na zakończenie zaś podano słownik polsko-angielski ważniejszych pojęć z tego
zakresu.
W tym miejscu pragnę wyrazić wdzięczność recenzentowi prof. dr. hab. inż. Krzyszto
fowi Goczyle za życzliwe sugestie; dziękuję również mgr. inż. Janowi Wojtkiewiczowi za
pomoc edytorską oraz zespołowi Wydawnictwa PG za wnikliwą korektę. Z góry dziękuję
również studentom za wszelkie uwagi merytoryczne, które można kierować pocztą elektro
niczną pod podanym niżej adresem.
Gdańsk, lipiec 2009 r. Marek Kubale kubale@eti.pg.gda.pl
1. WPROWADZENIE
Przez wieki nie było zgodności wśród autorów różnych książek o obliczeniach oraz
badaczy algorytmów co do formalnej definicji algorytmu. Mimo to od czasów Euklidesa
(rok 300 pne.), nie martwiono się tym specjalnie, tylko tworzono opisy rozwiązywania
różnych problemów - i dzisiaj nazywamy je algorytmami. Nawet konstruktorzy pierwszych
liczydeł, kalkulatorów i maszyn cyfrowych nie dociekali specjalnie, jak zdefiniować to coś,
co da się wykonać za pomocą ich maszyn.
Na przełomie XIX i XX wieku matematyków zainteresowało udzielenie odpowiedzi na
dość ogólne pytanie: co można obliczyć, jakie funkcje są obliczalne, dla jakich problemów
istnieją algorytmy, i ogólniej - czy wszystkie twierdzenia można udowodnić (lub obalić)?
W 1900 r. matematyk niemiecki D. Hilbert, wśród 23 wyzwań dla matematyków zaczynają
cego się stulecia, jako dziesiąty problem sformułował pytanie: czy istnieje algorytm, który
dla dowolnego równania wielomianowego wielu zmiennych o współczynnikach w liczbach
całkowitych znajduje rozwiązanie w liczbach całkowitych? Dopiero po prawie siedemdzie
sięciu latach matematyk rosyjski J.V. Matjasiewicz odpowiedział negatywnie na to pytanie
[11]. Dziesiąty problem Hilberta wywołał olbrzymie zainteresowanie wśród matematyków
obliczalnością - dziedziną, która zajmuje się poszukiwaniem odpowiedzi m.in. na pytanie,
jakie problemy mają rozwiązanie w postaci algorytmu, a jakie nie mają.
Formalizacją pojęć algorytmu i obliczalności zajęło się w pierwszej połowie ubiegłego
stulecia wielu matematyków. Wprowadzono wiele różnych definicji obliczeń, przy czym
większość z nich jest równoważna między sobą w tym sensie, że definiuje tę samą klasę
funkcji obliczalnych. Do najpopulamiejszych należy formalizm wprowadzony przez A.
Turinga, zwany dzisiaj maszyną Turinga (ang. Turing machine). Obecnie maszynę Turinga
przyjmuje się za precyzyjną definicję pojęcia algorytmu. Zatem nie ma algorytmu dla takie
go problemu, którego nie można rozwiązać za pomocą maszyny Turinga.
1.1. Problemy algorytmiczne
W niniejszej książce będziemy zajmowali się wyłącznie problemami algorytmicznymi (ang. algorithmic problems), tj. takimi, które mogą być rozwiązane za pomocą odpowied
nich algorytmów komputerowych. Powiedzenie, że problem może być rozwiązany za po
mocą algorytmu, oznacza tutaj, że można napisać program komputerowy, który
w skończonym czasie da poprawną odpowiedź dla dowolnych poprawnych danych wej
ściowych przy założeniu dostępu do nieograniczonych zasobów pamięciowych. Badania
problemów algorytmicznych rozpoczęły się już w latach trzydziestych, tj. przed nadejściem
ery komputerowej. Ich celem było scharakteryzowanie tych problemów, które mogą być
rozwiązane algorytmicznie, i ujawnienie niektórych problemów, które nie posiadają takiej
własności. Jednym z ważnych negatywnych rezultatów teorii obliczeń (ang. computability theory) było odkrycie przez A. Turinga nierozstrzygalności problemu stopu. Problem stopu
8 l. Wprowadzenie
(ang. halting problem) polega na odpowiedzi na pytanie, czy po wczytaniu danych określo
ny algorytm (lub program komputerowy) kończy się w skończonym czasie, czy też pętli się.
Okazuje się, że nie istnieje program komputerowy rozwiązujący ten problem w przypadku
ogólnym. Oczywiście, nierozstrzygalność jakiegoś problemu w przypadku ogólnym nie
oznacza, że każdy jego przypadek szczególny jest również nierozstrzygalny. Poza tym w
klasie problemów nierozstrzygalnych wyróżnia się podklasę tak zwanych problemów półrozstrzygalnych (ang. semidecidable problems), tj. takich, dla których odpowiedź
"tak" jest
zawsze otrzymywana w skończonym czasie, ale brak takich gwarancji, gdy odpowiedź
brzmi "nie" (np. problem stopu).
Stwierdzenie, że jakiś problem jest algorytmiczny, nie mówi nic o tym, czy problem
ten jest rozwiązywalny efektywnie czy nie. Wiadomo na przykład, że można napisać pro
gram komputerowy, który grałby w szachy w sposób doskonały. Jest bowiem skończona
liczba sposobów rozmieszczenia figur na szachownicy, zaś partia szachów musi się zakoń
czyć po skończonej liczbie ruchów. Znając konsekwencje każdego ruchu przeciwnika,
można pokusić się o wskazanie najlepszego możliwego posunięcia. Szacuje się, że liczba
liści w pełnym drzewie przeszukiwania rozwiązań dla szachów sięga 1 0123 [7]. Zatem przy
obecnej szybkości komputerów program sprawdzający je wszystkie musiałby się wykony
wać wiele miliardów lat. Przykład ten nie został wybrany przypadkowo - 1 0123 to przybli
żona liczba wszystkich atomów we wszechświecie. Aby uzmysłowić sobie jak wielka to
liczba odnotujmy, że ilość działań matematycznych wykonanych dotychczas przez ludzi i
komputery szacuje się na 1 025. Na marginesie, najbardziej skomplikowaną grą planszową,
dla której napisano program komputerowy grający w sposób doskonały, są warcaby.
Istnieje wiele innych problemów praktycznie użytecznych, które mogą być rozwiązane
algorytmicznie, ale wymagania czasowe i pamięciowe z tym związane są tak ogromne, że
odpowiednie' algorytmy mają znaczenie jedynie teoretyczne. Wymagania na czas
i przestrzeń mają kluczowe znaczenie dla gałęzi informatyki zwanej teoriq złożoności obliczeniowej (ang. computational complexity theory).
Reasumując, wszystkie problemy z dziedziny optymalizacji dyskretnej można podzie
lić z punktu widzenia długości wyjścia, jak i z punktu widzenia złożoności obliczeniowej.
Zgodnie z pierwszą kategoryzacją wyróżniamy:
l. Problemy decyzyjne (ang. decision problems). Są to problemy, które wyrażamy py
taniami ogólnymi, zaczynającymi się od słowa "
czy". Odpowiedzią na nie jest słowo "tak"
lub "nie". Innymi słowy, algorytm ma zdecydować, czy dane wejściowe spełniają określoną
własność. Przykładem jest tu wspomniany problem stopu .
. 2. Problemy optymalizacyjne (ang. optimization problems). W tym przypadku algo
rytm ma znaleźć obiekt matematyczny spełniający zadaną własność, np. najlepsze posunię
cie w danym stadium gry w szachy.
Z drugiej strony, wszystkie problemy z dziedziny optymalizacji dyskretnej można po
dzielić na pięć klas.
l. Problemy niealgorytmiczne (ang. nonalgorithmic problems). Problemy takie nie
mogą być rozwiązane za pomocą algorytmów o skończonym czasie działania. Przykładem
takiego problemu jest wspomniany problem stopu. Innym przykładem jest znany problem kafelkowania (ang. tiling problem), który polega na rozstrzygnięciu, czy można pokryć
1.1. Problemy algorytmiczne 9
płaszczyznę identycznymi kopiami danego wielokąta. Jeśli mamy nieskończenie wiele ta
kich samych kwadratów, to można je ułożyć w taki sposób, iż pokrywają całą płaszczyznę.
To samo można uczynić z trójkątami równobocznymi i sześciokątami foremnymi, ale nie
np. z pięciokątami foremnymi. Od czasów starożytnych wiadomo bowiem, że istnieją
tylko 3 parkietaże regularne i jednorodne. "Regularne", to znaczy takie, których wszyst
kie kawałki są identycznymi wielokątami foremnymi; przez ,jednorodność" rozumiemy,
że ułożenie kafelków w każdym wierzchołku jest jednakowe. Jednakże w przypadku figur
nieforemnych jest to problem nierozstrzygalny. Na rys. 1 .1 podajemy przykład nieregu
larnego i niejednorodnego kafelkowania płaszczyzny oparty na motywach genialnego
rysownika M. C. Eschera.
Rys. 1 . 1 . Kafelkowanie oparte na motywach Eschera Regular space division III
Zauważmy na marginesie, że istnienie problemów niealgorytmicznych, tj. takich,
z którymi nie radzą sobie komputery (w przeciwieństwie do ludzi), jest dowodem na to, iż
umysł ludzki potrafi robić coś więcej, niż mogą wykonywać komputery - mianowicie może
pracować niealgorytmicznie. Tym samym stworzenie sztucznej inteligencji dorównującej
inteligencji właściwej człowiekowi nie jest możliwe.
2. Problemy przypuszczalnie niealgorytmiczne (ang. presumably nonalgorithmic problems). Dla problemów tych nie udało się dotychczas podać algorytmu skończonego, ale
brak też dowodu, że taki algorytm nie istnieje. Można więc powiedzieć, że problemy te
mają status tymczasowy: w momencie gdy skonstruowany zostanie algorytm, który je roz
wiąże lub ktoś udowodni, że taki algorytm nie istnieje, przeniesie się je bądź w dół bądź w
górę. Wielu przykładów takich problemów 'dostarcza teoria liczb, zwłaszcza teoria równań
10 l. Wprowadzenie
diofantycznych. Rozwiązywanie równań diojantycznych (ang. Diophantine equations) polega na znajdowaniu liczb całkowitych rozwiązujących równanie algebraiczne o współczynnikach całkowitych (np. X2 + l = i jest jednym z równań diofantycznych). Zauważmy na marginesie, że problem równań diofantycznych jest w przypadku ogólnym nierozstrzygalny. Jest to wniosek wynikający z rozwiązania 1 0. problemu Hilberta, dotyczącego równań diofantycznych.
Innym ciekawym przykładem takiego problemu teorio liczbowego jest tzw. problem "pomnóż przez 3 i dodaj l ", zwany też problemem liczb gradowych (ang. hailstone numbers problem) lub problemem Col/atza (ang. Collatz problem) za L. Collatzem, który sformułował ten problem w roku 1 937 [ 1 5] . Poczynając od pewnego naturalnego k, gdy k jest parzyste, podstawiamy k := k/2, w przeciwnym razie k := 3k + l . Działa:nia te kontynuujemy dopóki k *" l (por. zadanie 1 .2). Najbardziej naturalnym pytaniem jest pytanie o to, czy taki proces obliczeniowy zatrzymuje się dla każdej naturalnej wartości k. Mimo usilnych starań wielu matematyków zajmujących się teorią liczb nie znamy odpowiedzi na to pytanie. Wiemy jedynie, że jeżeli procedura ta nie kończy się stopem, to wpada w cykl liczb nie zawierający l bądź w ciąg liczb rosnący do nieskończoności . Dlatego stwierdzenie, czy dla pewnego k procedura realizująca problem Collatza się pętli, może być problemem niealgorytmicznym. Dzisiaj , dzięki użyciu komputerów, wiemy, że procedura Collatza zatrzymuje się dla wszystkich k 5, 5" 1 018• Zauważmy na marginesie, że jak poprzednio problemy iteracji tego typu bywają nierozstrzygalne. (Jak wiemy, problem Collatza znany jest pod wieloma nazwami . Jedną z nich jest "spisek radziecki" . Nazwa wzięła się stąd, że w latach 50., kiedy stał się on popularny z uwagi na liczne nagrody za jego rozwiązanie, w USA prawie wszyscy matematycy tracili czas, bezskutecznie poszukując rozwiązania [ 1 0].)
Innym przykładem takiego problemu jest słynna hipoteza C. Goldbacha z roku 1742. Głosi ona, że każda liczba parzysta większa od 2 jest sumą dwóch liczb pierwszych. Nie jest znany żaden wyjątek od tej tezy, ale też nie podano żadnego jej dowodu. Aby ocenić skalę trudności zauważmy, że nie zrobiono w tej sprawie żadnego postępu aż do roku 1 930, kiedy to L. Schnirelmann pokazał, że istnieje taka liczba n, iż każda liczba naturalna od niej większa może być zapisana jako suma co najwyżej 300 tys. liczb pierwszych. Obecnie wiemy, że ta skończona ilość liczb pierwszych nie przekracza 6, nikt jednak nie wie, jak duże jest n. W siedem lat później Winogradow dowiódł, że począwszy od 1 045000 każda liczba całkowita nieparzysta jest sumą trzech liczb pierwszych (zaś komputerowo zweryfikowano to do 1 020 [ 1 3]). Tym samym została udowodniona tzw. "mała hipoteza Goldbacha" . Jako ciekawostkę odnotujmy, że za udowodnienie hipotezy Goldbacha zaoferowano nagrodę w wysokości l mln. USD. Odnotujmy na marginesie, że za największą liczbę spotkaną do tej pory w teoriach matematycznych uchodzi l O 1 000000000034•
Zauważmy, że nie można udowodnić nierozstrzygalności tego typu stwierdzeń jak hipoteza Goldbacha. Gdyby bowiem np. hipoteza Goldbacha była nierozstrzygalna, to można by wyciągnąć wniosek, że jest prawdziwa! Rozumujemy następująco: jeśli hipoteza jest fałszywa, to istnieje taka liczba parzysta p, która nie jest sumą dwóch liczb pierwszych od niej mniejszych. Zatem procedura, która przeszukiwałaby systematycznie kolejne liczby parzyste w poszukiwaniu liczby p, kiedyś by się skończyła (por. zadanie 1 .5). Znaczyłoby to jednak, że hipoteza Goldbacha jest rozstrzygalna - a to z założenia nie jest prawdą.
1.1. Problemy algorytmiczne 11
Jedyna możliwość to ta, że w trakcie trwających bez końca poszukiwań żaden taki kontr
przykład nie pojawi się. Lecz jeśli nie ma kontrprzykładu, to hipoteza jest prawdziwa. Za
tem hipoteza Goldbacha jest rozstrzygalna (można nawet podać zestaw dwóch prostych
algorytmów, z których jeden zwraca "tak", a drugi "nie" w odpowiedzi na pytanie, czy jest
prawdziwa), lecz mimo to procedura szukania liczby p może być nieskończona.
Jeszcze inny przykład dotyczy rozwinięcia liczby 11:. Jak wiadomo liczba 11: jest niewy
mierna. Tym niemniej znamy procedury obliczeniowe potrafiące wypisywać rozwinięcia
dziesiętne złożone z dowolnie wielu cyfr. Aktualnie znamy 11: z dokładnością do tryliona
cyfr dziesiętnych (podobno trylionową cyfrą jest O). Czy możemy powiększyć tę dokład
ność? Tak, pytanie jest tylko o sens takich działań. Przypuśćmy, że interesuje nas pewna
własność tego rozwinięcia, np. pojawienie się 1 00 określonych bezpośrednio po sobie na
stępujących cyfr, która jest przypadkowa. Znaczy to, że nie znamy żadnego powodu, dla
czego ta własność jest bądź wykluczona, bądź wynika z definicji. Na przykład,
dla sprawdzenia, czy gdziekolwiek w rozwinięciu 11: znajduje się ciąg stu kolejnych zer, nie
znamy żadnej procedury z wyjątkiem generowania rozwinięcia i zliczania zer. Tak daleko
jak 11: została współcześnie wyliczona, takiego ciągu nie ma. Jeśli wygenerowalibyśmy
pierwszy bilion cyfr i znaleźlibyśmy tam ciąg stu zer, to oczywiście kwestia byłaby roz
strzygnięta. Z drugiej strony, gdyby nie było 1 00 zer, nie bylibyśmy ani o jotę mądrzejsi,
niż byliśmy na początku: nie wiemy nic o drugim bilionie cyfr. A nawet, gdyby się okazało,
że jest ciąg 1 00 zer w obliczonym przez nas rozwinięciu, moglibyśmy zmienić problem na
1 000 kolejnych dziewiątek, na przykład. Na marginesie odnotujmy, że gdyby chodziło nie o
1000 dziewiątek, lecz o 6 dziewiątek w szeregu, to ten problem został rozstrzygnięty: na
pozycji 762 jest taki ciąg, a miejsce to jest znane jako punkt Feynmana (ang. Feynman 's point). Istota sprawy polega na tym, że są dzisiaj i zawsze będą proste pytania odnoszące się
do liczby n:, .,fi , e, itd., na które nie możemy spodziewać się odpowiedzi. Niech P oznacza
pytanie: "Czy w rozwinięciu dziesiętnym n: pojawia się dany na wejściu ciąg cyfr?". Czy
jest to problem algorytmiczny? Nie wiemy. Możemy tylko przypuszczać, że nie. Czy jest to
problem decyzyjny? Tu odpowiedź znamy: brzmi "tak".
3. Problemy wykładnicze (ang. e.xponential problems). Problemy te nie mają algoryt
mów działających w czasie ograniczonym przez wielomian zmiennej rozmiaru problemu.
Przykładem takiego problemu jest zadanie wygenerowania wszystkich ustawień ciągu n elementów. Ponieważ takich ustawień jest n!, czas działania dowolnego algorytmu nie może
rosnąć wolniej niż n ! , a więc musi rosnąć szybciej niż jakikolwiek wielomian. Innym przy
kładem tego typu jest słynny problem wież w Hanoi, którego złożoność sięga 2n (patrz
przykład 1 . 1 O). 4. Problemy przypuszczalnie wykładnicze (ang. presumably e.;t:ponential problems).
Dla problemów tych nie udało się dotychczas podać algorytmu wielomianowego, ale brak
też dowodu, że taki algorytm nie istnieje. Tym niemniej panuje dość powszechny pesymizm
odnośnie do możliwości skonstruowania dla nich algorytmu działającego w czasie wielo
mianowym, co niekiedy bywa podstawą dla współczesnych systemów szyfrowania danych.
Przykładem takiego problemu jest jaktoryzacja (ang. jactorization), czyli znalezienie roz
kładu danej liczby na czynniki pierwsze. Największą znaną liczbą pierwszą jest 46. znale
ziona pierwsza liczba typu Mersenne'a o wartości 243 112609_ 1 . Do zapisu tej liczby trzeba
.:.
1 2 l. Wprowadzenie
prawie 1 3 milionów cyfr dziesiętnych! Liczba ta została znaleziona na komputerze zainsta
lowanym w UCLA w roku 2008 w wyniku tzw. projektu GIMPS (the Great Internet Mersenne Prime Search) [ 1 6] , jako 1 2. liczba Mersenne'a znaleziona w tym projekcie. Piszemy,
że ta liczba jest 46. "znaleziona", a nie
"kolejna", gdyż w przedsięwzięciu GIMPS, w któ
rym bierze udział 50 000 amatorów i kilkudziesięciu zawodowców (łączna zgromadzona
moc obliczeniowa jest rzędu ponad 20 teraflopsów), liczby nie muszą być sprawdzane sys
tematycznie. Obecnie ufundowano specjalną nagrodę w wysokości 1 50 tys. USD dla tego,
kto poda liczbę pierwszą o długości powyżej 1 00 milionów cyfr dziesiętnych. Zauważmy na
marginesie, że testowanie liczb pierwszych jest problemem wielomianowym (patrz pkt 5). Jeszcze innym przykładem jest znany problem komiwojażera (ang. travelling salesman
problem). W problemie tym dane jest n miast i odległość między każdą ich parą. Zadanie
polega na znalezieniu naj krótszej trasy zamkniętej przechodzącej jednokrotnie przez każde
z miast. Jeden z możliwych algorytmów polega na sprawdzeniu wszystkich n! permutacji.
Metoda pełnego przeglądu jest dość szybka, gdy marny 1 0 miast. Wówczas jest do przej
rzenia 9!12 = 1 8 1 440 cykli i komputer przeglądający je z szybkością 1 06 cykli na sekundę
poradzi sobie z problemem w czasie krótszym niż ćwierć sekundy. Jednakże, gdy mamy 20 miast, to liczba możliwych marszrut wynosi około 6. 1 0 16 i komputer analizujący je wszyst
kie w tym samym tempie będzie potrzebował 2000 lat nieprzerwanej pracy. Oczywiście, nie
oznacza to, że jest to najlepsza metoda rozwiązania tego problemu. Co więcej, nie oznacza
to, że problem komiwojażera może być rozwiązany wyłącznie algorytmem o złożoności
niewielomianowej. Na marginesie, odnotujmy postęp, jaki obserwujemy na świecie w za
kresie możliwości rozwiązania tego problemu. W roku 1 998 uczeni z Rice University
(USA) opracowali program, który znalazł optymalne rozwiązanie dla wszystkich 13 509 miast amerykańskich o liczbie mieszkańców powyżej pół tysiąca. Obliczenia na sieci kom
puterów dużej mocy trwały około 3 miesiące.
Na zakończenie tego punktu odnotujmy ciekawą inicjatywę naukowców z Clay Ma
thematics Institute w Massachusetts (USA), którzy wzorując się na pomyśle Hilberta z roku
1 900 sformułowali 7 otwartych problemów matematycznych do rozwiązania w nadchodzą
cym wieku XXI [ 1 7] . Są to najbardziej znane problemy opierające się przez długie lata
rozwiązaniu. Za rozwiązanie każdego z tych problemów milenijnych ufundowano nagrodę
w wysokości l mln. USD. Na czele tej listy jest pytanie, czy P = NP. Gdyby udało się
udzielić pozytywnej odpowiedzi na to pytanie, to problemy takie jak faktoryzacja i problem
komiwojażera przeszłyby do następnej klasy, tj. problemów wielomianowych. Jak dotych
czas, rozwiązano tylko jeden z tych problemów, mianowicie hipotezę Poincarego.
5. Problemy wielomianowe (ang. polynomial problems). Problemy te mają algorytmy
rozwiązujące je w czasie ograniczonym wielomianem zmiennej rozmiaru problemu. Najlep
szym przykładem takiego problemu jest zagadnienie sortowania. Ciąg n liczb można upo
rządkować rosnąco, na przykład metodą przestawiania sąsiednich par. Wówczas maksymal
na liczba porównań nie przekracza n2/2 . Ale istnieją jeszcze lepsze, bardziej wydajne algo
rytmy sortowania. Wbrew pozorom nie należy do nich metoda Quieksort, która w najgor
szym przypadku wymaga również czasu kwadratowego (np. dla uporządkowanych danych),
choć w przypadku średnim jej liczba operacji jest proporcjonalna do nlogn . Do klasy tej
zaliczamy również problemy, o których wiemy, że mają algorytmy wielomianowe, mimo że
1.2. Język PseudoPascal 13
nikt ich jeszcze nie podał (może nawet nikt ich nigdy nie poda). Odnotujmy na marginesie,
że jednym z naj ciekawszych odkryć ostatnich lat było udowodnienie w roku 2004, że udzie
lenie odpowiedzi na pytanie, czy dana liczba naturalna jest pierwsza, może być dokonane w
czasie wielomianowym względem długości tej liczby. Jednakże wielomian ten jest stosun
kowo wysokiego stopnia, ok. O (n\ Celem niniejszego skryptu jest zapoznanie czytelnika z tymi technikami projektowania
algorytmów i struktur danych, które okazały się użyteczne w praktyce, oraz podstawami
teoretycznymi i narzędziami służącymi do analizy algorytmów i programów realizujących te
algorytmy. Będziemy analizowali głównie długość czasu i wielkość pamięci, niezbędne do
wykonania tych programów. Będziemy wreszcie analizowali złożoność obliczeniową pro
blemów jako takich, tzn. wewnętrzną złożoność problemów, abstrahując od złożoności
algorytmów stosowanych do ich rozwiązania.
W trakcie analizy algorytmów cały czas będą nam towarzyszyły pytania o możliwości za
projektowania jeszcze szybszych algorytmów i kwestie istnienia bardziej stosownych struktur
danych. Pytania takie winien stawiać sobie każdy inżynier informatyk. Programowanie jest
bowiem procesem stałego ulepszania produktu softwarowego w całym jego cyklu życia.
W książce będą pojawiały się często zręby programów komputerowych pisanych
w języku wysokiego poziomu. Będzie to język zwany tutaj PseudoPascalem, ponieważ
będzie zawierał podstawowe konstrukcje pascalowe. Nie będą to jednak gotowe programy
do wykonania na komputerze, gdyż wiele szczegółów implementacyjnych zostanie pominię
tych. Co więcej, będą się pojawiały polecenia zapisane w języku naturalnym. Podejście
takie jest wystarczające do celów analizy złożoności obliczeniowej algorytmów. Z drugiej
strony doświadczony programista nie powinien mieć kłopotów z rozwinięciem programów
napisanych w PseudoPascalu do postaci zgodnej z gramatyką np. Turbo Pascala.
1.2. Język PseudoPascal
Jak zaznaczyliśmy wcześniej, algorytmy będziemy zapisywali w uproszczonym dialek
cie Pascala, który nazwaliśmy PseudoPascalem. W programach zapisanych w tym języku
brak jest deklaracji i szczegółów syntaktycznych. Nie są one istotne dla potrzeb analizy
złożoności, przeciwnie - jedynie zaciemniają obraz. Często konkretne deklaracje zmien
nych można uzupełnić na podstawie kontekstu.
Język PseudoPascal różni się od standardowego Pascala sposobem analizy wyrażeń
boolowskich. Dla przykładu wyrażenie
(1.1) (i <= n) and (A[i]<>x)
jest typowe dla sterowania pętlą wbiJe. W niektórych wersjach Pascala wszystkie wyrazy są
wartościowane przed określeniem, czy całe wyrażenie jest prawdziwe, czy fałszywe.
A zatem w przypadku, gdy macierz A ma wymiar [l. .n], zaś i = n + l, nastąpi błąd odwo
łania się do nieistniejącej lokacji A[n + l]. W naszym Pascalu przyjmiemy, że wyrazy obli
czane są od lewej, przy czym gdy pierwszy operand określa wartość wyrażenia, następne
nie są już wartościowane. Dla uproszczenia będziemy się pozbywali nawiasów w wyrażeniu
14 1. Wprowadzenie
typu (1.1) i będziemy wprowadzali standardową notację matematyczną. Tak więc wyrażenie
(1.1) zapiszemy w PseudoPascalu następująco:
(1.2) i � n and A [i] ;t:x
Często, aby uniknąć podawania wielu nieistotnych szczegółów, polecenia będziemy
zapisywali w języku naturalnym. Dla przykładu możemy napisać: "niech x będzie najwięk
szym elementem tablicy A" lub "wstaw l na początek listy L". Najważniejszą niestandardową, aczkolwiek spotykaną w Turbo Pascalu, instrukcją jest
return, którą wprowadzamy, ponieważ pozwala pisać bardziej przejrzyste programy bez
używania instrukcji skoku dla przeniesienia sterowania. Instrukcję tę będziemy stosowali w
formie
(1.3) return(�rażenie)
gdzie �rażenie jest opcjonalne. Procedurę zawierającą instrukcję (1.3) możemy zamienić
na Pascal standardowy w następujący sposób. Najpierw deklarujemy nową etykietę, np.
999, i stawiamy ją przed ostatnim słowem kluczowym end tej procedury. Jeśli instrukcja
return(x) występuje w funkcjijl, to zastępujemy ją instrukcją złożoną
begin
end
jl :=x; goto 999
Przykład 1 . 1 Poniższy program przedstawia funkcję rekurencyjną obliczania silni, zapisaną z użyciem
instrukcji return(·).
function silnia(n: integer): integer;
begin
end;
if n � 1 then return(l)
else return(n*silnia(n -l))
Stosując konsekwentnie powyższą transformację, otrzymamy
function silnia(n: integer): integer;
label 999; begin
if n � 1 then begin
end else
silnia := l;
goto 999
1.3. Podstawy matematyczne 15
begin silnia := n*silnia(n - l); go to 999
end; 999: end; •
Czasami będziemy numerowali kolejne wiersze programu, aby można było łatwo odwoływać się do nich w trakcie analizy.
1.3. Podstawy matematyczne
W podręczniku będziemy posługiwali się wielokrotnie pojęciami matematycznymi. W niniejszym punkcie zgromadziliśmy najbardziej elementarne z nich. Inne, takie jak symbole oszacowań arytmetycznych i relacje rekurencyjne, pojawią się w kolejnych punktach niniejszego rozdziału.
1.3.1. Logarytmy i zaokrąglenia całkowite
Dla dowolnej liczby rzeczywistej x symbol LxJ - czytany podłoga x (ang. fioor) lub spód x - jest największą liczbą całkowitą nie większą od x. Natomiast symboli x l- czytany sufit x (ang. ceiling) lub pułap x - jest naj mniejszą liczbą całkowitą nie mniejszą od x. Na przykład L2.SJ = 2 i IS.2l = 9.
Jak wiadomo, 10gbx jest wykładnikiem potęgi, do której należy podnieść podstawę b, aby otrzymać liczbę logarytmowaną x. Funkcja ta ma następujące własności: l) 10gb jest funkcją różnowartościową i rosnącą dla b > l.
2) 10gb l = O
3) 10gb ba = a
4) 10gb (xy) = logbx + 10gbY 5) 10gb (xa) = alogbx 6) x10gbY = ylOgiJX
7) logbx = (logax)/(loga b) W teorii złożoności obliczeniowej mamy najczęściej do czynlema z logarytmami
o podstawie 2, dlatego logarytmy takie zapisywać będziemy jako 19, tzn. 19 x = logzx. Logarytmy naturalne, czyli przy podstawie e, zapisujemy jako In. Zatem In x oznacza to samo co logex. L iczby logarytmowane będą najczęściej naturalne. Gdy n jest potęgą 2, powiedzmy n = 2\ to 19 n = k. Gdy n nie jest potęgą 2, to istnieje liczba k taka, że i < n < 2k+l . Wówczas LIg nJ = k i lIg n l = k + 1. Można sprawdzić, że dla każdego n mamy
n � llgn 1< 2n oraz n/2 < 2 l1gnJ � n.
Co więcej, pochodna (In x)' = lIx i (lg x)' = (lg e)/x .
---
16 l. Wprowadzenie
1.3.2. Sumy szeregów
Przy analizie algorytmów często pojawiają się sumy szeregów. Poniżej przypominamy
najważniejsze z nich.
( l A)
(1. 5)
( 1 .6)
(1.7)
Ogólnie
( 1 .8)
fi= n(n+l)
i=O 2 f i2 = n(n+0.5)(n+l)
i=O 3
f(�J=2/J i=O l
n 2:2i=2n+l_l i=O
n n+l l 2:Xi=� i=O x-l
Wzór ( lA) odkrył K.F. Gauss w wieku 7 lat na lekcj i matematyki. Nauczyciel , aby zająć
czymś swych uczniów, polecił im dodawać wszystkie liczby naturalne od l do 100. Spodzie
wał się, że to zadanie rachunkowe zajmie im całą godzinę. Jednakże młody Gauss zauważył,
że liczby te można skojarzyć w pary: (pierwszą i ostatnią) + (drugą i przedostatnią) + . . . o su
mie 1 0 1 w każdej parze. Ponieważ par takich jest 50, rezultat był natychmiastowy.
Wzór ( 1 . 7) łatwo wyprowadzić, traktując każdą l iczbę po lewej s tronie jako jeden bit
w n-bitowym rozwinięciu binarnym.
Czasami trudno jest podać dokładną postać sumy szeregu l iczbowego. Jednakże,
w większości przypadków, jest względnie łatwo oszacować tempo wzrostu takiej sumy.
Weźmy dla przykładu funkcję
n f(n) = l>2 =12 +22 + . . . +n2. i=O
Zgrubne oszacowanie nie jest trudne, gdyżj(n):S n2 + n2 + . . . + n2 = n3. Ale chcieliby
śmy oszacowaćj(n) bardziej precyzyj nie. Zacznijmy od przeanalizowania rysunku 1.2.
1.3. Podstawy matematyczne
y
2 8 n -1 n n +1
Rys. 1.2. Górne oszacowanie sumy
Widzimy, że
W podobny sposób dokonujemy dolnego oszacowania wartościj{n).
a podstawie rysunku 1 .3 widzimy, że
y
Zatem
n -1 n
Rys. 1 .3 . Dolne oszacowanie sumy
czylij{n) ::::: n3/3, co zgadza się ze wzorem (1.5).
17
W większości przypadków takie oszacowanie jest całkowicie wystarczające. Gdyby
jednak było inaczej , naszą analizę możemy kontynuować, badając błąd przybliżenia
e(n) = j{n) - n3/3 . Ponieważj{n) spełnia warunekj{n) = j{n - l) + n2, więc
1 8 1. Wprowadzenie
e(n) = j{n) - n3/3 = j{n - l ) + n2 - n3/3 = e(n - 1 ) + (n - 1 )3/3 + n2 - n3/3 = = e(n - l ) + n - 1/3 ,
skąd łatwo obliczyć metodą wielokrotnego podstawiania (patrz wzór 1 . 1 8), że e(n) = n(n + 1 )/2 - n13.
Ogólnie, jeślij{-) jest funkcją rosnącą, to
(1.9) b b b+1 f f(x)dx :S; L f(i) :S; ff (x) dx.
a-I i=a a
Podobnie, gdy j{-) jest funkcją malejącą, mamy
( 1 . 1 O) 6+1 b b f f(x)dx :s; L fCi) :S; ff(x)dx. a i=a a-I
1.4. Symbole oszacowań asymptotycznych
W teońi złożoności obliczeniowej kluczową kwestią jest tempo wzrostu l iczby opera
cji wykonywanych przez algorytm do momentu zakończenia obliczeń w miarę wzrostu
rozmiaru danych. Oznacza to, że nie wnikamy, ile czasu będzie wykonywany algorytm dla
konkretnych danych. Istnieje szereg powodów przemawiających za takim uproszczeniem.
Przede wszystkim czas realizacj i programu zależy od konkretnej maszyny, np. częstotliwo
ści zegara i średniej liczby operacj i wykonywanych w ciągu sekundy, a my nie chcemy
rozwijać teorii jednego komputera (taka teoria byłaby interesująca dla jednego tylko czło
wieka -jego właściciela). Poza tym czas wykonania się programu zależy od języka pro
gramowania, jego kompilatora, a nawet od stylu programisty. W praktyce dla porównania
efektywności dwóch algorytmów wystarczy porównanie tempa wzrostu liczby tych operacji ,
których ilość rośnie najszybciej w miarę wzrostu rozmiaru danych. A zatem jesteśmy zain
teresowani głównie asymptotyczną oceną wzrostu tej liczby, abstrahując od stałych współ
czynników proporcjonalności występujących w tej analizie.
W dalszym ciągu skryptu obowiązywać będą następujące oznaczenia:
N = {0, 1 ,2 , . . . } y = { l ,2,3, . . . } R = zbiór liczb rzeczywistych
R+ = zbiór dodatnich liczb rzeczywistych
R* = R+u {O}
1.4.1 . Symbol 0(·) Niech g: R*� R* będzie funkcją rzeczywistą zmiennej x. Przez O(g) oznaczymy zbiór
funkcj i! R*� R takich, że dla pewnego c E R+ i Xo E R* mamy j{x) :S; cg(x) dla wszystkich
x � Xo. Symbol O(g) czytamy "o duże od g", zaś o funkcj i fmówimy, że "jest o duże od g" lub, że "jest co najwyżej rzędu g" .
1.4. Symbole oszacowań asymptotycznych
Przykład 1 .2 2sin x = O(1og x) x3 + sx2
+ 7cos x = 0(x4) 1 1( 1 + x2) = 0( 1 )
1 9
•
Kiedy mówimy, że j{x) jest O(g(x)), to mamy na myśli fakt, że funkcja f nie rośnie szybciej niż g. A zatem j{x) rośnie wolniej lub tak samo szybko. Zauważmy, że może być j{x) = O(g(x)) nawet wówczas, gdy j{x) > g(x) dla wszystkich x, np. 3 + sin x = O( l ) .
Zależność między funkcjamifi g można zwykle stwierdzić badając granicę ich ilorazu, . . . nuanOWlCIe
j{x) = O(g(.x)), gdy lim f(x) = c dla pewnego c ?: O.
x ..... '" g(x)
To znaczy, jeśli granica j1g istnieje i jest różna od aJ, to j{x) rośnie nie szybciej niż g(x). Jeżeli tą granicąjest aJ, to funkcjaj{x) rośnie szybciej niż g(x), czylij{x) * O(g(x)) .
Gdy fi g są funkcjami ciągłymi i różniczkowalnymi, to dla obliczenia granicy możemy skorzystać z reguły de L'Hóspitala.
Jeśli lim f(x) = lim g(x) = aJ, to lim f(x) = lim I'(x) x ..... '" X->CO x ..... '" g(x) x ..... '" g '(x) ,
o ile ta druga granica istnieje. Zauważmy, że w pierwszym przykładzie powyżej moglibyśmy napisać 2sin x =
0(2·log x) lub 2sin x = 0(10g2 X). Jednakże, unika się pisania stałych w nawiasach symboli oszacowań asymptotycznych, gdyż np. 210g x = O(log x), a więc stała * 1 nie wnosi żadnej dodatkowej informacj i. Z tego samego względu unika się podawania podstawy logarytmu, ponieważ, jak wynika z poprzedniego punktu, 10g2 x = ( l/ 1 0gb 2)10gbX. Zatem logarytm przy określonej podstawie może być wyrażony jako logarytm przy dowolnej innej podstawie razy pewna stała proporcjonalności, a tę ostatnią po prostu pomija się.
1 .4.2. Symbol 0(')
Niech g: R* -+ R* będzie funkcją zmiennej x. Przez o(g) oznaczymy zbiór funkcj i f R* -+ R takich, że dla dowolnego c E R+ istnieje Xo E R* takie, żej{x) < cg(x) dla wszystkich x ?: Xo'
Przykład 1 .3 X2 = o(xs) 1Ix = 0(1) 1 4 .[; = o(x).
Z definicji tej wynika, że
j{x) = o(g(x)), gdy lirn f(x) = O .
x ..... '" g(x)
•
20 l. Wprowadzenie
Kiedy mówimy, że j{x) jest o(g(x» , to rozumiemy, że funkcja I rośnie wolniej niż g. Zatem symbol 00 niesie więcej informacj i niż 0(·), ponieważ wiemy wówczas nie tylko, że
j{x) jest zdominowana przez g(x) dla prawie wszystkich x, ale i to, że iloraz j7g � O. Jednakże, w większości przypadków praktycznych wystarcza oszacowanie przez O(J
1 .4.3. Symbol n(·)
Definicja symbolu Q(g) jest dualna wobec defmicj i O(g). Mówiąc nieprecyzyjnie, symbol ten jest negacją 0(') w tym sensie, że j{x) = Q(g(x» oznacza, że j{x) #. o(g(x» . Zatem Q(g) jest dolnym oszacowaniem tempa wzrostu funkcj i! Formalna definicja tego symbolu jest następująca.
Niech g: R* � R*. Przez Q(g) rozumiemy zbiór funkcj i ! R* � R* takich, że dla pewnego c E R+ i pewnego Xo E R*, g(x) � cj{x) dla wszystkich x ;?: Xo'
Przykład 1 .4 (2x + 1 )2 = Q(x2) xlg x = Q(x) x + 2sin 2x = Q(x). •
Najprostszą metodą pokazania, że 1= Q(g), jest wykazanie, iż g = O(f). Inną metodą jest skorzystanie z własności:
j{x) = Q(g(x» , jeśli l iro I(x) = 00 lub l iro I(x)
= c > O . . HOO g(x) x ..... oo g(x)
Oczywiście, dla obliczenia tej granicy można zastosować regułę de L'Hóspitala.
1 .4.4. Symbol 00(') Symbol ro(g) niesie więcej informacj i o dolnym oszacowaniu tempa wzrostu funkcj i
Iniż Q (g) , podobnie jak o(g) niesie więcej informacj i o górnym oszacowaniu niż O(g). Formalna definicja jest następująca.
Niech g: R* � R*. Symbol ro(g) jest zbiorem funkcji! R* � R* takich, że dla dowolnej stałej c > O istnieje stała Xo E R+ taka, że g(x) < cj{x) dla wszystkich x ;?: Xo'
Przykład 1 .5 X
2 = U>(x Fx ) 2x - 2 = w(log x) 3X = w(2X). • Symbolu tego używamy do określenia dolnego tempa wzrostu funkcj i/, która rośnie istotnie szybciej niż w(g). Z definicj i powyższej wynika, że symbol ten wyklucza się wzajemnie z 0(·), czyli j{x) = ro(g(x» oznacza, że j{x) #. O(g(x». Co więcej , j{x) = ro(g(x» wtedy i tylko wtedy, gdy g(x) = o(f{x» .
.4. Symbole oszacowań asymptotycznych 2 1
Zależność między funkcjamifi g można zwykle stwierdzić badając granicę ich ilorazu, ::nianowicie
f(x) = ro(g(x» , jeśli lim f(x) = 00 . x ..... oo g(x)
Oczywiście, gdy f i g są ciągłe i różniczkowalne, to dla obliczenia granicy możemy skorzystać z reguły de L'Hóspitala.
1 .4.5. Symbol 8(·)
Niech g: R* � R*. Mówimy, żej(x) jest 0(g(x» , gdy istnieją stałe C " C2 E R+ i Xo E R* :.akie, że c ,g(x) <.:;,j(x) <.:;, czg(x) dla wszystkich x � Xo' Mówimy wówczas również, że funkcje ::'i g są tego samego rzędu lub, że f jest dokładnie rzędu g.
rzykład 1 .6
�3 +.fi; = 0(X"4) l + 3/xr = 0(l) x� + 5x)/(x3 + l) = 0(l/x). •
Symbol 0(·) jest znacznie bardziej precyzyjny niż 00 i 0(-) . Na przykład, jeśli wiemy, żej(x) = 0(x2), to wiemy, żej(x)/x2 zawiera się pomiędzy dwiema niezerowymi stałymi dla
rawie wszystkich x. Tempo wzrostu funkcj i j(x) jest ustalone: rośnie ona z kwadratem x. Z definicji symbolu 0(-) wynika, że 0(g) = O(g) (\ O(g) oraz że
3 więc c '* O.
j(x)= 0(g(x)), jeśli lim f(x) = c dla pewnego c E R+, .HOO g(x)
1 .4.6. Symbol 8 (-) Coraz częściej w literaturze fachowej pojawiają się symbole ES (g) i 5 (g). Mówią one,
że funkcjaf jest tego samego rzędu co g z dokładnością do czynnika logarytmicznego, czyli J = ES (g), jeżeli istnieje stała k > O taka, że f=0(glogkg). Formalna definicja symbolu ES (g) jest następująca.
Niech g: R* � R*. Mówimy, że j(x) jest ES (g(x» , gdy istnieją stałe c " c2,kER* i xQ E R*
takie, że c,g(x)logkg(x) <.:;,j(x) <.:;, czg(x)logkg(x) dla wszystkich x 2: XQ. Zatem funkcjaj(x) jest szacowana z dołu przez g(x), lecz rząd ich obu jest w przybliżeniu ten sam.
Przykład 1 .7 x�logx = ES (x) , 3 - 2 x-log x + x = 0 (x )
2Xloglogx = 0(2' ) .
.--
22 1. Wprowadzenie
Podobnie w literaturze spotyka się notację 0'0. Jest to notacja 0(·) z pominięciem czynników wielomianowych, które są mniej istotne od czynników wykładniczych. Na przykład występujący w tabeli 2. 1 . symbol e(n23n) możemy zapisać jako e'(3n).
Ilustrację zakresów działania omówionych wyżej symboli oszacowań asymptotycznych zawiera rys. 1 .4.
Rys. l A. Ilustracja zakresów działania symboli oszacowań asymptotycznych
1.5. Równania rekurencyjne niejednorodne
W punkcie tym podamy sposoby rozwiązywania wybranych równań rekurencyjnych niejednorodnych, to jest takich, w których n-ty wyraz nie zależy wyłącznie od wyrazów poprzednich, ale i od pewnej funkcji zmiennej n. Równania takie są naturalnym sposobem opisywania złożoności obliczeniowej algorytmów rekurencyjnych (ang. recursive algorithms), tzn. takich, które wywołują same siebie.
1 .5 . 1 . Równania typu "dziel i rządź"
Rozważania nasze rozpoczynamy od równania postaci
( 1 . 1 1 ) T(n) = {C, aT(n/b) + d(n),
gdy n = 1 gdy n = bk
Z równaniami takimi spotykamy się w przypadku podziału problemu rozmiaru n na a podproblemów, każdy rozmiaru n/b. Takie podejście do rozwiązywania trudnych problemów obliczeniowych nosi nazwę dziel i rządź (ang. divide-and-conquer).
Rozwiązując ( 1 . 1 1 ) metodą wielokrotnego podstawiania, otrzymujemy
T(n) aTen/b) + den)
a(aT(n/b2) + den/b)) + den) = a2T(n/b2) + ad(n/b) + den)
a3T(n/b3) + a2d(n/b2) + ad(n/b) + den)
1.5. Równania rekurencyjne niejednorodne
i-l aiT(n/b) + La} den/b} ) dla pewnego i � k, gdzie k = logb n.
}=o
Korzystając z faktu, że T(n/b') = T(l) = c, otrzymujemy
1 . 12 ) k-l
k .
k · T(n) = ca + I aJ d(b -J ). }=o
23
Przypominamy, że k = 10gb n. Wówczas pierwszy wyraz można zapisać jako calogbn lub cn
loghU. A więc jest to składnik wielomianowy. Dla przykładu, gdy a = b = 2, to ak = n i cak = O(n). Ogólnie, im większe jest a, tzn. im więcej trzeba rozwiązać podproblemów, � większy będzie wykładnik. Podobnie im większe jest b, to znaczy im mniejsze będą poszczególne podproblemy, tym mniejszy będzie wykładnik.
Obecnie przyjrzyjmy się obu wyrażeniom we wzorze ( 1 . 12) . Pierwsze, czyli cak lub cnloglfl, nazywamy rozwiązaniem jednorodnym (ang. homogeneous solu/ion). Rozwiązanie m byłoby rozwiązaniem ogólnym (ang. general solution), czyli rozwiązaniem całości, gdy
y funkcja den) = O dla wszystkich n. Funkcję den) nazywamy funkcją wiodącą (ang. driving junction). Zauważmy, że rozwiązanie jednorodne reprezentuje koszt rozwiązywania
-szystkich podproblemów. Drugi człon ( l . 1 2) nazywamy rozwiązaniem szczegółowym (ang. particular solution).
Rozwiązanie to jest zależne od funkcji wiodącej i od parametrów a i b. Mówiąc ogólnie, gdy rozwiązanie jednorodne dominuje nad funkcją wiodącą, to rozwiązanie szczegółowe ma identyczne tempo wzrostu jak rozwiązanie jednorodne i tym samym wszystkie trzy rozwiązania: ogólne, szczegółowe i jednorodne mają asymptotycznie takie samo tempo �LIostU.
Obecnie oszacujemy tempo wzrostu rozwiązania szczegółowego w przypadku ogólnym. W tym celu przyjmiemy pewne uproszczenie dotyczące funkcji wiodącej . Zakładamy mianowicie, że den) jest fonkeją iloczynową (ang. multiplicative), tzn. taką, że
_ xy) = j(x)f{y) dla wszystkich x,y E N. Dla przykładu, funkcja typu nO. jest iloczynowa, ponieważ dlaj(n) = nO. mamy (xy t = xo.yo.. Tak więc, skoro funkcja den) jest iloczynowa, to d(bk-i) = d(bt/d(bY = d(bf-i. Zatem
1 . 1 3) Ia}d(b/-}= d(b/ I(_a_)} = d(b/ (a/d(b))k - 1
= ak _ d(b)k
, }=o }=o d(b) a/d(b) - 1 a/d(b) - l
o ile a "* d(b).
Obecnie rozważymy trzy przypadki szczególne.
1 . Gdy a > d(b), to wyrażenie ( 1 . 1 3 ) jest O(ak). W tym przypadku oba rozwiązania składowe są tego samego rzędu, gdyż są zdominowane przez ak = n
lOglfl, które zależy jedynie
od wartości a i b. Zmniejszanie funkcj i den) jest tu bezcelowe.
2. Gdy a < d(b), to ( 1 . 1 3) jest O(d(b/), lub - co równoważne - O(n1og!Jd(b)) . W tym
przypadku rozwiązanie szczegółowe dominuje nad jednorodnym. Dlatego wszelkie ulep-
24 1. Wprowadzenie
szenia T(n) mogą pochodzić od zrrmiejszenia den) i b. Zwróćmy uwagę na ważny przypadek szczególny, taki jak w przykładzie, czyli den) = no.. Wówczas d(b) = bO. i logb(bo.) = a. Zatem rozwiązanie szczegółowe i ogólne jest O(no.) = O(d(n)).
3. Gdy a = d(b), to we wzorze ( 1 . 1 3 ) mamy dzielenie przez zero. Zatem rozwiązanie szczegółowe winniśmy oszacować inaczej . Mianowicie
( 1 . 1 4)
Skoro a = d(b), to rozwiązanie ( 1 . 1 4) jest logbn razy większe od jednorodnego i ponownie dyktuje rozwiązanie ogólne. W powyższym przypadku szczególnym, gdy den) = nO., to mamy d(b) = bO., więc ( 1 . 1 4) sprowadza się do O(d(n)- Iogn).
Rozważania nasze podsumujemy w postaci następującego twierdzenia.
Twierdzenie 1 .1 . Niech a i b będą stałymi dodatnimi, zaś d(n) fonkeją iloczynową. Rozwiązaniem równania rekurencyjnego postaci
T(n) = ' {8(1)
aTen/b) + den),
gdzie k jest liczbą naturalną, jest funkcja
k-I
gdy n = l gdy n = bk
T(n) = 8(ak ) + Lajd(bk-j ), j=O
o wartościach
Przykład 1 .8 Rozważmy następujące równania rekurencyjne: 1) T(n) = 4T(n/2) + n 2) T(n) = 4 T(n/2) + n2
3) T(n) = 4T(n/2) + n3
przy czym w każdym przypadku T(l) = 1 .
gdy a < d(b) gdy a = d(b) gdy a > d(b). •
Zauważmy, że a = 4, b = 2, więc rozwiązanie jednorodne wynosi dokładnie nlg4, czyli n2• 1 . W pierwszym równaniu mamy den) = n, czyli d(b) = 2. Ponieważ 4 = a > d(b) = 2,
więc rozwiązanie szczegółowe jest również kwadratowe. Zatem T(n) = 8(n2). 2. W drugim równaniu mamy d(b) = d(2) = 4 = a, więc stosujemy wzór ( 1 . 1 4). Ponie
waż den) jest postaci nO., więc rozwiązanie szczegółowe i tym samym T(n) są postaci 8(n210g n). W tym przypadku możemy również napisać, że T(n) = El (n2).
3. W trzecim równaniu mamy den) = n3 i d(b) = d(2) = 8, czyli a < d(b). Przeto rozwią-
1.5. Równania rekurencyjne niejednorodne 25
zanie szczegółowe jest O(n 1ogb"'(b») = O(n3) i również T(n) = 8(n\ Widzimy zatem, że w
,5tocie rozwiązanie szczegółowe jest tego samego rzędu co den) = n3, a więc że jest zdeterminowane funkcją wiodącą. •
1 .5.2. Równania typu "jeden krok w tył"
Obecnie rozważymy rekurencyjne równanie niejednorodne postaci
1 . 15) T(n) = {C,
aTen - l) + den),
gdy n = l gdy n > l
Takie równania niejednorodne pojawiają się na przykład przy rekurencyjnym rozwią�waniu problemów wykładniczych, gdy dla rozwiązania problemu rozmiaru n korzystamy
z rozwiązania podproblemu rozmiaru n - L Równania typu ,jeden krok w tył" nazywamy furmalnie równaniami rekurencyjnymi pierwszego rzędu (ang. fzrst-order), ponieważ ::lowa wartość ciągu j est obliczana na podstawie tylko jednej wartości bezpośrednio poprzedzającej .
Równanie ( 1 . 1 5) moglibyśmy próbować rozwiązać jak poprzednio metodą wielokrot::tego podstawiania, jednakże szybko otrzymalibyśmy bardzo skomplikowaną formułę. Dla:ego dokonamy najpierw podstawienia
1 . 1 6)
orrzymując
zyli
T(n) = anU(n) dla n � 0,
U(n) = U(n - l) + d(n)/an.
Przyjmując dla uproszczenia, że e(n) = d(n)/an dla n = l , 2, . . . , otrzymujemy uproszczoną postać
1 . 1 7) U(n) = U(n - l ) + e(n).
Rozwiązanie równania ( l . 1 7) jest już dość łatwe. Mianowicie:
[;( l ) = c + e(l)
(;(2) = U(l) + e(2) = (c + e(l)) + e(2)
U(3) = U(2) + e(3) = (c + e(l) + e(2)) + e(3)
n (j(n) = c + e(l) + e(2) + . . . + e(n) = c + L e(j) .
j=1 Obecnie powracamy do ( 1 . 1 6) , aby wyrazić nasze rozwiązanie w terminach zmiennych
T(n).
( 1 . 1 8) n .
T(n) = can + L an-J d(j) . j=1
� ----
26 1. Wprowadzenie
Widzimy, że jak poprzednio ogólne rozwiązanie równania ( 1 . 1 5) jest sumą rozwiązania jednorodnego, uzyskanego przy założeniu, że funkcja wiodąca den) = O, i rozwiązania szczegółowego. Rozwiązanie to jest 0(an), gdy rozwiązanie jednorodne dominuje nad tempem wzrostu sumy szeregu związanego z den).
Obecnie oszacujemy tempo wzrostu rozwiązania szczegółowego. W tym celu założymy, że funkcja wiodąca jest monotoniczna (ang. monotonic) względem an, tzn. d(n)/an jest funkcją niemalejąca lub nierosnącą. Poniżej rozważymy cztery przypadki szczególne.
l. Gdy den) = O(an/n"), E > l , to dla każdego j = l , . . . , n istnieje stała c taka, że d(j)j· < cd. Zatem d(l)/al + . . . + d(n)/an < c( 1 I 1 " + . . . + l in") � ccl, gdzie CI jest granicą szeregu, gdyż szereg harmoniczny rzędu E > l jest zbieżny. Obecnie mnożąc obustronnie przez an, otrzymujemy d(l)an-I + . . . + d(n)ao < cclan = 0(an). Ponieważ can jest również rzędu 0(an), więc T(n) = can + 0(an) = 0(an).
2. Gdy den) = o(an), to dla każdego i = l , . . . , n d(i)/ai < Ci, gdzie Ci są stałymi takimi, że
Ci > Ci+1 oraz lim Ci = O. Zatem d(l)/al + . . . + d(n)/an < (CI+" '+Cn) = o(n), gdyż średnia arytme
tyczna ciągu dąży do zera, gdy ciąg zmierza do zera. Czyli d(l)an-I + . . . + d(n)ao = o(na). Ponieważ rozwiązanie jednorodne can = o(nan), więc T(n) = o(nan).
3. Gdy den) = 0(an), to istnieją stałe CI i C2 takie, że dla wszystkichj mamy C I � d(j)/d � C2, czyli cln � d(l)/a + . . . + d(n)/an � C2n. Mnożąc obustronnie powyższe nierówności przez an, otrzymujemy d(l)an-I + . . . + d(n)ao = 0(nan). Zatem rozwiązanie szczegółowe dominuje nad jednorodnym i w konsekwencji T(n) = 0(nan).
4. Gdy den) = co(an), to rozpatrzymy dwa przypadki. Jeżeli a = l , to d(l)an-I + . . . + d(n)ao = d(l) + . . . + den) � nd(n). Jeżeli a > l , to den) = a"j(n), gdzie jen) jest funkcją niemalejącą. Dlatego dla każdegoj d(j)an-J = d!U)an-J = a"j(j) � a"j(n) � den). Wobec tego d(l)an-I
+ . . . + d(n)ao � nd(n) = O(nd(n» . Zatem rozwiązanie szczegółowe ponownie dominuje nad jednorodnym, czyli T(n) = O(nd(n» .
Rozważania nasze podsumujemy w postaci następującego twierdzenia.
Twierdzenie 1 .2. Niech a będzie stalą takCŁ że a ;::: 1 , zaś d(n) funkcją monotoniczną względem an. Rozwiązaniem równania rekurencyjnego postaci
jestjunkcja
o wartościach
Przykład 1 .9
T(n) = ' {B(l) aTen - 1) + den),
gdy n = 1 gdy n > 1
n T(n) = 0(a) + Lan-J d(j) ,
J=I
{0(an), T(n) = O(nan),
O(nd(n» ,
gdy den) = O(an In" ), E > l gdy den) = O(an ) gdy d(n) = m(an ) •
Rozważmy równanie T(n) = 3 T(n - l ) + n przy warunku T(O) = O. Oczywiście, den) = n i n l+" = 0(3n), zatem nasze rozwiązanie jest rzędu 0(3n). Jednakże interesuje nas również
!.5. Równanźa rekurencyjne nźejednorodne
rozwiązanie dokładne. Dlatego podstawiając do ( 1 . 1 8) , otrzymujemy
T(n) = 3" · fi }=I Y
27
.-\by obliczyć powyższą sumę, skorzystamy ze wzoru ( l .8). Obliczając pochodną obu stron 3>żsamości względem zmiennej x, otrzymujemy
l 2 3 2 n-I _ (n + l)x" (x - l) - (x"+I - l) _ x" (nx - n - l) + l
+ x + X + . . . + nx - 2 - 2 (x - l) (x - l)
Obecnie rrmożymy obustronnie przez x
2 2 3 3 " _ x(x" (nx - n - l) + 1) x + x + x . . . +nx - 2 (x - l)
Podstawiając teraz x = 1 /3 dostajemy
-:zyli
ti = 3((1 / 3)" (n / 3 - n - I) + 1
}=I y 4
T(n) = 3" (3( 1 1 3) " (n /3 - n - l) + 1 )/4 = (3 · 3" + 3(n/3 - n - 1)) / 4 = (3"+1_ 2n - 3)/4.
Doprawność tego rozwiązania można sprawdzić metodą indukcji zupełnej . • Jak widzimy, dokładne rozwiązanie równania ( l . 1 5 ) wymaga znalezienia sumy szeregu
:'I1ikającego z istnienia funkcj i wiodącej . Obliczenia te można uprościć dla pewnych ty. ?Owych wartości funkcji den). Przyporrmijmy, że rozwiązanie ogólne jest sumą rozwiązania . ednorodnego i szczegółowego. W naszym przypadku równanie jednorodne ma postać T(n) = aTen - l ), więc jego rozwiązanie ogólne wyraża się wzorem T(n) = Aan, gdzie A jest �wną stałą proporcjonalności. Przypuśćmy, że T*(n) jest pewnym rozwiązaniem szczegó.owym dla ( 1 . 1 5), tzn. T(n) = aT*(n - l ) + den). Wówczas T(n) musi spełniać
T(n) = Aan + T*(n) = (aAan - ') + (aT*(n - l ) + den)) = = a(Aan - ' + T*(n - l )) + den) = aTen - 1 ) + den).
-tałą A w rozwiązaniu ogólnym dobiera się tak, aby spełniała warunek początkowy. W tym -elu należy ustalić rozwiązanie szczegółowe, a następnie obliczyć T*(O).
Gdy a :f. l , to znamy ogólną postać rozwiązań szczegółowych dla typowych postaci jmkcji den). Mianowicie, rozwiązaniem szczegółowym równania rekurencyjnego postaci : . 1 5) jest funkcja
r Bln + Bo , pen) = 2 B2n + Bln + Bo ,
Bd" ,
gdy den) = d
gdy den) = dn
gdy den) = dn2
gdy den) = d" ,
28 l. Wprowadzenie
gdzie B, Bo, Bh . . . są stałymi, które należy obliczyć z warunków początkowych. Łatwo zauważyć, że rozwiązanie ogólne jest w tym przypadku ograniczone przez
O(an) lub O(c!') w zależności od tego, czy stała a > d. Jedynym wyjątkiem jest przypadek, gdy rozwiązanie szczegółowe jest jednocześnie rozwiązaniem równania jednorodnego. Jednakże mamy wówczas T(n) = O(nan).
Przykład 1 . 1 0 Rozważmy problem wież w Hanoi (ang. to wers oj Hanoi). Jest to łamigłówka wymyślona przez E. Lucasa w 1 883r. , złożona z trzech pionowych pałeczek i różnej wielkości krążków, które nasadzono na pierwszą z nich w ten sposób, że średnice krążków rosną ku podstawie. Zadanie polega na przeniesieniu n krążków z pierwszej pałeczki na trzecią przy ograniczeniu, że w jednym kroku przenosimy tylko jeden krążek i nie wolno kłaść krążka o większej średnicy na krążek o mniejszej średnicy. Druga pałeczka spełnia rolę pomocniczą. Łatwo zauważyć, że liczba przeniesień podwaja się przy wzroście liczby krążków o l . Zatem czas działania odpowiedniego algorytmu rośnie proporcjonalnie do funkcji 2n. Z zadaniem wież w Hanoi związana jest legenda głosząca, że w pewnym klasztorze w Hanoi mnisi buddyjscy przenoszą 64 złote krążki w tempie l krążek na sekundę. Z chwilą przeniesienia ostatniego krążka nastąpi koniec świata. Zatem ile nam zostało jeszcze czasu? (264- 1 sekund to 500 miliardów lat. Wiek Ziemi ocenia się na około 4 .5 miliarda lat. Więc zostało nam jeszcze sporo czasu).
Rys. 1 .5 . Wieże w Hanoi
Rozwiążemy równanie rekurencyjne T(n) = 2 T(n - l ) + l przy warunku T(l) = l . Zauważmy, że zależność ta określa liczbę przeniesień krążków w problemie wież w Hanoi (patrz zadanie 1 .4). Ogólne rozwiązanie równania jednorodnego postaci T(n) = 2 T(n - l ) jest oczywiście T(n) = A2n. Ponieważ w tym przypadku den) = l , jako rozwiązanie szczegółowe wybieramy T*(n) = B. Podstawiając do równania wyjściowego, otrzymujemy
B = T*(n) = 2 T*(n - l ) + l = 2B + l ,
czyli B = -l . Zatem T*(n) = - l jest rozwiązaniem szczegółowym, zaś rozwiązanie ogólne
T(n) = A2n + T*(n) = A2n - l .
Obecnie określamy A z warunku początkowego l = T(l) = Al ' - l otrzymując, że A = l . Zatem poszukiwanym rozwiązaniem ogólnym jest T(n) = 2n_ l . Poprawność tego rozwiązania można sprawdzić metodą indukcji . •
Zadania 29
Zadania
1 . 1 . Załóżmy, że istnieje procedura funkcyjna WlasnośćStopu, która prawidłowo odpowiada na �tanie, czy dany algorytm kończy się. Zastosuj ją do oceny następującej funkcji zagadka.
function zagadka: boolean; begin
end;
whiJe WlasnośćStopu(zagadka) do begin end
1 .2 . Przeanalizuj działanie następującego programu. Zaimplementuj go w dowolnym języprogramowania. Wydrukuj maksymalną wygenerowaną liczbę k dla wartości początko
.·ych k = 3 1 i k = 32. Znajdź możliwie największy stosunek liczby obiegów pętli repeat do �k. Zweryfikuj spostrzeżenie Gao mówiące, że liczby od 7083 do 7099 wymagają wyko
:::mia takiej samej liczby obiegów pętli repeat (jaka to liczba?). Czy dopuszczenie ujem-= ch liczb całkowitych zmienia status tego problemu jako przypuszczalnie niealgorytmicz--ego?
procedure Collatz; begin
end;
read(k); repeat
if even(k) then k := kl2 else k := 3k + l ; until k = l
l:waga! Hipoteza Collatza została zweryfikowana dla wszystkich k :=; 5 ' 1 018•
13. Zbadaj "burzę gradową" (problem Collatza) z regułą: if even(k) then k := kl2 else k := 3'- - 7. Czy program kończy się dla każdego k?
1 .4. Poniższą procedurę w PseudoPascalu, rozwiązującą problem wież w Hanoi, rozwiń ":u postaci pełnego programu pascalowego. Zmierz czas jego wykonania dla liczby krążków
= 2, 3 , . . . , 1 2 .
procedure Hanoi(A,B,C: pałeczka; n: integer); begin
end;
if n = l then przenieś(A,C) else begin
end
Hanoi(A,C,B, n - l ); przenieś(A,C); Hanoi(B,A,C, n - l )
30 1. Wprowadzenie
1 .5. Przeanalizuj działanie następującego programu. Zaimplementuj go w dowolnym języku programowania. Sprawdź jego zachowanie dla n � 1 000.
procedure Goldbach; begin
k := 2; ListaPierwszych := { 2 } ; następny := true; wbite następny do begin
k := k + 2; if k -l jest pierwsza tben dołącz k -l do ListaPierwszych; if dla wszystkich par p,q w ListaPierwszych p + q "* k
end;
end; write(k)
then następny := false
Uwaga! Hipoteza Goldbacha została zweryfikowana dla wszystkich 4 � k � 4. 1 0 14.
1 .6. Zastosuj metodę całkową do oszacowania wartości funkcji n ! . Otrzymane rozwiązanie
porównaj ze wzorem Stirlinga: n ! "" (n/e)17 .J2nn . Wskazówka: Oszacuj wpierw ln(n ! ).
1 .7. Udowodnij , że
fi = (n(n + l)) 2
;=0 2
1 .8. Które z poniższych zależności są prawdziwe?
a) (x2 + 3x + 1 )3 = o(x6) g)
b) (.[; + 1)
= 0(1 ) h) 2
c) el/x = 0( 1 ) i)
d) � = 0(1) j) x
e) x3(log(log x)/ = o(x3log x) k)
f) �logx + l = 8(log log x) I)
2 + sinx = .0( 1 ) cos x = 0(1)
x sdt = O(ln x) 4 t
t --i = 0(1) j=1 }
x 2) = 8(x) j=1 x fe-t2 dl = 0(1) o
Zcdania 3 1
i .9. Które z symboli oszacowań asymptotycznych są przechodnie, tzn. jeśli f= O(g) i g = � . to czy f= OCh)?
i . 1 0. Poniższe funkcje ustaw w rosnącym porządku rzędów wzrostu dla dużych n, tzn. wje tak, że każda jest 0(') od następnej .
2.;; elog"J 3 .0 1 2,,1 , , n ,
1 . 1 1 . Znajdź funkcjęj{x) taką, że
j{x) = O(xl+E)
prawdziwe dla każdego !; > 0, ale nie zachodzij{x) = O(x) .
. 1 2 . Znajdź dwie funkcje rosnące i różniczkowalne j, g: R*-t R* takie, że = Q{g) u O{g) i g "* D.(j) u O(j) .
. 13. iech TI(n) = D.(f{n)) i Tz(n) = D.(g(n)). Czy to prawda, że:
TI(n) + T2(n) = D.(f{n) + gen))
T,(n) * Tz(n) = D.(f{n)g(n)).
1 . 1 .. t. Uporządkuj podane funkcje pod względem tempa wzrostu: ( 1 Jn ( 3 Jn ( J'OglJ
2�, 2n, .rn, logn, log logn, lo� , .rn lOg n, "3 ' "2 , 1 7, %
1 . 1 6. Dane są funkcje j{n) = n/3 i gen) = ( l /3r. Odpowiedz, czy jen) = O{g(n)),
: = O(f{n)) itd. dla pozostałych symboli oszacowań asymptotycznych.
1 . 1 7. Czy funkcjaj{n) = 2';; rośnie szybciej niż:
logn, lecz wolniej niż .r;; ?
.r;; , lecz wolniej niż n?
n, lecz wolniej niż nZ?
n2, lecz wolniej niż ..r;; ?
..r;; , lecz wolniej niż 2"?
32
1 . 1 8. Udowodnij , że In 2x = O(XO.O I ) .
1 .1 9 . Oszacuj rozwiązanie równania postaci
T(n) = 2T(n/2) + log2n,
przyjmując, że T(l) = o.
1 .20. Oszacuj rozwiązanie równania postaci
T(n) = 2 T( L ..Jn J ) + In n, gdzie T(l) = O.
Wskazówka: Przyjmij m = In n.
1 .2 1 . Rozwiąż dokładnie równanie rekurencyjne
T(n) = 8 T(nl2) + n3, gdzie T(l) = l
i sprawdź swoje rozwiązanie metodą indukcji matematycznej .
1 .22. Oszacuj rozwiązanie równania rekurencyjnego dla T(l) = l
T(n) = 3 T(n/2) + 2n..Jn .
Wskazówka: Przyjmij , że U(n) = T(n)/2 dla wszystkich n.
1 .23. Rozwiąż następujące równania rekurencyjne:
a)
b)
c)
T(n) = T(n - l) + 3(n - l ), T(n) = T(n - l ) + n(n + l ), T(n) = T(n - l ) + 3n2,
T(O) = l T(O) = 3
T(O) = 1 0
1 .24. Rozwiąż następujące równania rekurencyjne:
a)
b)
c)
T(n) = 3 T(n - l ) - 2,
T(n) = 2 T(n - l) + n,
T(n) = 2 T(n - 1 ) + (-lr,
T(O) = O T(O) = l T(O) = 2 .
1. Wprowadzenie
1 .25. Zakładając, że T( l ) = l , oszacuj rozwiązanie poniższego równania rekurencyjnego
Wskazówka: Przyjmij, że U(n) = ren) dla wszystkich n.
1 .26. Przyjmując, że T(l) = l , znajdź dokładne rozwiązanie równania rekurencyjnego
T(n) = aT(n/2) + den)
dla każdego z następujących przypadków szczególnych:
a) a = l , d(n) = e; b) a = 2, d(n) = e; c)
d) a i' 2, a = 2,
den) = en; den) = en.
Zadania
1 .27. Rozważ funkcję F(x) zdefiniowaną następująco:
if even(x) then F := x div 2 else F := F(F(3x + 1 ));
�-dowodnij , że F(x) kończy się dla wszystkich x. --rskazówka: Rozważ liczby całkowite postaci (2i + 1 )2k - l i zastosuj indukcję.
1 .28. Poniższe dwie procedury definiują tę samą funkcję./Cn).
functio n p l(n: integer): integer; begin
if n � 2 then retum(2*n) else retum(2*pl(n - 1) -pl(n - 2))
end;
function p2(n: integer) : integer; begin
if n � 3 then retum(2*n) else return(p2(n - l) + p2(n - 2) -p2(n - 3))
end;
_ Oszacuj złożoności obliczeniowe procedur p l i p2. _ apisz procedurę pO, która oblicza wartość funkcj i./Cn) w czasie 0(1).
33
: Pokaż, że istnieje nieskończona liczba procedur rekurencyjnych definiujących funkcję ./Cn).
['waga! Zadanie 1 .28 podaje przykład funkcj i, dla której istnieje nieskończenie wiele algorytmów obliczających jej wartość. W ogólności istnieją funkcje, dla których nie ma żadnych procedur obliczających.
1 .29. Pokaż, że równanie rekurencyjne
T(n) = ' {0(1) T(n /p(n)) + 1,
gdy n � no gdy n > no
ma rozwiązania: T(n) = O(logn), T(n) = O(loglogn), T(n) = 0( 1 ), T(n) = log*n,
gdy pen) = 2 gdy pen) = ..Jn gdy pen) = en gdy pen) = n/logn.
['waga! Funkcja log*n mówi, ile razy należy zastosować logarytm przy podstawie 2, aby sprowadzić wynik do wartości � 1 .
1 30. Dany jest liczący 4 tysiące lat algorytm o nazwie zagadka
function zagadka(a,n: integer): integer; begin
34
end;
if n = O then return(1 ) else begin
end;
half := zagadka(a,LnI2J) ; half:= half * half; if odd(n) then half:= half * a
return(haij)
a) Jaka jest jej złożoność obliczeniowa zagadki? b) Co oblicza zagadka, gdy n = 1 5?
l. Wprowadzenie
c) Podaj inny sposób obliczenia wartości zagadka(a, 1 5), wymagający jedynie 5 mnożeń.
1 .3 1 . Niechf R�R będzie dowolną funkcją ciągłą, zaś a, b takinli liczbanli rzeczywistynli, że a < b. Ustal, ponliędzy którynli z poniższych liczb:
VI = nlin./{x), V2 = nlin./{ Ix l ), V3 = min l./{x)l, V4 = I min./{x)l,
Vs = nlin l./{ I xl ) l, V6 = I nlin./{ I x l ) l, V7 = I nlin I./{x) l l, V8 = I nlin l./{ I x l ) I I zachodzą relacje równości i nierówności, gdzie nlinimum jest rozciągnięte na wszystkie wartości xE [a, b]. Następnie narysuj digraf, którego wierzchołkanli są liczby V I , . . . , V8, a łuk łączy wierzchołek Vi z wierzchołkiem Vj' o ile Vi � Vj'
1 .32. Bardzo efektywny sposób obliczania rozwinięcia liczby n polega na wykorzystaniu następującego związku
� = 1 2 . � (-1/ (6k) ! . (1 359 1 409 + 545 140 l 34 k)
n to (3k) !(k!) 3 · 6403203k+1 .5
Już pierwszy wyraz tego szeregu (k = O) daje l 3 znaczących cyfr liczby n, a dodanie kolejnego wyrazu polepsza dokładność o mniej więcej dalszych 1 4 cyfr. Jest to naj lepszy odtajniony wzór służący do wyznaczania wartości n. Zaimplementuj odpowiedni algorytm i znajdź 1 000 pierwszych cyfr rozwinięcia.
1 .33. Zbadaj rozstrzygalność następujących problemów: a) Dany jest skończony łańcuch w; czy w jest prefiksem rozwinięcia dziesiętnego liczby n? b) Dany jest program i dane dla niego d; czy wynik działania programu na danych d jest
rozwinięciem dziesiętnym liczby n?
2. PODSTAWY ANALIZY ALGORYTMÓW
2.1. Wstęp
Mówiąc bardzo nieformainie, algorytm (ang. algorithm) to pewien opis sposobu postępowania, które prowadzi do osią"anięcia zamierzonego celu. Określenie to jest na tyle ogólne, że mieści w sobie tak przepisy kulinarne, jak i programy komputerowe. Samo słowo �algorytm" pochodzi od nazwiska matematyka arabskiego Abu Ja'far Mohammed ibn Musa :U Khowarizmi, który żył w IX wieku na terenie obecnego Iraku. To nazwisko pisane po mcinie przyjęło postać Algorismus.
Każdy algorytm składa się ze skończonej sekwencj i kroków, z których każdy wykonu�e jedną lub więcej (ale zawsze skończoną liczbę) operacji (ang. operations). Oczywiście, ruda taka operacja musi być jednoznacznie określona (ang. uniqually determined), przez -o rozumiemy, że np. polecenia typu
x := 5/0
x := 5 lub 6
::tie są dozwolone. Inną ważną cechą każdej operacji jest jej skończoność (ang. finiteness) rozumiana w tym sensie, że każdy krok winien być wykonywalny przez człowieka lub maszynę w skończonym czasie. Przykładem operacj i skończonej jest wykonywanie dowolnej operacj i arytmetycznej na liczbach całkowitych, natomiast wykonywanie takich operacj i na liczbach rzeczywistych niekoniecznie prowadzi do operacj i skończonych, ponieważ liczby :akie mają często nieskończone rozwinięcia dwójkowe.
Ogólnie, badania algorytmów można rozpatrywać z co najmniej czterech różnych punktów widzenia. Można mianowicie zapytać:
1 . W jaki sposób tworzyć algorytmy? Pytanie to dotyczy inwentyki algorytmów. Jest to 5ZtUka, która zapewne nigdy nie zostanie w pełni zautomatyzowana. Tym niemniej jednym z celów dodatkowych tego wykładu jest dokonanie przeglądu różnych technik programo. an.ia, które okazały się skuteczne w dotychczasowych implementacjach.
2. W jaki sposób przedstawiać algorytmy? Jak wiadomo, istnieje wiele metod, poczy�jąc od opisu słownego (spotykanego np. w książkach kucharskich) do programów kompu:erowych, które są sformalizowanymi opisami algorytmów w konkretnych językach programowania. Na pewno można odrzucić zapis w postaci sieci działań jako zbyt rozwlekły. Obecnie większość algorytmów publikuje się w języku, który nazwaliśmy PseudoPascalem patrz punkt 1 .2) . Typowym postulatem jest wymóg strukturalności programu.
3. W jaki sposób analizować algorytmy? Wykonywanie algorytmu na komputerze po, 'oduje angażowanie jego zasobów, takich jak: czas, pamięć, procesory. Analiza algorytmów dotyczy problemu określenia, ile czasu, ile pamięci (operacyjnej , pomocniczej),
Teszcie i le procesorów (arytmetycznych, komunikacyjnych) wymagać będzie dany pro-
36 2 . Podstawy analizy algorytmów
gram oraz odpowiedzi na pytanie, czy wykonuje się on poprawnie i daje precyzyjne wyniki. Typowym zagadnieniem jest kwestia zachowania się algorytmu w najlepszym, średnim i naj gorszym przypadku danych.
4. W jaki sposób testować programy realizujące algorytmy? Przez testowanie rozumiemy tutaj zarówno uruchamianie, jak i profilowanie. Unlchamianie (ang. debugging) to próba wykonania programu na przykładowych danych dla stwierdzenia, czy daje on poprawne wyniki i jeśli nie - poprawienie go. Uruchamianie może ewentualnie wykazać obecność błędów, a nie ich brak. Dowód poprawności programu jest przeto bardziej wartościowy niż tysiące testów, gdyż gwarantuje poprawność dla wszystkich danych. Profilowanie (ang. profiling) jest procesem wykonywania poprawnego programu na pewnych interesujących nas zestawach danych oraz mierzenie czasu i pamięci zajmowanej przez ten program. Oczywiście, pojawia się tu pytanie, na jakich danych należy testować programy.
W niniejszym rozdziale zajmiemy się odpowiedzią na pytania sformułowane w punkcie 3. W szczególności, zwrócimy uwagę na następujące czynniki, które należy brać pod uwagę przy numerycznym rozwiązywaniu każdego problemu.
a) Struktury danych. Jakie wielkości są danymi rozwiązywanego zadania? Jakie przestrzenie danych i wyników (ich struktury, normy) naj lepiej odpowiadają sensowi fizycznemu rozważanego problemu? Jaka jest złożoność pamięciowa struktur danych?
b) Efektywność. Co wiemy o złożoności obliczeniowej naszego problemu? Jaka jest efektywność metod rozwiązujących dany problem? Czy rozpatrywany algorytm jest optymalny, tzn. czy miara kosztu jego wykonania jest równa złożoności problemu? Jeśli nie, to czy jest to najtańsza bądź najprostsza ze znanych metod?
c) Jakość numeryczna. Czy zadanie nie jest zbyt wrażliwe na zaburzenia danych? Czy algorytm użyty do rozwiązania jest odporny na błędy zaokrągleń? Jeśli nie, to czy istnieje metoda bardziej stabilna numerycznie? Jaka może być utrata dokładności obliczeń?
d) Dokładność. Czy dany algorytm gwarantuje uzyskanie rozwiązania optymalnego? Jeśli nie, jaka jest dokładność danej heurystyki dla najgorszego przypadku danych? Dla jakich klas danych algorytm daje rozwiązania optymalne? Jaki jest naj mniejszy trudny algorytmicznie zestaw danych?
Podstawowym celem naszych rozważań jest próba usprawnienia znanych algorytmów oraz ilościowa ocena wartości jednego algorytmu względem drugiego. Dlatego, zanim omówimy konkretne algorytmy opisane w dalszych rozdziałach, w kolejnych punktach niniejszego rozdziału przedstawimy podstawy analizy algorytmów. W szczególności omówimy następujące kryteria dotyczące algorytmów:
l) poprawność, 2) wymagania czasowe, 3) wymagania pamięciowe, 4) optymalność czasowa, 5) stabilność numeryczna, 6) prostota, zwięzłość, 7) wrażliwość.
_ .2. Poprawność algorytmów 37
2.2 . Poprawność algorytmów
W ogólności, nie można udowodnić matematycznie poprawności programu wykomuącego się na maszynie fizycznej, ale można udowodnić poprawność jego modelu matematycznego.
Weryflkację poprawności algorytmu przeprowadza się na różnych poziomach abstrakcj i. Przede wszystkim, trzeba ustalić, co oznacza "poprawność" w danym konkretnym przypadku, tzn. na jakich danych algorytm będzie działał i j aki jest prawidłowy wynik dla każdej danej . Dopiero wówczas można przystąpić do dowodu poprawności przyjętej metody. Jego celem jest przekonanie każdego (ale przede wszystkim autora), że jeżeli dane spełniają wymagane warunki, określone jako warunki wstępne, to wynik działania algorytmu będzie spełniał warunek końcowy. Mówiąc prościej , po ułożeniu algorytmu musimy udowodnić, że algorytm ten dla dobrych danych robi to, co trzeba - że rzeczywiście rozwiązuje zadany problem. Jest to szczególnie istotne, gdy komputery decydują o zdrowiu i życiu ludzi.
Często pisząc program, wiemy od razu, że zastosowany algorytm jest dobry, że dla każdych danych zrobi to, co trzeba. Tak jest zwykle wówczas, gdy problem rozwiązujemy wprost, bez żadnych sztuczek. Rozpatrzmy dla przykładu zamianę wartości zmiennych Liczbowych. Najprostsze rozwiązanie to użycie zmiennej pomocniczej . Gdy przeniesiemy wartość zmiennej x do zmiennej pomocniczej , potem wartość zmiennej y do x i w końcu wartość zmiennej pomocniczej do y, to oczywiście problem rozwiązaliśmy. Ponadto algorytm jest tak prosty, że nie ma co dowodzić. Ale zamianę wartości zmiennych możemy zrealizować również, stosując inny algorytm: dodajmy wartość zmiennej y do wartości zmiennej x i sumę tę przechowajmy jako nową wartość zmiennej x. Teraz za zmienną y podstawmy różnicę nowej wartości zmiennej x i wartości zmiennej y i w końcu za zmienną x znów różnicę wartości zmiennych x i y. Symbolicznie
x := x + y;
y := x - y;
x := x - y;
Przy tym algorytmie dowód jest już potrzebny. Jest on wprawdzie krótki, gdyż wystarczy zauważyć, że
(x + y) - y = x (x + y) - «x + y) -y) = y,
ale nie można go pominąć stwierdzeniem, że wszystko jest oczywiste. W analizie algorytmów dany algorytm uważa się za poprawny, gdy umiemy o nim udowodnić dwa fakty. Pierwszy to tzw. własność stopu (ang. halting property), a więc to, iż dla każdych danych dopuszczalnych algorytm ten zatrzymuje się i daje wynik. Drugi fakt, to taki, że wynik ten jest tym, czego szukaliśmy. Oczywiście zawsze dążymy do dowodu poprawności algorytmu. Gdy jednak nie uda się udowodnić, że algorytm zawsze się zatrzymuje, nie znaczy to, że jest całkowicie zły. Jeśli potraflmy udowodnić, że wynik działania algorytmu (o ile wynik ten otrzymamy) będzie prawidłowy, to mówimy o częściowej poprawności (ang. partial correctness) algorytmu lub też, że mamy do czynienia z półalgorytmem (ang. semia/gorithm). Zatem algorytm jest całkowicie poprawny (ang. foli correctness), gdy jest częściowo poprawny i ma własność stopu.
38 2 . Podstawy analizy algorytmów
Z chwilą wykazania poprawności algorytmu przystępujemy do napisania programu re
alizującego dany algorytm. Gdy algorytm jest dostatecznie prosty, to zwykle stosujemy metody nieformalne dla upewnienia się, że fragmenty programu rzeczywiście robią to, co
powinny. Możemy dokładnie sprawdzić wartości początkowe zmiennych sterujących pętli i wykonać program na uproszczonych danych. Co prawda, żadna z tych metod nie daje gwa
rancji poprawności, ale w praktyce są one wystarczające. Większość programów profesjonalnych to programy długie i zawiłe. Aby udowodnić
poprawność takiego programu, musimy podzielić go na mniejsze fragmenty. Następnie
pokazać, że jeżeli poszczególne fragmenty są poprawne, to i cały program jest poprawny. Wreszcie wykazać poprawność wszystkich fragmentów. Postępowanie takie jest możliwe jedynie wtedy, gdy algorytmy i programy pisane są modułowo, czyli z zastosowaniem techniki programowania strukturalnego (ang. structural programming), polegającej na podzieleniu algorytmu na logicznie spójne bloki funkcjonalne i wyeliminowaniu instrukcji goto. Takie niezależne bloki funkcjonalne mogą być analizowane oddzielnie.
Jedną z metod dowodzenia poprawności jest metoda niezmienników pętli (ang. loop invariants). Niezmienniki to warunki i relacje spełniane przez zmienne i struktury danych na początku lub końcu każdej iteracj i pętli. Niezmienniki pętli formułuje się tak, aby precy
zyjnie stwierdzić, że po ostatniej iteracj i algorytm zrobi to, co miał wykonać. Do dowodu używa się indukcji matematycznej względem liczby iteracji . Dowód wymaga szczegółowego przeanalizowania instrukcj i wykonywanych w pętli.
Przykład 2.1 Metodę niezmienników pętli zilustrujemy na przykładzie poszukiwania w tablicy (lub na liście) elementu o wartości x. Algorytm porównuje x kolejno z każdym elementem wektora i jeżeli nastąpi zgodność, to zwraca indeks danego elementu. Jeśli x nie znajduje się na liście,
to algorytm zwraca o.
Algorytm 2.1 Dane: L, n, x, gdzie L jest tablicą n-elementową L[l. .n], zaś x jest wartością poszukiwaną. Wyniki: index, tj . pozycja x w L (lub O, gdy nie występuje) .
begin 1 . index := l ; 2 . while index ::; n and L[index] * x do begin 3 . index := index + l 4. end; 5. if index > n then index := O end;
Zanim udowodnimy poprawność algorytmu, winniśmy bardzo dokładnie określić, co ma on robić. W naszym przypadku odpowiednie twierdzenie brzmi następująco:
Twierdzenie. Mając daną n-elementową tablicę L (n :2: O) oraz x, algorytm 2. 1 kończy się z wartością index równą pozycji pierwszego wystąpienia x w L, jeśli x występuje, i równą O w przypadku przeciwnym.
_ .2. Poprawność algorytmów 39
Wykażemy najpierw następujący lemat metodą indukcji matematycznej .
Lemat. Dla każdego k = l , 2, . . . , n + l , jeśli sterowanie dociera do linii 2 po raz k-ty, to spełnione są następujące niezmienniki pętli:
index = k i L[i] -:t= x dla i = 1 , 2, . . . , k - 1 .
Dowód. Dowodzimy przez indukcję względem k. Zgodnie ze schematem indukcj i musimy najpierw sprawdzić, czy nasz niezmiennik jest spełniony przed rozpoczęciem działania pętli. Niech k = 1 . Wówczas index = k = l i nie istnieje i < k, dla którego L[i] = x. Obecnie pokażemy, że jeżeli warunki te są spełnione dla pewnego k < n + l , to zachodzą również dla k + l . Na podstawie założenia indukcyjnego L[i] -:t= x dla l ś i < k i index = k, gdy linia 2 wykonywana jest po raz k-ty z rzędu. Jeśli warunki w linii 2 sprawdzane są ponownie, czyli po raz (k + l )-szy, to wnioskujemy, że były one spełnione poprzednio. Zatem L [index ] -:t= x i L [k] -:t= x. Poza tym index jest zwiększany w pętli, więc (k + l )-sze sprawdzenie warunków oznacza, że index = k + l . Kończy to dowód kroku indukcyjnego - od poprzedniego wykonania pętli przeszliśmy do obecnego, a warunek nie zmienił się. •
Dowód twierdzenia. Obecnie przypuśćmy, że testy w linii 2 były wykonane dokładnie k razy, gdzie l ś k ś n + l . Rozważmy dwie możliwe sytuacje, w których wykonuje się instrukcja warunkowa z linii 5. Wynik inde.x = O jest wtedy i tylko wtedy, gdy k = n + l . Rzeczywiście, na podstawie prawdziwości niezmiennika pętli wnosimy, że dla każdego i = 1 , 2, . . . , n, L[i] -:t= x, więc wynik O jest prawidłowy. Zauważmy, że sytuacja ta zawiera przypadek, gdy n = O i lista jest pusta. Z drugiej strony wynik index = k ś n otrzymujemy wtedy i tylko wtedy, gdy pętla zakończyła się, ponieważ na mocy lematu L[k] = x. Ponieważ L[i] -:t= x dla i = 1 , 2 , . . . , k - l , więc wnioskujemy, że k jest pozycją pierwszego wystąpienia wartości x w rej tablicy. Zatem algorytm jest poprawny. •
W tak prostym przypadku jak algorytm 2. 1 dowód poprawności był parokrotnie dłuższy od samego algorytmu. Możemy więc sobie wyobrazić, jak długi musi być dowód dla programu zawierającego powiedzmy kilkadziesiąt linii. Algorytm taki zawiera z pewnością kilka pętli zagnieżdżonych jedna w drugiej . Tym niemniej zawsze można znaleźć w algorytmie pętlę naj głębiej zagnieżdżoną, tzn. taką, która nie zawiera w swojej treści żadnej innej pętli. Przeprowadzamy wówczas dowód poprawności dla tej pętli i od tego momentu możemy ją traktować jak zwykłą instrukcję, o której wiadomo, że kończy prawidłowo swoje działanie. Teraz znowu znajdujemy najbardziej zagnieżdżoną pętlę, nie licząc oczywiście już zbadanej , i przeprowadzamy formalny dowód. Postępujemy tak, aż udowodnimy poprawność całego algorytmu.
Powyższa metoda postępowania jest skuteczna przy założeniu, że badany algorytm nie jest rekurencyjny. W przypadku algorytmu rekurencyjnego dowód komplikuje się jeszcze bardziej .
40
2.3. Złożoność czasowa algorytmów
2.3.1. Operacje podstawowe
2. Podstawy analizy algorytmów
W jaki sposób mierzymy ilość pracy wykonanej przez algorytm? Oczywiście, miara taka winna umożliwiać porównanie efektywności dwóch różnych algorytmów rozwiązujących ten sam problem. Dobrze by było, gdyby nasza miara mówiła też o faktycznym czasie wykonywania obu programów na tych samych danych. Jednakże czas wykonywania programu nie może być podstawą miarodajnej oceny efektywności algorytmu z przyczyn, o których mówiliśmy szerzej w poprzednim rozdziale. Poszukujemy bowiem miary, która mówiłaby nam o wydajności metody, abstrahując od komputera, języka programowania, umiejętności programisty i szczegółów technicznych implementacji (sposobu inkrementacji zmiennych sterujących pętli, sposobu obliczania indeksów zmiennych ze wskaźnikami itp.) .
Prosty algorytm może zawierać na przykład kilka instrukcj i inicjalizujących obliczenia i jedną pętlę. Liczba obiegów pętli jest dobrą miarą pracochłonności takiego algorytmu. Oczywiście, ilość pracy wykonanej w trakcie każdego przejścia przez pętlę może się różnić i jeden algorytm może mieć znacznie więcej instrukcj i do wykonania w pętli niż inny. Tym niemniej , obliczenie liczby przejść przez wszystkie pętle jest dobrym przybliżeniem czasochłonności algorytmu.
W większości przypadków możemy wyodrębnić jedną operację jako podstawową (ang. basic) dla badanego problemu lub klasy rozważanych algorytmów. Ignorujemy wówczas pozostałe operacje pomocnicze, takie jak instrukcje inicjalizacji , instrukcje organizacj i pętli, i liczymy jedynie operacje podstawowe. Na ogół taka operacja podstawowa występuje przynajmniej raz w każdym przejściu przez główne pętle algorytmu. Gdy mamy wątpliwości, jako operację podstawową możemy przyjąć tę, która jest najczęściej wykonywana. W tym celu można skorzystać np. z systemu profilowania programów. Poniżej podajemy przykłady takich operacj i podstawowych dla typowych problemów obliczeniowych.
Problem l . Znalezienie x na liście nazwisk. 2. Mnożenie dwóch macierzy liczb rze
czywistych. 3 . Porządkowanie liczb.
4. Trawersowanie grafu w postaci listy sąsiadów.
Operacja Porównanie x z pozycją na liście. Mnożenie dwóch liczb typu real (lub mnożenie i dodawanie). Porównanie dwóch liczb (lub porównanie i zamiana). Operacja na wskaźniku l isty.
Jeśli operacja podstawowa została wybrana właściwie i łączna liczba operacj i jest proporcjonalna do liczby operacj i podstawowych, to dysponujemy dobrą miarą pracochłonności algorytmu i dobrym kryterium dla porównywania algorytmów. Podejście to ma również uzasadnienie praktyczne. Na przykład często interesuje nas tempo, w jakim rośnie czas działania programu, gdy wykonuje się on na coraz większej liczbie danych. Jeżeli łączna liczba operacj i jest z grubsza proporcjonalna do liczby operacj i podstawowych, to możemy przewidzieć zachowanie się algorytmu dla dużych rozmiarów danych. Ponadto wybór op e-
_ .3. Złożoność czasowa algorytmów 4 1
racj i podstawowej jest dość swobodny. W skrajnym przypadku można wybrać rozkazy
maszynowe konkretnego komputera. Z drugiej strony jedno przejście przez instrukcje pętli
można również potraktować jako operację podstawową. W ten sposób możemy manipulo
wać stopniem precyzj i w zależności od potrzeb.
Zauważmy jak poprzednio, że większość programów profesjonalnych to programy zło
żone z wielu modułów lub podprogramów. W każdym takim podprogramie inna instrukcja
może grać rolę operacji podstawowej . Dlatego fragmenty większej całości analizuje się
zwykle oddzielnie i na podstawie skończonej liczby takich modułów szacuje się czaso
chłonność algorytmu jako całości.
2.3.2. Rozmiar danych
Poprzednio zaproponowaliśmy miarę ilości pracy wykonywanej przez algorytm. Obec
nie chcielibyśmy wyrazić tę miarę w sposób zwięzły. Jednakże pracochłonność algorytmu
nie może być wyrażona jako liczba wykonań operacj i podstawowej , ponieważ wielkość ta
zależy od rozmiaru danych wejściowych. Rzeczywiście, czasochłonność algorytmu rośnie
wraz ze wzrostem rozmiaru rozwiązywanego problemu. Dla przykładu, ustawienie 1 000
nazwisk w kolejności alfabetycznej wymaga więcej operacji niż ustawienie 1 00 nazwisk,
podobnie jak rozwiązywanie 1 0 równań z 1 0 niewiadomymi trwa dłużej niż w przypadku,
gdy są tylko 2 równania do rozwiązania. Co więcej , jeśli ograniczymy się do danych tego
samego rozmiaru, to liczba operacji wykonywanych przez algorytm może zależeć od specy
ficznego układu danych. Algorytm porządkujący nazwiska może mieć bardzo mało pracy
do wykonania, gdy są one ustawione niemal poprawnie, podobnie jak układ 1 0 równań
liniowych nie będzie trudny do rozwiązania, gdy większość współczynników przy niewia
domych będzie zerowa.
Widzimy zatem, że potrzeba nam pewnej miary rozmiaru danych problemu. Jeżeli
słowo maszyny jest na tyle długie, by pomieścić każdą z kodowanych binarnie liczb, to jako
miarę rozmiaru danych możemy przyjąć liczbę bitów potrzebnych do zakodowania wszyst
kich liczb i znaków występujących na wejściu algorytmu. Taki sposób kodowania spełnia
postulat jednoznaczności i zwięzłości kodowania, tzn. nie powoduje sztucznego wzrostu
rozmiaru problemu. Mimo że taka metoda kodowania jest naturalna dla komputerów, nie
jest zbyt wygodna w użyciu przez ludzi. Dlatego w praktyce wygodnie jest wyrazić rozmiar
konkretnego problemu za pomocą jednego lub dwóch parametrów określających liczbę
danych. Na szczęście dość łatwo jest wskazać parametr charakteryzujący rozmiar danych w
każdym konkretnym przypadku. Na przykład:
Problem l . Znalezienie x na liście nazwisk.
2. Mnożenie dwóch macierzy.
3. Porządkowanie liczb.
4. Trawersowanie grafu.
5 . Rozwiązywanie układu równań liniowych.
Rozmiar danych Liczba nazwisk na liście.
Liczba wierszy i kolumn.
Liczba kluczy do sortowania.
Liczba wierzchołków i liczba krawędzi.
Liczba równań i liczba niewiadomych.
42 2. Podstawy analizy algorytmów
2.3.3. Pesymistyczna złożoność obl iczeniowa
W jaki sposób przedstawiamy wyniki naszej analizy? Najczęściej obliczamy liczbę operacji podstawowych wykonywanych w naj gorszym przypadku danych jako funkcję rozmiaru danych. Funkcję tę nazywamy zlożonością obliczeniową najgorszego przypadku danych (ang. worst-case complexity) lub pesymistyczną złożonością obliczeniową. Bardziej formalnie, niech D" będzie zbiorem danych rozmiaru n dla rozważanego problemu i niech l będzie elementem zbioru Dn. Niech t(I) będzie liczbą operacji podstawowych wykonywanych przez algorytm na danych l. Funkcję tę nazywać będziemy liczbą kroków algorytmu A. Wówczas definiujemy funkcję Wjako
(2. 1 ) Wen) = max {t(I): l E Dn} .
Najczęściej Wen) nie jest zbyt trudna do obliczenia, a zwłaszcza do oszacowania z góry. Znajomość funkcji (2. 1 ) jest bardzo ważna, gdyż daje gwarancje, iż dany algorytm nie będzie wykonywał więcej niż Wen) operacj i podstawowych. Jest to szczególnie istotne w systemach czasu rzeczywistego, które decydują o zdrowiu i życiu ludzi. W dalszym ciągu, mówiąc o złożoności obliczeniowej , będziemy mieli na myśli złożoność najgorszego przypadku danych.
2.3.4. Oczekiwana złożoność obl iczeniowa
Dobre bądź złe zachowanie się algorytmu w najgorszym przypadku danych nie rozstrzyga jeszcze o jego przydatności w praktyce. Bardzo wolne działanie algorytmu w najgorszym przypadku danych ostrzega nas jedynie przed możliwym fiaskiem szybkiego znalezienia rozwiązania, nie mówiąc nic o prawdopodobieństwie jego wystąpienia. Z praktyki zaś wiadomo, że naj gorszy przypadek danych pojawia się zazwyczaj niezmiernie rzadko. Z drugiej strony może zdarzyć się i tak, że naj lepsze rezultaty daje metoda nie będąca optymalną w żadnym sensie. Tak jest np. w przypadku metody simpleksów dla rozwiązywania
zadań programowania liniowego. Zatem ważny praktycznie jest nie tyle naj gorszy przypadek, co średni przypadek (ang. average-case) danych dla danego algorytmu. Podejście to ma głębokie uzasadnienie praktyczne, gdyż okazuje się, że grafy występujące w zastosowaniach są zwykle rzadkie, macierze rozrzedzone, l isty częściowo uporządkowane itp. Jednakże analiza średniego przypadku danych jest o wiele trudniejsza, gdyż wymaga określenia rozkładu danych wejściowych, a najczęściej bywa tak, że rozkłady odpowiadające rzeczywistym problemom są matematycznie niezbadane. Dotychczas dokonano szczegółowej analizy jedynie najprostszych algorytmów (zwłaszcza dla sortowania) i to przy założeniu najprostszego możliwego rozkładu prawdopodobieństwa, jakim jest rozkład równomierny.
Zgodnie z powyższym musimy obliczyć liczbę operacji wykonywanych dla każdego układu danych rozmiaru n, a następnie obliczyć wartość średnią. W praktyce pewne dane mogą pojawiać się częściej niż inne, więc średnia ważona byłaby bardziej na miejscu. Niech p(I) będzie prawdopodobieństwem występowania danych l. Wówczas złożoność obliczeniową średniego przypadku (ang. average-case complexity) lub oczekiwaną złożoność obliczeniową (ang. expected complexity), lub po prostu średnią złożoność definiujemy jako
2.3. Złożoność czasowa algorytmów 43
(2.2) A(n) = L P(J)t(J). leD"
Funkcję t(I) można obliczyć, analizując postać źródłową algorytmu, lecz p(I) nie może
być policzona analitycznie. Jeśli funkcja p(I) jest skomplikowana, to oszacowanie oczeki
wanej złożoności obliczeniowej jest trudne. Oczywiście, jeśli rozkład prawdopodobieństwa
zależy od konkretnego zastosowania algorytmu, to funkcja (2.2) opisuje złożoność oblicze
niową średniego przypadku jedynie dla tego zastosowania.
Przykład 2.2 Problem: Niech L będzie tablicą n-elementową. Znaleźć pozycję x, jeśli L zaWIera x, i zwrócić O w przypadku przeciwnym.
Algorytm: Algorytm 2 . 1 . Operacja podstawowa: Porównanie x z pozycją na liście.
Analiza najgorszego przypadku: W naj gorszym przypadku x zajmuje ostatnią pozycję lub w
ogóle nie występuje w L. W obu przypadkach x jest porównywane ze wszystkimi pozycja
mi, zatem Wen) = n. Analiza średniego przypadku: Na wstępie poczynimy kilka założeń upraszczających. Mia
nowicie, że wszystkie elementy w L są różne, że x na pewno należy do L i że x może być na
każdej pozycj i z jednakowym prawdopodobieństwem. Zbiór możliwych danych rozmiaru n możemy podzielić na klasy równoważności według tego, na jakiej pozycj i występuje x. Zatem wystarczy rozważyć n typów danych wejściowych. Dla i = 1 , 2 , . . . , n niech I; repre
zentuje przypadek, gdy x znajduje się na i-tej pozycji . Wówczas niech t(I) oznacza liczbę
porównań wykonywanych przez algorytm 2 . 1 , czyli liczbę wartościowań warunku L [index] * x w linii 2. Oczywiście t(I;) = i dla każdego i = l , 2, . . . , n. Zatem
A( ) - � (I ) (I ) _ � J.. . _ J.. � . _ l n(n + l) _ n + l n - L- P ; t ; - L- 1 - L- 1 - - --;=1 ;=1 n n ;=1 n 2 2
Jest to zgodne z naszą intuicją, że średnio połowa listy będzie przejrzana.
Obecnie rozważmy sytuację, w której x być może nie znajduje się na liście, przy czym,
jak poprzednio, wszystkie elementy są różne. Musimy rozważyć teraz n + l przypadków.
Dla i = l , 2, . . . , n symbol I; reprezentuje przypadek, gdy x jest na i-tej pozycj i, In+1 reprezen
tuje przypadek, gdy x nie ma na liście, zaś q oznacza prawdopodobieństwo, że x jest na
liście, przy czym żadna pozycja nie jest uprzywilejowana w sensie prawdopodobieństwa.
Wówczas dla l � i � n mamy p(I;) = q/n, p(In+ I) = l - q. Jak poprzednio t(I;) = i oraz t(In+I ) = n . Zatem
n+l n q q n q n(n + l) A(n) = L P(J; )t(J; ) = L - i + (l - q)n = -L i + (l - q)n = - + ( l - q)n = � � n n � n 2
n + l = q- + (l - q)n .
2
Jeśli q = l , to jak poprzednio A(n) = (n + 1 )/2. Jeśli q = 1 /2, to A(n) = (n + 1 )/4 + n/2, czyli sprawdzanych jest około 3/4 pozycji na liście L. •
44 2. Podstawy analizy algorytmów
Powyższy przykład pokazuje, w jaki sposób należy interpretować zbiór Dn. Zamiast
rozważać wszystkie możliwe listy nazwisk, ciągi liczb itd., które mogą pojawić się poten
cjalnie na wejściu, identyfikujemy te własności danych, które mają wpływ na zachowanie
się algorytmu. W naszym przypadku jest to fakt, czy x znajduje się na liście, a jeśli tak, to
na której pozycj i . Element l w Dn może być rozumiany jako podzbiór (lub klasa równoważ
ności) wszystkich list i wartości x takich, że x występuje na określonym miejscu (lub nie
występuje w ogóle). Wówczas t(I) jest liczbą operacj i wykonywanych dla konkretnych
danych w klasie l. Zauważmy również, że dane, dla których algorytm działa najwolniej ,
zależą od konkretnego algorytmu, a nie od problemu. Dla algorytmu 2 . 1 naj gorszy przypa
dek ma miejsce wówczas, gdy x znajduje się na końcu listy. Gdyby analogiczny algorytm
sprawdzał listę L od końca (tzn. poczynając od index = n), byłby to dla niego najlepszy
przypadek.
Zauważmy wreszcie, że powyższy przykład ilustruje założenie, często przyjmowane
przy analizie średniego przypadku algorytmów sortowania, że elementy są różne. Analiza
taka daje dobre przybliżenie w przypadku, gdy istnieje niewielka liczba powtórzeń elemen
tów. Jeżeli liczba powtórzeń jest duża, to trudniej przyjąć jakieś sensowne założenia odno
śnie do prawdopodobieństwa, że x pojawia się po raz pierwszy na określonej pozycji .
Dla niektórych algorytmów nie ma żadnej różnicy pomiędzy ilością pracy wykonywa
nej w naj lepszym, średnim i naj gorszym przypadku. Wówczas złożoność zależy wyłącznie
od rozmiaru danych. Mówimy wówczas, że algorytm jest mało wrażliwy czasowo. Poniżej
podajemy przykład takiego algorytmu.
Przykład 2.3 Problem: Niech A = [aij] i B = [b,J będą dwiema macierzami kwadratowymi rozmiaru nxn.
Obliczyć macierz C = A x B. Algorytm: Zastosować algorytm wynikający z definicji macierzy C:
Algorytm 2.2
begin for i := l to n do
n Cij = Laik · bkj k=!
for j := l to n do begin cij := O;
dla l ś" ij ś" n.
for k := l to n do cij := cij + aik * bkj end
end;
Operacja podstawowa: Mnożenie liczb zmiennoprzecinkowych.
Analiza: Aby obliczyć jeden element macierzy, należy wykonać n mnożeń. Macierz C ma n2
elementów, więc
A(n) = Wen) = n3• •
2.4. Złożoność pamięciowa 45
Dla innych algorytmów przejście od analizy najgorszego przypadku danych do analizy
średniej liczby działań powoduje zaobserwowanie ogromnego skoku w złożoności oblicze
niowej ; bywa, że nawet od złożoności wykładniczej do wielomianowej .
Widzimy, że rzeczywiście dla niektórych algorytmów A(n) = Wen). Jednakże dla in
nych algorytmów, rozwiązujących ten sam problem, nie musi to być prawdą. Co więcej , obie
funkcje mogą się różnić rzędem wielkości. Przykładem takiego problemu jest zagadnienie
sortowania. Naiwny algorytm sortowania działający na zasadzie porównywania i ewentualnej
zamiany par sąsiednich wykonuje w naj gorszym i średnim przypadku O(n2) porównań. Lepsze
algorytmy sortowania wykonują w obu przypadkach O(nlog n) porównań. Najlepsze algoryt
my sortowania, jednakże oparte na idei podziału dystrybucyjnego, potrafią zrobić to samo
w oczekiwanym czasie O(n) I ), nie przekraczając nigdy O(nlog n) działań. Jednakże, algorytmy
te wymagają O(n) dodatkowych komórek pamięci. Dlatego, jeśli zależy nam na jednoczesnym
oszczędzaniu czasu i pamięci, to godna polecenia jest metoda sortowania przez kopcowanie
[3]. Więcej na ten temat piszemy w następnym punkcie.
2.4. Złożoność pamięciowa
Historycznie rzecz biorąc, pierwszym celem analizy algorytmów było wykazywanie
poprawności algorytmów. Spowodowane to było tym, że obliczenia przeprowadzano ręcz
nie, a więc niewiele było mowy o ich pracochłonności, a już absolutnie nic o przestrzeni
potrzebnej dla zapisu danych, wielkości pomocniczych i wyników. Z chwilą pojawienia się
komputerów większą wagę przywiązywano do szacowania zajętości pamięci, niż do analizy
złożoności czasowej , gdyż pierwsze maszyny były wyposażone w stosunkowo niewielkie
pamięci operacyjne i pozbawione były całkowicie pamięci pomocniczych. Z drugiej zaś
strony uważano, że już taka szybkość obliczeń, jaką dysponowano, używając komputerów,
powinna gwarantować możliwość rozwiązywania wszystkich problemów praktycznych.
Dzisiaj , wraz z rozwojem technologii półprzewodnikowej, kwestie złożoności pamięciowej
mają znaczenie drugorzędne. Tym niemniej , ograniczenie pamięci często daje pożądany
skutek uboczny w postaci skrócenia czasu wykonania programu, bowiem niewielki program
szybciej się ładuje, a mniej danych może oznaczać krótszy czas ich przetwarzania.
Liczbę komórek pamięci używanych przez program nazywamy złożonością pamięciową (ang. space complexity). Liczba ta, podobnie jak liczba sekund wykonywania się pro
gramu, zależy od implementacj i. Jednakże pewne wnioski co do wymaganej pamięci mogą
być wyciągnięte już w czasie analizy algorytmu. Program wymaga pamięci komputera na
rozkazy, stałe i zmienne oraz na dane. Dane wejściowe mogą być zorganizowane w struktury danych (ang. data structures) mające różne zapotrzebowania na pamięć. Ponadto pro
gram może używać pamięci pomocniczej do organizacji obliczeń (np. stosu rekursji). Jeśli
pamięć pomocnicza jest stała względem rozmiaru danych n, to mówimy, że algorytm działa
w miejscu (ang. in place). Termin ten jest często używany w odniesieniu do algorytmów
sortowania, o których mówi się też, że działają in situ.
I ) Przy pewnych dodatkowych założeniach.
46 2. Podstawy analizy algorytmów
Mówiąc o liczbie komórek, nie precyzujemy rozmiaru jednej komórki, tzn. długości słowa wyrażonej w bitach. Tym niemniej czytelnik może przyjąć, że komórka jest wystarczająco duża, aby pomieścić każdą liczbę. Jeśli zapotrzebowanie na pamięć zależy nie tylko od rozmiaru danych, ale i od szczególnego układu tych danych, to możemy mówić o oczekiwanej złożoności pamięciowej (ang. expected space complexity) i złożoności pamięciowej najgorszego przypadku (ang. worst-case space complexity).
Dla niektórych problemów istnieje kompromis pomiędzy złożonością czasową i pamięciową, tzn. można uzyskać obniżenie złożoności czasowej kosztem wzrostu zapotrzebowania na pamięć. Na przykład dla pewnego problemu P mogą istnieć dwa algorytmy, mianowicie algorytm A) mający złożoność pamięciową O(n) i algorytm A2 osiągający złożoność czasową O(n2). Z tego nie wynika wcale, że istnieje algorytm, który osiąga oba te ograniczenia naraz. Badacze usiłują udowodnić, że sprawność każdego algorytmu spełnia pewne równanie, które wiąże pesymistyczną złożoność czasową T i złożoność pamięciową najgorszego przypadku S z rozmiarem danych wejściowych n. Typowym zagadnieniem jest pytanie, czy dla pewnych problemów wielomianowych, jak np. spójność grafu, można zmniejszyć zapotrzebowanie na pamięć roboczą do rozmiarów subliniowych, zachowując wielomianowość złożoności czasowej .
Przykład 2.4
Przypuśćmy, że jako zarówno dolne, jak i górne ograniczenie łącznej złożoności czasowo-pamięciowej pewnego zadania ustalono równanie
S*T= 8(n3 1 0g2n),
gdzie S oznacza kwadrat złożoności pamięciowej , a T złożoność czasową. Oznacza to, że jeżeli jesteśmy skłonni zużyć O(n3) czasu, to możemy rozwiązać zadanie używając tylko O(log n) pamięci. Jeśli natomiast nalegamy na poświęcenie nie więcej niż O(n2) czasu, to będziemy potrzebowali O ( fn log n) pamięci. •
Ciekawym zagadnieniem jest minimalizacja liczby komórek przeznaczonych na zmienne programu. Minimalizację taką można przeprowadzić opierając się na analizie tzw. grafit niezgodności (ang. incompatibility graph), którego wierzchołkami są zadeklarowane zmienne, a krawędziami związki informacyjne między nimi. Oszczędność pamięci uzyskujemy wówczas, gdy kilku zmiennym przyporządkowana jest jedna i ta sama komórka pamięci. Oszczędność pamięci jest niewielka, gdy zmienne zajmują pojedyncze komórki, lecz ma ogromne znaczenie, gdy są one tablicami. Analizę grafu niezgodności można przeprowadzić metodami kolorowania grafu.
Z pojęciem struktury danych związane jest pojęcie danych istotnych, tzn. takich, które nie mogą być pominięte w trakcie działania algorytmu, ponieważ zignorowanie ich mogłoby wypaczyć wynik. Ściślej, będziemy mówili, że rozważane zadanie ma n danych istotnych (ang. essential data), jeśli istnieją dane I = (d), dl, . . . , dn) E Dm dla których zmiana dowolnej ze składowych d;, i = l , 2, . . . , n powoduje zmianę wyniku. Na przykład w problemie obliczania śladu (ang. trace) macierzy kwadratowej, czyli sumy elementów leżących na głównej przekątnej, nie istotne są wszystkie elementy leżące poza główną przekątną· Jeżeli struktura
2.4. Złożoność pamięciowa 47
danych zawiera wyłącznie dane istotne, to złożoność pamięciowa ogranicza od dołu złożoność
czasową z dokładnością do stałej proporcjonalności. Dowodzi się, że jeżeli algorytm ma n danych istotnych, to minimalna liczba działań dwuargumentowych wynosi n/2. Wynika to
stąd, że dowolny algorytm będzie wymagał odwołania się przynajmniej raz do każdej komórki
pamięci po to tylko, aby wszystkie istotne elementy struktury danych zostały uwzględnione.
Zatem jeżeli struktura danych zawiera wyłącznie dane istotne, to złożoność pamięciowa ogra
nicza od dołu złożoność czasową z dokładnością do stałej proporcjonalności.
Z drugiej strony istnieje proste oszacowanie górne złożoności czasowej algorytmów.
iech C będzie maksymalną liczbą różnych wartości możliwych do zapisania w pojedyńczej
komórce pamięci. Jeżeli algorytm ma złożoność pamięciową S, to liczba wszystkich możli
wych stanów jego pamięci nie przekracza C. Stany te nie mogą powtarzać się w trakcie wy
konywania algorytmu, bo inaczej nastąpiłoby zapętlenie. Zatem dla każdego algorytmu musi
zachodzić
S S. T S. C. W praktyce istnieje kilka metod obniżania złożoności pamięciowej algorytmów. Podajemy
pięć podstawowych sposobów ograniczania wykorzystania pamięci roboczej programu.
1 . Wielokrotne obliczanie wartości. Pamięć potrzebna do przechowywania danego
obiektu może zmniej szyć się gwahownie, jeśli nie zapamiętamy go, a zamiast tego będzie
my obliczać jego wartość za każdym razem, gdy będzie ona potrzebna. Dla przykładu tabli
cę liczb pierwszych można zastąpić procedurą sprawdzającą, czy jakaś liczba naturalna j est
liczbą pierwszą. Czasami, zamiast pamiętać cały obiekt, przechowuj emy jedynie program,
który go generuje i wartość startową generatora, określającą ten konkretny obiekt.
2. Stosowanie struktur rozproszonych. Macierz rozrzedzona (ang. sparse matrix) to ta
ka tablica, w której większość elementów ma tę samą wartość (zazwyczaj zero). Różnorod
ne tablice, macierze, grafy używane w programach są często strukturami rozproszonymi. Do
ich implementacj i można używać specjalnych struktur listowych o złożoności pamięciowej
O(m), gdzie m jest liczbą elementów niezerowych.
3. Komprymowanie danych. Koncepcje umożliwiające ograniczanie pamięci przez stoso
wanie kompresji danych pochodzą z teorii informacji. Jeżeli elementy macierzy rzadkiej przyjmu
ją tylko dwie wartości, jak na przykład w teorii grafów, to możemy zapamiętać je w postaci upa
kowanej na bitach. Podobnie dwie cyfiy dziesiętne a i b można zapisać w jednym bajcie za po
mocą liczby n = 1 0a + b. Do odkodowania informacji służą wówczas dwie instrukcje:
a := n div 1 0;
b := n mod 1 0;
W ten sposób osiągamy oszczędność 50%, co ma istotne znaczenie, gdy takich liczb jest
bardzo dużo.
4. Strategie przydziału pamięci. Czasami ilość dostępnej pamięci nie jest tak ważna j ak
-posób jej wykorzystania. Do optymalizacj i przydziału pamięci stosuje się takie techniki,
jak dynamiczny przydział pamięci, rekordy zmiennej długości, odzyskiwanie pamięci
i dzielenie pamięci. Poniżej zilustrujemy tę ostatnią technikę.
48 2. Podstawy analizy algorytmów
Przykład 2.5 Jeżeli mamy dwie macierze symetryczne A i B o rozmiarach n x n, przy czym obie mają zera na głównej przekątnej , to możemy przechowywać tylko macierz trójkątną każdej z nich. Możemy zatem pozwolić, aby obie tablice dzieliły przestrzeń macierzy kwadratowej C[l . . .n], której jeden z rogów wyglądałby następująco:
� ___ 0 ____ 4-__ B�[�I ,�2]�-+ ___ B�[ I�,3�] __ 4-__ B�[�I ,�4]�-+
__ __ � __ A,,-[2.:......, 1,,-] __ +-___ 0 ____ 4-_B[2,3] B[2,4]
A[3, 1 ] A [3 ,2] O B[3,4] �--���--�----��---+----f---__ A!:...-[ 4:....-, 1.:!..-] __ +-__ A..!:...[ 4...:....,2....!.] __ 4-_
A [ 4,3] O
Wówczas do elementu A [iJ] odwołujemy się za pomocą
C[max(i,j), min(i,j)]
i analogicznie dla B, przestawiwszy jedynie min l max. •
5. Licznik probabilistyczny. Licznik probabilistyczny jest mechanizmem, który na n bitach pamięci pozwala zliczać wartości przekraczające 2n - l , o ile tylko godzimy się na sytuację, że jego wskazania mogą być obarczone pewnym błędem. W tym celu musimy zaimplementować 3 procedury: init(c), tick(c) i count(c), gdzie c oznacza nasz rejestr nbitowy. Wywołanie count(c) zwraca przybliżoną liczbę wywołań procedury tick(c) od czasu ostatniego wywołania procedury inicjalizacyjnej init(c). Innymi słowy, init zeruje licznik, tiek dodaje doń l , a count podaje jego aktualną wartość. Pokażemy, w jaki sposób zakres takiego licznika możemy zwiększyć do 22n-1 - l (dla n = 8 oznacza to więcej niż 5 x 1 076) .
Idea polega na utrzymywaniu w liczniku c oszacowania nie faktycznej liczby zdarzeń, lecz logarytmu dwójkowego z tej wartości. Dokładniej , eount(e) zwraca 2c - l (odejmujemy l , aby zero mogło być również reprezentowane), czyli cOl/nt(O) = O. Natomiast implementacja tiek(e) jest nieco bardziej skomplikowana. Przyjmijmy, że 2c - l jest dobrym oszacowaniem wartości tick(e). Po dodatkowym tyknięciu zegara oszacowanie winno wynosić 2c, ale to nie jest zgodne z ideą licznika probabilistycznego, gdyż dodajemy l do e z pewnym prawdopodobieństwem p « l . Dlatego nasze oszacowanie wynosi 2ct-1 - l z prawdopodobieństwem p i pozostaje 2c_l z prawdopodobieństwem l -p. Wartość oczekiwana licznika jest więc równa
zatem przyjęcie p = Te nadaje jej wartość 2c. Poniższe trzy procedury implementują ideę licznika probabilistycznego
procedure init(c); begin
c := 0
end; procedure tick(c);
2. 5. Optymalność
begin for i := 1 to c do rzuć monetę; if wypadły same orły then return( c+ 1 )
end; function count(c); begin
return(2C - 1 ) end;
49
Można udowodnić, że wariancja licznika po m tyknięciach zegara wynosi m(m - 1 )/2. Zastosowanie ułamkowej podstawy logarytmu pozwala zwiększyć dokładność licznika probabilistycznego.
Zauważmy na zakończenie, że jeżeli stwierdzamy, iż pewien algorytm ma złożoność czasową Wen), abstrahując od struktury danych, to rozumiemy, że Wen) jest minimalną możliwą liczbą kroków wykonywanych przez ten algorytm w naj gorszym przypadku danych, gdzie minimum jest rozciągnięte na wszystkie możliwe struktury danych. Musimy bowiem ciągle pamiętać o powiedzeniu N. Wirtha, które jest również tytułem jego książki, że ALGORYTMY + STRUKTURY DANYCH = PROGRAMY [ 1 4] .
2.5. Optymalność
Istnieje pewna granica, której nie można przekroczyć, poprawiając złożoność algorytmu. Granica ta podyktowana jest wewnętrzną złożonością problemu (ang. inherent problem complexity), tzn. minimalną ilością pracy niezbędnej do wykonania w celu rozwiązania zadania. Aby zbadać złożoność obliczeniową problemu, musimy wybrać operację podsta-
ową charakterystyczną dla danego problemu i klasy algorytmów go rozwiązujących. Na!itępnie odpowiedzieć na pytanie, ile takich operacji trzeba wykonać w najgorszym przypadku. Mówimy, że algorytm jest optymalny (ang. optima/), jeśli żaden algorytm w rozważanej klasie nie wykonuje mniej operacj i (w naj gorszym przypadku danych). Mówiąc, że żaden algorytm nie działa szybciej , mamy na myśli zarówno te algorytmy, które ludzie zaprojektowali , jak i te, które nie zostały jeszcze odkryte. Zatem "optymalny" oznacza tutaj Wnaj lepszy możliwy".
W jaki sposób pokazuje się, że algorytm jest optymalny? Naj częściej dowodzi się, że istnieje pewne dolne oszacowanie liczby operacj i podstawowych potrzebnych do rozwiązama problemu. Wówczas każdy algorytm wykonujący tę liczbę operacji będzie optymalny. A zatem musimy wykonać dwa zadania:
1. Zaprojektować możliwie naj lepszy algorytm, powiedzmy A. Następnie przeanalizo. 'ać algorytm A, otrzymując złożoność najgorszego przypadku Wen).
2. Dla pewnej funkcji F udowodnić twierdzenie mówiące, że dla dowolnego algorytmu . rozważanej klasie istnieją dane rozmiaru n takie, że algorytm musi wykonać przynajmniej
F(n) kroków. Jeśli funkcje W i F są równe, to algorytm A jest optymalny (dla najgorszego przypad
AU). Jeśli nie, to być może istnieje lepszy algorytm lub lepsze oszacowanie dolne. Oczywi-
50 2. Podstawy analizy algorytmów
ście, analiza danego algorytmu daje górne oszacowanie liczby kroków wymaganych do
rozwiązania problemu, a twierdzenie, o którym mowa w punkcie 2, daje dolne oszacowanie.
Poniżej podamy przykłady problemów, dla których znane są algorytmy optymalne, jak i
problemów, dla których wciąż istnieje luka pomiędzy oboma oszacowaniami.
Przykład 2.6 Problem: Znajdowanie największej wśród n liczb.
Klasa algorytmów: Algorytmy, które porównują liczby i przepisująje.
Operacja podstawowa: Porównanie dwóch wielkości .
Oszacowanie górne: Przypuśćmy, że liczby zapisane są w tablicy L. Następujący algorytm znajduje maksimum.
Algorytm 2.3 Dane: L, n, gdzie L jest tablicą n-elementową (n � l ) . Wyniki: max, największy element w L.
begin l . max := L[l] ; 2 . for index := 2 to n do 3. if max < L[ index] then max := L [index] end;
Porównania są realizowane w linii 3, która jest wykonywana n - l razy. Zatem n - l jest
górną granicą liczby porównań koniecznych do znalezienia maksimum w najgorszym przy
padku danych. Czy istnieje algorytm wykonujący mniej porównań?
Oszacowanie dolne: Przypuśćmy, że nie ma dwóch jednakowych liczb w L. Założenie takie
jest dopuszczalne, ponieważ dolne oszacowanie w tym szczególnym przypadku jest również
dolnym oszacowaniem w przypadku ogólnym. Gdy mamy n różnych liczb, to n - l z nich
nie są największymi. Ale żeby stwierdzić, że jakiś element nie jest maksymalny, trzeba go
porównać z przynajmniej jednym z pozostałych. Zatem n - l elementów musi być wyelimi
nowanych drogą porównania z pozostałymi. Ponieważ w każdym porównaniu biorą udział
tylko 2 elementy, więc trzeba wykonać przynajmniej n - l porównań. Zatem F(n) = n - l
jest poszukiwanym dolnym oszacowaniem i na tej podstawie wnioskujemy, że algorytm 2.3
jest optymalny. •
Powyższy rezultat można osiągnąć także nieco inną drogą. Gdyby bowiem istniał algo
rytm dający odpowiedź po n-2 porównaniach, to co najmniej jeden z tych elementów nie
byłby sprawdzony. Zatem można by skonstruować takie dane, że odpowiedź byłaby błędna.
Przykład 2.7 Problem: Dane są dwie macierze A = [aiJ i B = [bij] rozmiaru n x n. Obliczyć macierz
C = A x B. Klasa algorytmów: Algorytmy wykonujące dodawania, odejmowania, mnożenia i dzielenia
na elementach macierzy.
2.6. Dokładność numeryczna algorytmów 5 1
Operacja podstawowa: Mnożenie.
Oszacowanie górne: Jak wiadomo, zwykły algorytm wykonuje n3
mnożeń, zatem n3
jest
oszacowaniem z góry.
Oszacowanie dolne: Jak wiadomo, złożoność pamięciowa wynosi 2n2, więc Q(n2) mnożeń
jest niezbędnych.
Wniosek: Nie ma możliwości stwierdzenia na tej podstawie, czy algorytm klasyczny jest
optymalny, czy nie. Dlatego włożono wiele wysiłku w poprawienie oszacowania dolnego,
jak dotąd bezskutecznie. Z drugiej strony szuka się nowych, lepszych algorytmów. Obecnie
naj lepszy znany algorytm mnożenia dwóch macierzy kwadratowych wykonuje około n2,376
mnożeń. Czy jest to algorytm optymalny? Nie wiadomo, ciągle bowiem oszacowanie górne
przewyższa oszacowanie dolne. • Dotychczas rozważaliśmy jedynie optymalność w sensie najgorszego przypadku da
nych. Ale podobne rozumowanie można przeprowadzić w odniesieniu do średniej złożono
ści obliczeniowej . Mianowicie wybieramy jakiś dobry algorytm i obliczamy dla niego funk
cję A(n). Następnie dowodzimy, że każdy algorytm w rozważanej klasie algorytmów musi
wykonać średnio G(n) operacji podstawowych na danych rozmiaru n. Jeśli A(n) = G(n) lub
przynajmniej A(n) = 0(G(n)), to możemy powiedzieć, że oczekiwana złożoność naszego
algorytmu jest naj lepsza możliwa. Jeśli nie, to musimy szukać jeszcze lepszych algorytmów
albo jeszcze lepszych oszacowań.
Podobną metodą badamy wymagania pamięciowe problemu. Czy możemy znaleźć al
gorytm, który jest dla jakiegoś problemu optymalny zarówno z punktu widzenia zapotrze
bowania na czas, jak i na przestrzeń? Odpowiedź brzmi: czasami. Jak wiadomo, dla niektó
rych problemów istnieje tzw. kompromis przestrzenno-czasowy (ang. trade-ojJ between space and time), to znaczy można uzyskać obniżenie złożoności czasowej problemu kosz
tem wzrostu zapotrzebowania na pamięć i na odwrót.
2.6. Dokładność numeryczna algorytmów
Jak wiadomo, liczby rzeczywiste są reprezentowane w komputerze przez skończoną
liczbę cyfr ich rozwinięć binarnych. Jakie są konsekwencje przybliżonej reprezentacji ta
kich liczb?
2.6.1. Zadania źle uwarunkowane
Niech rd(x) oznacza wartość liczby x w jej reprezentacj i zmiennoprzecinkowej .
ówczas
rd(x) = x(l + E), I E I ,.::; TI �dzie t jest długością mantysy. Zatem liczby rzeczywiste są na ogół reprezentowane niedo
kładnie, ale z błędem względnym nie większym niż TI• Rozwiązując numerycznie takie
zadanie, musimy zdawać sobie sprawę z tego, że zamiast danych dokładnych 1 = (dJ, d2, . . . , :J.) dysponujemy tylko ich reprezentacjami l' = (rd(d,), rd(d2) , . . . , rd(dn)). Ta niewielka
zmiana danych może jednak powodować duże zmiany względne rozwiązania. Mówiąc nie-
52 2. Podstawy analizy algorytmów
formalnie, jeśli niewielkie względne zmiany danych zadania powodują duże względne
zmiany jego rozwiązania, to zadanie takie nazywamy źle uwarunkowanym (ang.
ill-conditioned). Fakt, czy dane zadanie jest dobrze uwarunkowane, czy źle, zależy od konkretnych da
nych, a nie od problemu bądź algorytmu użytego do jego rozwiązania. Na przykład zadanie
wyznaczania miejsc zerowych trójmianu kwadratowego X2 - 2px + q dla p *- 0, q *- 0, p - q > ° jest bardzo źle uwarunkowane, gdy i '" q, natomiast bardzo dobrze uwarunkowane,
gdy p2 » q. Sytuacja taka ma miejsce niezależnie od tego, czy pierwiastki liczymy metodą
klasyczną, czy też modyfIkowaną przy użyciu wzorów Viete'a.
Podamy teraz inny przykład, by odwołać się do interpretacj i geometrycznej zaistnia
łych trudności.
Przykład 2.8 Należy znaleźć rozwiązanie układu:
a,x + blY = CI
w tym celu pomnożymy pierwsze równanie przez dl = a2/al i odejmiemy od drugiego.
Otrzymamy
(2.4)
Dla współczynników: al = 3,000, bl = 4, 1 27, CI = 1 5 ,4 1 , a2 = 1 ,000, h2 = 1 ,374, C2 = 5 , 147 dokładnym rozwiązaniem układu są liczby x = 1 3,6658 i y = -6,2. Tymczasem,
korzystając ze wzoru (2.4), obliczmy w arytmetyce prowadzonej na 4 cyfrach znaczących
wartość y.
l ) a = jl( 15 ,4 1/3) 5, 1 37
2) � = jl(5, 1 47 - a) = 0,0 1 0
3 ) Y = jl(4, 1 27/3) 1 ,376
4) 8 =jl( 1 ,374 - y) = -0,002
5) y / = jl(�/8) = -5, gdzie jlO oznacza wynik działania zmiennoprzecinkowego. Zatem otrzymaliśmy bardzo
niedokładną wartość niewiadomej y. • Przyczyną tak dużego błędu są straty dokładności w krokach 2 i 4, gdzie są odej
mowane bliskie co do wartości l iczby. Ogólnie, odejmowanie bliskich sobie liczb może
być przyczyną dużych błędów względnych. Nie oznacza to, że zastosowany algorytm jest
zły numerycznie. Metoda, którą użyliśmy do wyznaczenia wartości y, jest szczególnym
przypadkiem eliminacji Gaussa (ang. Gaussian elimination), o której wiadomo, że jest
algorytmem stabilnym. Po prostu rozwiązywaliśmy układ równań źle uwarunkowany.
Oznacza to, że w tej sytuacj i każdy algorytm wyznacza rozwiązanie obarczone dużymi
błędami zaokrągleń.
2. 6. Dokładność numeryczna algorytmów 53
Istnieje proste geometryczne wytłumaczenie zaistniałych trudności. Rozwiązywany
układ reprezentuje dwie przecinające się proste. Można sprawdzić, że te proste przecinają
się pod bardzo małym kątem, równym około 0,036 stopnia. Przyj mijmy, że te proste są
narysowane kreską o grubości równej względnej dokładności obliczeń, tzn. 5 · 1 0-4. Ze
względu na to, że kąt między prostymi jest mały, punkt przecięcia jest rozmyty i niezbyt
widoczny na rysunku, a także niedokładnie widoczny dla algorytmu liczącego z dokładno
ścią do 4 cyfr dziesiętnych. W takich sytuacjach musimy stosować silniejszą arytmetykę,
tzn. używać podwójnej (lub wyższej) precyzji, a nawet korzystać z maszyny o większej
długości słowa.
2.6.2. Stabilność numeryczna
Zadania numeryczne wymagają zwykle bardzo wielu działań. Jeśli znaczna ich część
wprowadza błędy zaokrągleń, to mogą się one kumulować, powodując zauważalne znie
kształcenia wyników. W takim przypadku mówimy, że algorytm jest niestabilny (ang.
unstable). Zatem algorytm jest stabilny, gdy w trakcie jego wykonywania nie dochodzi do
niekontrolowanej kumulacji błędów zaokrągleń. Jeżeli zadanie jest dobrze uwarunkowane,
to błąd w algorytmie stabilnym numerycznie jest na poziomie nieuniknionego błędu rozwią
zania, wynikającego z przybliżonej reprezentacj i danych i wyników.
Przykład 2.9 Rozpatrzmy dwa różne algorytmy obliczania różnicy kwadratów dwóch liczb:
Al(a,b) = a2 _ b2
A2(a,b) = (a - b)(a + b)
Przy realizacji pierwszego z nich w arytmetyce jl otrzymujemy
gdzie
jl(a2 - b2) = (a * a( l + E l ) - b * b( l + E2))(( l + E3) =
= (a2 - b2)( 1 + (a2E l - b2E2)/(a2 - b2))( l + E3) =
= (a2 - b2)( l + 8),
? b2 a-E l - E2 0 = 2 ? (1 + E3 ) + E3 ,
a - b-
zaś I Ei I � T1 (i = 1 , 2 , 3) . Jeśli a2 jest odpowiednio bliskie b2, a E l i E2 mają przeciwne znaki,
to błąd względny 8 wyniku otrzymanego algorytmem Al może być dowolnie duży.
Nie jest tak w przypadku drugiego algorytmu, gdyż
jl((a - b)(a + b)) = ((a - b)(l + E l ) * (a + b))( l + E2))( l + E3) = (a2 - b2)( 1 + 8), gdzie
przy czym E4 jest sumą powstałą z dodawania odpowiednich błędów iloczynowych powy
żej , zatem błąd względny I 8 1 < 4-T1• •
54 2. Podstawy analizy algorytmów
Przytoczony wyżej przykład pokazuje, jak istotnie różne własności numeryczne mogą
mieć algorytmy równoważne w sensie klasycznej arytmetyki. Pokazuje również, że utrata
dokładności wyniku wcale nie musi wiązać się z wielką liczbą zaokrągleń w trakcie obliczeń.
2.7. Prostota algorytmów
Bardzo często najprostszy i naj krótszy algorytm rozwiązywania problemu nie jest naj
bardziej efektywny. Mimo to, prostota i zwięzłość algorytmu jest własnością bardzo pożą
daną w praktyce. Prostsze programy są bowiem łatwiejsze do analizy i weryfikacj i, łatwiej
się je pisze i uruchamia, a także modyfikuje i konserwuje. Na ogół mamy do wyboru kilka
algorytmów rozwiązujących dany problem:
1. Możemy wybrać algorytm, który jest łatwy do zrozumienia, zakodowania i uruchomienia.
2. Możemy wybrać algorytm, który efektywne korzysta z zasobów komputera
(w szczególności jest szybki).
Gdy mamy napisać program realizujący algorytm wielomianowy, który będzie wyko
nywany tylko raz lub najwyżej parę razy, to będziemy się kierować kryterium pierwszym.
Koszt pracy programisty przekroczyłby bowiem koszt wykonania algorytmu na danym
komputerze. Zatem musimy minimalizować ten pierwszy czynnik. Z drugiej strony, jeśli
mamy napisać program, który będzie wykonywany wiele, bardzo wiele razy, to koszty eks
ploatacj i takiego programu w systemie komputerowym przekroczą nakłady pracy programi
sty. Ale nawet wówczas warto zacząć od napisania jakiegoś prostego algorytmu, aby mieć
punkt odniesienia przy porównywaniu ewentualnych zysków czasowych.
Złożoność kodu źródłowego programu próbuje się formalizować na rozmaite sposoby.
Przyjmując jakiś model formalny, zwykle potwierdzony doświadczeniami praktycznymi,
próbuje się szacować trudność programu, czas potrzebny na jego implementację, a nawet
oczekiwaną liczbę błędów. Poniżej podamy jeden z takich sposobów.
Jest oczywiste, że długość postaci źródłowej programu zależy od liczby leksemów, zwanych też tokenami (ang. tokens), czyli od liczby operatorów i operandów użytych do
jego zakodowania. Niech
ni = liczba różnych typów operatorów w programie
n2 = liczba różnych typów operandów w programie
mi = łączna liczba wystąpień operatorów
m2 = łączna liczba �stąpień operandów
Na tej podstawie możemy zdefiniować przykładowe miary złożoności kodu:
długość programu (ang. program length) : m = mi + m2 objętość programu (ang. program volume): v = m l log2(nl + n2) stopień trudności (ang. program difficulty): d = 0.5mln/n2 poziom programu (ang. program level): I = l /d wysiłek programisty (ang. programmer effort): e = vII oczekiwana liczba błędów (ang. expected number oj errors): b = v/3000
2. 7. Prostota algorytmów
Przykład 2.10 Jako przykład rozważymy algorytm 2 . 1 . Mamy tutaj :
begin ... end
while ... do ::;
and :7; +
if ... then >
2 3
l
3
więc liczba operatorów równa się ni = 1 0, mi = l S , oraz
index 6 2
n 2 L [index]
x °
55
dlatego n2 = 6 i m2 = 1 3. Zatem m = l S + 1 3 = 28, v = 28Ig 1 6 = 1 1 2, d = I S ·SI6 = 1 2,S, 1= 1/ 1 2,S = 0,08, e = 1 1 2/0,08 = 1 400,8, b = 1 1 2/3000 = 0,037.
Komentując tę ostatnią wartość, można by powiedzieć, że statystycznie jeden błąd po-
jawi się w programie około 9 razy dłuższym. •
Oczywiście powyższa metoda ilościowej oceny programu jest jedną z wielu możliwych
i nie jest pozbawiona wad. Dostępność kilkuset języków programowania, z których każdy
ma własną składnię i strukturę, jest jednym z powodów, dla których ocena programu meto
dą liczenia leksemów i operandów okazuje się trudna i zawodna. Dlatego naukowcy próbują
opracować inne metody formalne. Jedną z nich jest metodajednostekjimkcjonalności (ang.
function points) biorąca pod uwagę pięć atrybutów: dane wejściowe, dane wyjściowe, inte
raktywne zapytania, zbiory zewnętrzne i interfejsy. Więcej szczegółów na ten temat czytel
nik znajdzie w artykule [ 1 ] .
Jeszcze inną miarą złożoności tekstu źródłowego programu jest liczba cyklomatyczna
grafu przepływu sterowania. Graf przepływu sterowania (ang. program control graph) powstaje w ten sposób, że wierzchołki odpowiadają instrukcjom (z wyjątkiem wierzchołka
początkowego, któremu odpowiada pierwszy begin, i końcowego, któremu odpowiada
ostatni end), zaś łuki możliwym transferom sterowania. W grafie przepływu sterowania
każdy wierzchołek może być osiągnięty z wierzchołka początkowego i istnieje przynajmniej
jedna ścieżka prowadząca z dowolnego wierzchołka do wierzchołka końcowego. Liczba cyklomatyczna (ang. cyclomatic number) spójnego modułu programu jest równa y(G) = m -n + 1 , gdzie m i n są odpowiednio: liczbą łuków i liczbą wierzchołków w grafie przepływu
56 2. Podstawy analizy algorytmów
sterowania G. Formalnie, l iczba ta jest równa maksymalnej liczbie liniowo niezależnych
cykli grafu (rozmiarowi bazy cykli). Jest to parametr charakteryzujący złożoność kodu
źródłowego, zwany złożonością cyklomatyczną (ang. cyclomatic complexity), równy zara
zem liczbie instrukcj i decyzyjnych i liczbie podstawowych ścieżek programu minus jeden. Ścieżki podstawowe odgrywają istotną rolę w testowaniu programu, ponieważ za ich pomo
cą można wygenerować każdą możliwą drogę przepływu sterowania. Powszechnie przyjmu
je się, że jeżeli liczba cyklomatyczna grafu programu przekracza 1 0, to należy podzielić go
na moduły spełniające warunek y(G) < 10 .
Przykład 2.1 1 Poniższy graf G reprezentuje graf przepływu sterowania dla algorytmu 2 . 1 . Mamy tutaj
m = 8, n = 7, więc y(G) = 2. Zatem liczba podstawowych ścieżek wynosi 3 . Które to ścieżki?
Rys. 2. 1 . Graf przepływu sterowania dla algorytmu 2. 1 •
2.8. Wrażliwość algorytmów
Funkcje złożoności obliczeniowej Wen) i A(n) mówią nam, jak bardzo szybki asympto
tycznie jest wzrost czasu obliczeń na określonych zestawach danych. Aby stwierdzić, na ile
funkcje te są reprezentatywne dla wszystkich danych wejściowych rozmiaru n, rozważa się
2.8. Wrażliwość algorytmów 57
dwie miary wrażliwości algorytmu: wrażliwość najgorszego przypadku (ang. worst-case sensitivity), zwaną też wrażliwością pesymistyczną, oraz wrażliwość średniego przypadku (ang. average-case sensitivity) zwaną też wrażliwością oczekiwaną. Dla danego algorytmu A niech D", 1, t(I) i p(I) będą zdefiniowane tak jak w punkcie 2 .3 .3 . Wówczas wrażliwość pesymistyczna to
(2.5)
Definicja wrażliwości oczekiwanej jest bardziej skomplikowana i wymaga wprowadzenia zmiennej losowej X", której wartościąjest t(I) o rozkładzie p(I) dla 1 EDn' Wówczas
(2.6) ben) = dev(Xn)
gdzie dev(Xn) = �var(Xn ) jest standardowym odchyleniem zmiennej losowej X", co ozna
cza, że wariancja zmiennej losowej Xn spełnia równanie
var(Xn) = Ł (t(J) - ave(Xn» 2 p(I) lED"
gdzie ave(Xn) jest wartością oczekiwaną zmiennej Xn. Im większe są wartości fi.mkcji Ć1(n) i ben), tym algorytm jest bardziej wrażliwy na dane wej
ściowe i tym bardziej jego zachowanie w przypadku rzeczywistych danych może odbiegać od zachowania opisanego fi.mkcjami Wen) i A(n). Łatwo zauważyć, że O ::::: o(n) ::::: Ć1(n) < Wen) dla każdego n i dla każdego algorytmu A . W przypadku skrajnym wrażliwość pesymistyczna i średnia może sięgać - w sensie rzędu - złożoności obliczeniowej najgorszego przypadku. Poniżej podajemy przykład takiej sytuacji, który został zaczerpnięty z [3] .
Przykład 2.12 Problem: Niech L będzie tablicą n-elementową. Znaleźć pozycję x w L przy założeniu, że L zawiera x. Algorytm: Algorytm 2 . 1 .
Operacja podstawowa: Porównanie x z pozycją na liście. Złożoność najgorszego przypadku: Wen) = n (przykład 2.2) . Złożoność średniego przypadku: A(n) = (n + 1 )/2 (przykład 2.2).
Wrażliwość najgorszego przypadku: W naj gorszym przypadku x zajmuje ostatnią pozycję na liście, wówczas t(I) = n. W najlepszym przypadku x zajmuje pierwszą pozycję w L, czyli t(I) = 1 . Zatem Ć1(n) = n - L Wrażliwość średniego przypadku: Najpierw obliczymy wariancję zmiennej losowej Xn
n ( n + l ) 2 l l n ( n + l )2
var(X17 ) = Ł t{1; ) -- ' - = - Ł i -- = ;=\ 2 n n ;=\ 2
=�[ n(n + I)(2n + l) 2(n + l) n(n + l)
+ n(n + l)2 )
= n 6 2 2 4
(n + 1)(2n + l )
6
58 2 . Podstawy analizy algorytmów
Obecnie mamy S(n) "" �n2 /12 "" O .29n = O(n). Zatem wszystkie cztery funkcje są liniowe,
co oznacza dużą wrażliwość algorytmu na dane wejściowe. •
2.9. Programowanie a złożoność obliczeniowa
Jak wiadomo, algorytmy mogą być wyrażone na różnym poziomie abstrakcj i. Programowanie jest procesem przekształcania opisu algorytmu i struktur danych na program dla określonego komputera. W trakcie programowania winniśmy uściślić wyniki analizy a priori. Na przykład, jeśli analizowaliśmy frekwencje wykonania dwóch operacj i podstawowych, to należy wprowadzić wagi odpowiadające ich czasom wykonania. Można również dokonać szczegółowej analizy zapotrzebowania programu na pamięć.
2.9 .1 . Rząd złożoności obliczeniowej
Rząd złożoności obliczeniowej jest najważniejszym czynnikiem wpływającym na ocenę przydatności algorytmu. Zanim tezę tę uzasadnimy licznymi przykładami praktycznymi, wprowadzimy określenia funkcj i najczęściej spotykanych w teorii złożoności obliczeniowej .
Niech j{n) będzie funkcją rzeczywistą zmiennej n E !T. O funkcj i j{n) powiemy, że jest stala (ang. constant), gdy j{n) = 0(1). Funkcje stałe występują na przykład przy opisie pojedynczych instrukcj i . Funkcjęj{n) nazywać będziemy polilogarytmiczną (ang. polylogarithmic), gdy j{n) = ro( l ) i istnieje stała c > O taka, żej{n) = G(1otn) . Z funkcjami polilogarytmicznymi mamy do czynienia, analizując algorytmy równoległe. Funkcje stałe i polilogarytmiczne określa się niekiedy mianem subliniowe (ang. sublinear). Pod pojęciem fitnkcji liniowej (ang. linear) rozumiemy funkcję j{n) = G(n). Funkcje liniowe występują w przypadku niektórych algorytmów optymalnych. O funkcj i j{n) powiemy, że jest quasi-liniowa (ang. quasilinear), gdy j{n) = ro(n) i jen) = O(nlogn). W praktyce funkcje quasi-liniowe rosną prawie tak szybko jak liniowe, z wyjątkiem dużych wartości n. Analogicznie do funkcj i liniowej przez funkcję kwadratową (ang. quadratic) rozumiemy funkcję j{n) = G(n2) . Ogólnie, funkcja wielomianowa (ang. polynomial), to funkcja j{n) = G(n'), gdzie c jest pewną stałą dodatnią. Funkcjami rosnącymi szybciej niż jakakolwiek funkcja wielomianowa są funkcje superwielomianowe określone następująco. Funkcję j{n) nazywamy superwielomianową (ang. superpolynomial), jeżeli dla każdej stałej c > O mamy jen) = ro(nC) oraz dla wszystkich stałych E > O j{n) = 0((1 + En. Kolejną grupą funkcj i są funkcje wykładnicze w ścisłym tego słowa znaczeniu. Mówimy, że j{n) jest wykładnicza (ang. exponential), gdy istnieją stałe c,d > l takie, że j{n) = O(cn) i j{n) = O(d'). Wreszcie dochodzimy do funkcji
j{n), które rosną najszybciej . O funkcji takiej powiemy, że jest superwykladnicza (ang. superexponential), gdy dla każdej stałej c > O zachodzi j{n) = ro(cn). Ogólnie, trzy ostatnie typy funkcj i określa się mianem niewielomianowych (ang. non-polynomial). Z funkcjami niewielomianowyrni spotykamy się przy rozwiązywaniu trudnych problemów kombinatorycznych. W tabeli 2. 1 podajemy przykłady omówionych funkcji .
59 2.9. Programowanie a złożoność obliczeniowa ------------------------------------
Klasa funkcji Typ funkcji
stała subliniowa
polilogarytmiczna r-------------------�
wielomianowa
niewielomianowa
l iniowa I
quasi-liniowa I
kwadratowa
superwielomianowa
wykładnicza I
superwykladnicza
Tabela 2.1
Przykłady
0{1oglog n). 0{1og2n)
0(n). 0(n(1 +1/n)"}
0(n·log n). 0(n-loglog n)
Istnieje ogromna różruca pomiędzy algorytmami wielomianowymi i niewielomiano
wymi, która ujawnia się j uż przy średnich rozmiarach danych. Różnicy tej nie jest w stanie
zatrzeć fakt, że algorytmy o niższym rzędzie złożoności obliczeniowej mają zwykle znacz
nie wyższą stałą proporcjonalności. Oznacza to, że algorytmy niewielomianowe są prak
tycznie użyteczne jedynie dla bardzo małych wartości n. W tabeli 2.2 podajemy przykład postępu, jaki dokonał się w dziedzinie projektowania
algorytmów badających planamość grafu. Przypomnijmy, że graf jest p/anamy (ang. pla
nar) wtedy i tylko wtedy, gdy może być narysowany na płaszczyźnie bez przecinania się
krawędzi (patrz pkt 3 .4). Pewnego wyjaśnienia wymaga sens stałej proporcjonalności c
równej 1 0 milisekund. We wszystkich przypadkach stała c oznacza czas testowania grafu
jednowierzchołkowego, z wyjątkiem algorytmu A4, dla którego oznacza ona połowę czasu
potrzebnego na zbadanie grafu 2-wierzchołkowego.
Tabela 2.2
Czas Rozmiar obliczeń dla analizowanego grafu
Algorytm c = 1 0 ms w przypadku udostępnie-i n = 1 00 nia komputera na okres
Symbol Autor [rok) Złożoność minuty godziny
A, Kuratowski (1 930) cn6 325 lat 4 8
A2 Goldstein (1 963) cn3 2.8 godzin 1 8 7 1
A3 Lempel et al . (1 967) cn2 1 00 sekund 77 600
A4 Hopcroft-Tarjan (1971) cnlog2n 7 sekund 643 24 673
As Hopcroft-Ta�an (1 974) cn 1 sekunda 6 000 36.1 04
Można przeprowadzić dodatkowe obliczenia przy podanej wartości c, np. dla
n = 1 0, 20, . . . . , 90, aby przekonać się, jak szybko rośnie czas obliczeń dla wyższych złożo
ności obliczeniowych. Podobnie, znaczne zwiększenie wartości c dla algorytmów A4 i A5 nie spowalnia ich w istotny sposób, z wyjątkiem małych wartości n. Oznacza to, że dla
dużych rozmiarów danych algorytmy wolniejsze niż O(nlog n) są często niepraktyczne.
60 2. Podstawy analizy algorytmów
Obecnie przyjmijmy, że nasz zestaw algorytmów został uzupełniony algorytmem Ao
o złożoności 8(2n) . Przypuśćmy, że następna generacja maszyn cyfrowych będzie dziesięć
razy szybsza od obecnej . Interesuje nas wpływ wzrostu prędkości komputerów na maksy
malny rozmiar zagadnienia, które można rozwiązać w jednostce czasu.
Tabela 2.3 pokazuje dobitnie, że dopiero algorytmy liniowe potrafią w pełni wyzy
skać dobrodziejstwa płynące ze wzrostu szybkości komputerów. Na przykład dla algo
rytmu liniowego 1 00% wzrost szybkości owocuje w postaci 1 00% wzrostu maksymalne
go rozmiaru zagadnienia, natomiast odpowiednie współczynniki dla algorytmów 8(n2) i
8(n3) wynoszą zaledwie 32% i 22%. Co więcej , fakt, że z roku na rok potrafimy rozwią
zywać komputerowo coraz większe problemy, jest spowodowany głównie postępem w
dziedzinie inżynierii oprogramowania, a nie w dziedzinie technologii sprzętu liczącego.
To ogólne spostrzeżenie uzyskało szczególne potwierdzenie w latach 1 945-75 . Tym
samym dochodzimy do paradoksalnego wniosku: w miarę wzrostu szybkości maszyn
cyfrowych i spadku ich ceny zapotrzebowanie na efektywne algorytmy rośnie, a nie
maleje.
Rozważmy jeszcze jeden przykład. Naturalny algorytm dla problemu naj liczniej sze
go zbioru niezależnego w grafie n-wierzchołkowym działa w czasie 0(2n). Natomiast
najllOwszy algorytm opublikowany w roku 2006 przez Fomina i in. [4] ma złożoność
0(2o.288n), będąc jednocześnie niezwykle prostym w implementacj i . Zauważmy, że gdyby
zignorować stałe proporcjonalności ukryte w notacji asymptotycznej , to użycie nowego
algorytmu pozwoliłoby na przetwarzanie w tym samym czasie grafów niemal 4-krotnie
większych, podczas gdy 2-krotne zwiększenie mocy obliczeniowej komputera umożliwia
powiększenie rozmiaru grafu wejściowego jedynie o l wierzchołek!
Na zakończenie tych rozważań przytoczymy jeszcze jedną tabelę ilustrującą związek
pomiędzy rzędem złożoności, stałą proporcjonalności, rozmiarem danych i rzeczywistym
czasem obliczeń na minikomputerze i superkomputerze.
Program o złożoności 0(n3) był wykonywany na superkomputerze CRA Y- L Eksperymen
talnie stwierdzono, że jego złożoność wynosi 3n3 nanosekund dla danych rozmiaru n. Konkuren
cyjny wobec niego algorytm liniowy został wykonany na komputerze osobistym IBM PCI AT
286-1 6MHz. Jego stała proporcjonalności była 1 milion razy większa. Mimo że algorytm sze
ścienny wystartował z większym impetem, drugi algorytm, mający złożoność o 2 rzędy niższą,
dogonił go i okazał się szybszy dla n > 1000.
Tabela 2.3
Algorytm Maksymalny rozmiar zagadnienia
symbol złożoność przed wzrostem po 10-krotnym wzro-prędkości mc ście prędkości mc
Ao 8(2n) no no + 3.3
A1 0(n6) n1 1 .46n1
A2 8(n3) n2 2 . 1 5n2
A3 8(n2) n3 3 .1 6n3
A4 8(nlog n) n4 1 0n4 dla n4 »1
As 8(n) ns 1 0ns
------ - ----
2. 9. Programowanie a złożoność obliczeniowa 6 1
Tabela 2.4
n Cr�-1 3n ns
IBM PC/AT 3 000 OOOn ns
10 3 1ls 30 ms
100 3 ms 300 ms
1 000 3 s 3 s
10 000 49 min 30 s
1 000 000 95 lat 5 min
2.9.2. Stała proporcjonalności złożoności obliczeniowej
Jak wiadomo, złożoność algorytmu jest wyznaczana i podawana z dokładnością do stałego współczynnika proporcjonalności i z uwzględnieniem tylko naj istotniejszych członów.
Na przykład, jeżeli mówimy, że złożoność pewnego algorytmu jest O(i), to rzeczywista liczba operacj i wykonywanych dla danych rozmiaru n może być postaci
k k k k ( ) k k akn + . . . + aln + aO < akn + . . . + al n + aon = ao + a1 + . . . + ak n = cn .
Poprzednio pokazaliśmy, że stała c ma niewielki wpływ na faktyczną czasochłonność algorytmu. Obecnie wykażemy, że stopień wzrostu maksymalnego rozmiaru zagadnienia rozwiązywalnego w jednostce czasu nie zależy od wielkości tej stałej proporcjonalności . Wynika to z następującego rozumowania. Niech s będzie maksymalnym rozmiarem danych, które można przetworzyć rozważanym algorytmem w ustalonej jednostce czasu. Przypuśćmy, że naszą jednostkę czasu wydłużamy t razy lub, co równoważne, nasz komputer ma przełącznik turbo, po włączeniu którego prędkość działania rośnie t razy (lub że obliczenia przenieśliśmy na komputer nowszej generacj i działający t-krotnie szybciej) . Niech s' oznacza maksymalny rozmiar zagadnienia, które można rozwiązać w nowej sytuacj i. Wówczas
fis') = liczba kroków wykonywanych przez algorytm o złożonościjpo zmianie warunków = t razy liczba kroków wykonywanych przez nasz algorytm przed zmianą = t . fis).
Zatem
(2.7) fis') = t . fis).
Obecnie musimy rozwiązać równość (2.7) względem s'. Dla przykładu, jeżeli fis) = c2n, to s ' = s + 19 t. Odpowiednie mnożniki dla funkcji cn2 i en wynoszą odpowiednio fi oraz t i nie zależą od stałej proporcjonalności c (por. tabela 2.3.) . Rzeczywiście, skoro funkcjajwystępuje po obu stronach równości (2.7), to obustronne pomnożenie lub podzielenie przez dowolną stałą nie ma wpływu na stopień wzrostu rozmiaru rozwiązywanego problemu.
Stała c, która w praktyce wyrażona jest w ułamkach sekundy, zależy również od algorytmu i jego implementacji. Poniżej podamy kilka praktycznych sposobów zmniejszania tej stałej w programach w wyniku optymalizacji kodu. Jednakże należy pamiętać, że efektywność poniższych usprawnień zależy silnie od sprzętu, na którym wykonuje się dany algorytm.
62 2. Podstawy analizy algorytmów
1. Zastępowanie operacji arytmetycznych. Różne operacje, jak wiadomo, nie są wyko
nywane przez maszynę z jednakową szybkością. Najszybsze jest dodawanie i odejmowanie
(zwłaszcza stałoprzecinkowe), a najwolniejsze dzielenie zmiennoprzecinkowe. Dla przykładu
Zamiast Lepiej l . i := 2*j; i := j + j;
2. x := 32.7/5; x := .2*32.7;
3 . i := sqr(j); i := j*j;
4. i := trunc(i/j); i := i div j;
2. Eliminowanie wyrażeń. Można przyspieszyć proces obliczeń przez unikanie wielo
krotnych obliczeń wartości tych samych wyrażeń. I lustruje to dobrze znany przykład tan
gensa hiperbolicznego. Mianowicie
Zamiast tghx := (exp(x)-exp(-x))/(exp(x) +
exp(-x));
Lepiej expx := exp(x);
expodwr := 11 expx;
tghx := (expx-expodwr)/(expx+expodwr);
3. Eliminowanie zmiennych indeksowanych. Dane, do których najczęściej sięgamy,
powinny być dostępne najmniejszym kosztem. W szczególności, operowanie zmiennymi
indeksowanymi zabiera więcej czasu niż operowanie na zmiennych prostych. Toteż warto,
jeśli to możliwe, zrezygnować z tych zmiennych. Takie oszczędności można uzyskać w
przypadku, gdy dana zmienna ze wskaźnikami jest wykorzystywana więcej niż raz. Wów
czas można najpierw podstawić tę wielkość pod zmienną prostą (tak jak poprzednio), a
następnie odwołać się już do owej zmiennej prostej . Dla przykładu
Zamiast for i := l to n do
A [i] := B[k];
Lepiej x := B[k]; for i := l to n do A [i] := x;
4. Ograniczanie liczby pętli. Każda pętla wymaga wykonania, oprócz operacj i podsta
wowych, pewnych operacji organizacyjnych związanych z przejściem do następnika,
sprawdzeniem warunku końca itd. Wszystko to trwa, więc jeśli to możliwe, należy rozwijać
takie pętle. Na przykład
Zamiast Lepiej
l . for i := l to 3 do A [ l ] := A [l] + l ; A [i] := A [i] + i; A[2] := A[2] + 2;
A [3] := A[3] + 3 ;
2. for i := l to n do for i := l to n do begin
write(A[i)); write (A [i]);
for j := p to n do if i � P then write(B[i])
write(BU)); end;
2. 9. Programowanie a złożoność obliczeniowa 63
o ile kolejność wydruków nie gra roli. Oczywiście, powyższe nie musi być zawsze prawdą, gdyż zależy od kompilatora i użytego sprzętu.
5. Optymalizacja pętli wielokrotnych. Poprzednio, rozważając pętle, podaliśmy typowe przykłady ograniczania lub wręcz likwidacj i instrukcj i for. Jeśli jest to niemożliwe, to należy przynajmniej spróbować zoptymalizować ich organizację. Włożony trud opłaci się sowicie, ponieważ takie pętle wykonują się nierzadko tysiące razy. Jedną z zasad jest używanie pętli o mniejszej liczbie powtórzeń jako bardziej zewnętrznej, bowiem każde otwarcie pętli wymaga dodatkowego czasu. Ilustruje to następujący przykład
Zamiast for i := l to 1 00 do
for j := l to 10 do A [i,j] := i modj;
Lepiej forj := l to 1 0 do
for i := l to 1 00 do A[i,j] := i modj;
6. Umieszczanie wartownika na końcu tablicy. Uproszczenie warunku końca pętli while zazwyczaj skraca czas wykonania o przynajmniej kilkanaście procent. Dla przykładu
Zamiast i := l ; while i < n and A [i] '* t do
i := i + l ;
Lepiej A[n + l ] := t; i := 1 ;
w hile A [i] '* t do i : = i + l ;
gdzie t jest wartością poszukiwaną w tablicy A [l. . n]. Sześć powyższych technik podstawowych nie wyczerpuje wszystkich metod przyspie
szania realizacj i programów. Na przykład wiadomo, że przekazywanie parametrów do procedury przez wartość (ang. call-by-value) jest czasochłonne, gdyż wiąże się z deklarowaniem nowych zmiennych w procedurze i wykonywaniem dla nich instrukcj i przypisania. Ma to istotne znaczenie w przypadku dużej liczby zmiennych, a zwłaszcza tablic. Środkiem zaradczym jest stosowanie do tego celu zmiennych globalnych lub przesyłanie parametrów przez adres (ang. call-by-reference).
Wiele innych cech oprogramowania jest tak samo ważnych jak efektywność. D. Knuth zauważył, że przedwczesna optymalizacja kodu programów jest źródłem wielu niekorzystnych zjawisk - może naruszyć poprawność, funkcjonalność i łatwość konserwacji programów. Ponadto istnieje punkt krytyczny: praca wykraczająca poza ten punkt staje się trudna i daje niewielkie efekty.
2.9.3. Imperatyw złożoności obl iczeniowej i odstępstwa
Jest oczywiste, że powinniśmy stosować takie algorytmy, które mają najniższą możliwą złożoność obliczeniową. Jest to ogólna reguła, od której są liczne odstępstwa. W jakich sytuacjach złożoność czasowa nie jest decydującym czynnikiem przemawiającym za implementacją danego algorytmu? Jedną taką sytuację już poznaliśmy i sformułujemy jąjako punkt l .
1 . Gdy program będzie używany niewiele razy, to koszt napisania programu i jego uru
chomienia zdominuje pozostałe koszty. W tym przypadku wybieramy algorytm, który jest naj prostszy do implementacj i.
64 2. Podstawy analizy algorytmów
2. Jeśli program będzie wykonywany na ,,małych" danych, to rząd złożoności może nie być tak istotny jak wielkość współczynnika proporcjonalności . Co znaczy "mały" rozmiar danych, zależy od konkretnej sytuacj i . Są takie algorytmy, jak np. algorytm Schonhage i Strassena dla mnożenia liczb całkowitych, które są asymptotycznie najszybsze dla danego problemu, ale mimo to nigdy nie zostały użyte w praktyce, właśnie z uwagi na wysokie stałe proporcjonalności w porównaniu z innymi algorytmami nieoptymalnymi (patrz tabela 2 .4).
3. Efektywny, ale skomplikowany, algorytm komputerowy może nie być pożądany w sytuacj i, gdy napisanie programu powierzyliśmy komuś innemu, sami zaś musimy zająć się jego konserwacją. Wówczas winniśmy l iczyć się z tym, że taki program stanie się bezużyteczny, gdy pojawi się jakiś trudny do wykrycia błąd, tzw. "błąd ulotny", lub trzeba będzie dokonać pewnej drobnej przeróbki.
4. Znane są przykłady algorytmów, które są szybkie w sensie złożoności czasowej , ale potrzebują tak dużo pamięci, że ich implementacja wymaga użycia wolnej pamięci zewnętrznej . Częste odwołania do pamięci zewnętrznej mogą przekreślić praktyczną skuteczność takich algorytmów optymalnych (kompromis przestrzenno-czasowy).
5. Istnieją algorytmy, które są bardzo wolne w sensie złożoności naj gorszego przypadku danych, a które działaj ą bardzo szybko w przypadku przeciętnym. Takim algorytmem jest np. metoda simpleksów dla rozwiązania zadań programowania liniowego, która ma liniowy oczekiwany czas działania oraz wykładniczą pesymistyczną złożoność obliczeniową. Dlatego metoda ta jest powszechnie stosowana w praktyce, pomimo że znane są algorytmy wielomianowe dla tego problemu, np. algorytm elipsoidalny Chaczijana (lecz są to wielomiany wysokiego stopnia).
6. Gdy program działa na liczbach rzeczywistych, równie ważna jak złożoność obliczeniowa jest dokładność obliczeń. W algorytmach numerycznych czasami cechy te stają w sprzeczności i należy zdecydować się na algorytm nieco wolniejszy, lecz stabilny numerycznie.
2.10. Algorytmy probabil istyczne
Algorytmy probabilistyczne (ang. probabilistic algorithms) (inaczej randomizowane, ang. randomized algorithms) stanowią klasę algorytmów, która wywalczyła sobie bardzo solidną pozycję w informatyce w ciągu ostatnich lat. Historycznie, pierwszym ważnym algorytmem probabilistycznym był test pierwszości Millera-Rabina z roku 1 976. Dziś wiemy, że wiele problemów z rozmaitych dziedzin może byś rozwiązanych lepiej , gdy używamy algorytmów probabilistycznych zamiast klasycznych (tj . deterministycznych). Lepiej może przy tym oznaczać szybciej lub przy użyciu mniejszej ilości pamięci . Algorytm randomizowany może być również łatwiejszy w implementacj i równoległej niż jego deterministyczny odJJowiednik. Znanych jest także wiele przypadków, w których podejście probabilistyczne �aga dużo mniej skomplikowanych rozważań teoretycznych, a co za tym idzie, jego analiza i wykorzystywanie w praktyce są znacznie prostsze.
Algorytm probabilistyczny możemy nieformainie zdefiniować jako algorytm, który dysponuje idealną monetą i może wykonywać nią rzuty, uzależniając swoje postępowanie od wyników losowania. Ściślej, algorytm taki poza podstawowym wejściem związanym
2. 10. Algorytmy probabilistyczne 65
z problemem przyjmuje dodatkowe wejście w postaci pewnej liczby losowych bitów. W praktyce oznacza to, że korzystamy z generatora liczb pseudo losowych udostępnianego przez środowisko, w którym algorytm jest implementowany i wykonywany.
Algorytm probabilistyczny może, w przeciwieństwie do deterministycznego, wygenerować różne wyniki dla tych samych podstawowych danych wejściowych (w szczególności może, w zależności od bitów losowych, rozwiązać problem lub nie). Również liczba operacj i potrzebnych do zakończenia działania algorytmu może zmieniać się w zależności od użytych bitów losowych. Te dwa aspekty implikują dwie podstawowe klasy algorytmów probabilistycznych:
l . Algorytmy Monte Carlo. W algorytmach tej klasy kładziemy nacisk na pesymistyczną (tj . dla dowolnych bitów losowych) złożoność obliczeniową, dopuszczając z pewnym prawdopodobieństwem, że algorytm nie rozwiąże stawianego przed nim problemu.
2 . Algorytmy Las Vegas. Algorytmy tego typu zawsze rozwiązują problem, przy zadowalającej oczekiwanej złożoności obliczeniowej (tj . uśrednionej po wszystkich możliwych wartościach bitów losowych). Dopuszczamy jednak, by dla pewnych, rzadkich ciągów bitów losowych czas działania algorytmu był gorszy niż w przypadku średnim, a nawet gorszy niż w algorytmie deterministycznym. Podstawowa praktyczna różnica między dobrym algorytmem typu Las Vegas a algorytmem deterministycznym polega na tym, że spodziewana złożoność obliczeniowa dotyczy każdych możliwych danych wejściowych (nie ma "pechowych danych" możemy jedynie "pechowo losować").
Rozważmy dla przykładu następujący problem.
Przykład 2.1 3 Dane: Ciąg A długości n składający się z małych liter alfabetu łacińskiego, przy czym wszystkie litery występują w ciagu tyle samo razy (każda litera n/26 razy). Zadanie: Podać dowolną pozycję w ciągu, na której występuje litera a. Oczywisty algorytm deterministyczny rozwiązuje nasz problem w pesymistycznym czasie O(n), gdyż może być i tak, że wszystkie litery a są zgrupowane w drugiej połowie ciągu.
p rocedure FindAnyDeterministic(A, n); begin
for i := l to n do if A [i] = 'a' then return (i)
end;
Możemy jednak stosunkowo łatwo skonstruować algorytm Monte Carlo, który dla dowolnego z góry ustalonego e > O rozwiąże nasz problem z prawdopodobieństwem l-e w czasie O(m).
procedure FindAnyMonteCarlo(A, n, m); begin
for i := l to m do begin
j := liczba losowa ze zbioru { 1 , . . . , n } ;
66
end;
if AU] = 'a' then return (j)
end; return (jailure)
2. Podstawy analizy algorytmów
Powyższy algorytm ma pesymistyczną złożoność obliczeniową O(m) i zapewnia suk
ces z prawdopodobieństwem 1-(25/26t'. Mamy
E = (25/26)m
log E = mlog(25/26)
m = logE/log(25/26) = -logEl-Iog(25/26) = log( l IE)/log(26/25)
Ustalmy E. Przyjmując
I log(1h) l m = I log(26/25)
otrzymamy algorytm spełniający powyższe warunki. •
Wartości m wyznaczone dla kilku przykładowych prawdopodobieństw porażki zostały
zebrane w tabeli poniżej .
E m
0,1 59
0,01 1 18
0 ,001 177
0,0001 235
Dysponując algorytmem Monte Carlo i potrafiąc sprawdzić, czy jego wykonanie za
kończyło się sukcesem, możemy pokusić się o skonstruowanie algorytmu Las Vegas. Kon
strukcję taką można przeprowadzić na dwa sposoby:
1 . Wywołujemy algorytm Monte Carlo "do skutku", aż osiągniemy sukces. Podejście to
ma jednak tę wadę, że pesymistyczny czas działania otrzymanego w ten sposób algo
rytmu Las Vegas nie daje się oszacować. Dla dowolnego N > O istnieje ciąg wyborów
losowych, prowadzący do wykonania przez nasz algorytm więcej niż N operacj i.
2 . Wady tej pozbawione jest drugie podejście. Jeśli dysponujemy algorytmem determini
stycznym i algorytmem Monte Carlo oraz potrafimy sprawdzić, czy wykonanie algo
rytmu Monte Carlo kończy się sukcesem, to możemy stworzyć algorytm Las Vegas
(często bardzo efektywny) następująco:
• wykonaj algorytm Monte Carlo • jeśli wykonanie zakończyło się sukcesem, to koniec. W przeciwnym razie wyko
naj algorytm deterministyczny.
Przeanalizujmy to drugie podejście. Niech n oznacza rozmiar problemu. Jeśli przez
T(n, 6) oznaczymy pesymistyczną liczbę operacji wykonywanych przez algorytm Monte
Carlo przy prawdopodobieństwie porażki 6, zaś przez U(n) pesymistyczną liczbę operacji
Zadania 67
dla algorytmu deterministycznego, to spodziewany czas działania algorytmu Las Vegas wyniesie A(n) = 0«( 1- r::) T(n, E) + EU(n)).
Przykład 2.14 Wróćmy do problemu z przykładu 2 . 1 3 . Rozpatrzmy następujący algorytm:
procedure FindAnyLasVegas(A, n); begin
end;
v := FindAnyMonteCarlo(A, n, Ilogn/log(26/25) l); if v =f. failure then return (v) ; return (FindAnyDeterministic(A, n))
W naj gorszym przypadku czas wykonania algorytmu FindAnyMonteCarlo wynosi zatem O(logn). Oczekiwany czas wykonania FindAnyLas Vegas dla E = l in szacuje się w związku z tym przez 0«(1 - l /n)logn + n/n) = O(logn + l ) = O(logn). Jeśli chodzi o oszacowanie pesymistyczne, to w naj gorszym wypadku będziemy musieli wykonać jednokrotnie obie procedury, czyli pesymistyczny czas wykonania algorytmu FindAnylas Vegas szacuje się przez Wen) = O(logn + n) = O(n) . _
Zadania
2. 1 . Jako pierwszy nietrywialny algorytm uznaje SIę algorytm Euklidesa do obliczania największego wspólnego dzielnika liczb i oraz).
function Euklides(i,j: integer): integer; begin
end;
while i "* j do if i > j then i := i -j elsej := j - i;
retum(i)
a) Udowodnij poprawność tego algorytmu. b) Oszacuj pesymistyczną złożoność obliczeniową, gdy i, j są kolejnymi liczbami natural
nymi. c) Odpowiedz, czy złożoność ta jest wielomianowa czy niewielomianowa. d) Napisz wersję tego algorytmu "z dzieleniem", czyli z operacją i := j mod i, i oszacuj jej
złożoność obliczeniową. Wskazówka: Rozmiarem danych jest tu łączna liczba cyfr obu liczb.
2.2. Podaj dokładną specyfikację wejścia i wyjścia, po czym zastosuj metodę niezmienników pętli dla dowodu poprawności następującego algorytmu dodawania wektorów A [l . . n] i B[l. .n] .
68
begin
end;
i := l ; while i ::; n do begin
qi] := A [i] + B[i] ; i := i + l
end
2. Podstawy analizy algorytmów
2.3. Podaj dokładną specyfikację wejścia i wyjścia, po czym zastosuj metodę niezmienników pętli dla dowodu poprawności następującego algorytmu znajdowania największej wartości w wektorze L.
begin
end;
i := 2; max := L[I] ; while i :::; n do begin
end
if L[i] > max then max := L[i] ; i := i + l
2.4. Należy obliczyć wartość następuj ącej sumy:
1 + + 1 + 2 + + 1 + 2 + 3 +
+ l + 2 + 3 + . . . + n. Napisz trzy wersje programu rozwiązujące to zadanie za pomocą odpowiednio: 0(n2), O(n) i 0(1) dodawań.
2.5. Zadanie polegające na obliczeniu wartości n-tej liczby ciągu Fibonacciego l , l , 2, 3, 5, 8, . . . może być rozwiązane trzema różnymi metodami o złożoności polilogarytmicznej, liniowej i wykładniczej . Napisz odpowiednie procedury w PseudoPascalu.
2.6. Następujący algorytm oblicza wartość wielomianu
p(x) = anXn + an_,xn-' + . . . + a,x + ao·
begin
end;
p := ao; xpower := l ; for i := l to n do begin
xpower := x*xpower; p := p + a;*xpower
end
Zadania 69
a) Ile mnożeń trzeba wykonać w naj gorszym przypadku? A ile dodawań? b) Ile mnożeń wykonuje się w przypadku przeciętnym? c) Czy możesz napisać algorytm, który wykonuje jedynie n mnożeń i n dodawań?
Uwaga! W zadaniu 2.6(c) chodzi o schemat Homera, który jest naj szybszym możliwym sposobem obliczania wartości wielomianu p(x).
2.7. Przeanalizuj poniższy fragment programu
x := 0.0; for d := l to n do
for g := d to n do begin suma := 0.0;
end;
for i := d to g do suma := suma + A [i] ;
x := max(x, suma)
i odpowiedz na następujące pytania: a) Jaki jest efekt działania powyższego kodu? b) Jaka jest jego złożoność obliczeniowa? c) Czy potrafisz napisać program wykonujący to samo zadanie w czasie liniowym?
2.8. Niech f R+ � R będzie funkcją malejącą i zmieniaj ącą znak. Następujący fragment programu
i := l ; while j{i) � O do i := i + l ; n := i - l ;
oblicza największą liczbę naturalną n, dla której j(n) � O, lecz jego złożoność wynosi O(n). Znajdź algorytm o lepszej złożoności obliczeniowej .
2.9. Oszacuj złożoność obliczeniową procedury zagadka.
procedure zagadka(n: integer); begin
end;
for i := l to sqr(n) do begin
} := l ; while} < sqrt(n) do} :=} +}
end
2.10. Oszacuj złożoność obliczeniową procedury zagadka.
procedure zagadka(n: integer); begin
for i := l to sqr(n) do begin k := l ; 1 := l ;
70 2. Podstawy analizy algorytmów
while l < n do begin k := k + 2; l := 1 + k end end
end;
2 . 1 1 . Oszacuj złożoność obliczeniową procedury zagadka.
procedure zagadka(n: integer); begin
end;
for i := n -l downto l do if odd(i) then begin
for} := l to i do; for k := i + l to n do x := x + l
end
2 . 1 2. Oszacuj złożoność obliczeniową procedury zagadka.
procedure zagadka(n: integer); begin
end;
for i := n -l downto l do if odd(i) then begin
for} := l to i do for k := i + l to n do x := x + l
end
2. 13. Oszacuj złożoność obliczeniową procedury zagadka z dołu i z góry.
procedure zagadka(n: integer); begin
end;
for i := l to n - l do for} := i + l to n do
for k := l to} do;
2.14. Napisz program, który przesuwa cyklicznie n-elementowy wektor A [l. .n] o k pozycj i w lewo, gdzie k < n. Program winien mieć złożoność czasową O(n) i działać w miejscu (to znaczy wymagać 0(1) dodatkowej pamięci).
2.15. Potęgę x59 można obliczyć za pomocą poniższej procedury:
procedure x59; var x,y,x2,x4,-'C8,.:cl 6,x32 : real; begin
read(x);
Zadania
end;
x2 := x*x; x4 := x2*x2; x8 := x4*x4; x 1 6 := x8*x8; x32 := x 1 6*x1 6; y := x*x2; y := y*x8; y := y*x16; y := y*x32;
write(y)
Zminimalizuj liczbę zmiennych w tym programie.
71
2.16. Dla procedury x59 z poprzedniego zadania oblicz: długość, objętość, stopień trudności i poziom programu, wysiłek programisty oraz oczekiwaną liczbę błędów.
2 . 1 7. Dana jest procedura rekurencyjna:
procedure razy(x,y:integer): integer; begin
end;
if n = l then return(x*y) else begin
end
podziel ciągi bitów x i y na połowy, tj . XI , X2 i y" Y2; a := razy(xl + X2, Y I + Y2); b := razy(xl , YI ); c := razy(x2, Y2) ; return(b*2n + (a-b-c)*2n/2 + c)
gdzie x i y są dwiema l iczbami binarnymi n-bitowymi (n = 2k).
1 . Udowodnij , że procedura razy(x, y) zwraca i loczyn x*y. 2. Zakładając, że dodawania i przesunięcia (mnożenie przez potęgę 2) mogą być wykonane
w liniowym czasie, oszacuj złożoność obliczeniową tej procedury.
2.18. Pokaż, że klasyczny algorytm obliczania pierwiastków XI i X2 równania kwadratowego x2 - 2px + q = O o współczynnikach p, q ze zbioru D = {(P, q): p *- O, q *- O, i - q > O } według wzorów:
XI := P + sqrt(p*p - q), X2 := P - sqrt(p*p - q)
72 2. Podstawy analizy algorytmów
jest niestabilny numerycznie, gdy i » q. W jaki sposób można zmodyfikować algorytm klasyczny, aby usunąć tę niedogodność?
2 . 1 9. Oblicz: długość, objętość, stopień trudności i poziom programu oraz wysiłek programisty dla algorytmu podanego:
a) w zadaniu 2.6; b) w zadaniu 2 . 1 3 .
2.20. Narysuj graf przepływu sterowania, oblicz jego liczbę cyklomatyczną oraz wypisz wszystkie ścieżki podstawowe dla algorytmu podanego:
a) w zadaniu 2 . 1 ; b) w zadaniu 2 .3 .
2.2 1 . Dane są cztery algorytmy o złożoności 2n, 1 0n2, l OOn ..r;; i 2000n. Podaj przedziały zmienności n, w których te algorytmy są naj szybsze.
2.22. Dla pewnego problemu dane są dwa konkurencyjne algorytmy Al i A2 o złożoności odpowiednio cn i dn2. Pomiary czasów wykonania tych algorytmów dały następujące wyniki:
�r 1 024 2048
Algorytm
A1 128 "lS 256 11S
A2 1 6 11S 64 11S
Czy to prawda, że: a) podana informacja jest sprzeczna, bo algorytm O(n) musi być lepszy od O(n2)? b) Al wygrywa z A2 jedynie dla n < 1 6? c) A l zacznie wygrywać z A2, gdy n przekroczy 4096? d) A l zacznie wygrywać z A2, gdy n przekroczy 8 1 92? e) cn nigdy nie pokona algorytmu o złożoności dn2?
2.23. Stosując wyszukiwanie sekwencyjne lub binarne w tablicy nieposortowanej, wybieramy między czasem wyszukiwania, a czasem przetwarzania wstępnego. Jak wiele wyszukiwań binarnych trzeba wykonać w naj gorszym przypadku danych w posortowanej tablicy, ażeby opłacił się czas potrzebny na wstępne posortowanie tablicy? Przyjmując, że współczynniki proporcjonalności złożoności są równe 1 , odpowiedź sformułuj w terminach oszacowań asymptotycznych.
2.24. Udowodniono, że pewien algorytm A ma złożoność 0(n2,5). Które z poniższych stwierdzeń mogą być prawdziwe w odniesieniu do algorytmu A? a) Istnieją stałe C I i C2 takie, że dla wszystkich n czas działania A jest krótszy niż cln2,5 + C2
sekund.
Zadania 73
b) Dla każdego n istnieje zestaw danych rozmiaru n, dla którego czas działania A jest krótszy niż n2.4 sekund.
c) Dla każdego n istnieje zestaw danych rozmiaru n, dla którego czas działania A jest krótszy niż n2,6 sekund.
d) Dla każdego n istnieje zestaw danych rozmiaru n, dla którego czas działania A jest dłuższy niż n2,4 sekund.
e) Dla każdego n istnieje zestaw danych rozmiaru n, dla którego czas działania A jest dłuższy niż n2,6 sekund.
2.25. Wyznacz pesymistyczną wrażliwość procedury Euklides z zadania 2 . 1 .
2.26. Rozważ procedurę Hanoi z zadania l A. Podaj jej wrażliwość pesymistyczną �(n) i oczekiwaną ben).
3. PODSTAWOWE STRUKTURY DANYCH
Od wyboru właściwej struktury danych może zależeć wiele: złożoność obliczeniowa programu, możność jego łatwej modyfikacji, czytelność algorytmu, a nawet satysfakcja programisty. Zacytujmy raz jeszcze, że ALGORYTMY + STRUKTURY DANYCH = PROGRAMY. W rozdziale tym rozważamy podstawowe struktury danych, takie jak tablica, lista, zbiór, a zwłaszcza graf. Dla każdej z nich omawiamy metody implementacj i, ich złożoność pamięciową oraz czas dostępu do odpowiedniej informacj i. Będziemy zakładać, że elementy wchodzące w skład rozważanych struktur danych pochodzą z pewnego niepustego UlllwersUill.
3.1. Tabl ice
Tablica (ang. array) jest strukturą danych złożoną ze stałej liczby elementów (ang. items). W komputerze są one zwykle przechowywane w kolejnych komórkach pamięci. W przypadku tablic jednowymiarowych, zwanych też wektorami (ang. vectors), dostęp do elementu odbywa się poprzez podanie pojedynczego indeksu. Na przykład, deklaracja wektora liczb całkowitych mogłaby wyglądać następująco:
wektor: array[ l : 50] of integer;
Z punktu widzenia złożoności obliczeniowej istotne jest, że możemy obliczyć adres dowolnego elementu w stałym czasie. Nawet gdy doliczymy do tego czas potrzebny na weryfikację adresu, tj . ustalenie, że nie przekracza on zakresu dopuszczalnych wartości, czas potrzebny do odczytania odpowiedniej wartości lub zapisania nowej wartości wynosi 0( 1 ) . Tym samym możemy traktować te operacje jako elementarne.
Z drugiej strony dowolna operacja wykonywana na całej tablicy będzie tym dłuższa, im większa będzie tablica. Niech n będzie rozmiarem tablicy. Wówczas, jak wiadomo, inicjalizacja tablicy lub znalezienie największego elementu wymaga czasu proporcjonalnego do n, czyli O(n). Inaczej przedstawia się sytuacja, gdy chcemy zachować pewien porządek elementów w tablicy: numeryczny, alfabetyczny lub jakikolwiek inny. Wówczas, za każdym razem gdy musimy wstawić nową wartość, musimy stworzyć miejsce we właściwej pozycji albo przesuwając wszystkie wyższe wartości o jedna pozycję w prawo, albo przesuwając niższe wartości o jedną pozycje w lewo. Bez względu na to, jaką strategię kopiowania przyjmiemy, w najgorszym przypadku będziemy musieli przesunąć co najmniej n/2 elementów. Podobnie usunięcie elementu może wymagać przemieszczenia prawie wszystkich elementów tablicy. Zatem taka operacja może być wykonana w czasie O(n).
Oczywiście, powyższe rozważania mogą być uogólnione na tablice dwu i wielowymiarowe. Na przykład deklaracja tablicy dwuwymiarowej 400 liczb całkowitych mogłaby wyglądać następująco:
macierz: array[ l :20, 1 :20] of integer;
3. 1. Tablice 75
Dostęp do elementu takiej tablicy również wymaga czasu 0( 1) . Jednakże, jeśli oba wymiary takiej tablicy zależą od n, to operacje takie jak wyzerowanie każdego elementu macierzy bądź znalezienie maksymalnego elementu obecnie wymagają czasu 0(n2). Powiedzieliśmy poprzednio, że czas potrzebny do inicjalizacji tablicy rozmiaru n jest G(n). Jednakże czasami w praktyce nie musimy inicjalizować każdego elementu tablicy, a jedynie wiedzieć, czy dany element został ustalony czy nie i jeśli tak, to znać jego wartość. Wówczas, jeśli jesteśmy skłonni przeznaczyć więcej pamięci niż n komórek, możemy dokonać inicjalizacji w czasie o(n). Pozwala nam na to technika zwana inicjalizacją wirtualną (ang. virtual initializatian). Polega ona na tym, że jeśli chcemy zainicjalizować tablicę 11 l . . n ], to potrzebujemy dwóch dodatkowych tablic liczb całkowitych rozmiaru n, powiedzmy a[ 1 . .n] i b[ 1 . .n] oraz licznika caun/er. Początkowo caunter jest wyzerowany, zaś a, b i T mają wartości dowolne. W dalszej kolejności caun/er mówi nam, ile elementów T zostało ustalonych, zaś wartości od a[ l ] do a[caunter] mówią, które to elementy, np. a[ l ] wskazuje na zainicjalizowany jako pierwszy, a[2] na zainicjalizowany jako drugi itd. Ponadto, jeśli 11i] był k-tym kolejnym elementem podlegającym zainicjalizowaniu, to b[i] = k. Sytuację tę ilustruje następujący przykład.
Przykład 3.1 Przypuśćmy, że tablica do zainicjalizowania to 11 1 . .8] . W tablicy tej zainicjalizowano kolejno 114] = 1 7, 117] = 24, 112] = -6. Wówczas stan wektorów T, a, b zilustrowany jest na rys. 3 . 1 .
2 3 4 5 6 7 8
T
a
b
Rys. 3 . 1 . Przykład inicjalizacj i wirtualnej •
OgóLnie, aby sprawdzić, czy 11i] ma już ustaloną wartość początkową, sprawdzamy wpierw, czy l :<; b[i] :<; caunter. Jeśli nie, to z pewnością 11i] nie zostało zainicjalizowane. W przeciwnym razie nie mamy pewności, czy tak się stało. Może bowiem zdarzyć się i tak, że b[i] ma przypadkowo dopuszczalną wartość. Jeśli jednak 11i] rzeczywiście zostało zainicjalizowane, to było ono b[i]-tym z kolei elementem. Możemy to sprawdzić, gdyż a[b[i]] = i. Ponieważ pierwsze caun/er elementów wektora a z całą pewnością zostało ustalone, a l :<; b[i] :<; caun/er, to nie może być przypadkiem, że a[b[i]] = i, a więc ten test jest rozstrzygający: jeśli jest spełniony, to 11i] zostało uprzednio zainicjalizowane, w przeciwnym razie nie.
76 3. Podstawowe struktury danych
3.2 . Listy
Z matematycznego punktu widzenia lista (ang. list) jest skończonym ciągiem elemen
tów z pewnego rozważanego zbioru. W przeciwieństwie do tablic liczba elementów jest tu na ogół nieustalona i z góry nieograniczona. Lista umożliwia nam szybkie określenie, który
element jest pierwszy, który ostatni i który element jest poprzednikiem i/lub następnikiem
danej pozycj i na liście. Na rys. 3.2 podajemy przykład symbolicznego zapisu listy i sposób
jej implementacji w postaci dwóch tablic.
Przykład 3.2 Niech dana jest lista 4-elementowa, o wartościach odpowiednio Ilem l , . . . , Item 4.
Na rysunku poniżej podajemy jeden ze sposobów jej reprezentacji w postaci dwóch wekto
rów: Name i Next.
First �. ---911 /te m 1 ! �!1--�1 /tem 2 ! _. -t--7ł1 Item 3 ! �. --1!1-----3ł1 Item 4 ! Name Next
o - 1
Item 1 3
2 /tern 4 O
3 /tern 2 4
4 /tern 3 2
Rys. 3.2 . Reprezentacja listy 4-elementowej •
Najprostszą implementacją listy jest struktura dowiązań jednokierunkowych, tzw. pojedyncza liniowa (ang. linear singly connected), pokazana na rys. 3.2. Każdy element struk
tury składa się z dwóch miejsc pamięci. Pierwsze zawiera sam element, drugie zawiera
wskaźnik do elementu następnego. Inne możliwe warianty implementacj i dowiązaniowej ,
to: pojedyncza cykliczna, podwójna liniowa i podwójna cykliczna.
Listy używa się zwykle w specjalny sposób, ograniczając się do zmian jej końców.
Niech q = [XI , X2, . . . , xn] będzie listą n-elementową. Wówczas możliwe są następujące ope
racje standardowe tego typu:
front(q) = q[ l ] push(q,x) = [x]&q pop (q) = q[2 . . n] rear(q) = q[n] inject(q,x) = q&[x] eject(q) = q[ l . .n - l ]
pobranie lewego końca listy
wstawienie elementu X na lewy koniec
usunięcie bieżącego lewego końca listy
pobranie prawego końca listy
wstawienie elementu X na prawy koniec
usunięcie bieżącego prawego końca listy
3.3. Zbiory 77
Listę, na której można wykonać wszystkich sześć operacj i, nazywa się kolejką podwójną (ang. double queue). W szczególnych przypadkach, tzn. kiedy uwzględnia się tylko operacje front, push i pop, nazywa się stosem (ang. stack). Ponieważ ostatni element wstawiany na stos jest zawsze pierwszym usuniętym, o stosie mówimy czasami, że jest listą LIFO (ang. Last In First Out). Natomiast w przypadku gdy wykonuje się wyłącznie operacje front, pop i inject, to listę taką nazywa się kolejką (ang. queue). Operacje na kolejce powodują, że pierwszy element wstawiany będzie pierwszym usuniętym, dlatego o kolejce mówimy czasami, że jest listą FIFa (ang. First In First Out). Każda z powyższych operacji ma złożoność czasową 0( 1 ) w implementacji podwójnej cyklicznej . Wadą tej implementacj i jest użycie O(n) komórek pamięci pomocniczej na pamiętanie dowiązań.
Czasami trzeba znaleźć element znajdujący się "gdzieś w środku listy", np. aby go usunąć lub wstawić za nim nowy element. Jeśli taki element zajmuje k-tą pozycję, to operacja taka wymaga czasu O(k). Jednakże, gdy już znajdziemy element Xk, to usunięcie go może być wykonane praktycznie natychmiastowo, nawet wówczas, gdy element ten jest tablicą wielowymiarową. Generalnie, elementami list mogą być bowiem dowolne struktury, na przykład tablice. Wówczas nie warto wstawiać i usuwać całych tablic, ponieważ każda taka operacja miałaby koszt proporcjonalny do wielkości tablicy. Zamiast tego wstawiamy i usuwamy jedynie wskaźniki do obiektów będących w tym przypadku tablicami. Możemy zatem usuwać i wstawiać skomplikowane elementy w czasie 0( 1 ) pod warunkiem, że znamy miejsce (adres) tych elementów w liście.
3.3. Zbiory
W przeciwieństwie do elementów listy elementy w zbiorze (ang. set) S = {X I , X2, . . . , xn} nie są podane w żadnym ustalonym porządku. Liczbę n elementów w zbiorze S oznaczamy przez ISI i nazywamy rozmiarem (ang. size) zbioru S. Podstawowymi operacjami na zbiorach są: insert (x,S) = Su {x} delete(x,S) = S-{x} member(x,S) min(S) max(S) deletemin(S) = S-{min(S)} union(SI ,S2) = SIUS2
wstawienie elementu do zbioru S usunięcie elementu X ze zbioru S wynikiem jest true, gdy X E S, lub false, gdy X � S zwrócenie najmniejszego elementu w S zwrócenie największego elementu w S usunięcie najmniejszego elementu z S obliczenie sumy zbiorów SI i S2
Istnieją dwie podstawowe implementacje zbioru S = {X I , X2, . . . , xn} : za pomocą wektorów bitów i za pomocą list. W pierwszym przypadku zakładamy, że wszystkie rozważane zbiory są podzbiorami pewnego uniwersum U. Podzbiór S <;;;; U jest reprezentowany przez wektor Vs o I UI bitach, taki, że
Vs (x) = {l,
O,
gdy X E S gdy X � S
Wektor Vs nazywamy wektorem charakterystycznym (ang. characteristic vector) zbioru S.
78 3. Podstawowe struktury danych
Operacje insert, de/ete i member mają złożoność czasową 0( 1 ) . Złożoność pamięciowa jest proporcjonalna do rozmiaru lU]. Jak widzimy, zaletą tej implementacj i jest szybkość sprawdzenia należenia elementu do zbioru, bowiem wystarczy sprawdzić x-ty bit wektora Vs. Operacje takie jak SI u S2 i SI n S2 mogą być wykonane jako suma logiczna i iloczyn logiczny w komputerze. Jest to szczególnie cenne, gdy lU] jest nie większe niż rozmiar jednego słowa maszynowego. Jednakże, gdy rozmiary SI i S2 są małe w porównaniu z rozmiarem uniwersum, to suma i iloczyn są wykonywane w czasie 0(1 U]), a nie w czasie proporcjonalnym do liczby elementów w obu słowach.
Drugim konkurencyjnym sposobem implementacji zbioru jest lista. Pamięć potrzebna do reprezentacji zbioru jest wówczas proporcjonalna do liczby elementów zbioru. Czas potrzebny do wykonywania operacji na zbiorach zależy od charakteru tych operacji . Rozważmy na przykład operację iloczynu teoriomnogościowego SI n S2. Operacja ta wymaga czasu proporcjonalnego co najmniej do sumy rozmiarów zbiorów SI i S2, ponieważ listy reprezentujące zbiory SI i S2 muszą być przejrzane co najmniej raz. Zauważmy na marginesie, że jeżeli obie listy są posortowane, to możemy wyznaczyć ich część wspólną w czasie liniowym. Podobnie operacja SI u S2 wymaga czasu co najmniej proporcjonalnego do sumy liczb elementów tych zbiorów, ponieważ trzeba znaleźć elementy należące do obu zbiorów w celu uniknięcia dwukrotnego wystąpienia tego samego elementu w zbiorze wynikowym. Jeśli zbiory SI i S2 są rozłączne, to union(SJ , S2) możemy wyznaczyć w czasie niezależnym od ich rozmiarów, łącząc listy reprezentujące te zbiory. Jednakże sumowanie zbiorów rozłącznych przestaje być proste, jeśli dodatkowo wymagamy, aby sprawdzenie, czy element należy do danego zbioru, było wykonywane szybko.
3.4. Grały
Ogólnie, istnieją dwa zasadnicze problemy dotyczące komputerowego reprezentowania grafów: P l : graficzna reprezentacja grafów na ekranie komputera, P2: cyfrowa reprezentacja grafów w pamięci komputera.
Problem pierwszy sprowadza się do zagadnienia umieszczania grafów na płaszczyźnie. Problem ten wymaga przyjęcia pewnej estetyki, tzn. kryterium elegancj i rysunku. Kryteria takie mają charakter heurystyczny, gdyż dla jednego użytkownika może to być brak przecięć krawędzi, a dla innego ich prostoliniowość. Dobór estetyk jest przedmiotem osobistych preferencji, tradycji i kultury. Na ogół przyjmuje się następujące estetyki: l ) unikanie przecięć, 2) pokazywanie symetrii, 3) unikanie zagięć krawędzi, 4) unikanie dysproporcji, 5) oszczędzanie powierzchni rysunku.
Badania przeprowadzone wśród studentów na temat ich preferencji w stosunku do poszczególnych estetyk dowodzą, że najważniejszymi z nich są: unikanie (minimalizacja liczby) przecięć i pokazywanie symetrii . Jak wiadomo, całkowite wyeliminowanie przecięć jest
3.4. GrafY 79
możliwe jedynie wtedy, gdy graf G = ( V,E), gdzie I VI = n i lEI = m, jest planarny. Grafy
planarne są bardzo dobrze przebadaną rodziną grafów pod kątem algorytmów rysujących.
Można na przykład zażądać aby wszystkie krawędzie były odcinkami linii prostych. Istnieje
algorytm o złożoności O(n), który tworzy takie rysunki, o ile graf jest planarny. Dowolny
rysunek tego rodzaju nazywamy reprezentacją Fary 'ego (ang. Fary embedding). Co więcej ,
jeśli graf planarny G jest 3-spójny (ang. 3-connected) (graf jest 3-spójny, jeśli usunięcie
dowolnych dwóch wierzchołków z incydentnymi krawędziami nie powoduje jego rozspoje
nia), to istnieje reprezentacja Fary'ego, w której każda ściana skończona jest wielokątem
wypukłym. I tutaj odpowiedni rysunek można uzyskać w czasie O(n). Istnieje nawet algo
rytm o złożoności O(n) generujący rysunek grafu planarnego bez przecięć, który uwidacz
nia wszystkie występujące w nim symetrie, przy czym krawędzie są odcinkami linii pro
stych.
Jeśli G nie jest planarny, to znalezienie liczby przecięć (ang. cross ing number) Ś(G) jest problemem NP-trudnym. Ta trudność w osiąganiu optimum towarzyszy niemal wszyst
kim estetykom. Co więcej , są one ze sobą zwykle w konflikcie w tym sensie, że zoptymali
zowanie rysunku pod kątem jednego kryterium przeszkadza w optymalizacj i z punktu wi
dzenia innego. Dlatego w przypadku ogólnym stosuje się różne heurystyki. Całą rodziną
metod rysowania grafów są algorytmy oparte na modelach fizycznych. Najogólniej mówiąc
składają się one z dwóch części. Pierwszą z nich jest model sił zdefiniowany na grafie wej
ściowym. Drugim elementem jest technika pozwalająca znaleźć lokalne minimum w zdefi
niowanym modelu. Najczęściej sprowadza się ona do ciągu bardzo szybkich operacji , które
starają się symulować modelowany proces fizyczny. Przykładem opisanych algorytmów jest
tzw. metoda sprężynowa (ang. spring embedder). Jej celem podstawowym jest pokazywanie
symetrii i prostoliniowości krawędzi. Metoda sprężynowa jest oparta ma modelu fizycznym
układu obręczy i sprężyn. Mianowicie, proces ten jest symulowany za pomocą systemu
mechanicznego, w którym wierzchołki odpowiadają obręczom, a krawędzie - sprężynom.
Sprężyny przyciągają obręcze, gdy są one zbyt odległe i odpychają w przypadku przeciw
nym. Proces rysowania kończy się z chwilą osiągnięcia równowagi potencjału. Obrazy
uzyskane za pomocą opisanych metod ukazują zazwyczaj duży stopień symetrii i uwypukla
ją naturalne struktury zawarte w grafach. Największą ich wadą jest długi czas działania
spowodowany koniecznością symulowania zjawisk fizycznych oraz niedeterminizm wyni
ków.
Inną, popularną metodą rysowania grafów skierowanych jest rysowanie hierarchiczne (ang. hierarchical drawing). Przed narysowaniem grafu przyporządkowujemy każdemu
wierzchołkowi odpowiadającą mu warstwę. Odwzorowanie to wynika z kontekstu bądź też
jest generowane automatycznie przez algorytm. Wierzchołki z tej samej warstwy rysowane
są na jednej linii poziomej , podczas gdy krawędzie skierowane są z góry do dołu bądź z
dołu do góry, przy czym minimalizowana jest liczba ich przecięć. Metoda warstwowa jest
bardzo często stosowana do obrazowania grafów, które modelują dane zawierające pewną
hierarchię (stąd nazwa) takie jak procesy technologiczne czy zależności międzyludzkie.
Główne problemy, z którymi się spotykamy stosując wspomnianą metodę to: przydzielenie
warstw wierzchołkom tak, by minimalizować zarówno szerokość, jak i wysokość rysunku, a
także minimalizacja liczby przecięć krawędzi. Oba wspomniane problemy należą do klasy
problemów NP-trudnych, przy czym minimalizacja liczby przecięć krawędzi jest proble-
80 3. Podstawowe struktury danych
mem NP-trudnym nawet wówczas, gdy ograniczymy się do rozważania zaledwie dwóch
warstw. Ponieważ niezmiernie trudno jest skonstruować algorytm, który minimalizowałby
liczbę przecięć w całym grafie, często stosuje się technikę nazywaną zamiatanie warstwa po warstwie (ang. layer by layer sweep). Polega ona na początkowym zmodyfikowaniu grafu
tak, by wszystkie sąsiadujące wierzchołki leżały na sąsiednich warstwach, po czym rozwa
żaniu kolejno z góry do dołu tylko dwóch warstw leżących obok siebie.
Warto wspomnieć także o ważnej , z praktycznego punktu widzenia, metodzie rysowa
nia grafów - rysowaniu ortogonalnym. Przyjmujemy, że krawędzie składają się z odcinków
prostych na przemian pionowych i poziomych. Jest oczywiste, że stosując tylko taki rodzaj
krawędzi możemy narysować jedynie grafy, których wierzchołki mają stopień równy co
najwyżej cztery. Okazuje się, że jest to warunek wystarczający, gdyż wychodząc od rysunku
nie ortogonalnego możemy każdąjego krawędź aproksymować dostatecznie małymi odcin
kami prostych na przemian pionowych i poziomych i w ten sposób otrzymać rysunek orto
gonalny. Rysunki tego typu mają szczególnie duże zastosowanie w tworzeniu obwodów
drukowanych, schematów elektronicznych, czy blokowych. Staramy się tu minimalizować
liczbę przecięć oraz zgięć krawędzi. Oba wspomniane problemy optymalizacyjne należą do
klasy problemów NP-trudnych.
Wiele algorytmów rysowania grafów daje doskonałe rezultaty, gdy są stosowane dla
bardzo specyficznej , wąskiej rodziny grafów. Dlatego też czasami wydaje się być rozsąd
nym ich wykorzystanie modyfikując uprzednio graf w taki sposób, by zaspakajał wszelkie
wymagania danego algorytmu. Na końcu, otrzymany rysunek modyfikujemy w taki sposób, by odpowiadał grafowi wejściowemu. I tak, dla grafów pIanamych stworzonych zostało
wiele algorytmów mających wysokie walory estetyczne. Jeśli więc graf jest niemal planar
ny, opłaca się najpierw splanaryzować go (ang. planarization), np. usuwając jak najmniej
krawędzi, narysować w sposób planamy, po czym dodać usunięte krawędzie. Otrzymamy w
ten sposób estetyczny rysunek, prawie bez skrzyżowanych krawędzi. Osiągany rezultat
zależy głównie od tego jak bardzo etap planaryzacji zniekształcił graf wejściowy. Niestety
zadanie planaryzacj i grafu (w dowolny sposób) tak, by jak najmniej zaburzyć jego strukturę
jest kolejnym problemem NP-trudnym. Innym przykładem może być chęć wykorzystania
algorytmu rysującego grafy ortogonalnie. W tym celu należałoby najpierw usunąć z niego wystarczającą ilość krawędzi, tak, by móc zastosować specjalizowaną metodę rysowania,
po czym dodać usunięte elementy na rysunku. Czasami zamiast usuwać krawędzie bądź
wierzchołki zależy nam, by wzbogacić strukturę grafu na potrzeby danego algorytmu. Wiele
przykładów dostarczają tu algorytmy rysowania grafów pianamych. Na przykład okazuje
się, że większość z nich zakłada, że graf wejściowy jest 2-spójny.
Do tej pory zakładaliśmy, że rozważane przez nas grafy są nieskierowane. Istnieje pe
wien interesujący model rysowania grafów planamych, który bierze pod uwagę kierunek
krawędzi. Rysunek grafu nazywamy planarnym w górę (ang. upward planar drawing), jeśli
żadne dwie krawędzie nie krzyżują się, a ponadto wszystkie krawędzie są skierowane w
górę. To dodatkowe założenie o kierunku krawędzi sprawia, że przekraczamy granicę zło
żoności obliczeniowej , bowiem zadanie polegajace na zdecydowaniu, czy dany graf skiero
wany posiada reprezentację planamą w górę jest problemem NP-zupełnym. Interesujące, że
owo przejście jest bardzo drastyczne, gdyż podobny problem dla grafów nieskierowanych
ma złożoność liniową. Co więcej , jeśli założymy dodatkowo, że wejściowy graf skierowany
3. 4. Grąfj; 8 1
jest 2-spójny, to okazuje się, że zawsze można go narysować planarnie w górę. Oprócz estetyki, przy projektowaniu rysunku należy uwzględnić konwencję rysowania
i ograniczenia. Konwencja jest podstawową regułą, którą dany rysunek winien uwzględniać, aby być akceptowalnym. Na przykład przy rysowaniu schematów blokowych programów wierzchołki muszą być skrzynkami, zaś krawędzie liniami prostymi lub łamanymi pod kątem prostym. Najczęściej spotykane konwencje to: l ) krawędzie prostoliniowe, 2) krawędzie ortogonalne, 3) wierzchołki i krawędzie na siatce rastrowej , 4) łuki zorientowane w jednym kierunku, 5) wierzchołki podzielone na warstwy (ukazują pewną hierarchię), 6) rysunek grafu musi być planarny.
W odróżnieniu od estetyki i konwencj i, które dotyczą całego rysunku, ograniczenia dotyczą wybranych fragmentów grafu. Na przykład przy rysowaniu sieci PERT chcemy, by ścieżka krytyczna była wyprostowana i znajdowała się w centrum rysunku. Najczęściej spotykane ograniczenia to: l ) scentrowanie wybranego wierzchołka 2) zgrupowanie wybranych wierzchołków, 3) wyprostowanie wybranej ścieżki w kierunku poziomym lub pionowym.
Jako ciekawostkę odnotujmy, że od roku 1 994 odbywają się ogólnoświatowe zawody w automatycznym rysowaniu grafów. Zawody odbywają się zwykle w czterech zmieniających się z roku na rok kategoriach. W pierwszej połowie roku ogłaszane są w Internecie grafy w postaci listy sąsiedztwa, a w drugiej połowie roku w ramach konferencji "Graph Drawing" pięcioosobowe jury typuje zwycięzców (zwycięzcami są tutaj twórcy programów komputerowych), kierując się subiektywnym odczuciem piękna. Dla przykładu w roku 1 994 zwyciężył obraz pokazany na rysunku 3.3 .
Rys. 3 .3 . Zwycięzca z roku 1 994. Rysunek został uzyskany za pomocą programu GraphCAD 2.90
82 3. Podstawowe struktury danych
Problem umieszczania grafów na płaszczyźnie może być uogólniony na inne rodzaje powierzchni. Oto przykładowe zagadnienia tego typu: l) umieszczanie grafów w książkach, 2) umieszczanie grafów na torusie i wstędze M6busa, 3) umieszczanie grafów na powierzchni rodzaju g, 4) umieszczanie grafów w przestrzeni trójwymiarowej .
Powyższe należy uzupełnić o ważne z technicznego punktu widzenia zagadnienie umieszczania grafów na płaszczyźnie w obrębie siatki rastrowej . W problemie tym zakłada się, że wierzchołki grafu muszą być umiejscowione w węzłach siatki, a krawędzie są łamanymi ortogonalnymi biegnącymi wzdłuż linii tej siatki. Zadaniem jest znalezienie takiego umieszczenia danego grafu płaskiego, które minimalizuje powierzchnię rysunku. Wiadomo na przykład, że dla drzew i grafów zewnętrznie pianamych owa powierzchnia wynosi O(n), zaś dla grafów pianamych - 0(nlog2n). Odpowiednie ilustracje można znaleźć w książce [9] .
Ciekawym pomysłem rysowania grafów jest naj nowsza idea umieszczania grafów na płaszczyźnie w sposób konfluentny. Powiemy, że krzywa jest lokalnie monotoniczna (ang. locally monotonie), jeżeli nie przecina się sama ze sobą oraz nie ma ostrych zmian kierunku. Intuicyjnie, krzywa taka jest jak tor kolejowy (rys. 3 .4). Rysowanie konjluentne (ang. conjluent drawing) polega na przedstawianiu krawędzi w postaci krzywych lokalnie monotonicznych, scalanych w takie tory.
Rys. 3 .4. Krzywa lokalnie monotoniczna i 2 krzywe, które takimi nie są
Na przykład grafy Kuratowskiego można narysować bez przecięć w sposób konfluentny, co pokazuje rys. 3 . 5 .
Rys. 3.5. Kontluentne rysunki grafów K3,3 i K5
Najmniejszym znanym grafem niekonfluentnym jest graf Petersena bez jednego wierzchołka.
3.4. Graty 83
Warto dodać, że w obszarze problematyki rysowania grafów znajdują się także takie zagadnienia jak: rysowanie grafów o wierzchołkach i krawędziach mających zadane wielkości, rysowanie grafów ewoluujących (zmieniających się w czasie), rysowanie grajów klastrowych (ang. clustered graphs drawing), podpisywanie wierzchołków i krawędzi, nawigowanie po rysunkach grafów, uwidacznianie podgrafów w grafach, rysowanie grafów zwijanych i wiele innych. Wszystkie wspomniane zagadnienia prowadzą do interesujących problemów, które czytelnik znajdzie w literaturze specjalistycznej .
3.4.1. Macierz sąsiedztwa wierzchołków
Ogólnie, do zapisywania grafów używamy struktur macierzowych i listowych. Wśród tych pierwszych wyróżniamy macierze sąsiedztwa i macierze incydencj i. Macierz sąsiedztwa (ang. matrix oj adjacency) opisuje relację zachodzącą pomiędzy elementami tego samego zbioru, np. macierz sąsiedztwa wierzchołków czy macierz sąsiedztwa krawędzi. Macierz incydencji (ang. matri.x: oj incidence) opisuje relację zachodzącą pomiędzy elementami dwóch różnych zbiorów, np. macierz incydencj i wierzchołek-krawędź bądź macierz incydencji wierzchołek-cykl. Poniżej opiszemy szczegółowo macierz sąsiedztwa wierzchołków.
Macierz sąsiedztwa wierzchołków grafu G = ( V,E) jest macierzą zero-jedynkową A = [aij] rozmiaru nxn o elementach
aij = {�,
gdy {i, j} E E gdy {i, j} !i!O E
Przykład takiej reprezentacj i pokazuje rys. 3 .6 .
Przykład 3.3
2
2 3 4 5 6 7 1 o 1 1 1 o o o 2 1 o o 1 o o o 3 1 o o 1 1 1 o
A = 4 1 1 1 o o o 1
3 4 5 o o 1 o o 1 1 6 o o 1 o 1 o 1 7 o o o 1 1 1 o
6 fI------� 7
Rys. 3.6. Graf G i jego macierz A •
84 3. Podstawowe struktury danych
Po lewej stronie rys. 3.6 podajemy graf 7-wierzchołkowy w postaci graficznej, a po
prawej jego reprezentację w postaci macierzy sąsiedztwa wierzchołków.
Po pierwsze zauważmy, że ta struktura danych wymaga 0(n2) komórek pamięci bez
względu na gęstość grafu. Po drugie, że procedura boolowska B(iJ) zwracająca lnie, gdy
wierzchołki nr i oraz} są połączone krawędzią w grafie G, bądźfalse w przypadku przeciw
nym, może być wykonana w czasie 0( 1 ). Co więcej , dopisanie krawędzi lub usunięcie kra
wędzi może być dokonane w stałym czasie. Jeśli G jest nieskierowany, to A jest symetrycz
na i możemy zaoszczędzić na pamięci, przechowując tylko jej górną połowę. Jeśli G jest
skierowany i nie posiada cykli 2-wierzchołkowych, to macierz A jest antysymetryczna, czyli
aijaji = O dla wszystkich iJ, a zatem ponownie możemy zaoszczędzić i przechować jedynie
n(n - l )/2 komórek pamięci, podstawiając aij = - l , gdy aji = l (i < i).
Czy można jeszcze bardziej skomprymować macierz sąsiedztwa? Poniżej przedsta
wiamy sposób pamiętania macierzy A w 0(n2/10g n) komórkach pamięci, umożliwiający
wciąż stały dostęp do informacj i o sąsiedztwie. Przypuśćmy, że k=�log2 n jest liczbą cał-
kowitą. Zauważmy, że A może mieć co najwyżej i2 różnych podmacierzy k x k. Zapiszmy
A jako tablicę (nik) x (nik) wskaźników do owych podmacierzy. Wówczas liczba wymaga
nych komórek wynosi
2 2 n n 2 n k' 2 n 2 - · - + pk :::; - + 2 k = -- + n log n = 0(n / log n), k k k2 log n
gdzie p jest liczbą potrzebnych macierzy rozmiaru k x k. Oczywiście czas dostępu do
informacj i, czyli złożoność procedury B(i, i), jest 0( 1 ). Poniżej podajemy przykład zastą
pienia macierzy zero-jedynkowej przez tablicę wskaźników.
Przykład 3.4 Poniższa macierz 1 6 x 1 6
O O O O O O l O l O O O
O O l O O O O O O O O
O O O O
O O O
itd.
może być reprezentowana jako następująca macierz wskaźników
fa � y a � a y
itd.
gdzie
3. 4. Grafy 85
a = [� �] � = [� �] y = [� �] 8 = [� �] itd. •
Macierze sąsiedztwa wierzchołków są bardzo użyteczne przy rozwiązywaniu różnorodnych problemów dotyczących dróg w grafie. Jest to związane z faktem, że element a/ macierzy Ak jest większy od ° wtedy i tylko wtedy, gdy wierzchołki i ij są połączone drogą długości k w grafie G, gdzie Ak jest k-tą potęgą macierzy A.
Jeśli G jest grafem skierowanym, to powstaje ciekawy problem: jaka jest minimalna liczba dostępów c(P) do macierzy sąsiedztwa wierzchołków, gwarantująca możliwość rozstrzygnięcia, czy G posiada nietrywialną własność P. Własność P jest nietrywialna (ang. nontrivial), gdy zachodzi dla nieskończenie wielu grafów i nie zachodzi dla nieskończenie wielu grafów. Mogłoby wydawać się, że c(P) = n(n - 1 )/2 dla wszystkich nietrywialnych własności teoriografowych. Jednakże istnieje kontrprzykład, mianowicie problem istnienia ujścia (ang. sink) w digrafie, czyli wierzchołka o stopniu wejściowym n - l i stopniu wyjściowym 0, dla którego c(P) = 3n - LIog2nJ - 3 . Udowodniono, że dla wszystkich nietrywialnych własności teoriografowych P, c(P) � 2n - 4. Z drugiej strony istnieje wiele własności, dla których funkcja c(P) nie jest liniowa. Nazywamy je nieuchwytnymi (ang. elusive). Własnościami nieuchwytnymi są wszystkie wspomniane w tym podręczniku, np. hamiltoniczność, planamość, spójność.
O własności P grafu G mówimy, że jest monotoniczna (ang. monotone), gdy spełnia ją każdy n-wierzchołkowy nadgraf grafu G. Przykładami takich własności są: spójność, nieplanamość, hamiltoniczność. Udowodniono, że jeśli P jest nietrywialną własnością monotoniczną, to c(P) � n21 1 6. Więcej szczegółów na ten temat zawiera pozycja [9] .
3.4.2. Listy sąsiedztwa wierzchołków
Bardziej oszczędna struktura wykorzystuje listy sąsiedztwa wierzchołków. Dla każdego wierzchołka i E V tworzymy listę sąsiadów. Dla naszego przykładowego grafu G listy sąsiedztwa zilustrowano na rys. 3.7 .
Przykład 3.5 t<c-----..., 2
3 �----"' 4
61F--------'''ł 7
2 3 4 5 6
7
Rys. 3 .7. Graf G i jego listy sąsiedztwa •
86 3. Podstawowe struktury danych
Struktura ta wymaga n + 4m = O(m + n) komórek pamięci dla grafu nieskierowanego i n + 2m = O(m + n) komórek pamięci dla grafu skierowanego (zauważmy, że w obu przypadkach znaczenie m jest odmienne, gdyż w pierwszym przypadku oznacza liczbę krawę
dzi, a w drugim - liczbę łuków). Ponieważ O(m + n) = 0(n2), gdy m = o(n\ więc listy sąsiedztwa mają na ogół niższą złożoność pamięciową niż macierz sąsiedztwa. Jak poprzednio, dodanie nowej krawędzi może być zrealizowane w czasie O( l ), gdyż wystarczy wstawić wierzchołek-rekord na początek odpowiedniej listy. Jednakże usunięcie krawędzi wymaga uprzedniego znalezienia odpowiedniego elementu listy, co w naj gorszym przypadku wymaga przejrzenia całej listy. Z tego samego powodu jedno wykonanie procedury B(i, j) zajmuje czas O(n).
Jeśli G jest nie skierowany, to możemy zaoszczędzić prawie połowę pamięci, zapisując
na i-tej liście jedynie te krawędzie, które prowadzą do wierzchołków o numerachj > i. Czasami jest korzystnie przechowywać krawędzie { i, j} w porządku zgodnym z rosnącą nume
racją). Aczkolwiek struktura taka ma również złożoność pamięciową O(m + n), to dostęp do odpowiedniej informacj i o sąsiedztwie może być obniżony do poziomu O(log n) wskutek zastosowania metody poszukiwania połówkowego. Zauważmy na marginesie, że podobnie jak poprzednio listy sąsiedztwa mogą być skompresowane do O(n2/log n) komórek pamięci, zachowując przy tym podstawowe parametry dotyczące czasu dostępu, tj . stały
czas dodawania nowej krawędzi i liniowy czas usuwania starej .
3.4.3. Pęki wyjściowe
Jeżeli w trakcie działania algorytmu teoriografowego graf nie ulega zmianie, to możemy zrezygnować ze wskaźników i zapamiętać wszystkie krawędzie po kolei w jednym wektorze. Uporządkowany zbiór wszystkich sąsiadów wierzchołka i nosi nazwę pęków wyjściowych (ang.forward star) tego wierzchołka i stąd nazwa tej struktury. Dla każdego i = 2,
. . . , n sąsiedzi wierzchołka i są umieszczeni bezpośrednio za wierzchołkami-sąsiadami wierzchołka i - l . Przykład takiej struktury podajemy na rys. 3 .8 .
Przykład 3.6 Dla grafu G z rys. 3.6 pęki wyjściowe przedstawiają się w sposób pokazany poniżej .
Pnlr EndVerlex r-- � 2 1 3 14 1 1 1 4 1 1 1 4 1 5 1 6 1 1 1 2 1 3 1 7 1 3 1� 71 3 1 5 1 7 1 4 1 5 1 6 1 l f--
� j 4 f--6 f--10 r---14 r---
1 7 r---20 f--23 '---
Rys. 3 .8 . Pęki wyjściowe graf u G •
3.4. Grafy 87
Zauważmy, że krawędzie wychodzące z wierzchołka nr i to: {i, EndVertex[Pntr[i]] } , { i, EndVertex[Pntr[i] + l ] } , . . . , { i, EndVertex[Pntr[i + l ] - l ] } . Przypadek i = n jest załatwiony za pomocą wartownika, który wskazuje na adres m + l w wektorze EndVertex.
Dla grafu n-wierzchołkowego z m krawędziami pęki wyjściowe wymagają (n + l ) + 2m = O(m + n) komórek. Jedno wykonanie procedury B(i,)) trwa O(log n) jednostek czasu.
Zadania
3 . 1 . Początkowe wyzerowanie całej macierzy sąsiedztwa wierzchołków wymaga czasu O(n2). Podaj metodę, która pozwala uniknąć początkowego zerowania macierzy i zeruje element macierzy w momencie, gdy po raz pierwszy sprawdzamy wartość tego elementu. Wskazówka: Dla każdego zainicjalizowanego elementu tworzymy wskaźnik do wskaźnika
umieszczonego na stosie, wskazującego na dany element zainicjalizowany.
3.2. Jak wiadomo, macierz nazywamy rozrzedzoną, jeśli jej elementy są w większości zerami (patrz punkt 2 .4). Znajdź reprezentację wiązaną, w której będą występować tylko niezerowe elementy macierzy rozrzedzonej .
3.3. Dla zadania z poprzedniego punktu opracuj metodę mnożenia kwadratowych macierzy rozrzedzonych. Twoja metoda winna mieć złożoność O(glg2n\ gdzie gl jest gęstością pierwszej macierzy, a g2 - drugiej .
3.4. Podaj implementację listy dwukierunkowej . Zapisz algorytm wstawiania i usuwania elementów w języku wysokiego poziomu. Sprawdź, czy twój program działa wtedy, gdy lista jest pusta.
3.5. Napisz procedurę, która zamienia miejscami elementy xp i Xp+ 1 w liście pojedynczej liniowej .
3.6. Następująca procedura powinna usuwać wszystkie elementy x z listy L, lecz nie zawsze działa poprawnie. Dlaczego? Zaproponuj sposób jej naprawy.
procedure delete(x,L); begin
end;
p := front(L); while p "* end(L) do begin
end
if retrieve(p,L) = x then delete(p,L); p := next(p,L)
Uwaga! retrieve(p,L) zwraca wartość elementu stojącego na p-tej pozycji listy L.
3.7. Napisz algorytm na scalanie dwóch list posortowanych. Twój algorytm winien mieć złożoność liniową.
88 3. Podstawowe struktury danych
3.8. Napisz algorytm odwracania porządku elementów listy liniowej . Udowodnij jego po
prawność.
3.9. 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 pomocniczej .
3 . 1 0. Udowodnij , że graf może być umieszczony na płaszczyźnie wtedy i tylko wtedy, gdy
może on być umieszczony na powierzchni kuli.
3. 1 1 . Narysuj grafplanarny, który jest S-regularny.
3.12. Udowodnij , że w każdym umieszczeniu grafu planarnego na płaszczyźnie istnieje ta
sama liczba m - n + 2 ścian.
3.13. Pokaż, że liczba przecięć 'E;(K6) = 3 .
Wskazówka: Narysuj K6 z trzema przecięciami i potraktuj te punkty jako nowe wierzchołki
pewnego grafu planarnego.
3.14. Podaj algorytm obliczania wysokości drzewa binarnego, które jest reprezentowane
przez dwie tablice: LeftSon i RightSon jak na rysunku. Oszacuj jego złożoność obliczeniową.
3
9
2
3
4
5
6
7
8
9
LeftSon
2
3
O
O
O
7
O
O
O
RightSon
6
4
O
5
O
8
O
9
O
3 . 1 5 . Znajdź reprezentację Fary'ego grafu trójdzielnego K2.2.2 w siatce rastrowej . Twoja
reprezentacja winna zajmować naj mniej szą możliwą powierzchnię·
3 . 1 6. Podaj planamy graf dwudzielny, który nie może być umieszczony na płaszczyźnie w
taki sposób, że każda ściana z wyjątkiem zewnętrznej jest wielokątem wypukłym.
3.1 7. Zaprojektuj algorytmy przekształcające każdą z podanych na każdą z pozostałych repre
zentacji grafu: ( 1 ) macierz sąsiedztwa wierzchołków; (2) listy sąsiadów; (3) pęki wyjściowe.
3. 1 8. Jedna z efektywnych metod pamiętania struktury grafu rzadkiego oparta jest na idei
Zadania 89
liczby harmonicznej h(G) grafu G (ang. harmonious chromatic number): h(G) jest najmniejszą liczbą kolorów potrzebnych do pomalowania wierzchołków w taki sposób, aby żadna para kolorów nie pojawiała się dwukrotnie na końcach krawędzi. Metoda ta polega na pokolorowaniu harmonicznym grafu G, a następnie zapamiętaniu jego struktury w postaci wektora kolorów W, gdzie Wi E W jest numerem koloru przydzielonego Vi, oraz macierzy C o rozmiarze h(G) x h(G), której element cij = (u, v), gdy {u, v} E E jest krawędzią o końcach pomalowanych parą kolorów i, j, oraz cij = O w przypadku przeciwnym. Wiedząc, że
& < h(G) ś n, oszacuj z dołu i z góry:
l ) złożoność czasową procedury boolowskiej B(u, v);
2) złożoność pamięciową macierzy W i C.
3.19. Zaprojektuj macierzowy sposób reprezentacj i grafu nieskierowanego, który:
a) w czasie 0( 1 ) umożliwia sprawdzenie, czy dana para wierzchołków u, v jest połączona krawędzią;
b) w czasie O(degv) umożliwia przejrzenie wszystkich sąsiadów wierzchołka v. Naszkicuj procedurę boolowską B(u, v), która realizuje punkt (a).
3.20. Zaproponuj listowy sposób reprezentacj i drzew n-wierzchołkowych w pamięci rozmiaru O(n), umożliwiający sprawdzenie w czasie 0( 1 ), czy dana para wierzchołków jest połączona krawędzią.
3.2 1 . Zaproponuj sposób reprezentacji grafów planamych w pamięci liniowej , umożliwiający sprawdzanie w stałym czasie, czy dana para wierzchołków jest połączona krawędzią.
Wskazówka: Wykorzystaj fakt, że każdy grafplanamy ma wierzchołek stopnia co najwyżej 5 .
3.22. Napisz algorytm budowania macierzy sąsiedztwa krawędzi grafu G na podstawie jego macierzy sąsiedztwa wierzchołków. Twój algorytm winien mieć złożoność 0(m2) .
3.23. Zmodyfikuj listy sąsiedztwa wierzchołków tak, aby każda pierwsza krawędź na liście była usuwalna w stałym czasie. Wskazówka: Pamiętaj , że usuwając krawędź {i ,j} zawsze musimy usunąć dwa elementy
z tej struktury.
3.24. Zaprojektuj wielomianowy algorytm znajdowania spójnych składowych w grafie, w którym nie wykorzystuje się ani przechodzenia grafu w głąb, ani przechodzenia wszerz.
Wskazówka: Zastosuj metodę mnożenia macierzy A.
3.25. Napisz algorytm dla znajdowania ujścia (o ile ono istnieje) w digrafie zapisanym w postaci macierzy sąsiedztwa wierzchołków, który wymaga 3n - LtOg2nJ - 3 działań na elementach macierzy A.
90 3. Podstawowe stmktury danych
3.26. Skorpion (ang. scorpion) jest grafem, który zawiera wierzchołek k (korpus) stopnia n - 2, wierzchołek ż (żądło) stopnia l i wierzchołek o (ogon) stopnia 2, który łączy k z ż. Pozostałe n - 3 wierzchołki tworzą dowolny podgraf. Przykład skorpiona pokazano na
rysunku poniżej . Udowodnij, że własność bycia skorpionem spełnia c(P) :$ 6n - 1 0.
k O ż .---� __ ----�-----e
3.27. Udowodnij, że każda nietrywialna własność teorii grafów jest nieuchwytna, gdy n = 3 .
3.28. W historii problemu przydziału (LSAP) dla ważonych grafów dwudzielnych znane są
następujące coraz szybsze algorytmy:
( 1 946) Easterfielda
( 1 955) Khuna
( 1 969) Dinica-Kronroda
( 1 985) Gabowa
(J 988) Bertsekasa-Ecksteina
( 1 989) Gabowa-Tarjana
(200 l ) Kao i in.
O złożonościach O( .,r,;. Wlog(Cn2/W)/logn), O( .,r,;. mlog(nC)), O(n3/4mlogC), ?, O(nmlog(nC)), O(n4), O(n\ gdzie C jest największą wagą krawędzi, zaś W - sumą wag. Zakładając, że
C=O(l ), przypisz złożoności do algorytmów.
SŁOWNIK POLSKO-ANGIELSKI
A algorytm, 35
algorytm rekurencyjny, 22
algorytm probabilistyczny, 64
algorytm Monte Carlo, 65
algorytm Las Vegas, 65
c calkowita poprawność, 37
częściowa poprawność, 37
D dane istotne, 46
długość programu, 54
dziel i rządź, 22
E element, 74
eliminacja Gaussa, 52
F faktoryzacja, 1 1
FIFO, 77
fonkeja iloczynowa, 23
fonkeja monotoniczna, 26
fimkcja wiodąca, 23
G grafniezgodności, 46
grafprzepływu sterowania, 55
H harmoniczna liczba chromatyczna, 89
inicjalizacja wirtualna, 75
algorithm recursive algorithm probabilistic algorithm Monte Carlo algorithm Las Vegas algorithm
- fit/l correctness partial correctness
essential data - program length
divie-and-conquer
item Gaussian elimination
factorization First In First Out multiplicative function monotonie fimction driving fonction
incompatibility graph program eon troi graph
- harmonious chromatic num ber
- virtual initialization
92
J jednoznacznie określona, 35 jednostkajimkcjonalności, 55
K kolejka, 77 kolejka podwójna, 77 kompromis przestrzenno-czasowy, 5 1 kwadratowa, 58
L leksem, 54 liczba cyklomatyczna, 55 liczba harmoniczna, 89 liczba przecięć, 79 LlFO, 77 liniowy, 59 lista, 76 lokalnie monotoniczny, 82
M macierz incydencji, 82 macierz rozrzedzona, 47 macierz sąsiedztwa, 82 maszyna Turinga, 7 metoda sprężynowa, 79 monotoniczna, 26
N niestabilny, 53 nietrywialna, 85 nieuchwytna, 85 niewielomianowa, 58 niezmiennik pętli, 38
o objętość programu, 54 oczekiwana liczba błędów, 54 oczekiwana zlożoność obliczeniowa, 42 oczekiwana złożoność pamięciowa, 46 operacja, 35 optymalny, 49
Słownik polsko-angielski
uniqually determined junction point
queue double queue trade-ojf between space and time
- quadratic
token - cyclomatic num ber
harmonious chromatic num ber - crossing number
Last In First Out - linear - list
locally monotonie
- matrix ojincidence sparse matrix
- matrix oj adjacency Turing machine
- spring embedder monotonie
unstable nontrivial elusive non-polynomial loop invariant
program volume expected num ber oj errors ex:pected complexity expected space complexity operation optimal
Słownik polsko-angielski
p pesymistyczna złożoność obliczeniowa, 42
pęki wyjściowe, 86 pierwszy rząd, 25
planamy, 59 planamy w górę, 80
podłoga (liczby), 1 5
podstawowy, 40
pojedyncza liniowa (lista), 76 polilogarytmiczna, 58
poziom programu, 54
półalgorytm, 37 problem algorytmiczny, 7 problem Col/atza, 1 0
problem decyzyjny, 8
problem kaje/kowania, 9 problem komiwojażera, 1 2
problem liczb gradowych, 1 0
problem niealgorytmiczny, 8
problem optymalizacyjny, 8
problem półrozstrzyga/ny, 8
problem przypuszcza/nie niea/goryt-miczny, 9
problem przypuszczalnie wykładniczy, 1 1
problem stopu, 7 problem wielomianowy, 1 2
problem wykładniczy, 1 1
proji/owanie, 36 programowanie stnlkturalne, 38
pułap (liczby), 1 5
punkt Feynmana, 1 1
Q quasi-liniowy, 58
R reprezentacja Fary/ego, 79 rozmiar, 77 rozwiązanie jednorodne, 23
rozwiązanie ogólne, 23
rozwiązanie szczegółowe, 23
równanie diojantyczne, 1 0
worst-case comple.;rity jorward star jirst-order p/anar
- upward p/anar fioor basic linear singly connected
- polylogarithmic program level semi-algorithm
- algorithmic problem - Col/atz Problem - decision problem
tiling problem travelling salesman problem hailstone numbers problem nonalgorithmic problem optimalization problem semidecidable problem
presumably nonalgorithmic problem - presumably exponential problem
halting problem polynomial problem
- exponential problem proji/ing stnlctural programming ceiling
- Feynman 's point
- quasilinear
Fary embedding size homogeneous solution general sollition particu/ar solution Diophantine equation
93
94
rysowanie grafów klastrowych, 82
rysowanie hierarchiczne, 79
rysowanie konjluentne, 82
s skończoność, 35
skorpion, 90
spód (liczby), 1 5
stała, 58
stopień trudności (programu), 54
stos, 77
sln/ktury danych, 45
subliniowa, 58
sufit (liczby), 1 5
superwielomianowa, 58
superwykładnicza, 58
ś ślad macierzy, 46
średni przypadek, 42
T tablica, 74
teoria obliczeń, 7
teoria złożoności obliczeniowej, 8
token, 54
u ujście, 85
uruchamianie, 36
w wektor, 74
wektor charakterystyczny, 77
wewnętrzna złożoność problemu, 49
wieże w Hanoi, 28
wielomianowa, 58
własność monotoniczna, 85
własność stopu, 37
w miejscu, 45
wołanie przez adres, 63
Słownik polsko-angielski
eluster graph drawing hierarchical drawing conjluent drawing
finiteness scorpion jloor constant program difficulty stack data stn/ctures sublinear ceiling superpolynomial superexponential
trace average-case
array computability theory computational complexity theory token
sink debugging
- vector characteristic vector inherent problem complexity to wers of Hanoi polynomial monotone property halting property in place
- call-by-reference
Słownik polsko-angielski
wołanie przez wartość, 63
wrażliwość najgorszego przypadku, 5 7
wrażliwość oczekiwana, 57
wrażliwość pesymistyczna, 57
wrażliwość średniego przypadku, 57
wykładnicza, 58
wysiłek programisty, 54
z zamiatanie warstwa po warstwie, 80
zbiór, 77
złożoność cyk/omatyczna, 56
złożoność najgorszego przypadku, 42
złożoność średniego przypadku, 42
złożoność pamięciowa, 45
złożoność pamięciowa najgorszego przypadku, 46
, Z źle uwarunkowane, 52
ca//-by-value worst-case sensitivity average-case sensitivity worst-case sensitivity average-case sensi/ivity exponential program mer ejJort
fayer by /ayer sweep set cyc/oma/ic complexity wors/-case comp/exity average-case complexity space complexity
- worst-case space complexity
- il/-conditioned
95
LITERATURA
[ 1 ] Abran A., Robillard P. N. : Function point analysis: An empirical study of its measure
ment processes. IEEE Trans. Soft. Eng. 22, 1 996, 895-909. [2] Aho A., Hopcroft 1. E., Ullman J. D . : Projektowanie i analiza algorytmów kompute
rowych. Warszawa: PWN 1 983. [3] Banachowski L. , Diks K. , Rytter W. : Algorytmy i struktury danych. Warszawa: WNT
1 996.
[4] Fomin F. V., Grandoni F., Kratsch D. : Measure and conquer: A simple 0(20 288n) independent set algorithm. SODA 2006.
[5] Giaro K.: Złożoność obliczeniowa algorytmów w zadaniach. Gdańsk: Wydawnictwo
Politechniki Gdańskiej 2002. [6] Goczyła K. : Struktury danych. Gdańsk: Wydawnictwo Politechniki Gdańskiej 2002. [7] Herik H. J., Uiterwijk J. W. H. M. , Rijswijck 1. : Games solved: Now and in the future,
Art. Int. 1 34, 2002, 277-3 1 1 . [8] Kubale M. : Introduction to Computational Complexity. Gdańsk: Wydawnictwo
Politechniki Gdańskiej 1 994.
[9] Kubale M.: Introduction to Computational Complexity and Algorithmic Graph Color-ing. Gdańsk: WGTN 1998.
[ 1 0] Lines M. E. : Liczby wokół nas. Wrocław: OWPW 1 995. [ 1 1 ] Ribenboirn P . : Mała księga wielkich liczb pierwszych. Warszawa: WNT 1 997. [ 1 2] Sysło M. M. : Algorytmy. Warszawa: WSiP 1 997.
[ 1 3] Yan S . Y.: Teoria liczb w informatyce. Warszawa: PWN 2006. [ 1 4]Wirth N.: ALGORYTMY + STRUKTURY DANYCH = PROGRAMY. Warszawa:
WNT 1 980.
[ 1 5] http://www.astro.virginia.edul-eww6n/math/CollatzProblem.html
[ 1 6] http://www.mersenne.org/
[ 1 7] http://www.claymath.org/millennium/
top related