Tematy, które można by poruszyć 1. Algorytm Euklidesa 2. Algorytmy kompresji (np. algorytm kompresji filmów wideo MPEG-4) 3. Algorytmy sztucznej inteligencji 4. Algorytmy kryptograficzne 5. Algorytmy genetyczne 6. Algorytmy kwantowe 7. Algorytmy przeszukiwania drzew (rekurencja, możliwość graficznej wizualizacji) 8. Mistrz szachowy kontra maszyna (algorytm) 9. Co to są kodeki (od kompresor-dekompresor, ang. Codec) 10. Varia ALGORYTMY WOKÓŁ NAS Dlaczego na zajęciach z informatyki będziemy poznawać tajniki tworzenia i działania algorytmów? Jakie znaczenie ma nauka o algorytmach dla informatyki? Czy algorytmy występują tylko przy programowaniu komputerów? Czy natrafiamy na nie w innych dziedzinach życia? Czy potrafimy je tam dostrzegać? Zapewne samo słowo algorytm jest Ci obce. Pochodzi ono od nazwiska perskiego matematyka, Muhammada ibn Musa al-Chorezmi, który żył na przełomie VIII i IX wieku i większość życia spędził w Bagdadzie. Jednak przykłady algorytmów, zwłaszcza w matematyce, sięgają czasów starożytnych. Jednym z najbardziej znanych jest tzw. algorytm Euklidesa znajdowania największego wspólnego dzielnika dwóch liczb naturalnych (o którym powiemy szerzej w podrozdz. 8.6). Pomyślisz teraz - znowu ta matematyka... Otóż nie! Obiecujemy, że naszą wędrówkę po ciekawym świecie algorytmów rozpoczniemy od... gotowania zupy. Będziemy też zajmować się wyborami przewodniczącego klasy, a nawet strzelaniem rzutów karnych podczas meczów pucharowych! Dopiero pod koniec rozdziału - tylko dla miłośników matematyki - przygotowaliśmy przykłady algorytmów w matematyce. Czym jest ów tajemniczy algorytm - zapytasz? Najogólniej mówiąc, algorytm to szczegółowy przepis opisujący działania, czynności, które powinny być wykonane przez jakieś urządzenie lub przez człowieka, aby osiągnąć zamierzony cel. Oczywiście, algorytmy występują w informatyce, gdyż każdy program komputerowy działa według jakiegoś algorytmu. Języki programowania są tak zbudowane, aby ułatwić zapisywanie algorytmów. Inną ważną dziedziną, w której stykamy się z algorytmami, jest prawo. Często przepisy prawa mają tak naprawdę charakter algorytmiczny, chociaż prawnicy zamiast słowa algorytm używają określenia procedury prawne. Niektóre z algorytmów w Kodeksie Cywilnym są niezwykle skomplikowane i tylko najlepsi prawnicy potrafią je skutecznie wykorzystywać. Zauważ też, że wszelkie ordynacje wyborcze (prezydencka, sejmowa, gminna itp.) można potraktować, jako lepszy lub gorszy algorytm. Postaramy się przekonać Cię, że algorytmy występują w naszym świecie nader często. 8.1 Algorytmiczne gotowanie zupy Wyobraź sobie, że jesteś w kuchni i masz przygotować zupę. Gotowanie zupy to procedura, w której ze składników, za pomocą różnych przyrządów, wykonując określone czynności otrzymujesz końcowy wynik, czyli w naszym przypadku zupę. Ale jest tu jeszcze jeden niesłychanie ważny czynnik, potrzebny zwłaszcza dla tych, którzy nie mają jeszcze doświadczenia w gotowaniu. To oczywiście przepis na zupę z torebki. W naszym przykładzie przepis jest właśnie algorytmem gotowania zupy. Przyglądnijmy się dokładniej takiemu przykładowemu algorytmowi - przepisowi. Algorytm 1 (1) Zawartość torebki wymieszać w 3/4 litra zimnej wody. (2) Mieszając, doprowadzić do wrzenia. (3) Zmniejszyć ogień. (4) Gotować na małym ogniu ok. 10 minut. (5) Odstawić i przecedzić mieszaninę. (6) Stop - zupa jest gotowa. Rys. 8.1 Widzimy zatem, że algorytm zadaje czynności, które składają się na cały proces (w tym przypadku jest to proces gotowania zupy). Rozważmy kolejny przykład. Przypuśćmy, że dysponujemy listą/tabelą zawierającą dane o uczniach Twej szkoły. Wśród tych danych znajdują się m.in. nazwisko, imię oraz wzrost (podany w centymetrach). Naszym zadaniem jest znalezienie ucznia, który ma największy wzrost w szkole. Oczywiście, jeśli uczęszczasz do niewielkiego gimnazjum, prawdopodobnie wystarczy krótkie przeglądnięcie listy i będziesz znać odpowiedź. Jednakże w przypadku, gdy lista będzie długa (np. będzie na niej tysiąc pozycji), musimy zastosować ściśle określoną, systematyczną procedurę, aby uporać się z problemem. A oto jak można by słownie opisać algorytm, który będzie znajdował osobę o największym wzroście w gimnazjum. 1 2 3 4 5 850 Adamczyk Bogusławski Czapska Dobosz Dudzik Zwijacz Marek Roman Anna Jan Jacek Paulina 154 167 148 162 163 159 Algorytm 2 Dane: tabela zawierająca nazwiska (i imiona) oraz wzrost (w cm). (1) Zapamiętaj „na boku" nazwisko i imię pierwszego ucznia z listy oraz jego wzrost. (2) Weź kolejnego ucznia z listy. (3) Jeżeli jego wzrost jest większy od aktualnie zapamiętanego, to zapamiętaj nowe nazwisko (i imię) oraz wzrost tego ucznia w miejsce dotychczas pamiętanego. (4) Jeżeli dalej na liście są nazwiska, to wróć do punktu (2). (5) Podaj jako wynik ostatniego zapamiętanego ucznia i jego wzrost i zakończ algorytm. Ćwiczenie 8.1 Jak zachowa się algorytm 2, gdy w szkole jest kilku uczniów o największym wzroście? Który z nich będzie wypisany (pierwszy, ostatni, a może któryś ze środka)? Zauważ, że modyfikacja algorytmu tak, aby podawał on wszystkich uczniów, którzy mają ten największy wzrost nie będzie taka prosta. Wynika to stąd, że nie można uczniów o największym wzroście wypisywać na bieżąco w trakcie przebiegania listy. Aby znać ten największy wzrost musimy najpierw całą listę przeglądnąć do końca. PODSUMOWANIE Algorytm jest to precyzyjny i niezawodny przepis, który pozwala w „mechaniczny" sposób osiągnąć określony cel. Poszczególne instrukcje w tym przepisie muszą być ściśle określone, tak aby wynik był zawsze jednoznaczny. W życiu często stosujemy algorytmy, jednak przeważnie czynimy to w sposób nieświadomy; mówimy wtedy o przepisach, czy też procedurach postępowania. Algorytmy mają ogromne znaczenie w informatyce, gdyż każdy program komputerowy działa według jakiegoś algorytmu lub zbioru algorytmów. Algorytmy odgrywają także ważną rolę w życiu społecznym, regulując wiele dziedzin życia. 8.2 W jakiej kolejności wykonywać instrukcje? Rys 8.2 Wiesz już, że algorytmy składają się ze starannie dobranych instrukcji (poleceń). Dotychczas nie zastanawialiśmy się dokładniej nad kolejnością, w jakiej są one wykonywane. Zasadniczo kolejność ta jest określona przez zapis danego algorytmu w postaci kolejnych instrukcji. Niektóre z algorytmów składają się z pewnej listy instrukcji, które mają być bezwarunkowo wykonane w kolejności, w jakiej występują w zapisie danego algorytmu. Żartobliwy przykład takiego algorytmu oraz efekt po zmianie kolejności instrukcji przedstawiono na rys. 8.2. Takie algorytmy nazywamy algorytmami liniowymi. Nazwa ma sugerować, że kolejne kroki tego algorytmu następują jeden po drugim „w linii prostej" bez możliwości „zawracania" lub „rozgałęziania”. Zauważ, że nasz przepis gotowania zupy z torebki jest właśnie takim prostym algorytmem liniowym. Podać jakiś przykład Ćwiczenie 8.2 Podaj przykłady algorytmów liniowych, jakie znasz z codziennego doświadczenia. Algorytmy liniowe są bardzo proste i rzadko kiedy występują w praktyce w postaci „czystej". Zauważ, że taki algorytm ma dwie podstawowe słabości: • Nieco żartobliwie można powiedzieć, że jest on „bezmyślny", gdyż nie może podejmować żadnych decyzji. Algorytm liniowy w sposób wręcz nudny wykonuje czynność za czynnością i wszystkie kroki z góry można przewidzieć. • Nie jest odporny na błędne czy wyjątkowe sytuacje, które mogą się pojawić w trakcie jego wykonywania (w przypadku obliczeń, na przykład, czymś takim jest dzielenie przez zero). Z doświadczenia wiesz zapewne, że kolejność wykonywania różnych czynności nie jest zawsze taka sama i zależy od zachodzenia lub niezachodzenia różnych warunków. Szczególnie widoczne jest to w niektórych regulacjach prawnych (np. dotyczących dziedziczenia spadku w przypadku braku testamentu). W naszym podręczniku, dla ilustracji, odwołamy się do przepisów prawa cywilnego regulujących odpowiedzialność za sprzedaż towaru z wadami (prawnicy mówią tu o odpowiedzialności z tytułu rękojmii za wady). Oto najprostsza sytuacja w ujęciu algorytmicznym: Algorytm 3 (odpowiedzialność za wady) (1) W momencie, w którym sprzedano Ci towar z wadami, powstaje odpowiedzialność sprzedawcy za wady. (2) Jeżeli kupujący powiadomił sprzedawcę w ciągu miesiąca o pojawieniu się wady, wówczas idź do punktu (5), w przeciwnym razie idź do (3). (3) Jeżeli sprzedawca podstępnie/świadomie zataił wadę, idź do punktu (5), w przeciwnym razie idź do (4). (4) Wygasa odpowiedzialność sprzedawcy za wady - zakończ algorytm. (5) Domagaj się przysługujących Ci uprawnień (wymiany towaru na towar wolny od wad lub naprawy towaru, obniżenia ceny i zwrotu nadpłaty itp.) - zakończ algorytm. Zauważyłeś, że cechą charakterystyczną powyższego algorytmu jest występowanie wielu zwrotów: jeżeli „coś zachodzi", to „zrób ..." W algorytmie 3 jest pewien mechanizm sterujący kolejnością, według której instrukcje będą się wykonywały. Są to tzw. instrukcje warunkowe, które „popychają" algorytm w tym lub w innym kierunku w zależności od sytuacji, czyli od tego, jakie warunki są spełnione, a jakie nie. Przykład Ćwiczenie 3 Czy potrafisz wskazać (sam bądź z pomocą nauczyciela) inne procedury algorytmiczne występujące w prawie polskim (np. wynikający z Konstytucji tryb uchwalania aktów prawnych przez parlament). Czasami takie miejscach w algorytmie, gdzie występują warunki, określamy jako rozgałęzienia warunkowe. Ogólnie rozgałęzienia warunkowe mają jedną z dwóch postaci: (1) jeśli W, to wykonaj czynność A (2) jeśli W, to wykonaj czynność A, w przeciwnym razie wykonaj czynność B (tutaj W oznacza pewien warunek). Sytuację taką można bardzo obrazowo wyrazić w postaci następującego rysunku W A B Strzałki pokazują kierunek, w którym należy się poruszać wykonując algorytm. Wyobraź sobie, że jesteś w miejscu oznaczonym górną strzałką i wchodzisz do bloku z literą W oznaczającą warunek. Wejście jest jedno, ale wyjścia są dwa. Tak, należy podjąć decyzję. Algorytm zaczyna jakby myśleć! W zależności od tego czy warunek jest spełniony czy nie wejdziemy do bloku A lub B. Teraz też zaczynać już rozumieć dlaczego nstrukcje warunkowe czasami nazywa się rozgałęzieniami. W przepisach kucharskich natrafiamy często na tego typu zwroty: sprawdź, czy makaron jest miękki, jeśli nie, to gotuj jeszcze chwilę albo wedle życzenia podawać z bitą śmietaną. Oczywiście, są to instrukcje warunkowe, które sterują wykonywaniem algorytmu. Ćwiczenie 4 Podaj kilka przykładów instrukcji warunkowych, które mogą się pojawić w algorytmach. Aby jeszcze dokładniej poznać instrukcje warunkowe, zajmijmy się algorytmem, który ma za zadanie uporządkować trzy dowolne liczby. Jest to prosty przypadek pewnego ogólniejszego zadania algorytmicznego, zwanego w informatyce sortowaniem. Innym określeniem, którego można by tu użyć, jest porządkowanie. Wyobraź sobie, że masz bardzo długą listę pracowników (np. 5 tys. osób). Na liście tej obok imienia i nazwiska znajduje się też informacja o zarobkach. W pewnych sytuacjach wygodnie byłoby mieć tę listę uporządkowaną (posortowaną) według wysokości zarobków. Trzeba więc będzie taką listę poddać procesowi sortowania. Zrealizuje to oczywiście odpowiedni algorytm. Tak więc danymi wejściowymi zadania sortowania jest nieuporządkowana lista elementów. Zadaniem algorytmu sortowania jest przedstawienie tej listy w postaci uporządkowanej (posortowanej). Do tematu sortowania w ogólnej wersji jeszcze będziemy wracać, tutaj tylko sygnalizujemy sam problem. Teraz zgodnie z zapowiedzią omówimy algorytm sortowania dwóch, a potem trzech liczb. Algorytm 4 (porządkowanie dwóch liczb) Dane: a, b (dwie liczby). (1) Jeżeli a<b, to wypisz (a, b), w przeciwnym razie wypisz (b, a). Ten algorytm jest naprawdę prosty. Ale robi to, czego chcemy – sortuje listę dwuelementową Okazuje się jednak, że przypadek sortowania trzech liczb podobną metodą daje algorytm znacznie bardziej skomplikowany. Algorytm 4 (porządkowanie trzech liczb) Dane: a, b, c (trzy liczby). (1) Jeżeli a b, to idź do (2), w przeciwnym razie idź do (4). (2) Jeżeli c a, to wypisz (c,a,b) i zakończ algorytm, w przeciwnym razie idź do (3). (3) Jeżeli c b, to wypisz (a,c,b) i zakończ algorytm, w przeciwnym razie wypisz (a,b,c) i zakończ algorytm. (4) Jeżeli c b, to wypisz (c,b,a) i zakończ algorytm, w przeciwnym razie idź do (5). (5) Jeżeli a c, to wypisz (b,a,c) i zakończ algorytm, w przeciwnym razie wypisz (b,c,a). (6) Zakończ algorytm. Cóż, algorytm ten wydaje się nieco skomplikowany. Składa się on właściwie z samych instrukcji warunkowych (jeżeli..., to.., w przeciwnym razie...). Rzeczywiście, zawiłość tego algorytmu wynika właśnie z występowaniem wielu rozgałęzień warunkowych, gdzie algorytm jakby podejmuje decyzję, co robić dalej. Dlatego trudno jest śledzić taki algorytm, gdyż może on „pójść różnymi ścieżkami" w zależności od tego jakie były liczby a, b, c. Niemniej jednak algorytm ten nie jest trudny. Jego działanie polega właściwie na systematycznym sprawdzaniu (porównywaniu) relacji większości pomiędzy liczbami a, b, c. Ćwiczenie 5 Aby lepiej zrozumieć ten algorytm, spróbuj go sam wykonać dla przykładowych trójek liczb, np. (1,2,3), (3,1,2), (5,-1,0), czyli zachować się jak komputer, który wykonuje program oparty na algorytmie 4. Ćwiczenie 6 Postaraj się wymyślić jakiś algorytm porządkowania dwóch liczb. Po zapoznaniu się z algorytmem porządkowania trzech liczb spróbuj napisać ten algorytm „z pamięci". Oczywiście, nie musi to być wierna kopia - masz pewną swobodę w jego zapisaniu. PODSUMOWANIE AIgorytm liniowy to bardzo prosty typ algorytmu, który ma postać listy instrukcji, wykonywanych bezwarunkowo zgodnie z kolejnością, w której występują. Bardziej złożone algorytmy zawierają instrukcje warunkowe, które sterują kolejnością wykonywania algorytmu. Innymi słowy, instrukcje warunkowe mówią, co dalej wykonywać, a czego nie wykonywać. Instrukcje warunkowe (rozgałęzienia warunkowe) mają postać: jeżeli W, to wykonaj czynność A lub jeżeli W, to wykonaj czynność A, w przeciwnym razie wykonaj czynność B. Sortowanie (inaczej porządkowanie) to algorytm, który z listy nieuporządkowanych danych tworzy listę uporządkowaną. Kryteria porządkowania mogą być różne, np. porządek arytmetyczny (dla liczb), alfabetyczny (dla nazw lub tekstów - jak w książce telefonicznej). 8.3 Czy algorytm, który się wykonuje bardzo długo, można zapisać w kilku linijkach? Gdy dokładnie przyjrzymy się algorytmom dotychczas omawianym, zauważymy, że większość z nich zbudowane jest są one z dwóch podstawowych elementów: • zwykłego następstwa kolejnych kroków, • wyborów warunkowych. Jak Ci się wydaje, jakie są istotne ograniczenia takich algorytmów? Zauważ, że każdy fragment algorytmu zawierającego tylko zwykłe następstwo instrukcji oraz wybory warunkowe może wykonać się nie więcej niż jeden raz. A zatem taki algorytm nie może opisywać procesu, który będzie trwał dowolnie długo, w zależności od „rozmiaru" danych wejściowych dla algorytmu. Zastanówmy się nad następującym problemem. Komputer ma policzyć sumę zarobków wszystkich pracowników danej firmy. Mamy daną listę płac (imiona, nazwiska i zarobki w wierszach tabeli). Komputer musi zsumować wszystkie zarobki. Oto przykładowa realizacja: Algorytm 5 Dane: lista płac, liczba pracowników (oznaczona przez N). (1) Zapamiętaj płacę pierwszej osoby z listy. (2) Wykonuj N-1 razy poniższe dwie instrukcje: (2.1) Weź następną płacę z listy. (2.2) Dodaj pobraną płacę do zapamiętanej już wartości i wynik zapamiętaj. (3) Wypisz zapamiętaną wartość. Jak Ci się wydaje, co jest cechą charakterystyczną algorytmu 5? Zauważ, że ten krótki algorytm realizuje obliczenia, które mogą trwać coraz dłużej. Komputer, działający według programu opartego na algorytmie 5, może pracować dowolnie bardzo długo, gdy lista płac będzie coraz większa. Lista może się składać z 30 pozycji, jak i 30 tysięcy (czy nawet 30 milionów, gdyby istniały takie przedsiębiorstwa - kolosy)! A zatem krótki algorytm może opisywać bardzo długie procesy obliczeniowe. Kolejny przykład algorytmu o podobnej budowie będzie związany z następującym problemem: Jak znajdować w danym zbiorze pewien obiekt, który ma określoną cechę? Oto konkretne przykłady tego ogólnego problemu: • Czy wśród uczniów gimnazjum występuje, uczeń o nazwisku Skorupka? • Czy w bibliotece szkolnej znajduje się określona książka (np. Krzyżacy Henryka Sienkiewicza)? Ćwiczenie 8.7 Podaj podobnego typu przykłady, zaczerpnięte z codziennego doświadczenia. Jak zatem będzie wyglądał algorytm, który stanie się podstawą programu komputerowego do wyszukiwania uczniów o podanym nazwisku (w naszym przykładzie o nazwisku Skorupka)? Załóżmy, że komputer pamięta nazwiska uczniów w postaci jakiejś listy lub tabeli. Algorytm 6 Dane: lista nazwisk, liczba uczniów (oznaczona przez N). (1) Ustaw się na początku listy. (2) Wykonuj poniższe trzy instrukcje N razy. (2.1) Pobierz aktualne nazwisko z listy. (2.2) Jeżeli jest to nazwisko Skorupka, to wypisz: znalazłem Skorupkę. (2.3) Wskaż na następne nazwisko na liście. (3) Wypisz: „zakończyłem przegląd listy” i zakończ algorytm. Podobnie jak poprzednio, charakterystyczną cechą tego algorytmu jest instrukcja (2), która ogólnie mówi: wykonuj coś określoną ilość liczbę razy. Innymi słowy, fragment ten zawiera instrukcję powtarzania pewnych kroków. Informatycy używają tu terminu iteracja (od łacińskiego iteratio, czyli powtarzanie). Powtórzmy więc, że iterowanie polega na wielokrotnym powtarzaniu pewnych czynności. Dla algorytmu 6, podobnie jak algorytmu 5, danymi wejściowymi jest liczba elementów do przetworzenia, sam algorytm jest identyczny dla list o dowolnej długości. Jego zapis nie zależy od tego czy N=8 czy N=1000. W algorytmach 5 i 6 występuje najprostsza forma iteracji. Dokonajmy w algorytmie 6 zmian, aby otrzymać nieco ogólniejszą postać iteracji. Algorytm 7 Dane: lista nazwisk. (1) Ustaw się na początku listy. (2) Wykonuj poniższe trzy instrukcje aż do osiągnięcia końca listy: (2.1) Pobierz aktualne nazwisko z listy. (2.2) Jeżeli jest to nazwisko Skorupka, to wypisz: znalazłem Skorupkę. (2.3) Wskaż na następne nazwisko na liście. (3) Wypisz; zakończyłem przegląd listy i zakończ algorytm. Czy dostrzegłeś różnicę pomiędzy algorytmem 6 i algorytmem 7? Oczywiście chodzi o punkt (2), w którym pojawiło się wyrażenie: aż do końca listy. Jest to przykład tak zwanej iteracji warunkowej, która ma postać: wykonuj czynność Z, dopóki jest spełniony pewien warunek W lub w odwrotnej kolejności: dopóki jest spełniony pewien warunek W, wykonuj czynność Z. Przykładowo w przepisach kulinarnych iteracja warunkowa ukryta jest w poleceniach typu: ugniataj ciasto, aż będzie dostatecznie miękkie. Tak więc, charakterystyczne jest tutaj występowanie specjalnych warunków, które „mówią" algorytmowi, w jakiej sytuacji ma zakończyć działanie. Kolejny przykład algorytmu z iteracja jest opisem bardzo prostej gry w odgadywanie liczby. Załóżmy, że komputer (one to potrafią!) wylosował dowolną liczbę naturalną od 1 do 1000. Twoim zadaniem jest odgadnięcie tej liczby. Na każdą Twą propozycję komputer ma odpowiedzieć, czy zaproponowana liczba jest większa, czy też mniejsza od wylosowanej, i tak aż do trafienia w szukaną liczbę. Oczywiście, dobrze by było odgadnąć tę liczbę w możliwie jak najmniejszej ilości liczbie prób. Gdybyś grał z kolegą (kolega wymyślałby przypadkową liczbę), po wyjaśnieniu mu zasad naszej zgadywanki od razu można by przystąpić do zabawy. Natomiast komputer będzie potrzebował bardziej konkretnego przepisu – czyli algorytmu. Algorytm 8 (1) Wylosuj liczbę naturalną z przedziału 1...1000 i zapamiętaj ją. (2) Wykonuj instrukcje (2.1) - (2.3), aż zgadujący trafi w wylosowaną liczbę: (2.1) Poproś zgadującego o podanie liczby. (2.2) Jeżeli proponowana liczba jest mniejsza od wylosowanej, to wypisz komunikat: „szukana liczba jest większa od zaproponowanej”. (2.3) Jeżeli proponowana liczba jest większa od wylosowanej, to wypisz komunikat: „szukana liczba jest mniejsza od zaproponowanej”. (3) Liczba została odgadnięta - koniec algorytmu. Jak widzisz, jest to algorytm z iteracją warunkową. Warunkiem zakończenia jest podanie właściwej liczby. Program komputerowy oparty na algorytmie 8 zawiera elementy dialogu z grającym. Prosi o podanie liczby oraz wypisuje odpowiednie komunikaty. Szczegóły takich operacji nie są tu dla nas nazbyt istotne, gdyż zależą od typu komputera, zainstalowanego na nim systemu operacyjnego itp. Ważne jest natomiast to, że mamy możliwość komunikowania się z komputerem. Na ogół dane będziemy podawali przy pomocy klawiatury, a komunikaty odczytywali z ekranu monitora komputerowego. Czy zauważyłeś zaskakującą cechę tego algorytmu? Otóż nie jest wcale pewne, czy ten algorytm musi się kiedykolwiek zakończyć! Możemy sobie bowiem wyobrazić, że (niezbyt inteligentna) osoba będzie cały czas podawać (zgadywać) złe liczby, a wtedy instrukcja (2) będzie wykonywana „w nieskończoność". Możemy temu zapobiec w prosty sposób, często stosowany w podobnych sytuacjach. Aby ograniczyć liczbę wszystkich prób (zgadywań), czyli w tym przypadku liczbę wykonań się instrukcji (2), informatycy dodają tzw. licznik. Niech w naszym przypadku maksymalna liczba prób wynosi 7. Przyjrzyjmy się zmienionemu nieco algorytmowi. Algorytm 9 (1) Wylosuj liczbę naturalną z przedziału 1...1000 i zapamiętaj ją. (2) Przyjmij, że na początku licznik prób wynosi 1 (licznik=1). 211 (3) Wykonuj instrukcje (3.1) - (3.4), aż zgadujący trafi w wylosowaną liczbę lub licznik>7: (3.1) Poproś zgadującego o podanie liczby. (3.2) Jeżeli proponowana liczba jest mniejsza od wylosowanej, to wypisz komunikat, że szukana liczba jest większa od zaproponowanej. (3.3) Jeżeli proponowana liczba jest większa od wylosowanej, to wypisz komunikat, że szukana liczba jest mniejsza od zaproponowanej. (3.4) Jeżeli zgadujący nie trafił jeszcze w wylosowaną liczbę, to zwiększ licznik o 1 (tzn. za licznik przyjmij licznik +1). (4) Jeżeli licznik>7, to wypisz: wyczerpałeś liczbę prób i nie odgadłeś liczby, w przeciwnym razie wypisz: gratulacje! zgadłeś po k próbach (gdzie k- licznik). (5) Koniec. Teraz już widzisz, że ten algorytm na pewno się zakończy. Nawet gdybyśmy podawali nietrafne liczby, po siedmiu próbach iteracja z punktu (3) zakończy się ze względu na warunek: licznik>7. Zwróć uwagę, że warunek, kiedy zakończyć powtarzanie, jest bardziej złożony niż w poprzednich algorytmach: gdy zgadujący trafi w wylosowaną liczbę lub licznik przekroczy 7. W niektórych algorytmach warunki takie mogą być jeszcze bardziej skomplikowane i dlatego należy być ostrożnym przy ich formułowaniu, zwłaszcza gdy do opisu algorytmu używamy języka potocznego. Na szczęście programy komputerowe piszemy w specjalnych językach programowania (takich jak Pascal czy język C), które zapewniają niezbędną precyzję. Ponadto często w algorytmach pewne warunki są równoważne, mimo że nie jest to widoczne od razu. Weźmy dla przykładu dwa sposoby ujęcia warunku w punkcie (3) w algorytmie 9. I sposób: (3) Przerwij wykonywanie poniższych instrukcji, gdy zgadujący trafi w wylosowaną liczbę lub licznik>7. II sposób: (3) Przerwij wykonywanie poniższych instrukcji, gdy zgadujący trafi w wylosowaną liczbę lub gdy licznik= 8. Gdy zastanowisz się przez chwilę, z pewnością zauważysz, że oba te 212 warunki spełniają identyczną rolę w algorytmie 9. Wynika to stąd, że za każdym razem, gdy zgadujący nie trafi w wylosowaną liczbę w punkcie (3.4) zwiększamy licznik o 1. Przyjmuje on zatem wartości 1,2,3, 4, 5, 6, 7, 8. Widać, że pierwsze przekroczenie liczby 7 daje po prostu 8! Sytuację, w której chcemy uzyskać w informacji telefonicznej numer telefonu interesującej nas osoby lub instytucji, można ująć algorytmicznie, a warunek, kiedy zakończyć powtarzanie, komplikuje się jeszcze bardziej. Uzyskanie połączenia z informacją telefoniczną za pierwszym razem jest rzeczą trudną - wszyscy to znamy z własnego doświadczenia! Stąd też w naszym algorytmie pojawia się iteracja. Algorytm 10 (1) Podnosimy słuchawkę. (2) Wykręcamy numer 913. (3) Jeżeli nie uzyskaliśmy połączenia i nie znudziło się nam jeszcze, to powracamy do punktu (2), w przeciwnym razie przechodzimy do punktu (4). (4) Jeżeli uzyskaliśmy połączenie, to dowiadujemy się o numer telefonu interesującej nas osoby lub instytucji. (5) Koniec algorytmu. Oczywiście, użyty w tym algorytmie warunek: jeżeli nie znudziło się nam jeszcze jest wysoce nieprecyzyjny i może oznaczać dla każdego coś innego. Uparciuchy będą próbować dzwonić do informacji telefonicznej nawet dwadzieścia razy! Ktoś inny podda się już po dwóch próbach. Dokonajmy zmian w algorytmie 10 w taki sposób, aby próby dodzwonienia się do informacji telefonicznej ponawiane były najwyżej pięć razy: Algorytm 11 (1) Podnosimy słuchawkę. (2) Wykręcamy numer 913. (3) Jeżeli nie uzyskaliśmy połączenia i nie była to piąta próba, to powracamy do punktu (2), w przeciwnym razie przechodzimy do punktu (4). (4) Jeżeli uzyskaliśmy połączenie, to dowiadujemy się o numer telefonu interesującej nas osoby lub instytucji. (5) Koniec algorytmu. Teraz warunek, kiedy zakończyć powtarzanie, udało nam się wyrazić znacznie precyzyjniej. Po prostu albo uda nam się dodzwonić na informację telefoniczną (w co najwyżej pięciu próbach) i uzyskać numer telefonu, albo . zniechęceni rezygnujemy po pięciu próbach. Przy okazji, jaki jest Twój rekord nieskutecznych prób dodzwonienia się do informacji telefonicznej? Ćwiczenie 8 Podaj przykłady z życia codziennego, w których występuje iterecja (powtarzanie) jakiejś czynności. Jeżeli dobrze się zastanowisz, stwierdzisz, że wiele jest takich sytuacji. PODSUMOWANIE Algorytm iteracyjny zawiera instrukcje, które nakazują wielokrotne powtarzanie pewnych czynności. Iteracje występują w algorytmach w dwóch podstawowych odmianach: • iteracja z określoną liczbą powtórzeń: wykonuj czynność A dokładnie N razy, • iteracja warunkowa: wykonuj czynność A, dopóki jest spełniony warunek W. Algorytm iteracyjny może działać na danych o dowolnej długości (wielkości). Wokół nas - wszędzie tam, gdzie jakieś czynności powtarzane są wielokrotnie - możesz doszukać się algorytmów z iteracjami. 8.4 Skończoność - ważna własność algorytmów Niekiedy algorytm, który ma wykonać jakieś ważne zadanie, może zupełnie pobłądzić. Innymi słowy, będzie działać nie tak, jak zamierzaliśmy. Dla przykładu, pomimo poprawnych danych wejściowych nie będzie chciał się zatrzymać! Zamiast tego uparcie będzie wykonywał jakieś dziwne czynności. Oczywiście, algorytm mógł zostać przez nas niepoprawnie skonstruowany. Czy zawsze jednak stanowi to wystarczające wytłumaczenie? Ćwiczenie 8.9 Podaj przykłady algorytmów niepoprawnie skonstruowanych, które będą działać nie tak, jak zamierzaliśmy. Weźmy pod uwagę, dla przykładu, wykonywanie rzutów karnych, gdy w regulaminowym czasie mecz pucharowy nie został rozstrzygnięty. Algorytm 12 Dane wejściowe: dwie drużyny piłkarskie. (1) Wylosuj drużynę, która rozpocznie wykonywanie rzutów karnych. (2) Wykonuj rzuty karne na zmianę, aż któraś z drużyn uzyska przewagę (nie do odrobienia w pierwszej serii pięciu rzutów karnych) lub liczba prób wyniosła 5. (3) Jeżeli wynik pierwszej serii pięciu rzutów nie jest rozstrzygający, to idź do (4), w przeciwnym razie idź do punktu (5). (4) Wykonuj na zmianę po jednym rzucie karnym, aż któraś z drużyn uzyska przewagę, wtedy idź do punktu (5). (5) Koniec - wynik meczu jest rozstrzygnięty. Powyższy algorytm jest odbiciem regulaminu wykonywania rzutów karnych, które (w pewnych sytuacjach) mają wyłaniać zwycięzcę w rozgrywkach UEFA. Najpierw drużyny wykonują na zmianę pierwszą serię pięciu karnych. Gdy ta seria nie wyłoni zwycięzcy, wówczas drużyny strzelają na przemian po jednym rzucie karnym, aż któraś z nich uzyska przewagę. Czy nic Cię nie zaniepokoiło w tym algorytmie (regulaminie)? Przecież teoretycznie zawodnicy mogą strzelać rzuty karne bez końca! Wystarczy tylko, by po nie rozstrzygniętej pierwszej serii pięciu rzutów karnych obie drużyny uzyskiwały identyczne rezultaty: obaj zawodnicy wyznaczeni do kolejnego rzutu karnego albo strzelali bramkę, albo nie. Ta potencjalna nieskończoność algorytmu ukryta jest w punkcie (4). Oczywiście, w rzeczywistości nie ma praktycznie szans, aby strzelanie rzutów karnych nie wyłoniło w końcu zwycięzcy. Kibice piłkarscy mogą potwierdzić, że dotychczas w rozgrywkach pucharowych taka sytuacja nigdy się nie zdarzyła! Zauważ, że na skończoność algorytmów mają wpływ przede wszystkim iteracje, zwłaszcza warunkowe instrukcje iteracyjne (dopóki warunek jest spełniony, wykonuj coś). Instrukcje takie powinny zawierać prawidłowo określony warunek zakończenia. Jak przekonasz się wkrótce, znane są w informatyce zadziwiająco proste algorytmy, o których nie wiemy, czy (dla dowolnie wybranych danych wejściowych) zawsze zatrzymają się po skończonej liczbie kroków. Kolejne ćwiczenie dotyczy zagadnień praktycznych. Po raz kolejny odwołujemy się do procedur prawnych. Podać jakies informacje ia temat wyborów, może adres internetowy? Ćwiczenie 8.10 Dowiedz się, jak wygląda wybór burmistrza czy wójta w Twej miejscowości. Jest to ściśle określona procedura prawna (o charakterze algorytmicznym) opisana w Ustawie o Samorządzie Terytorialnym. Zwróć uwagę, że algorytm ten rozpoczyna się od lokalnych wyborów samorządowych (wybór rady gminy), następnie odbywa się zgłaszanie kandydatów w radzie gminy oraz głosowania. Czy może to trwać bez końca? Co się dzieje, gdy po kilku miesiącach rada gminy nie zdoła wybrać burmistrza (wójta)? Teraz nie trzeba Cię chyba przekonywać, że sprawne przeprowadzenie wyborów zależy od precyzji regulaminu (algorytmu). Zajmijmy się więc wyborem przewodniczącego klasy. Algorytm 13 (a raczej szkic algorytmu) Dane wejściowe: uczniowie Twojej klasy. Dane wyjściowe: przewodniczący klasy. (1) Zgłaszanie kandydatów. (2) Głosowanie (tajne). (3) Liczenie głosów. (4) Ogłoszenie wyników. (5) Jeżeli głosowanie nie wyłoniło zwycięzcy, powrót do (2). (6) Koniec - ogłoszenie ostatecznego wyniku. Zwróć uwagę, że jest to tylko szkic algorytmu. Wiele spraw należy jeszcze szczegółowo ustalić. Należy zadbać przede wszystkim o to, by wybory nie trwały bez końca. Będziecie musieli rozstrzygnąć, jak postępować w przypadku, gdy kilka osób otrzyma tę samą liczbę głosów, jak długo powtarzać głosowanie, kiedy głos jest ważny, na ilu kandydatów każdy uczeń może głosować. Następnym zadaniem, które stanie przed nami, jest przeprowadzenie rozgrywek szkolnych (np. w siatkówce). Zauważ, że najprostszy sposób („każdy z każdym") właściwie nie nadaje się już dla 10 drużyn. Należałoby wtedy rozegrać (10*9 )/2 = 45 meczów! Podobne kłopoty mają organizatorzy piłkarskich mistrzostw świata (w których uczestniczą 24 drużyny). Algorytm 14 (szkic) Dane wejściowe: pewna liczba drużyn. Dane wyjściowe: mistrz i wicemistrz. (1) Ustalenie liczby grup i sposobu przydziału drużyn do poszczególnych grup (na ogół odbywa się to poprzez losowanie). (2) Ustalenie, kto kwalifikuje się z danej grupy do następnego etapu rozgrywek. (3) Opis rozgrywek w drugim etapie (w szczególności - kto z kim ma grać). (4) Opis kolejnych etapów aż do rozgrywek półfinałowych i finałowych. (5) Koniec - mistrz i wicemistrz są wyłonieni. Ćwiczenie 8.11 Postaraj się doprecyzować szkic algorytmu 14, tak aby rzeczywiście można było na jego podstawie przeprowadzić rozgrywki. Czy wyłoniona w ten sposób drużyna wicemistrzowska wygrałaby z każdym, z wyjątkiem mistrza? Uwzględnij w regulaminie sytuację, w której dwie najlepsze drużyny znajdą się w wyniku losowania w tej samej grupie eliminacyjnej. Kolejny algorytm jest niezmiernie ciekawy. Choć wydaje się on niezbyt skomplikowany, do dziś nie wiadomo, czy zawsze musi się zatrzymać po skończonej liczbie kroków. (Mamy nadzieję, że potrafisz mnożyć i dzielić!). Algorytm 15 Dane wejściowe: liczba naturalna N. (1) Dopóki liczba N jest różna od 1, wykonuj: (1.1) Wypisz komunikat: to ja, algorytm, wciąż żyję!!! (1.2) Jeżeli liczba N jest parzysta, to podziel ją przez 2, wynik zapamiętaj jako nową wartość N i wróć do punktu (1), w przeciwnym razie idź do (1.3). (1.3) Pomnóż N przez 3 i dodaj 1, a wynik zapamiętaj jako nową wartość N i wróć do punktu (1). (2) Koniec algorytmu. Prześledźmy działanie tego algorytmu dla kilku przykładowych danych początkowych. Niech na początku A/=52. Wtedy kolejne wartości N będą następujące: 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1. STOP. Gdy na początku A/=9, wówczas kolejne wartości N będą następujące: 28, 14, 7, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1. STOP. Możesz spróbować samemu poeksperymentować z różnymi wartościami początkowymi N. Zapewne zawsze dojdziesz do końca w punkcie (2) i algorytm przestanie wypisywać radosny komunikat to ja algorytm, wciąż żyję!!! Wydaje się, że w pewnym momencie algorytm „umrze". Na pewno? To prawda, że do dzisiaj nikt nie natrafił na taką liczbę początkową N, dla której ten algorytm wykonywałby się w nieskończoność. Z drugiej strony, nikt też nie udowodnił, że ten prosty algorytm jest skończony. PODSUMOWANIE: Poprawnie zaprojektowany algorytm powinien zatrzymać się po skończonej liczbie kroków (chyba że w jakichś celach konstruujemy algorytm, który ma się nigdy nie skończyć). Problemy ze skończonością algorytmów pojawiają się przede wszystkim tam, gdzie występują instrukcje iteracyjne, zwłaszcza w postaci warunku: dopóki W, wykonuj A. W życiu społecznym (np. przy wyborze parlamentu, prezydenta) niezmiernie ważne jest, by pewne procedury (algorytmiczne) zawsze kończyły się po skończonej liczbie kroków, a nie trwały bez końca. Czasami, mimo pozornej prostoty algorytmu, trudno jest udowodnić, że dla wszystkich danych początkowych zatrzyma się po skończonej liczbie kroków. 8.5 Magiczne sztuczki, czyli co to jest rekurencja Słowo rekurencja zapewne nie jest Ci znane. Rekurencja jest pewną metodą konstruowania algorytmów. Choć wydaje się trochę dziwna, tajemnicza, często pozwala na przejrzyste i eleganckie rozwiązanie wielu problemów. Na czym zatem polega ogólna idea algorytmu rekurencyjnego? W pewnym momencie algorytm jest zatrzymywany i „proszony" o rozwiązanie takiego samego zadania, ale dla innych danych. Mówimy też, że algorytm rekurencyjny w trakcie wykonywania odwołuje się sam do siebie. Jest to trochę tajemnicze - przyznajcie sami! Rys. 8.3 Podobny rysunek rekurencyjny można znaleźć w znanej książce N. Wirtha Algorytmy + struktury danych = programy Kilka przykładów powinno jednak rozwiać Twoje wątpliwości. Oto pierwszy z nich. Wyobraź sobie, że mały Piotruś zrobił ogromny bałagan w pokoju. Mama poleciła mu, aby wszystkie porozrzucane zabawki powkładał do dużego pudła, w którym są normalnie przechowywane. Zadanie zatem brzmi: pozbieraj te zabawki i ułóż je w pudle. Jak sformułować algorytm, który będzie „sterował" Piotrusiem w trakcie sprzątania? Oczywiście Piotruś nie może wziąć wszystkich zabawek jednocześnie i sprawić, aby od razu znalazły się w pudle. Może natomiast wykonać nieco inne polecenie: weź jedną zabawkę i schowaj ją do pudła, a następnie posprzątaj to, co zastanie. Aby lepiej zobaczyć rekurencyjną naturę takiego postępowania, zapiszmy nasz algorytm. Algorytm 16 PIOTRUŚ SPRZĄTA ZABAWKI - wersja rekurencyjną Dane wejściowe: bałagan w pokoju. Dane wyjściowe: porządek w pokoju. (1) Jeśli jest bałagan, to weź jakąś zabawkę i włóż ją do pudła, w przeciwnym razie idź do (3). (2) PIOTRUŚ SPRZĄTA (pozostałe) ZABAWKI. (3) Koniec sprzątania. Zwróć uwagę, że algorytm 16 odwołuje się do siebie samego. Ma to miejsce w punkcie (2) - instrukcja stwierdza, aby jeszcze raz zastosować algorytm PIOTRUŚ SPRZĄTA ZABAWKI. Prześledźmy działanie algorytmu 16 w sytuacji, gdy Piotruś ma do posprzątania trzy zabawki: A, B, C. Krok 1. Instrukcja (1) Schowanie zabawki A do pudła. Krok 2. Instrukcja (2) PIOTRUŚ SPRZĄTA (pozostałe) ZABAWKI. Krok 3. Instrukcja (1) Schowanie zabawki B do pudła. Krok 4. Instrukcja (2) PIOTRUŚ SPRZĄTA (pozostałe) ZABAWKI. Krok 5. Instrukcja (1) Schowanie zabawki C do pudła. Krok 6. Instrukcja (2) PIOTRUŚ SPRZĄTA (pozostałe) ZABAWKI. Krok 7. Instrukcja (1) Nie ma bałaganu, więc idź do (3). Krok 8. Instrukcja (3) Koniec. Krok 9. Instrukcja (3) Koniec. Krok 10. Instrukcja (3) Koniec. Krok 11. Instrukcja (3) Koniec. Czy zauważyłeś, że powyższe zadanie dałoby się opisać algorytmem iteracyjnym? Algorytm 17 (Piotruś sprząta zabawki) - wersja iteracyjna Dane wejściowe: bałagan w pokoju. Dane wyjściowe: porządek w pokoju. (1) Jak długo jest bałagan, wykonuj poniższe kroki: (1.1) Weź zabawkę i włóż ją do pudła. (2) Koniec - pokój jest posprzątany. Mamy zatem rekurencyjny i iteracyjny algorytm. Oba rozwiązują to samo zadanie. Wydaje się, że algorytm iteracyjny jest prostszy. Dlaczego zatem wprowadzamy algorytmy rekurencyjne? Otóż okazuje się, że nie wszystkie problemy mają proste rozwiązania w postaci algorytmów iteracyjnych. Mogą natomiast mieć bardzo proste i eleganckie rozwiązania rekurencyjne. Rozważmy kolejny przykład. Załóżmy, że komputer wczytuje „z zewnątrz" (z klawiatury lub z pliku) jakiś ciąg znaków np., abcdxyz. Zadaniem programu komputerowego jest wypisanie (np. na monitorze) tych znaków, ale w odwrotnej kolejności, czyli zyxdcba. Oto algorytm rekurencyjny, który realizuje to zadanie. Algorytm 18 ODWRÓĆ CIĄG ZNAKÓW Dane wejściowy: ciąg znaków. Dane wyjściowe: odwrócony ciąg znaków. (1) Jeżeli ciąg znaków jest niepusty, to weź ostatni jego znak i ustaw na początku, w przeciwnym razie idź do (3). (2) ODWRÓĆ (pozostały) CIĄG ZNAKÓW. (3) Koniec. Postarajmy się wykonać ten algorytm dla przykładowego ciągu: abc. Uruchamiamy nasz algorytm ODWRÓĆ CIĄG ZNAKÓW dla ciągu abc. Krok (1) Instrukcja (1) Ustaw c na początku. Krok (2) Instrukcja (2) ODWRÓĆ (pozostały) CIĄG ZNAKÓW (tzn. ab). Krok (3) Instrukcja (1) Ustaw b na początku. Krok (4) Instrukcja (2) ODWRÓĆ (pozostały) CIĄG ZNAKÓW (składający się już tylko z jednej litery: a). Krok (5) Instrukcja (1) Ustaw a na początku. Krok (6) Instrukcja (2) ODWRÓĆ (pozostały) CIĄG ZNAKÓW (ale nie zawiera on już żadnej litery). Krok (7) Instrukcja (1) Ciąg jest pusty, więc idź do (3). Krok (8) Instrukcja (3) Koniec. Krok (9) Instrukcja (3) Koniec. Krok (10) Instrukcja (3) Koniec. Krok (11) Instrukcja (3) Koniec. W efekcie zastaną wypisane znaki: cba. Ćwiczenie 8.12 Czy algorytm 18 można wykorzystać w następującej sytuacji: mamy ustawionych w szeregu uczniów Twojej klasy; chcemy, by byli ustawieni w odwrotnej kolejności? Teraz nadszedł czas, abyś zapoznał się z trochę trudniejszym zadaniem chodzi o rozwiązanie pewnej łamigłówki. Wydaje się, że trudno byłoby ją rozwiązać nawet mądremu człowiekowi, gdyby nie zastosowanie odpowiedniego algorytmu rekurencyjnego. Z drugiej strony, zastosowanie algorytmu nie wymaga właściwie żadnego myślenia i polega na mechanicznym wykonaniu określonych czynności. Przykład ten znany jest jako problem Wież z Hanoi. Mamy trzy paliki wbite w podłoże i pewną liczbę krążków o różnych średnicach z otworami w środku. Krążki mogą być nakładane na pręty, tworząc w ten sposób „wieże". Załóżmy, że na pierwszym paliku mamy pewna ilość krążków ułożonych od największego do najmniejszego. Rys. 8.4 Patyczki i krążki tworzące „wieżę" Naszym zadaniem jest przeniesienie tych krążków na trzeci patyczek C przy pomocy patyczka B, ale musimy przestrzegać następujących reguł: • wolno przenosić tylko po jednym krążku, • nie wolno nigdy położyć większego krążka na mniejszym. Łamigłówka ta pojawiła się w Europie XIX wieku. Odwołuje się do legendy, pochodzącej z Tybetu, o mnichach przenoszących krążki w pewnym klasztorze, zgodnie z regułami opisanymi powyżej (przy czym krążków tych ma być dokładnie 64!). Z jednym krążkiem na początku nie ma problemu. Wystarczy po prostu przenieść ten krążek z patyczka A na C. Ćwiczenie 8.13 Rysując krążki na papierze (lub na tablicy), rozwiąż problem Wież z Hanoi dla N=2 i N=3 (gdzie N oznacza liczbę krążków). 222 Ćwiczenie 8.13 nie powinno sprawić Ci kłopotów. Jednak dla A/=4, A/=5 itd. (dla czterech, pięciu krążków) napotkasz już trudności. A jak postępowałbyś z 64 krążkami? Okazuje się, że algorytm rekurencyjny jest tu bardzo przydatny. Jak to w rekurencji bywa, rozwiązując zadanie dla N krążków, opieramy się na rozwiązaniu dla N-1 krążków. Zanim jednak zapoznamy się z ogólnym algorytmem rozwiązania, rozwiążemy go dla A/=4 krążków. (Zauważmy, że z trzema krążkami potrafimy sobie poradzić). Dla czterech krążków postępujemy następująco: DLA DOCIEKLIWYCH Algorytm 19 WIEŻE HANOI (dla A/=4) (1) Wykorzystując algorytm WIEŻE HANOI dla A/=3, przenosimy 3 górne krążki z patyczka A na B przy pomocy C. (2) Pozostały największy krążek z patyczka A przenosimy na patyczek C. (3) Wykorzystując algorytm WIEŻE HANOI dla A/=3, przenosimy 3 górne krążki z patyczka B na patyczek C przy pomocy patyczka A. Rys. 8.5 Kolejne etapy przenoszenia krążków Oto algorytm, który będzie rozwiązywał problem Wież z Hanoi w ogólnym przypadku. Składa się on z trzech głównych etapów i dwukrotnie odwołuje się do rozwiązania dla przypadku, gdy jest o jeden krążek mniej. Algorytm 20 WIEŻE HANOI (dla N krążków) Dane wejściowe: Trzy patyczki A, B, C. Na patyczku A jest N krążków. Dane wyjściowe: Trzy patyczki A, B, C. Na patyczku C jest N krążków. (1) Jeżeli A/=1, to przenieś krążek z A na C i zakończ, w przeciwnym razie idź do (2). (2) Wykorzystując algorytm WIEŻE HANOI (dla N-1 krążków), przenieś N-1 górnych krążków z patyczka A na B, używając C. (3) Pozostały największy krążek przenieś z patyczka A na C. 223 (4) Wykorzystując algorytm WIEŻE HANOI (dla N-1 krążków), przenieś N-1 krążków z patyczka B na C używając A. Algorytm 20 radzi sobie z przeniesieniem N krążków z patyczka A na C przy pomocy B. Jak on działa? Najpierw sprawdza, czy A/=1 i w tym przypadku przenosi po prostu ten jeden krążek z patyczka A na C. Jeżeli N>1, algorytm najpierw przenosi górne N-1 krążki z patyczka A na „pomocniczy" B. Potem bierze jedyny krążek pozostawiony na patyczku A (jest to największy krążek leżący na samym dole) i przenosi go na docelowe miejsce przeznaczenia, czyli na patyczek C. Potem znowu rekurencyjnie przenosi N-1 krążków (które chwilowo były „przechowywane" na patyczku B) na patyczek C. Zauważ, że w kolejnych krokach algorytmu rola patyczków A, B, C zmienia się! Oczywiście człowiekowi jest czasami trudno śledzić poszczególne kroki, natomiast komputery radzą sobie z tym wyśmienicie. Teraz już chyba lepiej rozumiesz, czym jest algorytm rekurencyjny. Najważniejszą cechą takiego algorytmu jest to, że w trakcie jego wykonywania w pewnym momencie algorytm taki odwołuje się sam do siebie. Jak zwykli mówić informatycy, algorytm rekurencyjny wywołuje samego siebie. Takie wywołanie musi nastąpić dla nieco innych danych (przeważnie jest to mniejszy zbiór danych), aby w końcu algorytm zatrzymał się. Tak samo jak dla przykładowych algorytmów z podrozdz. 8.4 (Skończoność - ważna cecha algorytmów) istotne jest, aby algorytm rekurencyjny się zatrzymał. Nietrudno jest natomiast podać przykłady algorytmów rekurencyjnych, które nigdy się nie kończą. Oto prosty przykład: Algorytm 21 MASZERUJ (może IDŹ?) (1) Zrób krok do przodu. (2) MASZERUJ. (3) Stop. Oczywiście widzisz, że jest to typowy algorytm rekurencyjny. W punkcie (2) mamy odwołanie się algorytmu MASZERUJ do siebie samego. Przyglądnij się jednak dokładniej temu algorytmowi. Czy coś Cię nie niepokoi? Czy algorytm ten zatrzyma się kiedykolwiek? Oczywiście, że nie. To nic, że w punkcie (3) czeka upragniona instrukcja Stop, skoro algorytm MASZERUJ nigdy do tej instrukcji nie dotrze. Będzie on w kółko robił „krok do przodu" i wywoływał siebie samego w nieskończoność. Bardzo ciekawym obszarem zastosowania algorytmów rekurencyjnych jest grafika. Wszędzie tam, gdzie występują obrazy graficzne złożone z pewnych wielokrotnie powtarzanych motywów, można dopatrzyć się rekurencji. Spróbujmy wygenerować spiralę, taką jak na rys. 8.3, przy pomocy algorytmu rekurencyjnego. Rys. 8.6 Spirala Aby łatwiej było nam opisać ten algorytm, wyobraź sobie, że mamy do dyspozycji jakieś urządzenie kreślące, którym możemy sterować przy pomocy kilku prostych poleceń. Wśród tych poleceń mamy: (I) ruch (x) - oznacza przesunięcie pisaka o x cm do przodu lub do tyłu w zależności od znaku liczby x, (II) obrót (a) - oznacza zmianę kierunku ruchu pisaka o kąt a. Obrót jest w kierunku przeciwnym do ruch wskazówek zegara, gdy kąt a jest dodatni. W algorytmie rysującym spiralę wykorzystamy te dwie instrukcje: ruch i obrót. Dodatkowo użyjemy zmiennej x, która na początku ma wartość 0,5 cm. Najwygodniej też będzie ustawić pisak na środku kartki oraz umówić się, że pierwszy ruch nastąpi w prawo. Algorytm 22 RYSUJ SPIRALĘ Dane wejściowe: czysta kartka papieru. Dane wyjściowe: rysunek spirali. (1) Wykonaj: ruch (o aktualną wartość x). (2) Wykonaj: obrót (o 90 stopni). (3) Do zmiennej x dodaj 0,5 cm. (4) Jeżeli x<5 cm, to RYSUJ SPIRALĘ. (5) Koniec. Postaraj się „wykonać" ten algorytm. Wystarczy coś do pisania i kartka papieru. Ustaw pisak na środku, a potem wykonuj krok za krokiem polecenia zawarte w algorytmie. Jeżeli będziesz uważny, algorytm bez trudu 225 posteruje Tobą tak, że narysujesz rozwijająca się spiralę. Jest to typowy algorytm rekurencyjny - w punkcie (4) odwołuje się sam do siebie! Zastanówmy się jeszcze nad możliwymi modyfikacjami algorytmu 22. Oczywiście w zależności od tego, jaka będzie początkowa wartość zmiennej x i jaka wartość ograniczająca wystąpi w punkcie (4), różne będą rozmiary spirali (w algorytmie 22 mamy: Jeżeli bok<5 cm, to...). Tak samo wpływ na narysowana spiralę będzie miała wartość „skoku", którą dodajemy w punkcie (3) (w algorytmie 22 jest to 0,5 cm). Dalsze modyfikacje mogą polegać na zmianie kąta, o który zmieniamy kierunek ruchu pisaka (w algorytmie 22 jest to 90 stopni). Zauważ, że algorytm ten zatrzyma się, gdyż warunek w punkcie (4) w pewnym momencie przestaje być prawdziwy, więc przechodzimy ostatecznie do punktu (5). Ćwiczenie 8.14 Zastanów się, ile razy wywołana zostanie instrukcja RYSUJ SPIRALĘ w punkcie (4). Odpowiedz na to samo pytanie, gdy początkowa wartość zmiennej x wynosi 2 cm, skok, o który ją zwiększamy w punkcie (3), wynosi 3 cm, a wartość ograniczająca x w punkcie (4) wynosi 15 cm. Ostatnim przykładem, który ma proste i przejrzyste rozwiązanie rekurencyjne, jest problem porządkowania, czyli sortowania (przypomnij sobie nasze rozważania w podrozdz. 8.2) Pamiętasz zapewne, że sortowanie ciągu liczb polega na ich uporządkowaniu (od najmniejszej do największej bądź odwrotnie). Z kolei sortowanie nazwisk na liście uczniów będzie polegało na uporządkowaniu alfabetycznym. W algorytmice skonstruowano wiele algorytmów, które wykonują sortowanie. Jednym z najlepszych jest tak zwany algorytm szybkiego sortowania. Idea jego działania jest niezmiernie prosta. W pierwszym etapie wyjściowy zbiór danych dzielimy na dwa mniejsze „worki", tak aby w pierwszym wszystkie liczby były mniejsze od wszystkich z drugiego. Potem tym samym algorytmem sortujemy oba mniejsze „worki". W ten sposób po połączeniu danych uporządkowanych z pierwszego i drugiego zbioru otrzymamy uporządkowany cały wyjściowy zbiór. Algorytm 23 SORTUJ Dane wejściowe: ciąg liczb. 226 Dane wyjściowe: uporządkowany ciąg liczb (od najmniejszej do największej). (1) Wybierz dowolną liczbę z ciągu. (2) Podziel wyjściowy ciąg na dwie części - dwa podciągi: pierwszy podciąg niech zawiera liczby mniejsze, a drugi podciąg - liczby większe bądź równe od liczby wybranej w punkcie (1). (3) SORTUJ pierwszy podciąg. (4) SORTUJ drugi podciąg. (5) Koniec - ciąg jest posortowany. Jak widzisz, jest to algorytm rekurencyjny. W punkcie (3) i (4) algorytm SORTUJ wywołuje siebie samego. PODSUMOWANIE: Algorytm rekurencyjny w trakcie wykonywania odwołuje się do samego siebie. Aby algorytm rekurencyjny mógł się zatrzymać, jego kolejne odwołania do siebie samego muszą zależeć od pewnego warunku, który zmienia się za każdy kolejnym odwołaniem. Dzięki algorytmom rekurencyjnym trudne problemy udaje się często rozwiązać w przejrzysty sposób. 8.6 Algorytmy w matematyce Być może zauważyłeś już, że wielokrotnie na lekcjach matematyki pojawiały się przykłady przepisów, które tak naprawdę można traktować jak algorytmy. Oczywiście słowo to nie było używane, ale to niczego nie zmienia. Na przykład przepis na tzw. pisemne dodawanie czy mnożenie liczb jest to w gruncie rzeczy opis algorytmu, który zapewnia otrzymanie sumy lub iloczynu danych liczb. Nieomal każdy potrafi stosować te algorytmy, choć - przyznajmy - niewielu je rozumie. Czy zastanawiałeś się nad tym, dlaczego pisemne dodawanie jest poprawnym przepisem? Rzeczywiście, okazuje się, że matematyka jest niewyczerpanym źródłem wielu algorytmów. Niektóre są dość banalne, a inne bardzo pomysłowe. Warto tu przypomnieć, że pierwsze komputery powstały na potrzeby matematycznych obliczeń. Wiele zagadnień, którymi zajmują się inżynierowie czy naukowcy, prowadzi bowiem do bardzo skomplikowanych, pracochłonnych obliczeń. Właściwie żaden człowiek ani grupa ludzi nie byliby w stanie ich wykonać. Nawet samo słowo komputer pochodzi od angielskiego czasownika compute oznaczającego: obliczać, wyliczać, liczyć. Przedstawimy teraz kilka algorytmów zaczerpniętych z matematyki. Algorytm Euklidesa Jest to jeden z najstarszych znanych algorytmów. Wielki matematyk starożytnej Grecji, Euklides, zamieścił go w swoim słynnym dziele pod tytułem Elementy. Było to ok. 300 roku p.n.e. Przypuszcza się, że algorytm ten był znany jeszcze wcześniej. Czego dotyczy ten algorytm? Jest to przepis na wyznaczanie tzw. największego wspólnego dzielnika dwóch liczb (w skrócie: NWD). Przypomnijmy, że największy wspólny dzielnik liczb naturalnych to największa liczba naturalna, przez którą można obie liczby podzielić bez reszty. Inaczej mówiąc: mając dane dwie liczby naturalne a i b, szukamy największej liczby naturalnej, która dzieli jednocześnie a i b. Nietrudno jest wymyślić następujący algorytm znajdowania NWD: Algorytm 24 Dane wejściowe: dwie liczby naturalne a i b. Dane wyjściowa: NWD (a,b). (1) Znajdź zbiór wszystkich dzielników liczby a. (2) Znajdź zbiór wszystkich dzielników liczby b. (3) Wybierz największą liczbę, która należy do obu zbiorów. (4) Stop. Oczywiście algorytm ten wynika wprost z definicji NWD. Jak widzisz, jest nie jest on zbyt pomysłowy. Na przykład jeżeli jedna liczba ma niewiele dzielników, a druga ma wiele dzielników, to wyznaczanie wszystkich dzielników może być zbędne. Konkretny przykład a=4, 6=180. Wtedy w punkcie (1) wyznaczamy: 1, 2, 4, a w punkcie (2): 2, 3, 5, 6, 9, 10, 12, 15, 18, 20, 30, 36, 45, 60, 90. Widać, że w (2) zupełnie niepotrzebnie wyznaczamy dzielniki 5, 6, 9, 10, 12, 15, 18, 20, 30, 36, 45, 60, 90, które są większe od a=4 i w związku z tym nie ma wśród nich NWD(a,ib). Można by starać się usprawniać „naiwny" algorytm 24, ale zostawmy to i zajmijmy się algorytmem Euklidesa. Zacznijmy od prześledzenia go na konkretnym przykładzie: a=3654, b=1365. Wykonujemy następujące operacje (dzielenie i branie reszty): 3654 dzielimy przez 1365, dostajemy resztę 924 1365 dzielimy przez 924, dostajemy resztę 441 924 dzielimy przez 441, dostajemy resztę 42 441 dzielimy przez 42, dostajemy resztę 21 42 dzielimy przez 21, dostajemy resztę 0 228 NWD(3654,1365)=21 Jak widzisz, idea tego algorytmu polega na dzieleniu przez kolejne reszty, aż uzyskamy resztę zero. Wtedy ostatnia niezerowa reszta to właśnie NWD. Zapiszmy ten algorytm bardziej formalnie: Algorytm 25 (algorytm Euklidesa wyznaczania NWD) Dane wejściowe: Dwie liczby naturalne a i b. Dane wyjściowe: NWD (a,b). (1) Niech m będzie mniejszą spośród liczb a i b oraz niech n będzie większą. (2) Jeśli m=0, to n jest szukanym NWD. Zakończ. (3) Oblicz pomocniczą resztę: r = reszta z dzielenia n przez m. (4) Za n podstaw m, a za m podstaw r. Wróć do (2). Ćwiczenie 8.15 Aby dobrze zrozumieć ten algorytm, postaraj się wykonać go dla kilku przykładowych par liczb naturalnych: a=48, 6=46; a=34, b=55 itp. Z pewnością zastanawiasz się, dlaczego ten algorytm rzeczywiście znajduje NWD. Trafne pytanie! Na razie zajmowaliśmy się tylko opisem samego algorytmu, a nie próbowaliśmy uzasadniać, że jest on poprawny, tzn. że w rezultacie jego działania osiągniemy zamierzony cel. Pozostawmy jednak dowód poprawności algorytmu Euklidesa nauczycielowi matematyki! Sprytny wybieg, nieprawdaż? Sprawdzanie, czy dana liczba jest liczbą pierwszą Kolejne przykłady algorytmów matematycznych będą znów związane z arytmetyką liczb naturalnych (tzn. nieujemnych liczb całkowitych). Podstawowym pojęciem jest tutaj liczba pierwsza. Przypomnijmy najpierw podstawowe definicje. Liczba naturalna jest liczbą pierwszą, gdy dzieli się tylko przez 1 i przez samą siebie. Przy czym przyjmujemy, że liczba 1 nie jest liczbą pierwszą. Jeżeli liczba naturalna ma dzielniki różne od 1 i od niej samej, to nazywamy ją liczbą złożoną. Liczbami pierwszymi są na przykład: 2, 3, 5, 7, 11, a liczbami złożonymi 6=2*3,15=3*5, 18=2*3*3. To są niezmiernie proste przykłady. Ale gdyby się ktoś zapytał, czy liczba 1243562713 jest liczbą pierwszą czy złożoną, wówczas trzeba się trochę napracować nad odpowiedzią. Oczywiście czasami nawet dla dużej liczby od razu widać, że jest złożona. Przykładowo, liczba 15423342 na pewno nie jest pierwsza, gdyż jest parzysta (dzieli się przez 2). Podobnie 229 65365115 dzieli się przez 5, więc jest liczbą złożoną. W ogólnym przypadku dla dużych liczb trzeba jednak stosować bardzo szybkie komputery i specjalne algorytmy, aby stwierdzić, czy dana liczba jest pierwsza czy złożona. Okazuje się, że liczby pierwsze mają duże znaczenie w kryptografii, czyli nauce zajmującej się szyfrowaniem i kodowaniem informacji. Takie szyfrowanie ma obecnie duże znaczenie w sieci Internet. Pewne dane, które przesyłane są w sieci, nie powinny być dostępne dla każdego. Przykładowo, mogą to być dane osobowe czy numery kart kredytowych. Okazuje się, że do szyfrowania potrzebne są duże liczby pierwsze. Dlatego obecnie tak wiele uwagi poświęca się znajdowaniu coraz większych liczb pierwszych. Jak zatem najprościej sprawdzić, czy dana liczba jest liczbą pierwszą? Oczywiście możemy brać wszystkie kolejne liczby począwszy od 2 do danej i sprawdzać, czy jest to dzielnik czy nie. Oto prosty algorytm: Algorytm 26 (najprostszy algorytm sprawdzania, czy liczba jest pierwsza) Dane wejściowe: N liczba naturalna różna od 1. (1) Niech k=2. (2) Dopóki (N nie dzieli się przez k), wykonuj: (2.1) Zwiększ k o 1 (czyli za k podstaw k+1). (3) Jeżeli k=N, to N jest liczbą pierwszą, w przeciwnym razie N jest liczbą złożoną. (4) Koniec. Ten algorytm naprawdę jest bardzo prosty. W instrukcji (2) wykonujemy dzielenia kolejno przez 2, 3, 4 itd. Jeżeli natrafimy na dzielnik, to przerywamy. Nasuwa się pytanie: Czy może się zdarzyć, że algorytm 26 nigdy się nie zatrzyma? Inaczej mówiąc, czy warunek: N nie dzieli się przez k będzie cały czas spełniony? Otóż, jak łatwo możesz zauważyć, algorytm zawsze się zatrzyma. W najgorszym bowiem przypadku dojdziemy w końcu z k do N (za każdym razem k jest zwiększane o 1, więc nie można „ominąć" liczby N), a wtedy k będzie dzielić N. Jeżeli w instrukcji (2) zatrzymamy się na N, to jest k będzie równe N, oznaczać to będzie, że wcześniej nie było żadnego dzielnika, a zatem mamy do czynienia z liczbą pierwszą. W przeciwnym razie N jest liczbą złożoną. Stąd na podstawie (3) otrzymujemy poprawną odpowiedź. Jak można by usprawnić ten algorytm? Pomyśl przez chwilę. Czy jest sens sprawdzać wszystkie wartości k=2,3,...,A/w poszukiwaniu dzielnika? Chyba jest Ci znany fakt, że liczba, która dzieli daną liczbę N, nie może być większa od N/2. Na przykład jeżeli A/=1200, to wszystkie możliwe dzielniki 230 liczby 1200 różne od niej samej nie mogą być większe od A//2=600. Tak więc proste usprawnienie naszego Algorytmu 26 może polegać na ograniczeniu się w instrukcji (2) do sprawdzania warunku: dopóki (N nie dzieli się przez k) i (kjest mniejsze bądź równe N/2). Taki nieco ulepszony algorytm będzie wyglądał tak: Algorytm 27 (udoskonalony algorytm sprawdzania, czy liczba jest pierwsza) Dane wejściowe: N liczba naturalna różna od 1. (1) Niech k=2. (2) Dopóki (N nie dzieli się przez k) i (k<N/2), wykonuj: (2.1) zwiększ k o 1 (czyli za k podstaw k+1). (3) Jeżeli k>N/2, to N jest liczbą pierwszą, w przeciwnym razie N jest liczbą złożoną. (4) Koniec. Chwili uwagi wymaga teraz tylko instrukcja (3), a w szczególności warunek: Jeżeli k>N/2, to.... Najlepiej sprawdzić ten algorytm na kilku przykładowych liczbach. Ćwiczenie 8.16 Wykonaj algorytm 27 dla kilku przykładowych danych: N=8, N=13, N=18. Co możemy powiedzieć o przewadze algorytmu 27 nad algorytmem 26? Widać, że w algorytmie 27 wykonujemy przeważnie o połowę mniej sprawdzeń w punkcie (2). W jednym przypadku k zmienia się maksymalnie do N, a w drugim do N/2. Można zatem powiedzieć, że algorytm 27 powinien być przeważnie dwa razy szybszy. Ale czy zawsze tak będzie? Ćwiczenie 8.17 Porównaj działanie algorytmu 26 i algorytmu 27 dla następujących danych: N=17, N=12, N=77. Czy potrafisz wysnuć jakieś ogólniejsze wnioski? Zauważmy przynajmniej jedno: Jeżeli liczba N jest liczbą pierwszą, to rzeczywiście instrukcja (2) w algorytmie 27 wykona się dwa razy szybciej niż 231 w algorytmie 26. W innych przypadkach, tzn. gdy N jest liczbą złożoną, tak już nie będzie. W skrajnym przypadku, gdy N jest parzysta, obie pętle wykonają się po jednym razie! Trudno, nasze usprawnienie okazało się niezbyt efektowne. Na szczęście, algorytm 27 można usprawnić o wiele bardziej, odwołując się do matematycznego twierdzenia o dzielnikach liczby naturalnej: Jeśli N jest liczbą złożoną, to ma dzielnik k spełniający warunek k*k< N. Przede wszystkim zastanówmy się, jak możemy wykorzystać to twierdzenie? Wynika z niego, że aby się przekonać, czy dana liczba jest złożona, wystarczy sprawdzać tylko takie liczby k, dla których k*k<N. Jeżeli zatem wśród tych liczb nie będzie dzielnika, mamy pewność, że dana liczba nie ma ich wcale (poza oczywiście 1 i N). To zaś oznacza, że jest liczbą pierwszą. Możemy zatem jeszcze raz ulepszyć algorytm sprawdzania, czy dana liczba jest pierwsza. Algorytm 28 (znacznie ulepszony algorytm sprawdzania, czy liczba jest pierwsza) Dane wejściowe: N liczba naturalna różna od 1. (1) Niech k=2. (2) Dopóki (/V nie dzieli się przez k) i (k*k</V), wykonuj: (2.1) zwiększ k o 1 (czyli za k podstaw k+1). (3) Jeżeli k*k>N, to N jest liczbą pierwszą, w przeciwnym razie N jest liczbą złożoną. (4) Koniec. Jak wygląda teraz porównanie tej wersji algorytmu z poprzednimi? Weźmy dla przykładu liczbę pierwszą AM0007 (dziesięć tysięcy siedem). Wtedy w algorytmie 26 instrukcja w punkcie (2) może wykonywać się około 10000 razy, a w algorytmie 28 tylko 100 razy (pierwiastek kwadratowy z 10000). A zatem widzisz, że teraz jest to istotne ulepszenie. Kolejny przykład będzie raczej ciekawostką. Nie dotyczy on jakiegoś problemu o praktycznym znaczeniu, niemniej jednak daje możliwość łatwego „eksperymentowania" z algorytmem i rodzi kilka interesujących pytań. Algorytm ten generuje pewien ciąg liczb. W szczególności ciekawy jest problem, czy algorytm w końcu zatrzyma się? Jest on podobny do algorytmu 15 z podrozdz. 8.4 {Skończoność - ważna własność algorytmów). Algorytm 29 Dane wejściowe: liczba naturalna n. Dane wyjściowe: pewien ciąg liczb naturalnych. (1) Weź daną liczbę n i wypisz ją. (2) Dopóki n>1 wykonuj poniższe instrukcje (2.1)-(2.2): (2.1) Jeżeli n jest liczbą parzystą, to za nową wartość n przyjmij n/2, w przeciwnym razie (czyli gdy n jest nieparzysta) za nową wartość n przyjmij n+1. (2.2) Wypisz aktualną wartość n. (3) Koniec. Jak widzisz, algorytm ten wypisuje pewne liczby naturalne, począwszy od zadanej na początku liczby. Reguła tworzenia tych kolejnych liczb jest zapisana w punkcie (2.1). W zależności od tego, czy aktualna wartość jest parzysta czy nie, obliczamy n/2 lub n+1. Przykładowo, gdy początkowa wartość n=11, to algorytm ten wygeneruje następującą listę wartości: 11, 12, 6, 3, 4, 2, 1, Stop. Instrukcja w punkcie (2) wykonuje się tak długo, jak długo wartość n jest różna od 1. Ćwiczenie 8.18 Postaraj się wykonać algorytm 29 dla kilku przykładowych danych początkowych: n-23, n=17, n-32. Czy zawsze na koniec otrzymasz 1? UWAGA Można łatwo wykazać, że algorytm 29 jest skończony (przeciwnie niż w przypadku algorytmu 15). PODSUMOWANIE: Matematyka dostarcza wielu ciekawych algorytmów. Jednym z najstarszych jest algorytm Euklidesa. Poprzez stopniowe udoskonalanie algorytmu osiągamy zamierzony cel, wykonując coraz mniej obliczeń. 8.7 Czy wszystko jest algorytmiczne? O algorytmach porozmawiamy z prof. Marianem Mrozkiem z Instytutu Informatyki Uniwersytetu Jagiellońskiego. Wielokrotnie w swych dowodach matematycznych korzystał on z pomocy komputerów. W ten sposób udowodnił ważne twierdzenie (o istnieniu chaosu w równaniach Lorenza), które przez lata było wyzwaniem dla matematyków. Nic dziwnego, że słynna Encyclopaedia Britannica uznała ten rezultat za jedno z czterech najważniejszych osiągnięć matematyki na świecie w 1995 r. Zacznijmy od... kucharzy. Jeśli przepisy kulinarne mają charakter algorytmiczny, dlaczego obok świetnych kucharzy jest cała rzesza partaczy kulinarnych? Podejrzewam, że pierwsi z nich postępują zgodnie z procedurą, a drudzy mniej dokładnie. Może się też zdarzyć, iż algorytm jest zapisany mało precyzyjnie, co sprzyja odstępstwom od prawidłowych czynności. W ogóle algorytmika uczy nas precyzji - np. zamiast wyrażenia szczypta soli powinno się podać konkretną liczbę miligramów. Algorytmy są wokół nas. Ale czy wszystko, czy procesy dokonujące się w przyrodzie albo zachowania człowieka są algorytmiczne? Zdecydowanie nie! Uczeń w drodze do szkoły często zachowuje się w sposób niealgorytmiczny - zboczy z utartego szlaku, aby zobaczyć niezwykłego motyla, przyśpieszy kroku, aby dogonić atrakcyjną koleżankę itd. Każdy z nas podejmuje niekiedy ważne decyzje, np. wybiera kierunek studiów, decyduje się założyć rodzinę. Nie wierzę, aby tego typu decyzje miały charakter algorytmiczny. Czy algorytmy są ważne w matematyce? Za każdym razem, kiedy uda się znaleźć algorytm, upraszcza się pewną cząstkę rzeczywistości matematycznej. Algorytmy mogą być czasami banalne, czasami bardzo trudne do wymyślenia. Zawsze jednak od momentu stworzenia algorytmu ta cząstka rzeczywistości matematycznej przestaje być interesująca dla matematyków. Paradoksalnie więc matematycy starają się coraz rozleglejsze obszary matematyki uczynić mechanicznymi, wręcz nudnymi! Ale całej matematyki nie da się chyba zalgorytmizować? Na szczęście nie - świat matematyki jest nazbyt bogaty! Poza tym zbudowane przez nas algorytmy prowokują nowe problemy, nowe zagadnienia logiczno-matematyczne. Pytamy się na przykład, czy dany algorytm jest poprawny - czy w rezultacie jego działania otrzymamy to, co zamierzaliśmy. Przeprowadźcie dyskusję na poniższe tematy: 1. Jakie zachowania codziennego życia wydają się nie mieć algorytmicznego charakteru? 2. Zachowania algorytmiczne a inwencja twórcza.