ANALIZA ALGORYTMÓW. Analiza algorytmów polega między innymi na odpowiedzi na pytania:

Podobne dokumenty
ANALIZA ALGORYTMÓW. Analiza algorytmów polega między innymi na odpowiedzi na pytania:

ANALIZA ALGORYTMÓW. Analiza algorytmów polega między innymi na odpowiedzi na pytania:

TEORETYCZNE PODSTAWY INFORMATYKI

Zaawansowane algorytmy i struktury danych

Jeśli czas działania algorytmu zależy nie tylko od rozmiaru danych wejściowych i przyjmuje różne wartości dla różnych danych o tym samym rozmiarze,

Zadanie 1 Przygotuj algorytm programu - sortowanie przez wstawianie.

Zaawansowane algorytmy i struktury danych

Teoretyczne podstawy informatyki

Podstawy Informatyki. Sprawność algorytmów

Matematyczne Podstawy Informatyki

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

TEORETYCZNE PODSTAWY INFORMATYKI

Wprowadzenie do złożoności obliczeniowej

Złożoność Obliczeniowa Algorytmów

Algorytmy i Struktury Danych

Analiza algorytmów zadania podstawowe

Rekurencja (rekursja)

Technologie informacyjne Wykład VII-IX

Efektywność algorytmów

Matematyka dyskretna dla informatyków

Złożoność obliczeniowa algorytmu ilość zasobów komputera jakiej potrzebuje dany algorytm. Pojęcie to

Rozwiązywanie układów równań liniowych

Rekurencje. Jeśli algorytm zawiera wywołanie samego siebie, jego czas działania moŝe być określony rekurencją. Przykład: sortowanie przez scalanie:

Wstęp do metod numerycznych Eliminacja Gaussa Równania macierzowe. P. F. Góra

Rozdział 1 PROGRAMOWANIE LINIOWE

Rozwiązywanie zależności rekurencyjnych metodą równania charakterystycznego

Algorytmy i struktury danych. Wykład 6 Tablice rozproszone cz. 2

Wykład 5. Metoda eliminacji Gaussa

Metody numeryczne Technika obliczeniowa i symulacyjna Sem. 2, EiT, 2014/2015

Złożoność algorytmów. Wstęp do Informatyki

Programowanie dynamiczne

1. Analiza algorytmów przypomnienie

Struktury danych i złożoność obliczeniowa Wykład 7. Prof. dr hab. inż. Jan Magott

Grzegorz Mazur. Zak lad Metod Obliczeniowych Chemii UJ. 14 marca 2007

UKŁADY ALGEBRAICZNYCH RÓWNAŃ LINIOWYCH

Obliczenia iteracyjne

Wykład 2. Poprawność algorytmów

Algorytm poprawny jednoznaczny szczegółowy uniwersalny skończoność efektywność (sprawność) zmiennych liniowy warunkowy iteracyjny

Wykład z równań różnicowych

ALGORYTMY I STRUKTURY DANYCH

Macierze. Rozdział Działania na macierzach

PODSTAWY AUTOMATYKI. MATLAB - komputerowe środowisko obliczeń naukowoinżynierskich - podstawowe operacje na liczbach i macierzach.

Obliczenia naukowe Wykład nr 8

Uwaga: Funkcja zamień(a[j],a[j+s]) zamienia miejscami wartości A[j] oraz A[j+s].

Algorytmy i Struktury Danych.

Rekurencja. Dla rozwiązania danego problemu, algorytm wywołuje sam siebie przy rozwiązywaniu podobnych podproblemów. Przykład: silnia: n! = n(n-1)!

Zaawansowane metody numeryczne

Teoretyczne podstawy informatyki

Programowanie dynamiczne cz. 2

1 Macierz odwrotna metoda operacji elementarnych

Zasady analizy algorytmów

Matematyka dyskretna. Andrzej Łachwa, UJ, /10

Wykład z Technologii Informacyjnych. Piotr Mika

Metody numeryczne Wykład 4

Układy równań liniowych i metody ich rozwiązywania

Strategia "dziel i zwyciężaj"

EGZAMIN - Wersja A. ALGORYTMY I STRUKTURY DANYCH Lisek89 opracowanie kartki od Pani dr E. Koszelew

Układy równań liniowych. Krzysztof Patan

Rozwiązywanie układów równań liniowych metody dokładne Materiały pomocnicze do ćwiczeń z metod numerycznych

Programowanie celowe #1

Logarytmy. Funkcje logarytmiczna i wykładnicza. Równania i nierówności wykładnicze i logarytmiczne.

Pętle i tablice. Spotkanie 3. Pętle: for, while, do while. Tablice. Przykłady

Język ludzki kod maszynowy

Wstęp do Informatyki zadania ze złożoności obliczeniowej z rozwiązaniami

Złożoność obliczeniowa zadania, zestaw 2

Wstęp do metod numerycznych Uwarunkowanie Eliminacja Gaussa. P. F. Góra

1 Macierze i wyznaczniki

Wstęp do programowania. Dziel i rządź. Piotr Chrząstowski-Wachtel

Metody numeryczne w przykładach

6. Pętle while. Przykłady

O MACIERZACH I UKŁADACH RÓWNAŃ

Rozdział 5. Macierze. a 11 a a 1m a 21 a a 2m... a n1 a n2... a nm

Algorytm selekcji Hoare a. Łukasz Miemus

Matematyka dyskretna. Andrzej Łachwa, UJ, /14

Procesy stochastyczne WYKŁAD 2-3. Łańcuchy Markowa. Łańcuchy Markowa to procesy "bez pamięci" w których czas i stany są zbiorami dyskretnymi.

Analiza algorytmów zadania podstawowe

2. Układy równań liniowych

Efektywna metoda sortowania sortowanie przez scalanie

Analiza numeryczna Kurs INP002009W. Wykłady 6 i 7 Rozwiązywanie układów równań liniowych. Karol Tarnowski A-1 p.

Poprawność semantyczna

Procesy stochastyczne WYKŁAD 2-3. Łańcuchy Markowa. Łańcuchy Markowa to procesy "bez pamięci" w których czas i stany są zbiorami dyskretnymi.

jest rozwiązaniem równania jednorodnego oraz dla pewnego to jest toŝsamościowo równe zeru.

Wykład z równań różnicowych

Układy równań i nierówności liniowych

Podstawy programowania. Wykład Funkcje. Krzysztof Banaś Podstawy programowania 1

Liczby pierwsze - wstęp

Schemat programowania dynamicznego (ang. dynamic programming)

5. Podstawowe algorytmy i ich cechy.

Metoda eliminacji Gaussa. Autorzy: Michał Góra

Za pierwszy niebanalny algorytm uważa się algorytm Euklidesa wyszukiwanie NWD dwóch liczb (400 a 300 rok przed narodzeniem Chrystusa).

Matematyka dyskretna. Andrzej Łachwa, UJ, /15

Metody numeryczne I. Janusz Szwabiński. Metody numeryczne I (C) 2004 Janusz Szwabiński p.1/61

Informacje wstępne #include <nazwa> - derektywa procesora umożliwiająca włączenie do programu pliku o podanej nazwie. Typy danych: char, signed char

Wykład 6. Metoda eliminacji Gaussa: Eliminacja z wyborem częściowym Eliminacja z wyborem pełnym

Algorytmy w teorii liczb

INFORMATYKA SORTOWANIE DANYCH.

0 + 0 = 0, = 1, = 1, = 0.

Wstęp do programowania INP001213Wcl rok akademicki 2017/18 semestr zimowy. Wykład 13. Karol Tarnowski A-1 p.

Wstęp do programowania INP001213Wcl rok akademicki 2018/19 semestr zimowy. Wykład 13. Karol Tarnowski A-1 p.

Klasa 2 INFORMATYKA. dla szkół ponadgimnazjalnych zakres rozszerzony. Założone osiągnięcia ucznia wymagania edukacyjne na. poszczególne oceny

Transkrypt:

ANALIZA ALGORYTMÓW Analiza algorytmów polega między innymi na odpowiedzi na pytania: 1) Czy problem może być rozwiązany na komputerze w dostępnym czasie i pamięci? 2) Który ze znanych algorytmów należy zastosować w danych okolicznościach? 3) Czy istnieje lepszy algorytm od rozważanego? Czy jest on optymalny? Konstruując algorytm należy zwracać uwagę na : - poprawność semantyczną - prostotę - czas działania - ilość zajmowanej pamięci - optymalność - okoliczności w jakich należy go używać, a w jakich nie Złożoność obliczeniową algorytmu definiuje się jako ilość zasobów komputerowych, potrzebnych do jego wykonania. Wyróżniamy złożoność pamięciową i czasową. Będziemy się zajmować głównie złożonością czasową!!! Miara złożoności musi być uniwersalna czyli oderwana od szczegółów natury "sprzętowej" tj. - Jaki komputer jest używany? - Jaka jest częstotliwość zegara taktującego procesor? - Czy program będzie jedynym wykonywanym na komputerze? Jeśli nie to jaki jest jego priorytet? - Jakiego kompilatora używamy? - Czy w kompilatorze włączono opcje optymalizacji kodu?... etc

Parametrem najczęściej decydującym o czasie wykonania algorytmu jest rozmiar danych, z którymi ma on do czynienia. Parametr ten może mieć różne znaczenie: - dla funkcji sortującej tablicę, czy funkcji wyszukiwania liniowego lub binarnego parametrem będzie rozmiar tablicy n - dla dodawania, mnożenia macierzy, parametrem również będzie rozmiar tablicy n - dla funkcji liczącej n! będzie to wielkość danej wejściowej n - dla ciągu Fibonacciego rozmiarem danych wejściowych może być również liczba symboli użytych do zakodowania liczny n (dla reprezentacji binarnej lg n +1 ) - jeśli na wejściu algorytmu będzie graf to możemy podawać liczbę wierzchołków i liczbę krawędzi jako rozmiar danych wejściowych (2 liczby) - dla niektórych zagadnień (problemów) mimo zastosowana tego samego algorytmu możemy mieć inny rozmiar danych W algorytmie zawsze można wyróżnić tzw. operacje dominujące lub operacje podstawowe (najbardziej czasochłonne) - takie, że łączna ich liczba jest proporcjonalna do liczby wszystkich operacji jednostkowych w dowolnej realizacji algorytmu. Dla sortowania operacją tą będzie zwykle porównanie dwóch elementów, lub także przestawienie elementów ciągu. Nie jest jednoznacznie określone, które operacje należy traktować jako dominujące, mogą to być porównania, ale również dodawanie lub mnożenie. Podstawowa analiza złożoności czasowej algorytmu określa, ile razy operacja dominująca jest wykonywana dla kazdej wartości rozmiaru danych wejściowych. W niektórych przypadkach liczba

ta zależy nie tylko od rozmiaru danych wejściowych, ale również od wartości wejściowych (np. przeszukiwanie liniowe). W innych przypadkach (np. dodawanie tablic), operacja dominująca jest wykonywana zawsze tę samą liczbę razy dla każdej realizacji problemu o rozmiarze n. W takich przypadkach T(n) definiujemy jako liczbę wykonań operacji dominującej przez algorytm dla realizacji o rozmiarze danych n. T(n) jest wówczas nazywane złożonością czasową w każdym wypadku. W niektórych przypadkach dla pełnej analizy algorytmu należy wybrać dwie różne operacje dominujące. Jednostką złożoności czasowej jest czas wykonania jednej operacji dominującej. W ogólności wyróżniamy: - złożoność pesymistyczną - zdefiniowaną jako ilość zasobów komputerowych, potrzebnych przy "najgorszych" danych wejściowych rozmiaru n - złożoność oczekiwaną - definiowaną jako ilość zasobów komputerowych, potrzebnych przy "typowych" danych wejściowych rozmiaru n W definicjach wykorzystujemy następujące wielkości: D n - zbiór zestawów danych wejściowych rozmiaru n t(d) - liczba operacji dominujących dla zestawu danych wejściowych d X n - zmienna losowa równa t(d) dla d D n P nk - rozkład prawdopodobieństwa zmiennej losowej X n, tzn prawdopodobieństwo, że dla danych rozmiaru n algorytm wykona k operacji dominujących ( k 0 )

Zwykle przyjmujemy, że każdy zestaw danych rozmiaru n może się pojawić z jednakowym prawdopodobieństwem. Pesymistyczna złożoność czasowa to funkcja : W(n) = sup{ t(d) : d D n } czyli kres górny t(d) Możliwe jest również określenie optymistycznej złożoności czasowej B(n) = inf{ t(d) : d D n }, ale jest ona rzadko używana. Oczekiwana złożoność czasowa (złożoność w średnim przypadku) to funkcja: A(n) = k 0 ( k*p nk ) czyli wartość oczekiwana E( X n ) Oczywiście jeśli istnieje T(n) to W(n) = A(n) = T(n) Aby stwierdzić, na ile W(n) i A(n) są reprezentatywne dla wszystkich danych wejściowych wprowadza się dodatkowo: - miarę wrażliwości pesymistycznej (n) = sup{ t(d 1 ) - t(d 2 ) : d 1, d 2 D n } - miarę wrażliwości oczekiwanej ( odchylenie standardowe ) σ(n) = dev( X n ) = var( X n ) = k 0 ( k - E( X n ) ) 2 * P nk Im większe wartości (n) i σ(n) tym algorytm jest bardziej wrażliwy na dane wejściowe i tym bardziej jego zachowanie może odbiegać od tego opisanego funkcjami W(n), A(n). Przykład: Przeszukiwanie liniowe zbioru (ciągu)

Problem: czy klucz x znajduje się w tablicy S, zawierającej n kluczy? Dane wejściowe: całkowita liczba dodatnia n, tablica kluczy S indeksowana od 1 do n oraz klucz x. Wynik: location lokalizacja klucza x w tablicy S (0, jeżeli x nie występuje w S) void linsearch(int n, const keytype S[], keytype x, index location) { location = 1; while (location <= n && S[location]!= x) location++; if(location > n) location = 0; } Rozmiar danych wejściowych: n Operacja dominująca: S[location]!= x Pesymistyczna złożoność czasowa: W(n) = n+1 Pesymistyczna wrażliwość czasowa: (n) = n Oczekiwana złożoność czasowa: A(n) =? Zakładamy, że P nk = 1/n dla k=1,2,3,...,n => A(n) = n k = 1 (k* P nk ) = 1/n * k = 1/n * n*(n+1)/2 = (n+1)/2 Oczekiwana wrażliwość czasowa? Var( X n ) = n k = 1 ( k - (n+1)/2 ) 2 *1/n = = 1/n * ( n*(n+1)*(2n+1)/6-2(n+1)/2 *n*(n+1)/2 +n*( (n+1)/2 ) 2 )

= (n+1)*(2n+1)/6 - (n+1) 2 /4 = (n+1)* (4n+2-3n-3)/12 = (n 2-1)/12 n 2 /12 zatem σ(n) 0.29*n Ponieważ W(n), A(n) oraz (n), σ(n) są liniowe w n, więc algorytm ma dużą wrażliwość liczby operacji dominujących na dane wejściowe. Faktyczna/praktyczna złożoność czasowa algorytmu (czas działania, T(n) ) zwykle różni się od wyliczonej teoretycznie współczynnikiem proporcjonalności zależnym od konkretnej realizacji algorytmu. Istotną informacją zawarta w W(n), A(n) jest rząd wielkości, czyli zachowanie asymptotyczne, gdy n ->. Przykład (wyznaczanie złożoności praktycznej): 0! = 1 N! = n! = n* (n-1)! gdy n 1 unsigned long int silnia(int x) { if (x==0) return 1; else return x*silnia(x-1); } Przyjmujemy, że operacją dominującą jest instrukcja porównania z czasem t c. Zatem: T(0) = t c T(n) = t c +T(n-1) dla n 1

Należy uzyskać nie-rekurencyjną funkcję T(n) T(n) = t c + T(n-1) T(n-1) = t c + T(n-2) T(n-2) = t c + T(n-3)......... T(1) = t c + T(0) T(0) = t c L = P => T(n) + T(n-1) +... + T(0) = (n+1)* t c + T(n-1) +...+ T(0) => T(n) = (n+1)* t c W praktyce nieistotna jest taka dokładność dla złożoności praktycznej T(n) i różnice między np. T(n) = (n+1)* t c i np. T(n) = (n+3)* t c można zaniedbać. Będziemy zatem szukać złożoności teoretycznej, tj. funkcji matematycznej występującej w T(n), która odgrywa w niej najważniejszą rolę, wpływając najsilniej na czas wykonania programu. Złożoność teoretyczną, inaczej klasę algorytmu O( T(n) ) definiujemy: O(T(n)) = {g:t:n->r + ( M R + )( n 0 N )( n n 0 ) [ g(n) M*T(n) ] } ( klasą O dowolnej funkcji T:N->R + jest taka funkcja g, że istnieje M oraz n 0 takie, że dla każdego n n 0 zachodzi g(n) M*T(n) ) np. T(n) = 3n+1 => O(T(n)) = O(n)

T(n) = n 2 -n+1 => O(T(n)) = O( n 2 ) T(n) = 2 n +n 2 +4 => O(T(n)) = O( 2 n ) Klasa O (będąca zbiorem funkcji ) jest wielkością asymptotyczną, pozwalającą wyrazić w postaci arytmetycznej wielkości z góry nie znane w postaci analitycznej. Własności funkcji O : c*o( f(n) ) = O( f(n) ) O( f(n) ) + O( f(n) ) = O( f(n) ) O(O( f(n) ) ) = O( f(n) ) O( f(n) ) * O( g(n) ) = O( f(n)*g(n) ) O( f(n) * g(n) ) = f(n)*o( g(n) ) Najczęściej spotykane złożoności czasowe algorytmów: 1) log(n) - występuje, np. dla algorytmów, w których zadanie rozmiaru n zostaje sprowadzone do zadania rozmiaru n/2 + pewna stała liczba działań ( np. przeszukiwanie binarne uporządkowanego ciągu) 2) n - występuje, np. dla algorytmów, w których jest wykonywana pewna stała liczba działań dla każdego z n elementów danych wejściowych (np. algorytm Hornera wyznaczania wartości wielomianu) 3) n*log(n) - występuje, np. dla algorytmów, w których zadanie rozmiaru n zostaje sprowadzone do dwóch podzadań rozmiaru n/2 + pewna liczba działań liniowa w n (np. niektóre metody sortowania - quicksort ) 4) n 2 - występuje, np. dla algorytmów w których jest wykonywana pewna stała liczba działań dla każdej pary elementów danych wejściowych (podwójna instrukcja iteracyjna, np. operacje na tablicach)

5) 2 n - występuje, np. dla algorytmów, w których jest wykonywana stała liczba działań dla każdego podzbioru danych wejściowych 6) n! - występuje, np. dla algorytmów, w których jest wykonywana stała liczba działań dla każdej permutacji danych wejściowych

Czas działania algorytmu/ programu o danym rozmiarze danych n silnie zależy od złożoności algorytmu: Tc = 1 µs Należy zawsze pamiętać o asymptotycznym charakterze klasy algorytmu O. Przykład: Mamy dwa algorytmy: W1 klasy O(logN) (złożoność praktyczna 100*log 2 N) W2 klasy O(N) (złożoność praktyczna 10*N) Dla N=2, dla W1, T(N)=100 > dla W2, T(N)=20 Dla N=1024, dla W1, T(N)=1000 < dla W2, T(N)=10240 Zatem dla wystarczająco dużego N algorytm W1 jest bardziej efektywny niż W2. Jeszcze jeden przykład wyznaczania złożoności czasowej algorytmu (zerowanie tablicy poniżej przekątnej wraz z nią):

int tab[n][n]; void zerowanie( ) { int i,j; i=0; // ta while (i< n) // tc { j=0; // ta while (j<=i) // tc } { j (tc+2ta) tab[i,j]=0; // ta j=j+1; // ta } i=i+1; // ta } gdzie ta - czas wykonania instrukcji przypisania tc - czas wykonania instrukcji porównania Pamiętając, że instrukcja while powtarza n razy instrukcje zawarte pomiędzy nawiasami, a warunek jest sprawdzany n+1 razy można zapisać:

T(N) = tc + ta + N i=1 ( 2*ta + 2*tc + i j=1 (tc + 2*ta) ) i po usunięciu wewnętrznej sumy dostajemy: T(N) = tc + ta + N i=1 ( 2*ta + 2*tc + i*(tc + 2*ta) ) = tc + ta + 2*N*(ta + tc) + N*(N+1)*(tc+2*ta)/2 ) i po uproszczeniu T(N) = ta (1+3*N+N 2 ) + tc*(1+2.5*tc+ N 2 /2 ) program jest klasy O(n 2 ) Powyższe przykłady miały jedną wspólną cechę: czas wykonania programu nie zależał od wartości danych, a jedynie od ich rozmiarów. Przykład (fragment programu) const n=10; int t[n]; void sumowanie( ) { int i,k; int suma=0; i=0; // ta = 0 while (i<n) // tc = 1 { j=0; // ta = 0 while (j<=t[i]) // tc = 1 { suma=suma+2; // ta = 0 j=j+1; // ta = 0 } i=i+1; // ta = 0 } } Problemem jest fakt, że nie znamy zawartości tablicy t[n]. T(n) = tc + N i=1 ( tc + t[i] j=1 ( tc ) )

T(n) = tc + N*tc + N i=1 (t[i]*tc ) T(n) = tc + N*tc + N*t[i]*tc T(n) = tc*( 1 + N + N*t[i] ) T(n) max( N, N*t[i] ) Możemy zatem jedynie powiedzieć, że czas wykonania jest proporcjonalny do większej z liczb N i N*t[i], ale nie jesteśmy w stanie określić dokładnej wartości. Problemem jest nieznajomość zawartości tablicy, która jest potrzebna do otrzymania ostatecznego wyniku. Jedyne co można zrobić to przeprowadzić analizę statystyczną zadania. Niekiedy możemy znacząco uprościć obliczenia zakładając: - zwracamy uwagę tylko na najbardziej czasochłonne operacje (najczęściej instrukcje porównania). - wybieramy wiersz programu znajdujący się w najgłębiej położonej instrukcji iteracyjnej (pętle w pętlach...) i obliczamy ile razy jest on wykonywany. Z wyniku dedukujemy złożoność teoretyczną. i=0; while (i<n) { j=0; while (j<=n) { suma=suma+2; j=j+1; } i=i+1; }

Wybierając instrukcję suma:=suma+2 obliczamy w prosty sposób, że wykona się ona N(N+1) razy. Zatem fragment programu ma złożoność teoretyczną O(n 2 ). Analiza równań rekurencyjnych W rozwiązaniach równań rekurencyjnych stosuje się zwykle dwie metody: - rozwinięcie równania do sumy - znalezienia funkcji tworzącej Rozwinięcie do sumy Przykłady wyznaczania praktycznej złożoności czasowej T(n) : 1) T(1) = 0 T(n) = T( n/2 ) + c dla n > 1 Zależność tą otrzymujemy jako równanie złożoności, gdy problem rozmiaru n sprowadza się do pod-problemu rozmiaru o połowę mniejszego. Podstawiamy n=2 k. T(n) = T( 2 k ) = T( 2 k-1 ) + c = T( 2 k-2 ) + c + c = T( 2 0 ) + k*c = k*c = c*logn O(T(n)) = logn 2) T(1) = 0 T(n) = T( n/2 ) + T( n/2 ) + c dla n > 1

Zależność tą otrzymujemy jako równanie złożoności, gdy problem rozmiaru n sprowadza się do dwóch pod-problemów rozmiaru n/2 + stała liczba działań. Podstawiamy n=2 k. T(n) = T( 2 k ) = 2*T( 2 k - 1 ) + c = 2*( 2*T( 2 k - 2 ) + c ) + c = 2 2 *T( 2 k - 2 )+2 1 *c + 2 0 *c = 2 k *T(2 0 ) + c*( 2 k - 1 + 2 k - 2 +...+ 2 0 ) = 0 + c*(2 k - 1) / (2-1) = c * (n-1) O(T(n)) = n 3) T(1) = 0 T(n) = T( n/2 ) + T( n/2 ) + c*n dla n > 1 Zależność tą otrzymujemy jako równanie złożoności, gdy problem rozmiaru n sprowadza się do dwóch pod-problemów rozmiaru n/2 + liniowa liczba działań. Podstawiamy n=2 k. T(n) =T(2 k ) = 2*T( 2 k - 1 ) + c* 2 k = 2*( 2*T( 2 k - 2 ) + c*2 k - 1 ) + c*2 k = 2 2 *T( 2 k - 2 ) + c*2 k + c*2 k = 2 k *T(2 0 ) + k*c* 2 k = = 0 + c*n*logn = c*n*logn O(T(n)) = n*logn

Zmiana dziedziny równania rekurencyjnego Niektóre równania charakteryzują się nieprzyjemnym wyglądem i nie dają się rozwiązać wcześniejszymi wzorami. Czasem skutkuje zmiana dziedziny. Przykładowo: a n = (3*a n-1 ) 2 a 0 = 1 Podstawiamy b n = log 2 a n, b n-1 = log 2 a n-1 i logarytmujemy obie strony równania b n = log 2 a n = log 2 (3*a n-1 ) 2 b 0 = log 2 1 = 0 Dostajemy wersję rozwiązania. b n = 2* log 2 3 + 2*b n-1 która nadaje się już do Funkcje tworzące Czasem trudno wyznaczyć rozwiązanie T(n) bezpośrednio z równania rekurencyjnego (brak zwięzłego wzoru). Poszukujemy wówczas funkcji tworzącej, definiowanej jako: F(z) = n 0 T(n)* z n Metodę tę stosuje się do znajdowania wartości oczekiwanej i wariancji zmiennej losowej X n. Funkcja tworząca dla rozkładu prawdopodobieństwa P nk zmiennej losowej X n ma postać: P n (z) = k 0 P nk * z k i wtedy k 0 P nk = 1 Wartość oczekiwaną i wariancję można wyznaczyć jako funkcję pochodnych funkcji P n (z) dla z = 1 :

E( X n ) = P n (1) (*) var( X n ) = P n (1) + P n (1) - P n (1) 2 (**) Ponieważ: P n (z) = k 1 k*p nk * z k - 1 P n (z) = k 2 k*(k-1)*p nk * z k - 2 Stąd: P n (1) = k 1 k*p nk (*) P n (1) = k 2 k*(k-1)*p nk Zatem: Var( X n ) = k 0 ( k - P n (1) ) 2 * P nk = k 0 k 2 * P nk - 2*P n (1)*( k 0 k*p nk ) + P n (1) 2 *( k 0 P nk ) = k 0 k*(k-1)*p nk + k 0 k*p nk - 2*P n (1) 2 + P n (1) 2 = P n (1) + P n (1) - P n (1) 2 Wielkości E( X n ) i var( X n ), a także złożoność oczekiwaną i wrażliwość algorytmu, można wyznaczyć nie znając rozkładu P nk, a tylko funkcję tworzącą. Funkcja Ackermanna Przykład pokazuje jak niegroźna z wyglądu funkcja rekurencyjna może być bardzo kosztowna w użyciu.

int A(int n, int p) { if (n==0) return 1; if ((p==0) && (n>=1)) if (n==1) return 2; else return n+2; if ((p>=1) && (n>=1)) return A( A(n-1,p), p-1 ); } Dlaczego pojawia się komunikat Stack, overflow (przepełnienie stosu)? Komunikat sugeruje, że podczas wykonania programu nastąpiła znaczna ilość wywołań funkcji Ackermanna. Z analizy funkcji A uzyskujemy: n 1, A(n,1) = A(A(n-1,1),0) = A(n-1,1) + 2 co daje n 1, A(n,1) = 2n Analogicznie dla 2 otrzymamy: n 1, A(n,2) = A(A(n-1,2),1) = 2A(n-1,2) co pozwala napisać n 1, A(n,2) = 2 n

W przypadku funkcji Ackermanna trudno jest nawet nazwać jej klasę. Stwierdzenie, że zachowuje się wykładniczo jest zdecydowanie niepoprawne. Rekurencja w postaci: Niehomogeniczna rekurencja liniowa a 0 t n + a 1 t n-1 + + a j t n-j = f(n) gdzie j oraz składniki a 0, a 1,, a j są stałymi, natomiast f(n) jest funkcją niezerową nazywamy niehomogenicznym liniowym równaniem rekurencyjnym ze stałymi współczynnikami.

Jeśli funkcja f(n) jest zerowa ( f(n)=0 ) to dostajemy homeogeniczne liniowe równanie rekurencyjne. Nie istnieje ogólna metoda rozwiązywania niehomogenicznych równań liniowych. Rozwiązanie jest możliwe gdy niehomeogeniczna rekurencja liniowa ma postać a 0 t n + a 1 t n-1 + + a j t n-j = b n p(n) gdzie b jest stałą, natomiast p(n) jest wielomianem zmiennej n. Wówczas można ją przekształcić w homeogeniczna rekurencja liniowa mającą równanie charakterystyczne postaci: (a 0 r j + a 1 r j-1 + + a j )(r-b) d+1 =0 gdzie d jest stopniem wielomianu p(n). Równanie charakterystyczne ma dwie składowe: część odpowiadającą rekurencji homogenicznej oraz część związaną z rekurencją niehomogeniczną. Jeśli po prawej stronie równania wyjściowego jest więcej wyrazów postaci b n p(n) to wszystkie wchodzą do równania charakterystycznego. Przykład: Rozważmy rekurencje T(n) = 2*T(n/2)+n-1 dla n>1 n i będącego potęgą 2 T(1) = 0 podstawiamy n=2 k

T(n) = T(2 k ) = 2T(2 k-1 )+2 k -1 t k = T(2 k ) t k-1 = T(2 k-1 ) t k -2t k-1 = 2 k -1 t k -2t k-1 = 2 k *k 0 + (-1)*k 0 *1 k [a 0 t n + a 1 t n-1 + + a j t n-j = b n p(n)] zatem d=0 zaś b przymuje dwie wartości 1 i 2 (r k -2r k-1 )(r-2) 1 (r-1) 1 = 0 [( a 0 r j + a 1 r j-1 + + a j )(r-b) d+1 =0] więc pierwiastkami równania są r = 1 i r = 2 (pierwiastek podwójny). Przydatne jest również twierdzenie o pierwiastku wielokrotnym. Jeżeli r jest pierwiatkiem wielokrotnym (m-krotnym) równania charakterystycznego dla rekurencji liniowej to wszystkie t n =r n, t n =n*r n, t n =n 2 *r n,, t n =n m-1 *r n, są rozwiązaniami rekurencji i każdy składnik wchodzi do rozwiązania ogólnego rekurencji. Zatem ogólne rozwiązanie w powyższym problemie to: tk= c1*1 k + c2*2 k + c3*k*2 k zaś współczynniki c1, c2, c3 można wyznaczyć z kilku (trzech) równań rekurencyjnych dla kilku (trzech) pierwszych wartości k (lub n). Należy jeszcze zwrócić uwagę, że mówiąc o analizie złożoności (czasowej lub pamięciowej) zwykle mamy na myśli analizę konkretnego algorytmu. Niemniej obok analizy samego algorytmu powinniśmy z zasadzie przeanalizować problem jako całość.

Przykładowo wiemy, że podstawowy algorytm mnożenia macierzy ma złożoność czasową n 3. Funkcja n 3 jest własnością tego konkretnego algorytmu i wcale nie musi być ogólną własnością mnożenia macierzy. Wiemy przecież, że algorytm Strassena charakteryzuje się złożonością n 2.81., a istnieje również metoda o klasie O(n 2.38 ). Ważne jest w takiej sytuacji określenie czy istnieje możliwość znalezienia jeszcze bardziej efektywnego algorytmu tzn. określenie dolnej granicy efektywności dla danego problemu (dla mnożenia macierzy jest to O(n 2 ) ).