97

Kubale - Wprowadzenie Do Algorytmow

  • Upload
    blus90

  • View
    5.366

  • Download
    8

Embed Size (px)

Citation preview

Page 1: Kubale - Wprowadzenie Do Algorytmow
Page 2: Kubale - Wprowadzenie Do Algorytmow

Łagodne wprowadzenie do analizy algorytmów

Marek Kubale

Gdańsk 2009

Page 3: Kubale - Wprowadzenie Do Algorytmow

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ą ([email protected]), 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

Page 4: Kubale - Wprowadzenie Do Algorytmow

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

Page 5: Kubale - Wprowadzenie Do Algorytmow

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

Page 6: Kubale - Wprowadzenie Do Algorytmow

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

Page 7: Kubale - Wprowadzenie Do Algorytmow

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 [email protected]

Page 8: Kubale - Wprowadzenie Do Algorytmow

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

Page 9: Kubale - Wprowadzenie Do Algorytmow

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 obli­czeniowej (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ć

Page 10: Kubale - Wprowadzenie Do Algorytmow

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 pro­blems). 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ń

Page 11: Kubale - Wprowadzenie Do Algorytmow

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 nierozstrzy­galny. Jest to wniosek wynikający z rozwiązania 1 0. problemu Hilberta, dotyczącego rów­nań 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 num­bers problem) lub problemem Col/atza (ang. Collatz problem) za L. Collatzem, który sfor­muł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 niealgo­rytmicznym. 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 wszy­scy 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 natu­ralna 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 zaofe­rowano 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 hi­poteza 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ą.

Page 12: Kubale - Wprowadzenie Do Algorytmow

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

Page 13: Kubale - Wprowadzenie Do Algorytmow

.:.

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 Mer­senne 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

Page 14: Kubale - Wprowadzenie Do Algorytmow

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

Page 15: Kubale - Wprowadzenie Do Algorytmow

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

Page 16: Kubale - Wprowadzenie Do Algorytmow

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 od­woł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 sym­bole 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. Loga­rytmy 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ów­czas 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 .

Page 17: Kubale - Wprowadzenie Do Algorytmow

---

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.

Page 18: Kubale - Wprowadzenie Do Algorytmow

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

Page 19: Kubale - Wprowadzenie Do Algorytmow

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" .

Page 20: Kubale - Wprowadzenie Do Algorytmow

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 wszyst­kich 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)

Page 21: Kubale - Wprowadzenie Do Algorytmow

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. Jed­nakż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» . Za­tem Q(g) jest dolnym oszacowaniem tempa wzrostu funkcj i! Formalna definicja tego sym­bolu 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 dowol­nej 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» .

Page 22: Kubale - Wprowadzenie Do Algorytmow

.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' ) .

Page 23: Kubale - Wprowadzenie Do Algorytmow

.--

22 1. Wprowadzenie

Podobnie w literaturze spotyka się notację 0'0. Jest to notacja 0(·) z pominięciem czynni­kó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ń asymptotycz­nych 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 algori­thms), 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 proble­mó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)

Page 24: Kubale - Wprowadzenie Do Algorytmow

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ól­nym. 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-

Page 25: Kubale - Wprowadzenie Do Algorytmow

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. Za­tem 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ą-

Page 26: Kubale - Wprowadzenie Do Algorytmow

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 zdeter­minowane 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 po­przedzają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

Page 27: Kubale - Wprowadzenie Do Algorytmow

� ----

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 tem­pem wzrostu sumy szeregu związanego z den).

Obecnie oszacujemy tempo wzrostu rozwiązania szczegółowego. W tym celu założy­my, ż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ą niema­leją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ż

Page 28: Kubale - Wprowadzenie Do Algorytmow

!.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" ,

Page 29: Kubale - Wprowadzenie Do Algorytmow

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 . Za­uważ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ąza­nia można sprawdzić metodą indukcji . •

Page 30: Kubale - Wprowadzenie Do Algorytmow

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ęzy­programowania. 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 )

Page 31: Kubale - Wprowadzenie Do Algorytmow

30 1. Wprowadzenie

1 .5. Przeanalizuj działanie następującego programu. Zaimplementuj go w dowolnym języ­ku 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

Page 32: Kubale - Wprowadzenie Do Algorytmow

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"?

Page 33: Kubale - Wprowadzenie Do Algorytmow

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.

Page 34: Kubale - Wprowadzenie Do Algorytmow

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

Page 35: Kubale - Wprowadzenie Do Algorytmow

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 rzeczywisty­nli, ż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 kolej­nego wyrazu polepsza dokładność o mniej więcej dalszych 1 4 cyfr. Jest to naj lepszy odtaj­niony 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?

Page 36: Kubale - Wprowadzenie Do Algorytmow

2. PODSTAWY ANALIZY ALGORYTMÓW

2.1. Wstęp

Mówiąc bardzo nieformainie, algorytm (ang. algorithm) to pewien opis sposobu po­stę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 ma­szynę 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 pro­gramowania. 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 algoryt­mów dotyczy problemu określenia, ile czasu, ile pamięci (operacyjnej , pomocniczej),

Teszcie i le procesorów (arytmetycznych, komunikacyjnych) wymagać będzie dany pro-

Page 37: Kubale - Wprowadzenie Do Algorytmow

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 rozu­miemy tutaj zarówno uruchamianie, jak i profilowanie. Unlchamianie (ang. debugging) to próba wykonania programu na przykładowych danych dla stwierdzenia, czy daje on po­prawne 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. Profilowa­nie (ang. profiling) jest procesem wykonywania poprawnego programu na pewnych intere­sujących nas zestawach danych oraz mierzenie czasu i pamięci zajmowanej przez ten pro­gram. Oczywiście, pojawia się tu pytanie, na jakich danych należy testować programy.

W niniejszym rozdziale zajmiemy się odpowiedzią na pytania sformułowane w punk­cie 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 prze­strzenie danych i wyników (ich struktury, normy) naj lepiej odpowiadają sensowi fizyczne­mu 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 opty­malny, 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 algo­rytmicznie 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ść.

Page 38: Kubale - Wprowadzenie Do Algorytmow

_ .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 abstrak­cj i. Przede wszystkim, trzeba ustalić, co oznacza "poprawność" w danym konkretnym przy­padku, 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 algo­rytm 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 algoryt­mó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 algoryt­mu. 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 wy­nik 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. semi­a/gorithm). Zatem algorytm jest całkowicie poprawny (ang. foli correctness), gdy jest czę­ściowo poprawny i ma własność stopu.

Page 39: Kubale - Wprowadzenie Do Algorytmow

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 tech­niki programowania strukturalnego (ang. structural programming), polegającej na podzie­leniu 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ółowe­go 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.

Page 40: Kubale - Wprowadzenie Do Algorytmow

_ .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 wyko­nania 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 algo­rytmie 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 swo­je 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 po­prawność 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 .

Page 41: Kubale - Wprowadzenie Do Algorytmow

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 pro­gramu 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 czaso­chł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 przy­kł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 pro­porcjonalna 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-

Page 42: Kubale - Wprowadzenie Do Algorytmow

_ .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.

Page 43: Kubale - Wprowadzenie Do Algorytmow

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ę roz­miaru 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 wykonywa­nych 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 przy­padku danych.

2.3.4. Oczekiwana złożoność obl iczeniowa

Dobre bądź złe zachowanie się algorytmu w najgorszym przypadku danych nie roz­strzyga jeszcze o jego przydatności w praktyce. Bardzo wolne działanie algorytmu w naj­gorszym przypadku danych ostrzega nas jedynie przed możliwym fiaskiem szybkiego zna­lezienia 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 opty­malną 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 przypa­dek, 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 zastosowa­niach są zwykle rzadkie, macierze rozrzedzone, l isty częściowo uporządkowane itp. Jed­nakż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 rzeczy­wistym 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ść oblicze­niową średniego przypadku (ang. average-case complexity) lub oczekiwaną złożoność obli­czeniową (ang. expected complexity), lub po prostu średnią złożoność definiujemy jako

Page 44: Kubale - Wprowadzenie Do Algorytmow

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. •

Page 45: Kubale - Wprowadzenie Do Algorytmow

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• •

Page 46: Kubale - Wprowadzenie Do Algorytmow

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ęcio­wą (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 struktu­ry 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.

Page 47: Kubale - Wprowadzenie Do Algorytmow

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 wystar­czają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 ocze­kiwanej 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ą czaso­wą 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 algo­rytmy, 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 uzysku­jemy wówczas, gdy kilku zmiennym przyporządkowana jest jedna i ta sama komórka pa­mię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 przepro­wadzić 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 dowol­nej 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łów­nej przekątnej, nie istotne są wszystkie elementy leżące poza główną przekątną· Jeżeli struktura

Page 48: Kubale - Wprowadzenie Do Algorytmow

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ę.

Page 49: Kubale - Wprowadzenie Do Algorytmow

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 bi­tach 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 n­bitowy. 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 prawdopo­dobień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);

Page 50: Kubale - Wprowadzenie Do Algorytmow

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 da­nych, 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ść algoryt­mu. 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 przy­padku. 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ąza­ma 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-

Page 51: Kubale - Wprowadzenie Do Algorytmow

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.

Page 52: Kubale - Wprowadzenie Do Algorytmow

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-

Page 53: Kubale - Wprowadzenie Do Algorytmow

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ń.

Page 54: Kubale - Wprowadzenie Do Algorytmow

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• •

Page 55: Kubale - Wprowadzenie Do Algorytmow

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

Page 56: Kubale - Wprowadzenie Do Algorytmow

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

Page 57: Kubale - Wprowadzenie Do Algorytmow

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ę

Page 58: Kubale - Wprowadzenie Do Algorytmow

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 wprowa­dzenia 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 śred­nia 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

Page 59: Kubale - Wprowadzenie Do Algorytmow

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. Progra­mowanie 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 prio­ri. 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. polyloga­rithmic), gdy j{n) = ro( l ) i istnieje stała c > O taka, żej{n) = G(1otn) . Z funkcjami poliloga­rytmicznymi mamy do czynienia, analizując algorytmy równoległe. Funkcje stałe i poliloga­rytmiczne 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 przy­padku 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 funk­cj 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 superwielo­mianową (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 kombinato­rycznych. W tabeli 2. 1 podajemy przykłady omówionych funkcji .

Page 60: Kubale - Wprowadzenie Do Algorytmow

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.

Page 61: Kubale - Wprowadzenie Do Algorytmow

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

Page 62: Kubale - Wprowadzenie Do Algorytmow

------ - ----

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' ozna­cza 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 podzie­lenie 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 algoryt­mu 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ść po­niższych usprawnień zależy silnie od sprzętu, na którym wykonuje się dany algorytm.

Page 63: Kubale - Wprowadzenie Do Algorytmow

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;

Page 64: Kubale - Wprowadzenie Do Algorytmow

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ę sowi­cie, 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 pro­cedury przez wartość (ang. call-by-value) jest czasochłonne, gdyż wiąże się z deklarowa­niem 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 niekorzyst­nych zjawisk - może naruszyć poprawność, funkcjonalność i łatwość konserwacji progra­mó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.

Page 65: Kubale - Wprowadzenie Do Algorytmow

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ży­teczny, 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 ze­wnętrznej . Częste odwołania do pamięci zewnętrznej mogą przekreślić praktyczną skutecz­ność takich algorytmów optymalnych (kompromis przestrzenno-czasowy).

5. Istnieją algorytmy, które są bardzo wolne w sensie złożoności naj gorszego przypad­ku 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ść obliczenio­wą. Dlatego metoda ta jest powszechnie stosowana w praktyce, pomimo że znane są algo­rytmy 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 algo­rytmem 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 algo­rytmó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 od­JJowiednik. 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

Page 66: Kubale - Wprowadzenie Do Algorytmow

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, wygene­rować 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 opera­cj 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 zadowa­lają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ścio­wych (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 } ;

Page 67: Kubale - Wprowadzenie Do Algorytmow

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

Page 68: Kubale - Wprowadzenie Do Algorytmow

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 za­tem 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 osza­cowanie 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ę niezmienni­ków pętli dla dowodu poprawności następującego algorytmu dodawania wektorów A [l . . n] i B[l. .n] .

Page 69: Kubale - Wprowadzenie Do Algorytmow

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ę niezmienni­ków pętli dla dowodu poprawności następującego algorytmu znajdowania największej war­toś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

Page 70: Kubale - Wprowadzenie Do Algorytmow

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 ;

Page 71: Kubale - Wprowadzenie Do Algorytmow

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);

Page 72: Kubale - Wprowadzenie Do Algorytmow

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 kwadratowe­go 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)

Page 73: Kubale - Wprowadzenie Do Algorytmow

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 pro­gramisty 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, wybie­ramy między czasem wyszukiwania, a czasem przetwarzania wstępnego. Jak wiele wyszu­kiwań 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 osza­cowań 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.

Page 74: Kubale - Wprowadzenie Do Algorytmow

Zadania 73

b) Dla każdego n istnieje zestaw danych rozmiaru n, dla którego czas działania A jest krót­szy niż n2.4 sekund.

c) Dla każdego n istnieje zestaw danych rozmiaru n, dla którego czas działania A jest krót­szy 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).

Page 75: Kubale - Wprowadzenie Do Algorytmow

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 wek­tora 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 dowol­nego elementu w stałym czasie. Nawet gdy doliczymy do tego czas potrzebny na weryfika­cję adresu, tj . ustalenie, że nie przekracza on zakresu dopuszczalnych wartości, czas po­trzebny 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 proporcjonalne­go 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 prze­suwając niższe wartości o jedną pozycje w lewo. Bez względu na to, jaką strategię kopio­wania przyjmiemy, w najgorszym przypadku będziemy musieli przesunąć co najmniej n/2 elementów. Podobnie usunięcie elementu może wymagać przemieszczenia prawie wszyst­kich 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 wielowymia­rowe. Na przykład deklaracja tablicy dwuwymiarowej 400 liczb całkowitych mogłaby wy­glądać następująco:

macierz: array[ l :20, 1 :20] of integer;

Page 76: Kubale - Wprowadzenie Do Algorytmow

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 cza­sami 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 ini­tializatian). Polega ona na tym, że jeśli chcemy zainicjalizować tablicę 11 l . . n ], to potrzebu­jemy 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 zainicjali­zowany 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 na­stępujący przykład.

Przykład 3.1 Przypuśćmy, że tablica do zainicjalizowania to 11 1 . .8] . W tablicy tej zainicjalizowano ko­lejno 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 zaini­cjalizowane, 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 usta­lone, 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 przeciw­nym razie nie.

Page 77: Kubale - Wprowadzenie Do Algorytmow

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. po­jedyncza 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

Page 78: Kubale - Wprowadzenie Do Algorytmow

3.3. Zbiory 77

Listę, na której można wykonać wszystkich sześć operacj i, nazywa się kolejką po­dwó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 wsta­wiany 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 opera­cje 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 implementa­cj 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 opera­cja 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 zna­my 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ą wekto­ró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.

Page 79: Kubale - Wprowadzenie Do Algorytmow

78 3. Podstawowe struktury danych

Operacje insert, de/ete i member mają złożoność czasową 0( 1 ) . Złożoność pamięcio­wa 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 jed­nego słowa maszynowego. Jednakże, gdy rozmiary SI i S2 są małe w porównaniu z rozmia­rem uniwersum, to suma i iloczyn są wykonywane w czasie 0(1 U]), a nie w czasie propor­cjonalnym 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 . Roz­waż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 margine­sie, ż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 reprezentowa­nia 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. Kry­teria 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 osobi­stych 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 po­szczególnych estetyk dowodzą, że najważniejszymi z nich są: unikanie (minimalizacja licz­by) przecięć i pokazywanie symetrii . Jak wiadomo, całkowite wyeliminowanie przecięć jest

Page 80: Kubale - Wprowadzenie Do Algorytmow

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-

Page 81: Kubale - Wprowadzenie Do Algorytmow

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

Page 82: Kubale - Wprowadzenie Do Algorytmow

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

Page 83: Kubale - Wprowadzenie Do Algorytmow

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ą łama­nymi 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 kierun­ku. Intuicyjnie, krzywa taka jest jak tor kolejowy (rys. 3 .4). Rysowanie konjluentne (ang. conjluent drawing) polega na przedstawianiu krawędzi w postaci krzywych lokalnie mono­tonicznych, 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 konfluent­ny, 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 wierz­chołka.

Page 84: Kubale - Wprowadzenie Do Algorytmow

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 kla­strowych (ang. clustered graphs drawing), podpisywanie wierzchołków i krawędzi, nawi­gowanie 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ąsiedz­twa (ang. matrix oj adjacency) opisuje relację zachodzącą pomiędzy elementami tego sa­mego zbioru, np. macierz sąsiedztwa wierzchołków czy macierz sąsiedztwa krawędzi. Ma­cierz 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 incy­dencji 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 •

Page 85: Kubale - Wprowadzenie Do Algorytmow

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

Page 86: Kubale - Wprowadzenie Do Algorytmow

3. 4. Grafy 85

a = [� �] � = [� �] y = [� �] 8 = [� �] itd. •

Macierze sąsiedztwa wierzchołków są bardzo użyteczne przy rozwiązywaniu różno­rodnych 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ść roz­strzygnię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 nietry­wialnych własności teoriografowych P, c(P) � 2n - 4. Z drugiej strony istnieje wiele wła­sności, dla których funkcja c(P) nie jest liniowa. Nazywamy je nieuchwytnymi (ang. elu­sive). Własnościami nieuchwytnymi są wszystkie wspomniane w tym podręczniku, np. ha­miltoniczność, 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ść, nie­planamość, hamiltoniczność. Udowodniono, że jeśli P jest nietrywialną własnością mono­toniczną, 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żde­go 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 •

Page 87: Kubale - Wprowadzenie Do Algorytmow

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 przy­padkach 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 poprzed­nio, dodanie nowej krawędzi może być zrealizowane w czasie O( l ), gdyż wystarczy wsta­wić wierzchołek-rekord na początek odpowiedniej listy. Jednakże usunięcie krawędzi wy­maga 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. Cza­sami 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) wsku­tek zastosowania metody poszukiwania połówkowego. Zauważmy na marginesie, że podob­nie 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że­my zrezygnować ze wskaźników i zapamiętać wszystkie krawędzie po kolei w jednym wek­torze. 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 •

Page 88: Kubale - Wprowadzenie Do Algorytmow

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ła­twiony 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 zaw­sze 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ą.

Page 89: Kubale - Wprowadzenie Do Algorytmow

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

Page 90: Kubale - Wprowadzenie Do Algorytmow

Zadania 89

liczby harmonicznej h(G) grafu G (ang. harmonious chromatic number): h(G) jest naj­mniejszą 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 posta­ci 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 roz­miaru 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żliwia­ją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 ele­mentach macierzy A.

Page 91: Kubale - Wprowadzenie Do Algorytmow

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.

Page 92: Kubale - Wprowadzenie Do Algorytmow

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

Page 93: Kubale - Wprowadzenie Do Algorytmow

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

Page 94: Kubale - Wprowadzenie Do Algorytmow

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

Page 95: Kubale - Wprowadzenie Do Algorytmow

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

Page 96: Kubale - Wprowadzenie Do Algorytmow

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

Page 97: Kubale - Wprowadzenie Do Algorytmow

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) inde­pendent 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/