Kubale - Wprowadzenie Do Algorytmow

Share Embed Donate


Short Description

Download Kubale - Wprowadzenie Do Algorytmow...

Description

Łagodne wprowadzenie do analizy algorytmów

Marek Kubale

Gdańsk 2009

PRZEWODNICZĄCY KOMITETU REDAKCYJNEGO WYDAWNICTWA POLITECHNIKI GDAŃSKIEJ

Romuald Szymkiewicz

REDAKTOR

Zdzisław Puhaczewski

RECENZENT

Krzysztof Goczyła

Wydanie V

-

2008 poprawione i uzupełnione

Wydano za zgodą Rektora Politechniki Gdańskiej

Wydawnictwa PG można nabywać w Księgarni PG (Gmach Główny, I piętro) bądź zamówić pocztą elektroniczną ([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

SPIS TREŚCI

::lRZEDMOWA

•.

.... . . . . .

............................................................................................. .................

WPROWADZENIE..............................................................................................................

1.1. Problemy algorytmiczne . .. . ...

1.2. Język PseudoPascal ...

. . . . . . ... .............. ........................................ ......

. ...... . . . . .

. . . . . . . . . . . . . . . . . ..

. . . . . .... . .. . . . .. . .. . .

1.3. Podstawy matematyczne .. ... ..... .

.

.

. .

.. ..

.

...

. ..

.

..

.

.

.

.

. .

1.4. Symbole oszacowań asymptotycznych

..

. . . . ........ .

.

.... ..........

.

... ...

...

.

.. . .

1.4.2. Symbol 0(') . .

.

.

.

.

.

.

.

..

.

. . . . . . . . . . . . .. . . . . . . . . . . . .

1.4.3. Symbol no . .. ..

. . ..

. . . . . . . . . .. . . . . . . . .

1.4.4. Symbol ro(.) ............. .... ......... .

.

.

. ... .

.

.

......... .......... .... 15 .

.

... . ....... . . .

.

.

. ..

....

..

..

..

..

. ..

16

. . . . ...................... 18

. . . . . . . . . . . . . . . .. .

. . . . .........

. 15

. ........... ... ....

. . . . . . . . . . . ..... . ... . . . . .. . .

1.4.1. Symbol 0(·). . . . . ......... . . . .. .... . . ... . . . . . . .. .

..

. ....

. ..........

. . .... .

1.3.2. Sumy szeregów ...... ... . .......... ... .

7 7

. . ......... ... ....... ............ .... 13

. . . . . ........ .................... .

..... .. . .. . .

1.3.1. Logarytmy i zaokrąglenia całkowite .

5

.

..

.

.

...... .......

.. . .. ..

..

..

... . . . . . .

... 18

...... . . . . . ............ . . . .... ........... . . ..... 19

....

.

. . . . . . .. .

...

.

.. . . .

.

. . . ..

..

. . . . ..... . . . .

.

.

............ .....

.

.

. . .

..

. . . 20

. . . . . . . . . . . . ..... . .

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

. . . . . . . . . . ........ . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .. . . . . .. . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . .. . . . . . . . . .

2. PODSTAWY ANALIZY ALGORyTMÓW .

. .. . . . .

. ......... .

2.1. Wstęp

... ..

. . . . . . . .. . . . . .

.

.

. ..

. . .....

. .. . . .......... .

.... . . . . ..

. . .. .

. .......

. .

. . .. . . . . .

.

.. . . . ..

.. .

.

..... . . . . . . . . . . . . . . . . ......

29 35

. . . ..... ... ............. ......................... 35 . . . ..

..

..

.

2.2. Poprawność algorytmów .......................................................................................... 37 2.3. Złożoność czasowa algorytmów .............................................................................. 40 2.3.1. Operacje podstawowe ........ ............... ....... . . . ........ ... . .. .

.

. .

.. . .

.

..

.

....

..

.....

. . . ..

..

. . 40

... . . .

2.3.2. Rozmiar danych ............................................................................................. 41 2.3.3. Pesymistyczna złożoność obliczeniowa ....... .............. ... . . .. ..... ....... ... .

.

..

.

..

.

.

...

..

42

2.3.4. Oczekiwana złożoność obliczeniowa ............................................................. 42 2.4. Złożoność pamięciowa............................................................................................. 45 2.5. Optymalność ............................................................................................................ 49 2.6. Dokładność numeryczna algorytmów ....... ... ................................ ....... .............. . 51 ..

2.6.1. Zadania źle uwarunkowane ... .

..

. .....

.

. . . ......

.

. . .. . . .. .

.

. . .......... .

.. . ...

.. .

.

.

. .. ............. 51

. . . . ..

.

2.6.2. Stabilność numeryczna................................................................................... 53 2.7. Prostota algorytmów ..................... ....... ... ......... ... . ..... . ..

.

.

.

..

..

...

.

.

. ..

. . . . . . . . . . . . . . . . . . ......

. . . ..

54

4

Spis treści 2.8. Wrażliwość algorytmów

..........................................................................................

2.9. Programowanie a złożoność obliczeniowa 2.9.1. Rząd złożoności obliczeniowej

..............................................................

58

......................................................................

58

2.9.2. Stała proporcjonalności złożoności obliczeniowej

........................................

61

.........................................

63

...................................................................................

64

........................... . . . ............................ .................................................................

67

2.9.3. Imperatyw złożoności obliczeniowej i odstępstwa 2.10. Algorytmy probabilistyczne

Zadania

56

3. PODSTAWOWE STRUKTURY DANyCH .

.

. . ................. . . . . . . . . ................................... . . . . . . .......

3.1. Tablice 3.2. Listy

.

74

.

76

.

77

...................... .................. ............................................................................

........................... . ................ ............................................................................

3.3. Zbiory 3.4. GrafY

. . ....................................... ............................................................................

.

.

....... ............................. ................... ................. ..............................................

3.4.1. Macierz sąsiedztwa wierzchołków 3.4.2. Listy sąsiedztwa wierzchołków 3.4.3. Pęki wyjściowe

Zadania .

74

.

.

.............. . . . . . . . ........... . . . . . . . . . . ................ . . . . .

78 83

......................................................................

85

........ .................. ............................... .....................................

86

.

....................................................... . .. . ..... .........................................................

87

SŁOWNIK POLSKO-ANGIELSKI ............................................................................................. 91 LITERATURA

.

................................................................................................................. ......

96

PRZEDMOWA Przekazywana do rąk czytelników książka jest szóstym wydaniem podręcznika akade­ mickiego, opublikowanego nakładem Wydawnictwa Politechniki Gdańskiej pod tym samym tytułem. Od momentu piątego wydania w roku 2008 wystąpiły nowe fakty w dziedzinie dużych liczb pierwszych i sposobów stymulowania nowych odkryć w zakresie teorii algo­ rytmów i teorii liczb. Stąd zrodziła się potrzeba kolejnego wydania, uzupełnionego o naj­ nowsze informacje z tych dziedzin. Ponadto, ostatnio ukazały się inne podręczniki w tej serii na temat algorytmów i struktur danych i fakt ten musiał być odnotowany w niniejszej publikacji. Oddawany do rąk czytelników podręcznik jest przeznaczony dla osób interesujących się podstawami informatyki, w tym przede wszystkim dla studentów kierunku Informatyka na Wydziale ET! Politechniki Gdańskiej. Formalnie rzecz biorąc, jego treść pokrywa pierwszą część wykładu z przedmiotu Podstawy analizy algorytmów", tj. algorytmy i pro­ " blemy wielomianowe, ale stanowi też miejscami rozszerzenie programu tego przedmiotu, który jest prowadzony na II roku kierunku Informatyka. W tym miejscu odnotujmy, że dru­ gą część wykładu doskonale pokrywa książka K. Giary

Złożoność obliczeniowa algoryt­ " mów w zadaniach" [5] oraz poprzedni skrypt autora [8]. W szczególności niniejszy pod­ ręcznik może służyć jako wprowadzenie do wykładu ,,Algorytmy i struktury danych". Jego fragmenty mogą być także wykorzystane w nauczaniu przedmiotu Matematyka dyskretna". " Sądzę, że książka może ponadto zainteresować studentów kierunku Informatyka na Wydzia­ le Matematyki, Fizyki i Informatyki Uniwersytetu Gdańskiego, oraz studentów kierunków pokrewnych, np. Matematyka Stosowana. Zakładam, że czytelnik ma pewne podstawowe przygotowanie z matematyki dyskretnej i że umie układać algorytmy w Pascalu lub innym języku wysokiego poziomu. Znajomość przedmiotów Metody i techniki programowania", Praktyka programowania" oraz Mate­ " " " matyka dyskretna" jest pożądana przy studiowaniu podręcznika. Niniejsza pozycja składa się z trzech rozdziałów. Rozdział l daje podstawy formalne, niezbędne przy analizie algorytmów pod kątem złożoności obliczeniowej. Podajemy tutaj klasyftkację problemów rozwiązywalnych za pomocą komputerów, przypominamy wybrane pojęcia matematyczne, definiujemy symbole oszacowań asymptotycznych. Jednakże naj­ więcej miejsca poświęcamy metodom najczęściej spotykanym przy analizie złożoności obliczeniowej algorytmów rekurencyjnych. Rozdział 2 wprowadza w zagadnienie analizy algorytmów z różnych punktów widze­ nia. Algorytmy, które tutaj rozważamy, są najprostsze możliwe, tj. szeregowe, scentralizo­ wane, statyczne i dokładne. Rozważamy tutaj takie zagadnienia, jak: poprawność, złożo­ ność czasowa, złożoność pamięciowa, optymalność, stabilność numeryczna, prostota i wrażliwość. Rozdział zamykamy przykładem algorytmu probabilistycznego. Ostatni rozdział 3 przedstawia podstawowe struktury danych, gdyż są one niezbędnym komponentem każdego rozwiązania algorytmicznego. W rozdziale tym rozważamy takie struktury, jak: tablica, lista zbiór, a zwłaszcza graf. Strukturom grafowym poświęcamy

6

Przedmowa

szczególnie wiele miejsca, gdyż grafy są najczęściej spotykanym modelem matematycznym w informatyce. Więcej informacji na ten temat można znaleźć w podręczniku K. Goczyły " "Struktury danych [6]. Co ważne i cenne dla czytelników studiujących zagadnienia złożoności obliczeniowej algorytmów, każdy z powyższych rozdziałów kończy się zestawem około 30 zadań nie­ zbędnych do sprawdzenia nabytej wiedzy i umiejętności oraz umożliwiających jej pogłę­ bienie. Na zakończenie zaś podano słownik polsko-angielski ważniejszych pojęć z tego zakresu. W tym miejscu pragnę wyrazić wdzięczność recenzentowi prof. dr. hab. inż. Krzyszto­ fowi Goczyle za życzliwe sugestie; dziękuję również mgr. inż. Janowi Wojtkiewiczowi za pomoc edytorską oraz zespołowi Wydawnictwa PG za wnikliwą korektę. Z góry dziękuję również studentom za wszelkie uwagi merytoryczne, które można kierować pocztą elektro­ niczną pod podanym niżej adresem.

Gdańsk, lipiec 2009

r.

Marek Kubale [email protected]

1. WPROWADZENIE Przez wieki nie było zgodności wśród autorów różnych książek o obliczeniach oraz badaczy algorytmów co do formalnej definicji algorytmu. Mimo to od czasów Euklidesa (rok 300 pne.), nie martwiono się tym specjalnie, tylko tworzono opisy rozwiązywania różnych problemów - i dzisiaj nazywamy je algorytmami. Nawet konstruktorzy pierwszych liczydeł, kalkulatorów i maszyn cyfrowych nie dociekali specjalnie, jak zdefiniować to coś, co da się wykonać za pomocą ich maszyn. Na przełomie XIX i XX wieku matematyków zainteresowało udzielenie odpowiedzi na

dość ogólne pytanie: co można obliczyć, jakie funkcje są obliczalne, dla jakich problemów istnieją algorytmy, i ogólniej - czy wszystkie twierdzenia można udowodnić (lub obalić)? W 1900 r. matematyk niemiecki D. Hilbert, wśród 23 wyzwań dla matematyków zaczynają­ cego się stulecia, jako dziesiąty problem sformułował pytanie: czy istnieje algorytm, który dla dowolnego równania wielomianowego wielu zmiennych o współczynnikach w liczbach całkowitych znajduje rozwiązanie w liczbach całkowitych? Dopiero po prawie siedemdzie­ sięciu latach matematyk rosyjski J.V. Matjasiewicz odpowiedział negatywnie na to pytanie [11]. Dziesiąty problem Hilberta wywołał olbrzymie zainteresowanie wśród matematyków obliczalnością - dziedziną, która zajmuje się poszukiwaniem odpowiedzi m.in. na pytanie, jakie problemy mają rozwiązanie w postaci algorytmu, a jakie nie mają. Formalizacją pojęć algorytmu i obliczalności zajęło się w pierwszej połowie ubiegłego stulecia wielu matematyków. Wprowadzono wiele różnych definicji obliczeń, przy czym większość z nich jest równoważna między sobą w tym sensie, że definiuje tę samą klasę funkcji obliczalnych. Do najpopulamiejszych należy formalizm wprowadzony przez A. Turinga, zwany dzisiaj maszyną Turinga (ang. Turing machine). Obecnie maszynę Turinga przyjmuje się za precyzyjną definicję pojęcia algorytmu. Zatem nie ma algorytmu dla takie­ go problemu, którego nie można rozwiązać za pomocą maszyny Turinga.

1.1. Problemy algorytmiczne W niniejszej książce będziemy zajmowali się wyłącznie problemami algorytmicznymi (ang. algorithmic problems), tj. takimi, które mogą być rozwiązane za pomocą odpowied­ nich algorytmów komputerowych. Powiedzenie, że problem może być rozwiązany za po­ mocą

algorytmu,

oznacza

tutaj,

że

można

napisać

program komputerowy, który

w skończonym czasie da poprawną odpowiedź dla dowolnych poprawnych danych wej­ ściowych przy założeniu dostępu do nieograniczonych zasobów pamięciowych. Badania problemów algorytmicznych rozpoczęły się już w latach trzydziestych, tj. przed nadejściem ery komputerowej. Ich celem było scharakteryzowanie tych problemów, które mogą być rozwiązane algorytmicznie, i ujawnienie niektórych problemów, które nie posiadają takiej własności. Jednym z ważnych negatywnych rezultatów teorii obliczeń (ang. computability

theory) było odkrycie przez A. Turinga nierozstrzygalności problemu stopu. Problem stopu

8

l. Wprowadzenie

(ang. halting problem) polega na odpowiedzi na pytanie, czy po wczytaniu danych określo­ ny algorytm (lub program komputerowy) kończy się w skończonym czasie, czy też pętli się. Okazuje się, że nie istnieje program komputerowy rozwiązujący ten problem w przypadku ogólnym. Oczywiście, nierozstrzygalność jakiegoś problemu w przypadku ogólnym nie oznacza, że każdy jego przypadek szczególny jest również nierozstrzygalny. Poza tym w klasie problemów nierozstrzygalnych wyróżnia się podklasę tak zwanych problemów pół­ rozstrzygalnych (ang. semidecidable problems), tj. takich, dla których odpowiedź tak" jest " zawsze otrzymywana w skończonym czasie, ale brak takich gwarancji, gdy odpowiedź brzmi "nie" (np. problem stopu). Stwierdzenie, że jakiś problem jest algorytmiczny, nie mówi nic o tym, czy problem ten jest rozwiązywalny efektywnie czy nie. Wiadomo na przykład, że można napisać pro­ gram komputerowy, który grałby w szachy w sposób doskonały. Jest bowiem skończona liczba sposobów rozmieszczenia figur na szachownicy, zaś partia szachów musi się zakoń­ czyć po skończonej liczbie ruchów. Znając konsekwencje każdego ruchu przeciwnika, można pokusić się o wskazanie najlepszego możliwego posunięcia. Szacuje się, że liczba liści w pełnym drzewie przeszukiwania rozwiązań dla szachów sięga 1 0123 [7]. Zatem przy obecnej szybkości komputerów program sprawdzający je wszystkie musiałby się wykony­ wać wiele miliardów lat. Przykład ten nie został wybrany przypadkowo 1 01 23 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ć -

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ń

10

l. Wprowadzenie

diofantycznych. Rozwiązywanie równań diojantycznych (ang. Diophantine equations) polega na znaj dowaniu liczb całkowitych rozwiązujących równanie algebraiczne o współ­ czynnikach całkowitych (np. X 2 + 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 teorioliczbowego 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 := 3 k + 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 zaj mują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 1 742. 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ą 34

liczbę spotkaną do tej pory w teoriach matematycznych uchodzi l O 1 0000000000 • 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ą.

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 1 000 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­ 43 26 ziona pierwsza liczba typu Mersenne'a o wartości 2 1 1 09_ 1 . Do zapisu tej liczby trzeba

.:.

12

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 najkró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­ 6 rzenia 9!12 = 1 8 1 440 cykli i komputer przeglądający je z szybkością 1 0 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 1 6 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

13

1.2. Język PseudoPascal

nikt ich jeszcze nie podał (może nawet nikt ich nigdy nie poda). Odnotujmy na marginesie, że jednym z najciekawszych 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

W książce będą pojawiały się często zręby programów komputerowych pisanych

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 l. 2) 10gb l = O

3) 10gb ba = a

4) 10gb (xy) = logbx + 10gbY 5) 10gb (xa ) = alogbx

6) x10gbY = lOgiJX y

7) logbx

=

(logax)/(loga b) W teorii złożoności obliczeniowej mamy naj częś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ą naj częś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 L Ig nJ = k i lIg nl = k + 1. Można sprawdzić, że dla każdego n mamy

n � llgn1< 2n

oraz

n/2 < 2 l1gnJ � n.

Co więcej, pochodna (In x)' = lIx i (lg x)' = (lg e)/x .

--16

l. Wprowadzenie

1.3.2. Sumy szeregów Przy analizie algorytmów częs to pojawiają się sumy szeregów. Poniżej przypominamy najważniejsze z nich.

fi= 2 i=O

n(n+l)

( l A)

(1. 5)

f i2 i=O

n(n+0.5)(n+l) =

3

f � =2/J i=O

(J

( 1 .6)

l

n 2:2i=2n+l_l i=O

(1. 7) Ogólnie

n+l l n 2:Xi=� i=O x-l

( 1 .8)

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ą liczbę po lewej s tronie jako jeden bit w n-bitowym rozwinięciu binarnym. Czasami trudno jest podać dokładną postać sumy szeregu liczbowego. Jednakże, w większości przypadków, jest względnie łatwo oszacować tempo wzrostu takiej sumy. Weźmy dla przykładu funkcję f(n)

n i=O

= l>2 = 12 +22 + . . . +n2.

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.

17

1.3. Podstawy matematyczne

y

2

8

Rys. Widzimy, że

1.2.

n -1

n

n +1

Górne oszacowanie sumy

W podobny sposób dokonujemy dolnego oszacowania wartościj{n). a podstawie rysunku 1 .3 widzimy, że

y

n -1

n

Rys. 1 .3. Dolne oszacowanie sumy Zatem

3

czylij{n) ::::: n /3, co zgadza się ze wzorem (1. 5). W większości przypadków takie oszacowanie jest całkowicie wystarczające. Gdyby jednak było i naczej , naszą analizę możemy kontynuować, badając błąd przybliżenia e(n) = j{n) - n 3/3 . Ponieważj{n) spełnia warunekj{n) = j{n - l) + n2, więc

18

1. Wprowadzenie

e(n) =j{n) - n3/3 = j{n - l ) + n2 - n3/3 = e(n - 1 ) + (n - 1 )3/3 e(n - l ) + n - 1 /3 ,

+

n2 - n3/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 funk cj ą rosnącą, to

(1.9)

b

b

b+1

a- I

i=a

a

f f(x) dx :S; L f ( i) :S; ff(x) dx.

Podobnie, gdy j{-) jest funkcją malejącą, mamy

( 1 . 1 O)

6+ 1

b

b

a

i=a

a-I

f f(x)dx :s; L fC i) :S; ff(x)dx.

1.4. Symbole oszacowań asymptotycznych W teońi złożoności obliczeniowej kluczową kwestią jest tempo wzrostu liczby opera­ cji wykonywanych przez algorytm do momentu zakończenia o bliczeń 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 rozwij ać 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ą o ceną 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* mamyj{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" .

19

1.4. Symbole oszacowań asymptotycznych

Przykład 1 .2 2sin x = O(1og x) 2 x3 + sx + 7cos x = 0(x4) 2 1 1( 1 + x ) = 0( 1 )



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 f(x) j{x) = O(g(.x)), gdy lim = 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), czyli j{x) * O(g(x)) . Gdy f i g s ą funkcjami ciągłymi i różniczkowalnymi, t o dla obliczenia granicy możemy skorzystać z reguły de L'Hóspitala. Jeśli lim f(x) . x ....'"

=

lim g(x)

X->CO

= aJ,

f(x) to lim x.....'" g(x)

=

I (x) lim ' 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. 2 10g 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 pomij a 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, że j{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 f(x) j{x) = o(g(x)), gdy lirn x.....'" g(x)

=

O.

l. Wprowadzenie

20

Kiedy mówimy, że j{x) j est 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) j est 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 j est 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: I(x ) 00 I(x) = = lub l iro j{x) = Q(g(x» , jeśli liro .HOO g(x) . g(x) c > O . x ....oo 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 j ak 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) j est 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

2 X

= U>(x Fx )

2x - 2 = w(log x) w X • = (2 ). 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ę wzaj emnie 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» . 3X

.4.

21

Symbole oszacowań asymptotycznych

Zależność między funkcjami fi g można zwykle stwierdzić badając granicę ich ilorazu, ::nianowicie f(x) 00 . f(x) = ro(g(x» , jeśli lim . 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, że j(x) jest 0(g(x» , gdy istniej ą stałe C " C2 E R+ i Xo E R* :.akie, że c , g(x) l , to den) = a"j(n), gdzie jen) jest funkcją niema­ n I 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 )a + . . .+ 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 T(n)

=

{B(l)

jestjunkcja

o wartościach

{

'

aTen - 1) + den),

T(n) = 0(a) +

0(a n ),

T(n) = O(nan), O(nd(n» ,

gdy

gdy n 1 gdy n > 1 =

n

L a n-J d(j) ,

J= I

den) = O(an In" ),

gdy

den) = O(a n )

gdy

d(n) = m(a n )

E>l •

Przykład 1 .9 Rozważmy równanie T(n) = 3 T(n - l ) + n przy warunku T(O) = O. Oczywiście, den) = n 0(3n), zatem nasze rozwiązanie jest rzędu 0(3n). Jednakże interesuje nas również

i n l +" =

!.5.

27

Równanźa rekurencyjne nźejednorodne

rozwiązanie dokładne. Dlatego podstawiając do ( 1 . 1 8), otrzymujemy

T(n) = 3" ·

fi

}=I Y

.-\by obliczyć powyższą sumę, skorzystamy ze wzoru ( l .8). Obliczając pochodną obu stron 3>żsamości względem zmiennej x, otrzymujemy

l + 2x + 3X 2 + . . . + nx

n-I

-

_

(n + l)x" (x - l) - (x"+ I - l) x" (nx - n - l) + l _ (x - l) 2 (x - l) 2

Obecnie rrmożymy obustronnie przez x

x + 2x 2 + 3x 3 . . . +nx " _ -

x(x " (nx - n - l) + 1) (x - l) 2

Podstawiając teraz x = 1 /3 dostajemy

ti = }= y

3((1 / 3)" (n / 3 - n - I) + 1 4

I

-:zyli

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

pen)

=

r

gdy

den) = d

Bl n + Bo ,

gdy

B2 n + Bl n + Bo ,

gdy

den)

gdy

den)

2

Bd" ,

den)

= dn = =

dn 2

d" ,

l. Wprowadzenie

28

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 ojHanoi). 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 j est legenda głosząca, że w pewnym klasztorze w Hanoi mnisi buddyj scy 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) = A 2n

-

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.

29

Zadania

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

whiJe WlasnośćStopu(zagadka) do begin end 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 read(k); repeat if even(k) then k := kl2 else k := 3k + l ; until k = l end; 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 := 7. Czy program kończy się dla każdego k?

3'-

-

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

if n = l then przenieś(A ,C) else begin Hanoi(A ,C,B, n l ); przenieś(A, C); Hanoi(B,A, C, n l ) -

-

end end;

1. Wprowadzenie

30

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 then następny := false -

end;

write(k) end; 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 = ;=0

(

)

n(n + l) 2

2

1 .8. Które z poniższych zależności są prawdziwe?

a) b)

6 (x2 + 3x + 1 )3 = o(x ) ( .[; + 1)

2

= 0(1 )

g) h)

2 + sinx = .0( 1 ) cos x x

=

0(1)

c)

l/ e x = 0( 1 )

i)

s dt = O(ln x)

d)

� = 0(1 )

j)

t --i = 0(1)

e)

x3 (log( log x) / = o(x3 log x)

f)

x

�log x + l

4

j=1 } x

k)

2 ) = 8(x) j=1 x

=

8(log log x)

t

I)

fe-t2 dl = 0(1)

o

31

Zcdania

i .9. Które z symboli oszacowań asymptotycznych są przechodnie, tzn. jeśli f= O (g) i g = �

.

to czyf= OCh)?

i . 1 0. Poniższe funkcje ustaw w rosnącym porządku rzędów wzrostu dla dużych n, tzn.

w j e tak, że każda jest 0( ') od następnej . 2 .;;, elog "J , n 3 . 0 1 , 2 ,,1

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 =

Q{g) u O{g) i g "* D.(j) u O(j) .

. 13.

..

iech TI(n) = D.(f{n)) i Tz(n)

=

różniczkowalne j, g: R*-t R* takie, że

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 3

2�, 2n, .rn, logn, log logn,

'

, 1 7,

i gen) = ( l /3r. Odpowiedz, czy jen) = O{g(n)), = O(f{n)) itd. dla pozostałych symboli oszacowań asymptotycznych.

1 . 1 6. Dane są funkcje j{n) = n/3

:

( Jn ("2Jn (%J'OglJ

� , .rn lOg n, "3 lo

1 . 1 7. Czy funkcjaj{n) = 2

';;

logn, lecz wolniej niż

rośnie szybciej niż:

.r;; ?

.r;; , lecz wolniej niż n? n, lecz wolniej niż nZ? n2 , lecz wolniej niż

..r;; ?

..r;; , lecz wolniej niż 2"?

1. Wprowadzenie

32 O OI

1 . 1 8. Udowodnij , że In 2x = O(X . ). 1 .1 9 . Oszacuj rozwiązanie równania postaci

T(n) = 2 T(n/2) + log2n, przyjmując, że T(l) =

o.

1 .20. Oszacuj rozwiązanie równania postaci

T(n) = 2 T( L..JnJ ) + 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 ) + (-l r,

T(O) = O T(O) = l T(O) = 2.

1 .25. Zakładaj ąc, że T( l ) = l , oszacuj rozwiązanie poniższego równania rekurencyjnego

Wskazówka: Przyjmij , że U(n) = r en) 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)

a i' 2,

den) = en;

d)

a = 2,

den) = en.

33

Zada ia

n

Rozważ funkcję F(x) zdefiniowaną następująco: even(x) F x 2 F F(F(3x 1 )); �-dowodnij , żeRozważ F(x) kończy si ę dl a wszystki c h x. liczby całkowite postaci (2i )2k - l i zastosuj indukcję. Poniższe dwie procedury definiują tę samą funkcję./Cn). integer): integer; n 2 retum(2*n) retum(2*pl(n - -pl(n -2)) 2(n: integer): integer; n 3 retum(2*n) return(p2(n - 2(n -2) -p2(n -3)) Oszacuj złożoności obliktóra czeniobloweiczaprocedur i p2. ./Cn) w czasie 0(1). api s z procedurę wartość funkcji : Pokaż, że i s tni e je ni e skończona l i c zba procedur rekurencyjnych defini u jących funkcję ./Cn). Zadani e 1. 2 8 podaje przykład funkcji, dl a której i s tni e je ni e skończeni e wi e l e czającychobljejiczających. wartość. ogólności istnieją funkcje, dla których nialgeorytmów ma żadnychobliprocedur Pokaż, że równanie rekurencyjne gdy n no T(n) {0(1) T(n/p(n)) gdy n > no rozwiązania: T(n) O(logn), gdy n) 2 T(n) O(loglogn), gdy n) ..Jn T(n) 0(1),* gdy n) en T(n) log n, gdy n/logn. *n mówi, ile razy należy zastosować logarytm przy podstawie 2, aby Funkcja l o g sprowadzić wynik do wartości Dany jest liczący tysiące lat algorytm o nazwie integer): integer; 1 .27.

if

:=

then

div

else

:=

+

+1

--rskazówka:

1 .28.

functio n p l(n: begin if

else



then

1)

end;

function p begin if



then

l) + p

else

end;

pl

_

pO,

_

['waga!

W

1 .29.

=



'

+

1,

ma

=

pe

=

=

pe

=

pe

=

=

pen) =

=

['waga!

� 1.

1 30.

4

function zagadka(a,n: begin

zagadka

l. Wprowadzenie

34 if n = O then return( 1 ) else begin

half := zagadka(a,LnI2J); half:= half * half; if odd(n) then half:= half * a

return(haij) end; end;

a) Jaka jest jej złożoność obliczeniowa zagadki? b) Co oblicza zagadka, gdy n = 1 5? 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./{ Ixl ),

V3 =

min l./{x)l, V4 = I min./{x)l,

Vs = nlin l./{ Ixl )l, V6 = Inlin./{ Ixl )l, V7 = I nlin I./{x)l l, V8 = I nlin l./{ Ixl ) 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

� n

=

12 .

� (-1/ (6k) ! . (1 359 1 409 + 545 1 40 l 34 k) 1 (3k) !(k!) 3 · 640320 3k+ .5 to

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

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 sekwencji 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 operacji skończonej jest wykonywanie dowolnej operacji arytmetycznej na liczbach całkowitych, natomiast wykonywanie takich operacji na liczbach rzeczywistych niekoniecznie prowadzi do operacji 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 . Wjaki 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 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.

z

2. Wjaki 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. Wjaki 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 ile procesorów (arytmetycznych, komunikacyj nych) wymagać będzie dany pro-

2. Podstawy analizy algorytmów

36

gram oraz odpowi eedzinienam jest pytanikwesti e, czya wykonuje siaęsionę poprawni e iwdajenajleprecyzyjne wynimkii. Typowym zagadni zachowani al g orytmu pszym, średni najgorszym przypadku danych. Przez testowani e rozu­ mipróbaemywykonani tutaj zarówno uruchaminaaprzykładowych nie, jak i profilodanych wanie. dla stwierdzeni(ang. to a programu a , czy daje on po­ prawne wyniki i jeśla niie niiceh brak. - poprawi enipoprawności e go. Uruchamiprogramu anie może ewentual nie ewykazać obecność błędów, Dowód jest przet o bardzi j wart o ­ ściowy(ang.niż tysiące testów, gdyż gwarantuje poprawność dla wszystki ch danych. jest procesem wykonywani a poprawnego programu na pewnych intere­ sujących nas zestawach danych oraz mi e rzeni e czasu i pami ę ci zajmowanej przez ten pro­ gram. Oczywi ś ci e , pojawi a si ę tu pytani e , na jakich danych nal e ży testować programy. niszczegól niejszymnrozdzi ale zajmimyeuwagę my się naodpowi edzią naczynniki pytania, sformułowane w punk­ ciuwagę e przy ości , zwróci następujące które nal e ży brać pod numerycznym Jaki rozwie ąwizywani u każdego probl e mu. e l k ości są danymi rozwi ą zywanego zadani a ? Jaki e prze­ strzenirozważanego e danych i probl wyniekmu? ów (iJaka ch struktury, normy)paminajlęciepiowaej odpowi adają sensowi fizyczne­ jest złożoność struktur danych? b)wność metod rozwi Co wiązujących emy o złożoności oblem?iczeni owej naszego probl emu?jestJakaopty­ jest efekt y dany probl Czy rozpatrywany al g orytm czy miarabądźkosztunajprostsza jego wykonani a jestmetod? równa złożoności problemu? Jeśli nie, to czymalnjesty, tzn.to najtańsza ze znanych Czya jestzadaniodporny e nie jestna błędy zbyt wrażl iweenań? zaburzeni atodanych? Czyeje almetoda gorytmbardzi użytyejdostabirozwi ą zani zaokrągl Jeśl i ni e , czy i s tni lCzy na numeryczni e? Jakagwarantuje może byćuzyskani utrata dokładności oblaicoptymal zeń? nego? dany al g orytm e rozwi ą zani Jeśli niklas e, jakadanychjest aldokładność danej heurystyki dla nnajgorszego przypadku danych? Dlago­ jakich g orytm daje rozwi ą zania optymal e? Jaki jest naj mni e jszy trudny al rytmiPodstawowym cznie zestaw danych? celewartości m naszychjednego rozważańalgorytmu jest próbawzglusprawni enia eznanych algorytmów oraz i l o ści o wa ocena ę dem drugi go. Dl a tego, zanim omówi m y konkretne al g orytmy opi s ane w dal s zych rozdzi a łach, w kol e jnych punktach niwinmieyjszego rozdzi a łu przedstawi m y podstawy anal i z y al g orytmów. szczegól n ości omó­ następujące kryteria dotyczące algorytmów: poprawność, wymaganiaa pami czasowe, wymagani ęciowe, 4) optymalność czasowa, stabilnośćzwinumeryczna, prostota, 7) wrażliwość. ęzłość, 4. W jaki sposób testować programy realizujące algorytmy? Unlchamianie

debugging)

Profilowa­

nie

profiling)

W 3. W

a) Struktury danych.

mu

Efektywność.

c) Jakość numeryczna.

d) Dokładność.

W

l)

2)

3)

5) 6)

_ .

37

2 Poprawność algorytmów .

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­ cji. 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: dodaj my 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 j uż 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.

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 j est 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 j est tablicą n-elementową L[l..n], zaś x j est wartością poszukiwaną. Wyniki: index, tj . pozycja x w L (lub O, gdy nie występuje). begin

1. 2.

3. 4.

5.

index := l ; while index ::; n and L [index] index : = index + l end; if index > n then index := O

*x

do begin

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.

_ .

39

2 Poprawność algorytmów .

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 indukcji 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 indukcyj nego - 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 .

40

2. Podstawy analizy algorytmów

2.3. Złożoność czasowa algorytmów

2.3.1. Operacje podstawowe 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. Poszukuj emy 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 ope-

_ .3. Złożoność czasowa algorytmów

41

racj i podstawowej jest dość swobodny. W skrajnym przypadku można wybrać rozkazy maszynowe konkretnego komputera. Z drugiej strony j edno 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 j ak 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 kolej noś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

Rozmiar danych

l . Znalezienie x na liście nazwisk.

Liczba nazwisk na liście.

2. Mnożenie dwóch macierzy. 3. Porządkowanie liczb.

Liczba wierszy i kolumn.

4 . Trawersowanie grafu.

Liczba wierzchołków i liczba krawędzi.

5 . Rozwiązywanie układu równań liniowych.

Liczba równań i liczba niewiadomych.

Liczba kluczy do sortowania.

42

2. Podstawy analizy algorytmów

2.3.3. Pesymistyczna złożoność obliczeniowa W jaki sposób przedstawiamy wyniki naszej analizy? Najczęściej obliczamy liczbę operacji podstawowych wykonywanych w najgorszym 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} .

Naj częściej Wen) nie jest zbyt trudna do obliczenia, a zwłaszcza do oszacowania z gó­ ry. Znaj omość funkcji (2. 1 ) j est 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ść obliczeniowa 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 j edynie 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 j est 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ż okazuj e się, że grafy występujące w zastosowa­ niach są zwykle rzadkie, macierze rozrzedzone, listy częściowo uporządkowane itp. Jed­ nakże analiza średniego przypadku danych jest o wiele trudniej sza, gdyż wymaga określenia rozkładu danych wej ściowych, a naj częściej bywa tak, że rozkłady odpowiadaj ące rzeczy­ wistym problemom są matematycznie niezbadane. Dotychczas dokonano szczegółowej analizy jedynie naj prostszych algorytmów (zwłaszcza dla sortowania) i to przy założeniu najprostszego możliwego rozkładu prawdopodobieństwa, j akim 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 miej scu. 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 j ako

43

2.3. Złożoność czasowa algorytmów

( 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 funkcj a p(I) j est 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) opisuj e 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ępuj e w L. W obu przypadkach x j est 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 j ednakowym prawdopodobieństwem. Zbiór możliwych danych rozmiaru n możemy podzielić na klasy równoważności według tego, na j akiej pozycj i występuje x. Zatem wystarczy rozważyć n typów danych wejściowych. Dla i 1 , 2 , ... , n niech I; repre­ zentuj e przypadek, gdy x znajduj e 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 =



. � J.. 1. - J.. � L- 1 -

A( n ) - L- P (I; )t (I; ) - L;

=

_

;

1

=

1

_

n

_

n;

1

=

l n(n + l) n + l - -2 2 _

n

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 znajduj e 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 A(n) = L P(J; )t(J; ) �

=

n q q n q n(n + l) 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 j ak poprzednio A(n) (n + 1 )/2. Jeśli q czyli sprawdzanych j est około 3/4 pozycji na liście L . =

=

1 /2, to A(n)

=

(n

+

1 )/4 + n/2, •

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 j est 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 j ako podzbiór (lub klasa równoważ­ ności) wszystkich list i wartości x takich, że x występuje na określonym miej scu (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żą o d konkretnego algorytmu, a nie o d problemu. D l a 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 j est 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: Cij

n

= L aik · bkj k=!

dla l

ś"

ij ś" n.

Algorytm 2.2 begin for i := l to n do for j := l to n do begin

cij := O; for k :=

l

to n do cij := cij + aik

* bkj

end end;

Operacja podstawowa: Mnożenie liczb zmiennoprzecinkowych. Analiza: Aby obliczyć jeden element macierzy, należy wykonać n mnożeń. Macierz C ma n2 elementów, więc

A(n) = Wen) = n3 •



2.4. Złożoność pamięciowa

45

Dla innych algorytmów przej ście od analizy naj gorszego przypadku danych do analizy średniej liczby działań powoduj e 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 2 zamiany par sąsiednich wykonuje w naj gorszym i średnim przypadku O(n ) 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 I) w oczekiwanym czasie O(n) , 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 j uż 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 j uż 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 j est często używany w odniesieniu do algorytmów sortowania, o których mówi się też, że działają in situ.

I) Przy pewnych dodatkowych założeniach.

46

2. Podstawy analizy algorytmów

Mówiąc o liczbie komórek, nie precyzujemy rozmiaru jednej komórki, tzn. długości słowa wyrażonej w bitach. Tym niemniej czytelnik może przyj ąć, że komórka jest 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 A 2 osiągający 2 złożoność czasową O(n ). 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 =

=

47

2.4. Złożoność pamięciowa

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 danych istotnych, to minimalna liczba działań dwuargumentowych wynosi

n

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.

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­ iech

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­

a i b można zapisać w jednym bajcie za po­ b. Do odkodowania informacji służą wówczas dwie instrukcje:

kowanej na bitach. Podobnie dwie cyfiy dziesiętne mocą liczby n = 1 0a +

a := n div 1 0; b := n mod 1 0; W ten sposób osiągamy oszczędność 50%, co ma istotne znaczenie, gdy takich liczb jest bardzo dużo.

4. Strategie przydziału pamięci. Czasami ilość dostępnej pamięci nie jest tak ważna j ak -posób jej wykorzystania. Do optymalizacj i przydziału pamięci stosuje się takie techniki, jak dynamiczny przydział pamięci, rekordy zmiennej długości, odzyskiwanie pamięci

i dzielenie pamięci. Poniżej zilustrujemy tę ostatnią technikę.

48

2. Podstawy analizy algorytmów

Przykład 2.5 Jeżeli mamy dwie macierze symetryczne A i B o rozmiarach n x n, przy czym obie maj ą zera na głównej przekątnej , to możemy przechowywać tylko macierz trójkątną każdej z nich. Możemy zatem pozwolić, aby obie tablice dzieliły przestrzeń macierzy kwadratowej C[ l . . .n], której jeden z rogów wyglądałby następuj ąco: �

0

4-

__ _ __ __

, 1,,]... � A,,[2.:... __

A[3, 1 ]

+-

__

B [� I� ,2]� � -+

__

B� ,3� [ I� ] __4B[2,3]

__ _

0

4-_

__ _ __ __

A [3 ,2]

O

�--���--�----��---+---[4:... , 1.:!..,2....!. f--- A!:...].+- A..!:... A [4,3] [4...:. ... ] __

__

__

I� ,4]� B �[� -+ B[2,4]

__ __

__

__

4-_

B[3,4] 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 2 takiego licznika możemy zwiększyć do 2 n- 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);

49

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; Można

m(m

-

udowodnić,

że

wariancj a

licznika

po

m tyknięciach zegara wynosi

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 j est 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ę podstaową 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 , j ak i te, które nie zostały j eszcze 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ść naj gorszego 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 j est optymalny (dla naj gorszego przypad­ AU). Jeśli nie, to być może istniej e lepszy algorytm lub lepsze oszacowanie dolne. Oczywi-

50

2. Podstawy analizy algorytmów

ście, analiza danego algorytmu daje górne oszacowanie liczby kroków wymaganych do rozwiązania problemu, a twierdzenie, o którym mowa w punkcie 2, daje dolne oszacowanie. Poniżej podamy przykłady problemów, dla których znane są algorytmy optymalne, j ak i problemów, dla których wciąż istnieje luka pomiędzy oboma oszacowaniami.

Przykład 2.6 Problem: Znaj dowanie 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 znajduj e maksimum. Algorytm 2.3

Dane: L, n, gdzie L j est 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 end;

:=

L [index]

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 istniej e 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ą naj większymi. Ale żeby stwierdzić, że j akiś element nie jest maksymalny, trzeba go porównać z przynajmniej j ednym 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 j est poszukiwanym dolnym oszacowaniem i na tej podstawie wnioskujemy, że algorytm 2.3 j est 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 j eden 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, odej mowania, mnożenia i dzielenia na elementach macierzy.

51

2. 6. Dokładność numeryczna algorytmów Operacja podstawowa: Mnożenie. Oszacowanie górne: Jak wiadomo, zwykły algorytm wykonuje

3

n

mnożeń, zatem

n

3

j est oszacowaniem z góry. 2 Oszacowanie dolne: Jak wiadomo, złożoność pamięciowa wynosi 2n , więc Q(n2) mnożeń jest niezbędnych. Wniosek: Nie ma możliwości stwierdzenia na tej podstawie, czy algorytm klasyczny j est 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 2,3 76 naj lepszy znany algorytm mnożenia dwóch macierzy kwadratowych wykonuje około n 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 naj gorszego 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 j est naj lepsza możliwa. Jeśli nie, to musimy szukać jeszcze lepszych algorytmów albo j eszcze lepszych oszacowań. Podobną metodą badamy wymagania pamięciowe problemu. Czy możemy znaleźć al­ gorytm, który jest dla j akiegoś problemu optymalny zarówno z punktu widzenia zapotrze­ bowania na czas, jak i na przestrzeń? Odpowiedź brzmi: czasami. Jak wiadomo, dla niektó­ rych problemów istnieje tzw. kompromis przestrzenno-czasowy (ang. trade-ojJ between space and time), to znaczy można uzyskać obniżenie złożoności czasowej problemu kosz­ tem wzrostu zapotrzebowania na pamięć i na odwrót.

2.6. Dokładność numeryczna algorytmów Jak wiadomo, liczby rzeczywiste są reprezentowane w komputerze przez skończoną liczbę cyfr ich rozwinięć binarnych. Jakie są konsekwencje przybliżonej reprezentacji ta­ kich liczb?

2.6.1. Zadania źle uwarunkowane Niech rd(x) oznacza wartość liczby x w jej reprezentacj i zmiennoprzecinkowej . ówczas

rd(x) = x(l + E), I

E

I ,.::; TI

�dzie t jest długością mantysy. Zatem liczby rzeczywiste są na ogół reprezentowane niedo­ kładnie, ale z błędem względnym nie większym niż TI• Rozwiązując numerycznie takie zadanie, musimy zdawać sobie sprawę z tego, że zamiast danych dokładnych 1 = (dJ, d2 , .. , :J.) dysponujemy tylko ich reprezentacjami l' = (rd(d,), rd(d2), ... , rd(dn)). Ta niewielka zmiana danych może jednak powodować duże zmiany względne rozwiązania. Mówiąc nie.

52

2. Podstawy analizy algorytmów

formalnie, jeśli niewielkie względne zmiany danych zadania powoduj ą duże względne zmiany jego rozwiązania, to zadanie takie nazywamy źle uwarunkowanym (ang.

ill-conditioned). Fakt, czy dane zadanie j est 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 2 wyznaczania miejsc zerowych trójmianu kwadratowego X - 2px + q dla p *- 0, q *- 0, p - q > ° jest bardzo źle uwarunkowane, gdy i '" q, natomiast bardzo dobrze uwarunkowane, 2 gdy p » 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 , 1 47 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)

2) 3) 4)

a = jl( 1 5 ,4 1/3)

� = jl(5, 1 47 - a) =

Y

= jl(4, 1 27/3)

5, 1 37 0,0 1 0 1 ,376

8 =jl( 1 ,374 - y) = -0,002

= -5, y / = jl(�/8) l gdzie j O 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­ 5)

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 j est zły numerycznie. Metoda, którą użyliśmy do wyznaczenia wartości y, j est 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ń.

53

2. 6. Dokładność numeryczna algorytmów

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 j est 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ć silniej szą 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 j est niestabilny (ang. unstable). Zatem algorytm j est stabilny, gdy w trakcie j ego 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:

2 A l(a,b) = a

_

2

b

A 2(a,b) = (a - b)(a + b) Przy realizacji pierwszego z nich w arytmetyce jl otrzymuj emy

2 2 jl(a - b ) = (a * a( l + E l ) - b * b( l + E 2))(( l + E3 ) = 2 2 2 2 2 2 = (a - b )( 1 + (a El - b E 2)/(a - b ))( l + E3 ) = 2 2 = (a - b )( l + 8), gdzie

0= a 2

?

- E l - b2 E 2 2

?

a - b-

(1 + E 3 ) + E 3 , 2

zaś I Ei I � T1 (i = 1 , 2, 3). Jeśli a jest odpowiednio bliskie b , a E l i E2 mają przeciwne znaki, to błąd względny 8 wyniku otrzymanego algorytmem A l 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 + E 3 ) = (a2 - b2)( 1 + 8), gdzie przy czym E4 j est sumą powstałą z dodawania odpowiednich błędów iloczynowych powy­ • żej , zatem błąd względny I 8 1 < 4-T1•

54

2. Podstawy analizy algorytmów Przytoczony wyżej przykład pokazuje, jak istotnie różne własności numeryczne mogą

mieć algorytmy równoważne w sensie klasycznej arytmetyki. Pokazuje również, że utrata dokładności wyniku wcale nie musi wiązać się z wielką liczbą zaokrągleń w trakcie obliczeń.

2.7. Prostota algorytmów Bardzo często najprostszy i naj krótszy algorytm rozwiązywania problemu nie j est naj­ bardziej efektywny. Mimo to, prostota i zwięzłość algorytmu jest własnością bardzo pożą­ daną w praktyce. Prostsze programy są bowiem łatwiej sze do analizy i weryfikacj i, łatwiej się j e 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 j est ł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 j akiś model formalny, zwykle potwierdzony doświadczeniami praktycznymi, próbuje się szacować trudność programu, czas potrzebny na j ego 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): objętość programu (ang. program volume): stopień trudności (ang. program difficulty): poziom programu (ang. program level): wysiłek programisty (ang. programmer effort): oczekiwana liczba błędów (ang. expected number oj errors):

m = mi + m2 v ml log2(nl + n2) d = 0.5mln/n2 I = l /d e = vII b v/3000 =

=

55

2. 7. Prostota algorytmów Przykład 2.10 Jako przykład rozważymy algorytm 2 . 1 . Mamy tutaj : begin... end

2 3

while ... do ::; and

:7; +

l

3 if...then

> więc liczba operatorów równa się ni = 1 0, mi = l S , oraz

index n L [index] x

6 2 2

°

dlatego n2 = 6 i m 2 = 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 okazuj e 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 j est 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 -

2. Podstawy analizy algorytmów

56

sterowania G. Formalnie, liczba ta j est 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 j eżeli liczba cyklomatyczna grafu programu przekracza 1 0, to należy podzielić go na moduły spełniające warunek y(G) < 1 0 .

Przykład 2.1 1

Poniższy graf G reprezentuj e 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ę

57

2.8. Wrażliwość algorytmów

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) Definicj a 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 E Dn ' Wówczas

ben) = dev(Xn)

(2.6) 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 )

lED"

-

2 ave(Xn» p(I)

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 . 1 2 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 naj lepszym 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

17 Ł ( -) �[

var(X ) =

=

n

;=\

t{1; ) -

n+l

2

2

n(n + I)(2n + l) 6

n

(n + 1)(2n + l ) 6

l

l n

n

n

'- =-

Ł ;=\

( -) i-

n+l 2

2

=

2(n + l) n(n + l) n(n + l) 2 + 2 2 4

)

=

2. Podstawy analizy algorytmów

58 Obecnie mamy S(n) ""

�n 2 /1 2

"" O . 2 9n = O(n). Zatem wszystkie cztery funkcj e 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 funkcj a 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ą funkcj e 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 funkcj i 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 . =

=

=

=

=

59

2.9. Programowanie a złożoność obliczeniowa

------

Tabela 2 . 1

Przykłady

Typ funkcji

Klasa funkcji stała

subliniowa

r-------� wielomianowa

I I

polilogarytmiczna

0{1oglog n). 0{1og2n)

liniowa

0(n). 0(n(1 +1/n)"}

quasi-liniowa

0(n·log n). 0(n-loglog n)

kwadratowa superwielomianowa

niewielomianowa

I

wykładnicza superwykladnicza

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 obliczeń dla c 1 0 ms i n 1 00

Algorytm

=

=

Symbol A, A2 A3 A4 As

Autor [rok) Kuratowski (1 930) Goldstein (1 963) Lempel et al . (1 967) Hopcroft-Tarjan (1971) Hopcroft-Ta�an (1 974)

Złożoność cn6 cn3 cn2 cnlog2n cn

Rozmiar analizowanego grafu w przypadku udostępnienia komputera na okres minuty

godziny

325 lat

4

8

2.8 godzin

18

71

1 00 sekund

77

600

7 sekund

643

24 673

sekunda

6 000

36 . 1 04

1

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 A 5 nie spowalnia ich w istotny sposób, z wyjątkiem małych wartości n. Oznacza to, ż e dla dużych rozmiarów danych algorytmy wolniejsze niż O(nlog n) są często niepraktyczne.

60

2. Podstawy analizy algorytmów

Obecnie przyjmijmy, że nasz zestaw algorytmów został uzupełniony algorytmem Ao o złożoności 8(2 n) . 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 3 8(n ) wynoszą zaledwie 3 2% 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(2 o.2 88n) , 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. 3 Program o złożoności 0(n ) był wykonywany na superkomputerze CRAY- L Eksperymen­ 3 talnie stwierdzono, że jego złożoność wynosi 3n nanosekund dla danych rozmiaru n. Konkuren­ cyjny wobec niego algorytm liniowy został wykonany na komputerze osobistym IBM PCIAT 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 > 1 000. Tabela 2.3

Maksymalny rozmiar zagadnienia

Algorytm

przed wzrostem prędkości mc

po 10-krotnym wzroście prędkości mc

8(2n)

no n1

no + 3.3 1 .46n1

3 8(n ) 8(n2)

n2

2.1 5n2 3.1 6n3

symbol

złożoność

Ao A1 A2 A3 A4 As

0(n6 )

8(nlog n) 8(n)

n3 n4 ns

1 0n4

dla

1 0ns

n4 »1

------ - ----

61

2. 9. Programowanie a złożoność obliczeniowa

Tabela 2.4

n

Cr�-1

10

3 1ls

30 ms

3 ms

300 ms

100 1 000

3n ns

IBM PC/A T

3s

3s

10 000

49 min

1 000 000

95 lat

3 000 OOOn ns

30 s 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, j eż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 ak n + . . . + a l n + aO < ak n + . . . + al n + aon = (ao + a 1 + . . . + ak ) n k = cn k . 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 proporcj onalnoś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ą j ednostkę 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 funkcja jwystę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.

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

Zamiast i := 2 *j;

Lepiej i := j + j;

2.

x := 3 2.7/5;

x := .2* 3 2.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

Lepiej

tghx : = (exp(x)-exp(-x))/(exp(x) +

expx := exp(x);

exp(-x));

expodwr := 11expx; 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ę j uż 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 rozwij ać takie pętle. Na przykład

l.

Zamiast for i := l to 3 do A [i] := A [i] + i;

Lepiej A [ l ] := A [l]

+

l;

A [2] := A [2] + 2; A [ 3] := A [3 ] + 3 ;

2.

for i := l to n do write(A [i)); for j := p to n do write(BU));

for i := l to n do begin write (A [i]); if i � P then write(B[i]) end;

63

2. 9. Programowanie a złożoność obliczeniowa

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 j est 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ń j ako 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 forj := l to 10 do

Lepiej forj := l to 1 0 do for i := l to 1 00 do

A [i,j] := i m odj;

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]

i := i + l ;

Lepiej '*

A [n + l ] := t; t do

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 j est 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ść, funkcj onalność 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 obliczeniowej i odstępstwa Jest oczywiste, że powinniśmy stosować takie algorytmy, które mają najniższą możliwą zło­ żoność obliczeniową. Jest to ogólna reguła, od której są liczne odstępstwa. W jakich sytuacjach złożoność czasowa nie jest decydującym czynnikiem przemawiającym za implementacją danego algorytmu? Jedną taką sytuację już poznaliśmy i sformułujemy jąjako punkt l . 1 . Gdy program będzie używany niewiele razy, to koszt napisania programu i jego uru­ chomienia zdominuje pozostałe koszty. W tym przypadku wybieramy algorytm, który jest naj prostszy do implementacj i.

64

2. Podstawy analizy algorytmów 2. Jeśli program będzie wykonywany na ,,małych" danych, to rząd złożoności może nie

być tak istotny jak wielkość współczynnika proporcjonalności . Co znaczy "mały" rozmiar danych, zależy od konkretnej sytuacj i . Są takie algorytmy, j ak np. algorytm Schonhage i Strassena dla mnożenia liczb całkowitych, które są asymptotycznie naj szybsze 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ę j akiś 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 j est powszechnie stosowana w praktyce, pomimo że znane są algo­ rytmy wielomianowe dla tego problemu, np. algorytm elipsoidalny Chaczij ana (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 probabilistyczne 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ż łatwiej szy w implementacj i równoległej niż j ego deterministyczny od­ JJowiednik. Znanych j est 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ć j ako 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

65

2. 10. Algorytmy probabilistyczne

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.

2.

A lgorytmy 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. A lgorytmy 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 j

:=

:=

l to

m

do begin

liczba losowa ze zbioru { 1 , . . , n } ; .

66

2. Podstawy analizy algorytmów if A U] = 'a' then return (j) end; return (jailure) end;

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

m =

I log(1h) I log(26/25)

l •

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 daj e 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

67

Zadania

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 FindAnyLas Vegas(A , n); begin v

: = FindAnyMonteCarlo(A, n, I logn/log( 2 6/2 5)l);

if v =f.failure then return (v) ;

return (FindAnyDeterministic(A , n))

end; 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 uznaj e SIę algorytm Euklidesa do obliczania największego wspólnego dzielnika liczb i oraz). function Euklides(i,j: integer): integer; begin while i "* j do if i > j then i : = i -j elsej := j - i; retum(i)

end; 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 j est 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] .

2. Podstawy analizy algorytmów

68 begin

i := l ; while i ::; n do begin q i] := A [i] + B[i] ; i := i + l end end; 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 naj większej war­ tości w wektorze L.

begin

i := 2; max :=

L[I] ; while i :::; n do begin if L[i] > max then max i := i + l

:=

L [i] ;

end end;

2.4. Należy obliczyć wartość następuj ącej sumy:

1 + +1 +2+ + 1 +2+3+ + l + 2 + 3 + . . .+ n.

2

Napisz trzy wersje programu rozwiązujące to zadanie za pomocą odpowiednio: 0(n ), 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 + a _,xn-' + . . + a,x + ao · n .

begin

p := ao ; xpower := l ; for i := l to n do begin xpower := x*xpower; p := p + a;*xpower end end;

69

Zadania a) Ile mnożeń trzeba wykonać w naj gorszym przypadku? A ile dodawań? b) Ile mnożeń wykonuj e 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; for i : = d to g do suma := suma + A [i] ; x := max(x, suma) end; i odpowiedz na następujące pytania: a) Jaki jest efekt działania powyższego kodu? b) Jaka j est 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 n := i l ;

:=

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 for i := l to sqr(n) do begin } := l ; while } < sqrt(n) do } := } +} end end; 2 . 1 0. Oszacuj złożoność obliczeniową procedury zagadka.

procedure zagadka(n: integer); begin for i := l to sqr(n) do begin k := l ; 1 := l ;

70

2. Podstawy analizy algorytmów

end

while l < n do begin k := k + 2; l := 1 + k end

end; 2 . 1 1 . Oszacuj złożoność obliczeniową procedury zagadka.

procedure zagadka(n: integer); begin 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 end; 2 . 1 2. Oszacuj złożoność obliczeniową procedury zagadka.

procedure zagadka(n: integer); begin 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 end; 2. 13. Oszacuj złożoność obliczeniową procedury zagadka z dołu i z góry.

procedure zagadka(n: integer); begin for i := l to n l do for} : = i + l to n do for k := l to} do; -

end; 2 . 1 4. 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 miej scu (to znaczy wymagać 0(1) dodatkowej pamięci).

2 . 1 5. Potęgę x59 można obliczyć za pomocą poniższej procedury:

procedure x59; var x,y,x2,x4,-'C 8,.:cl 6,x32 : real; begin read(x);

71

Zadania x2 := x*x; x4 := x2*x2; x8 := x4*x4; x 1 6 := x8*x8; x32 := x 1 6*x 1 6; y

:= x*x2;

y

:= y*x8;

y

:= y*x 1 6;

y

:= y*x32 ;

write(y)

end; Zminimalizuj liczbę zmiennych w tym programie.

2 . 1 6. Dla procedury x5 9 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 j est procedura rekurencyjna:

procedure razy(x,y: integer): integer; begin if n = l then return(x*y) else begin 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, Y I ); c : = razy(x2, Y2); return(b*2n + (a-b-c)*2n/2 + c) end end; 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 . 1 8. Pokaż, że klasyczny algorytm obliczania pierwiastków XI i X2 równania kwadratowe­

2 go x - 2px + q = O o współczynnikach p, q ze zbioru D = {(P, q): p *- O, q *- O,

według wzorów:

XI := P + sqrt(p*p q), X2 := P - sqrt(p*p - q) -

i

-

q > O}

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

zmienności

n,

..r;;

i 2000n. Podaj przedziały

w których te algorytmy są naj szybsze.

2.22. Dla pewnego problemu dane są dwa konkurencyjne algorytmy Al i A 2 o złożoności odpowiednio cn i dn2 . Pomiary czasów wykonania tych algorytmów dały następujące wyniki:

�r Algorytm A1 A2

1 024 1 28 "lS 1 6 11S

2048 256 11S 64 11S

Czy to prawda, że: a) podana informacja jest sprzeczna, bo algorytm O(n) musi być lepszy od O(n2)? b) c) d) e)

Al wygrywa z A 2 jedynie dla n < 1 6? A l zacznie wygrywać z A 2, gdy n przekroczy 4096? A l zacznie wygrywać z A2, gdy n przekroczy 8 1 92? 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? 25 a) Istnieją stałe C I i C2 takie, że dla wszystkich n czas działania A jest krótszy niż c l n , + C2 sekund.

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ż­ 4 szy niż n2, sekund. e) Dla każdego n istnieje zestaw danych rozmiaru n, dla którego czas działania A jest dłuż­ szy niż n2 ,6 sekund. 2.25. Wyznacz pesymistyczną wrażliwość procedury Euklides z zadania 2 . 1 . 2.26. Rozważ procedurę Hanoi z zadania l A. Podaj jej wrażliwość pesymistyczną �(n) i

oczekiwaną ben).

3. PODSTAWOWE STRUKTURY DANYCH Od wyboru właściwej struktury danych może zależeć wiele: złożoność obliczeniowa programu, możność jego łatwej modyfikacji, czytelność algorytmu, a nawet satysfakcja programisty. Zacytujmy raz jeszcze, że ALGORYTMY + STRUKTURY DANYCH = PROGRAMY. W rozdziale tym rozważamy podstawowe struktury danych, takie jak tablica, lista, zbiór, a zwłaszcza graf. Dla każdej z nich omawiamy metody implementacj i, ich zło­ żoność pamięciową oraz czas dostępu do odpowiedniej informacj i. Będziemy zakładać, że elementy wchodzące w skład rozważanych struktur danych pochodzą z pewnego niepustego UlllwersUill.

3.1. Tablice 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, deklaracj a 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 operacj a 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;

75

3. 1. Tablice

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 2 bądź znalezienie maksymalnego elementu obecnie wymagają czasu 0(n ). 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 inicjalizacji wirtualnej



OgóLnie, aby sprawdzić, czy 11i] ma j uż ustaloną wartość początkową, sprawdzamy wpierw, czy l :
View more...

Comments

Copyright ©2017 KUPDF Inc.
SUPPORT KUPDF