Obrazy rekurencyjne Zastosowanie rekurencji w algorytmice AUTOR: Martin Śniegoń
Zdolność procedury/funkcji do wywoływania samej siebie Podstawowa i jedna z najważniejszych technik programistycznych Umożliwia rozkładanie problemów większych na mniejsze, w analogiczny sposób Jasno określone warunki zakończenia funkcji rekurencyjnej Dlaczego rekurencja?
Krótszy i bardziej przejrzysty zapis kodu źródłowego na wszystkich etapach rozwoju aplikacji Ułatwione dowodzenie poprawności algorytmu, a także obliczanie złożoności Zminimalizowane ryzyko popełnienia błędu podczas konstruowania algorytmu Zalety rekurencji
Ryzyko braku optymalnego rozwiązania problemu algorytmicznego (np. niewłaściwie zastosowana rekurencja znacząco podnosi złożoność algorytmu) Błędy programistyczne podczas określania warunków zakończenia rekurencji Brak wsparcia sprzętowego w typowych językach programistycznych (wyjątkiem są języki funkcyjne typu Haskell, F#) Czy rekurencja ma wady?
Zasada zastępowania rekurencji iteracją, gdzie tylko jest to możliwe Wielokrotne wywołanie, a kwestia przepełnienia stosu Brak obliczeń częściowych (problem przechowywania danych) Zmiana środowiska programistycznego celem poprawy wydajności Rozwiązywanie problemów związanych z rekursją
Względnie prosty schemat generowania grafiki, a także mniejszy rozmiar obrazu Możliwość ustalenia za pomocą dodatkowej zmiennej dokładności wyświetlanej ilustracji (im większa liczba wywołań funkcji rekurencyjnej, tym dokładniejszy obraz) Łatwe wykrywanie kolizji, a także punktów należących do obrazu Zalety obrazów rekurencyjnych
Uzyskiwanie nietypowych, realnych kształtów, figur czy obiektów Niejednolite elementy graficzne Tylko samopodobne, powtarzające się fragmenty Problemy z projektowaniem algorytmów tworzących skomplikowane ilustracje Ograniczenia obrazów tworzonych rekurencyjnie
Definicja (brak ścisłego określenia) Zbiór minimum kilku następujących cech: Nietrywialna struktura w dowolnej skali Względnie prosta reguła powstawania (rek.) Samopodobieństwo (niekoniecznie dokładne) Wymiar Hausdorfa > wymiar topograficzny Naturalny wygląd (często poszarpany) Struktura trudna do opisania w geometrii euklidesowej Czym jest fraktal?
Atraktory IFS (iterated function system), czyli fraktale generowane za pomocą odpowiednich funkcji rekurencyjnychiterowanych (możliwość określenia ilości kroków do wykonania) Zbiory i funkcje określone na liczbach zespolonych (typowe przykłady to zbiór Julii i Mandelbrota) Powstawanie fraktali
Przykładowe fraktale (mat.)
Przykładowe fraktale (grafika)
Przykładowe fraktale (świat)
Konstrukcja W kroku pierwszym ustalamy wymiary (współrzędne wierzchołków trójkąta równobocznego) figury Następnie wyliczamy środki wszystkich boków Trójkąt zbudowany na środkach tych boków kolorujemy na ustaloną barwę Dla pozostałych trzech trójkątów wywołujemy funkcje rekurencyjne Dodatkowa zmienna przekazywana jako parametr określa ilość kroków rekurencji Trójkąt Sierpińskiego
Uszczelka Sierpińskiego (ilustracja)
Konstrukcja W pierwszym kroku określamy kwadrat i jego wierzchołki za pomocą współrzędnych Kolejno każdy bok dzielimy na 3 równe części, w taki sposób, że środkowy kwadrat jest kolorowany Dla pozostałych 8 kwadratów wywołujemy rekurencyjnie tę samą funkcję podziału i kolorowania W implementacji musimy pamiętać o określeniu liczby wywołań rekursji (często warunki brzegowe są określane wg widoczności obrazu i dopasowywane do rozdzielczości) Dywan Sierpińskiego
Dywan Sierpińskiego
WZÓR (wymiar fraktalny): Uproszczony model n * s r = 1, gdzie n oznacza liczbę wywołań rekurencyjnych (liczba atraktorów obrazu głównego), s oznacza skalę podobieństwa kolejnego atraktora w stosunku do poprzednika, a r jest szukanym wymiarem Udowodnić można, że wymiar fraktalny dywanu Sierpińskiego wynosi ok. 1,8928 Pole dywanu (a także innych prostych figur, w których wycinane są pewne elementy w sposób regularny) wynosi 0 Dywan Sierpińskiego (obliczenia)
Ciekawostkę stanowi również odmienna strategia generowania figur rekurencyjnych, tzw. gra w chaos Polega ona na losowym wybieraniu odpowiednich punktów i łączeniu ich w taki sposób, aby powstał fraktal Trójkąt Sierpińskiego można otrzymać w ten sposób, a początkowe punkty są losowane zamiast definiowane (trójkąt nie musi być równoboczny) Gra w chaos
Kluczowym elementem płatku Kocha jest połączenie trzech odcinków (w kolejnych krokach krzywych) w trójkąt równoboczny, który stanowi podstawę płatka (im więcej kroków tym bardziej przypomina płatek śniegu) Każdy krok oznacza zmniejszenie pojedynczego odcinka w skali 1/3 w stosunku do poprzedniego kroku Krzywa i płatek Kocha
Krzywa Kocha
Chcąc wykonać krzywą Kocha należy zadbać o wykonanie odpowiedniej sekwencji wywołań rekurencyjnych Rysowanie odbywa się w najniższej wywołanej funkcji rekurencyjnej Obroty są wykonywane w każdym wywołaniu rekursywnym Alternatywny sposób generowania płatku Kocha za pomocą trójkątów równobocznych nie stanowi optymalnego rozwiązania Krzywa Kocha - generowanie
Kolejne kroki tworzenia krzywej Sierpińskiego Warto zwrócić uwagę na powtarzający się schemat: -Krzywa tworzy zamknięty zbiór punktów na płaszczyźnie - Domknięcia występują zawsze w tych samych miejscach (narożniki: góra po prawej, dół po prawej, dół po lewej, góra po lewej) - Rysowanie odbywa się wg kolejności narożników, które rysowane są tylko w głównym wywołaniu procedury Krzywa Sierpińskiego
Warto zwrócić uwagę na 4 kawałki powtarzające się podczas rysowania: Oznaczmy jako A Oznaczmy jako C Oznaczmy jako B Oznaczmy jako D Krzywa Sierpińskiego
Figura: An Bn Cn Dn Powyższe strzałki oznaczają domknięcia Elementy An, Bn, Cn i Dn oznaczają odpowiednio fragmenty rysunku wykonane z dokładnością do n kroków: górnej, prawej, dolnej i lewej części Należy zdefiniować rekurencyjnie An, Bn, Cn i Dn, aby krzywa mogła powstać Krzywa Sierpińskiego (algorytm)
An = An-1 Bn-1 Dn-1 An-1 Bn = Bn-1 Cn-1 An-1 Bn-1 Cn = Cn-1 Dn-1 Bn-1 Cn-1 Dn = Dn-1 An-1 Cn-1 Dn-1 Zauważyć należy, iż długość odcinków, z których składa się krzywa określamy na początku lub uzależniamy od liczby kroków rekurencji Rysowanie zaczyna się od górnej lewej strony, a nie od środka! Krzywa Sierpińskiego (def. rek.)
Krzywa Hilberta
Krzywa składa się z 4 fragmentów (niedomkniętych kwadratów, odpowiednio obróconych względem pustego boku) Aby otrzymać kolejny krok rekurencyjny należy skleić powyższe 4 fragmenty w 3 miejscach (łączenie następuje zawsze po lewej stronie, u góry i po prawej) Ciekawostkę stanowi fakt, że krzywa Hilberta wypełnia całą powierzchnię po n- krokach Krzywa Hilberta (budowa)
Krzywa Hilberta (obserwacje)
Krzywa Hilberta (schemat)
Zgodnie z ustalonym porządkiem łączenia (można wybrać inną kolejność i zmodyfikować nieznacznie algorytm): Zaczynamy rysowanie figury Cn, po którym następuje łączenie w górę z kolejnym elementem krzywej Bn, po którym następuje łączenie w prawo Bn, po którym następuje łączenie w dół An, które kończy rysowanie W kolejnych krokach rekurencyjnych dochodzi element Dn (wstawiany w odpowiednie miejsce, schemat powstał w oparciu 3 kroki r.) Krzywa Hilberta (rysowanie)
Program KrzyweHilberta () Const n = 5; h = 100; Var i, h, x, y, x0, y0; procedure A(i) if i>0 then begin D(i-1); x:=x - h; A(i-1); y:=y - h; A(i-1); x:=x + h; B(i-1); end procedure B(i) if i>0 then begin C(i-1); y:=y + h; B(i-1); x:=x + h; B(i-1); y:=y h; A(i-1); end procedure C(i) if i>0 then begin B(i-1); x:=x + h; C(i-1); y:=y + h; C(i-1); x:=x h; D(i-1); end procedure D(i) if i>0 then begin A(i-1); y:=y - h; D(i-1); x:=x - h; D(i-1); y:=y + h; C(i-1); end begin i := 0; h:= h0; x0 := h/2; y0 := x0; repeat i := i + 1; h := h/2; x0 := x0 - h/2; y0 := y0 - h/2; x := x0; y := y0; UstawPioro(x,y); B(i); until i = n; end Krzywa Hilberta (kod źródłowy)
W kodzie źródłowym rysujemy n- początkowych krzywych Hilberta Poza zaprogramowaniem wywołań rekurencyjnych, należy zwrócić uwagę na początek rysowania (ustawienie kursora w początkowym położeniu, aby w trakcie rysowania nie wyjść poza ustalony obszar) Funkcja UstawPioro rozpoczyna rysowanie, a pisak porusza się zgodnie z wytycznymi w poszczególnych procedurach po osi X i Y Krzywa Hilberta (komentarz)
Istnieją alternatywne algorytmy tworzenia tych samych lub podobnych fraktali (najczęściej wybiera się optymalny) Z definicji fraktale są nieskończone, ale na potrzeby algorytmów zawsze określa się moment stopu (określone n, albo rozmiar najmniejszej części) Złożoność obliczeniowa podanych przykładów jest rzędu wykładniczego (z tego powodu nie wykonuje się bardzo wielu kroków rekurencyjnych) Podsumowanie
W wielu algorytmach tworzących fraktale wykorzystuje się metodę gry w chaos Powyższa technika pozwala na szersze zastosowanie iteracji i otrzymanie zadowalających rezultatów przy stosunkowo niewielkiej liczbie obliczeń (zalecane zwłaszcza dla bardzo rozbudowanych i nietypowych fraktali) Większość fraktali ma wymiar podobieństwa (fraktalny) z przedziału (1;2). Wymiar ten jest zawsze >0 Podsumowanie c.d.
Podziękowania DZIĘKUJĘ ZA UWAGĘ!