• Nie Znaleziono Wyników

Chyba najważniejszym (a w każdym razie skutecznym w wielu przypadkach) środkiem do obliczania złożoności obliczeniowej różnych algorytmów iteracyjnych i rekurencyjnych jest rozwiązywanie równań różnicowych. Możemy użyć tego środka, jeśli dany algorytm rozwiązywania zadania o rozmiarze k > 1 wymaga rozwiązania ustalonej liczby zadań mniejszych (np. o rozmiarze k − 1 lub k/2), oraz wykonania pewnej liczby f(k) operacji w celu uzyskania końcowego rozwiązania.

4.9

Niech liczby a0, . . . , an−1 będą dane i niech dla każdego całkowitego k ≥ n ak= cn−1ak−1+· · · + c0ak−n+ f(k), (*) gdzie liczby c0, . . . , cn−1i funkcja f są ustalone. Równanie o podanej wyżej postaci nazywa się równaniem różnicowym liniowym rzędu n. Razem

z warunkiem początkowym, tj. danymi liczbami a0, . . . , an−1, określa ono jednoznacznie nieskończony ciąg liczbowy (ak)k=0. Koszty i złożoności wielu algorytmów (a także inne wielkości występujące w różnych zastosowaniach) można przedstawić za pomocą równań różnicowych. Aby uzyskać wynik w postaci jawnej (w której jest od razu widoczny na przykład rząd złożoności algorytmu), trzeba rozwiązać takie równanie.

Równanie o postaci

ak= cn−1ak−1+· · · + c0ak−n, (**)

nazwiemy równaniem jednorodnym. Łatwo możemy zauważyć, że jeśli ciągi (ak)k=0i (bk)k=0są rozwiązaniami równania (*), to różnica tych ciągów jest rozwiązaniem równania (**) i na odwrót: suma rozwiązania równania (*) i dowolnego rozwiązania odpowiadającego mu równania jednorodnego

(otrzymanego przez usunięcie składnika f(k)) też jest rozwiązaniem równania (*).

Metoda rozwiązywania równań różnicowych liniowych jest następująca. Należy znaleźć (np. przez odgadnięcie, co bywa łatwe dla pewnych funkcji f) dowolne rozwiązanie równania. Następnie należy znaleźć takie rozwiązanie równania jednorodnego, aby suma obu rozwiązań spełniała warunki początkowe. Zaczniemy od zbadania, jakie ciągi spełniają równania jednorodne.

Zbadamy hipotezę, że rozwiązanie równania jednorodnego ma postać ak= λk. Wstawiając to do (**), po podzieleniu stron przez λk−ni uporządkowaniu, dostaniemy tzw. równanie charakterystyczne:

λn− cn−1λn−1−· · · − c1λ − c0= 0,

czyli w(λ) = 0. Wielomian w stopnia n nazywa się wielomianem

charakterystycznym równania; liczba λ musi być jego miejscem zerowym. Jeśli wielomian ten ma n różnych (jednokrotnych) miejsc zerowych, to mamy n liniowo niezależnych ciągów liczbowych spełniających równanie jednorodne.

Przykład. Jednorodne równanie różnicowe drugiego rzędu Fk= Fk−1+ Fk−2 z warunkiem początkowym F0= 0, F1= 1określa znany ciąg Fibonacciego.

4.10

Podstawiając ak= λk, otrzymamy równanie kwadratowe λ2− λ − 1 = 0,

którego rozwiązaniami są liczby λ1=12(1 −√

5)i λ2= 12(1 +√

5). Dowolne rozwiązanie równania jednorodnego, w tym rozwiązanie poszukiwane, ma postać Fk= b1λk1+ b2λk2. Na podstawie warunków początkowych

F0= b1+ b2= 0, F1= b1λ1+ b2λ2= 1, skąd możemy obliczyć b1= −1

5, b2= 1

5, czyli ostatecznie Fk= 1

√5

 1 +√ 5 2

k

− 1 −√ 5 2

k!

Jeśli pierwiastki wielomianu w są zespolone, to (dla równania o współczynnikach rzeczywistych) występują w parach sprzężonych, (λi, λi), i możemy znaleźć dla każdej takiej pary dwa liniowo niezależne ciągi liczb rzeczywistych, (λki + λi

k)k=0 oraz (i(λki − λik))k=0, które spełniają równanie (**). Ten przypadek ma mniejsze znaczenie w obliczaniu kosztów algorytmów, bo rozwiązania odpowiadające zespolonym pierwiastkom równania charakterystycznego oscylują (przyjmując także wartości ujemne).

Jeśli wielomian w ma miejsca zerowe o krotności większej niż 1, to liniowo niezależnych ciągów geometrycznych spełniających równanie (**) jest mniej niż n, zatem może ich nie wystarczyć do znalezienia rozwiązania spełniającego dowolne warunki początkowe. Jeśli liczba λ jest miejscem zerowym wielomianu

charakterystycznego o krotności r, to ciąg (d0+ d1k +· · · + dr−1kr−1k jest rozwiązaniem. Aby spełnić warunek początkowy, należy odpowiednio dobraćk=0

współczynniki d0, . . . , dr−1.

Przykład. Rozwiążemy równanie ak= 2ak−1− ak−2 z warunkiem a0= 1, a1= 2.

Mamy w(λ) = λ2− 2λ + 1 = (λ − 1)2, skąd wynika, że liczba λ = 1 jest

pierwiastkiem wielomianu w o krotności 2. Zatem rozwiązanie ogólne ma postać (d0+ d1k)· 1k, i łatwo możemy sprawdzić, że ciąg arytmetyczny ak= k + 1spełnia to równanie i warunek początkowy.

Teraz zajmiemy się znajdowaniem rozwiązań szczególnych równania

niejednorodnego. Postać rozwiązania zależy od funkcji f. Jeśli f(k) = p(k) · µk, gdzie p jest wielomianem stopnia s (funkcje o tej postaci mają dla nas największe

4.11

znaczenie) i µ nie jest miejscem zerowym wielomianu charakterystycznego w, to istnieje rozwiązanie szczególne o postaci q(k) · µk, gdzie q jest wielomianem stopnia s. Jeśli liczba µ jest miejscem zerowym o krotności r wielomianu charakterystycznego, to pewne rozwiązanie szczególne równania (*) ma postać krq(k)· µk, gdzie wielomian q ma stopień s; współczynniki tego wielomianu otrzymamy, podstawiając odpowiednie wyrażenie do równania.

Przykład. Niech ak= 2ak−1+ k, oraz a0= 0. Funkcja f jest tu wielomianem stopnia 1, liczba µ = 1 nie jest miejscem zerowym wielomianu w(λ) = λ − 2, zatem pewne rozwiązanie szczególne ma postać ak= ck + d. Podstawiamy,

ck + d = 2 c(k − 1) + d + k, ck + d = 2ck + 2(d − c) + k,

−ck + 2c − d = k,

skąd wynika −c = 1, 2c − d = 0, czyli c = −1, d = −2. Rozwiązanie równania jednorodnego ak= 2ak−1ma postać ak= e· 2k, zatem rozwiązanie ogólne naszego równania niejednorodnego ma postać ak= −k − 2 + e· 2k. Na podstawie warunku początkowego −2 + e · 20= 0, skąd e = 2. Poszukiwanym rozwiązaniem równania jest ciąg ak= 2k+1− k − 2.

Więcej przykładów poznamy, analizując konkretne algorytmy.

4.12

Zadania i problemy

1. Udowodnij, że algorytm

for ( x = a, e = b, z = 1; ; ) { if ( e % 2 == 1 ) z *= x;

e /= 2;

if ( e == 0 ) break;

x *= x;

}

jest poprawnym algorytmem obliczania liczby z = abdla a, b ∈ N (o ile nie wystąpi nadmiar w obliczeniach); wszystkie zmienne są typu int.

Wskazówka: Przedstaw wykładnik b i kolejne wartości zmiennej e w układzie dwójkowym.

2. Wskaż w algorytmie z poprzedniego zadania instrukcje, które można uznać za operacje dominujące. Jaka jest złożoność tego algorytmu?

3. Algorytm FFT obliczania dyskretnej transformaty Fouriera (informacja, co to jest, będzie podana później) ciągu liczb (zespolonych) o długości n wykonuje

f(n) = cn(n1+ . . . + nk)działań arytmetycznych, gdzie c jest stałą dodatnią niezależną od n, natomiast n1, . . . , nk są to liczby pierwsze, których iloczynem jest n.

Znajdź funkcje monotoniczne g i h, takie że f(n) = O(g(n)) oraz f(n) = Ω(h(n)).

Czy istnieje taka funkcja monotoniczna k, że f(n) = Θ(k(n))?

4. Zadanie polega na obliczeniu iloczynu trzech macierzy: D = ABC, gdzie A ∈ Rm,n, B∈ Rn,k i C ∈ Rk,l. Podaj koszty (czasowe i pamięciowe) dwóch algorytmów rozwiązywania tego zadania oparte na „zwykłym” wzorze na iloczyn macierzy.

Pierwszy z tych algorytmów oblicza najpierw E = AB, a następnie D = EC, a drugi najpierw F = BC, a następnie D = AF.

Napisz podprogram w C, który na podstawie liczb m, n, k, l podaje informację, który z tych dwóch algorytmów wykonuje mniej działań.

5. Dla zadania mnożenia macierzy A ∈ Rm,ni B ∈ Rn,kdana jest tylko sumaryczna liczba współczynników tych macierzy: l = (m + k)n. Jaki może być maksymalny koszt mnożenia (w „zwykły” sposób) tych macierzy?

Porównaj otrzymany wynik z kosztem mnożenia macierzy, jeśli wiadomo, że m = n = k(tj. l = 2n2), a także w przypadku, gdy m = k = 1 (tj. l = 2n).

6. Na podstawie definicji udowodnij, że dowolny wielomian w zmiennej n stopnia k jest funkcją rzędu co najwyżej nk(czyli w(n) = O(nk)).

7. Oblicz złożoność pesymistyczną i optymistyczną algorytmu sortowania przez wstawianie (z pierwszego wykładu), przyjmując za operację dominującą porównywanie elementów sortowanego ciągu.

4.13

Zakładając, że wszystkie permutacje, które porządkują ciąg, są jednakowo prawdopodobne, oblicz złożoność średnią tego algorytmu.

Jakie są rzędy wszystkich tych złożoności?

8. Rozważmy zadanie mnożenia liczb n-cyfrowych (którego wynikiem jest liczba 2n-cyfrowa). Dodanie dwóch takich liczb jest wykonalne kosztem O(n) operacji, ponieważ trzeba wykonać odpowiednie działanie na n parach cyfr, do których dochodzą przeniesienia. Natomiast mnożenie w „zwykły” sposób zabiera O(n2) operacji; wszystkie cyfry jednego czynnika mnożymy przez każdą cyfrę drugiego czynnika, a potem trzeba dodać wyniki tych mnożeń (odpowiednio je przesuwając

— to jest algorytm mnożenia „pisemnego”).

Aby uzyskać algorytm o mniejszej złożoności, przedstawimy czynniki, które są liczbami n-cyfrowymi (dla n > 1), w postaci a + bx i c + dx, za pomocą liczb

⌈n/2⌉-cyfrowych a, b, c, d i odpowiednio dobranej liczby x (x = 2⌈n/2⌉albo x = 10⌈n/2⌉, zależnie od tego, jakiej podstawy układu używamy). Jeśli pomnożymy

(a + bx)(c + dx) = ac + (bc + ad)x + bdx2,

to sprowadzimy zadanie do czterech mnożeń liczb n/2-cyfrowych (dla uproszczenia zaniedbuję zaokrąglenie n/2 w górę); trzeba obliczyć ac, bc, ad i bd, a następnie wykonać dodawania (w czasie proporcjonalnym do n). W ten sposób otrzymujemy rekurencyjny algorytm mnożenia w czasie cn2, bo najwięcej czasu zabierają w nim mnożenia; 4 · c(n/2)2= cn2.

Jeśli jednak obliczymy

e = (a + b)(c + d) = ac + ad + bc + bd,

oraz iloczyny ac i bd, to możemy następnie obliczyć (bc + ad) = e − ac − bd.

W ten sposób sprowadziliśmy zadanie do obliczenia tylko trzech iloczynów liczb n/2-cyfrowych (ściślej biorąc, liczby a + c oraz b + d mogą być

(⌈n/2⌉ + 1)-cyfrowe, ale to zaniedbamy, tak samo jak koszt dodawań i odejmowań

— w dokładnych rachunkach oczywiście nie można robić takich zaniedbań, ale tu chodzi o przedstawienie idei).

Napisz odpowiednie równanie różnicowe i wyznacz na jego podstawie rząd złożoności opisanego wyżej algorytmu mnożenia liczb.

9. Algorytm „zwykły” mnożenia macierzy n × n wymaga wykonania 2n3− n2działań arytmetycznych (mnożeń i dodawań zmiennopozycyjnych).

Algorytm Strassena rozwiązuje to samo zadanie kosztem 7nlog27− 6n2działań.

Dla jakich n algorytm Strassena wykonuje mniej działań niż algorytm „zwykły”?

5.1