POGLAVLJE 8 Rekurzivni algoritmi U prošlom dijelu upoznali smo kako rekurzije možemo implementirati preko stogova, u ovom dijelu promotriti ćemo probleme koje se mogu izraziti na rekurzivan način Vremenska složenost takvih algoritama je izražena u obliku rekurzivnih formula 81 Algoritmi s rekurzijama U poglavlju o stogovima, vidjeli smo da upravo stogovi omogućuju funkijske pozive unutar drugih funkija, pa je rekurzivno pozivanje funkije speijalan slučaj tog svojstva Promotrimo na primjeru računanja faktorijela Sljedeći pseudokod predstavlja algoritam za nalaženje n! (za bazni kriterij je uzet n = 1, isto tako se može uzeti n = 0): Fatorial(n) 1 if n = 1 2 then return 1 3 else return n Fatorial(n 1) Neka je T(n) funkija vremenske složenosti Fatorial algoritma za broj n Vremensku složenost možemo sada iskazati rekurzivno kao O(1), n 1 T(n 1) + O(1), inače Svaki rekurzivni poziv predstavlja novu funkiju koja ima izvršiti neko računanje Bitna stvar je uočiti slijed izvršavanja rekurzivnih poziva Radi jednostavnosti praćenja rekurzivnih poziva ponekad se izvršenje rekurzivnog algoritma bilježi preko rekurzivnog stabla Rekurzivno stablo za Fatorial(5) = 120 i određivanje vremenske složenosti prikazano je na Slii 81 Ukupno vrijeme potrebno za algoritam je zbroj ijena potrebnih po svakom nivou rekurzivnog stabla, u ovom slučaju n + T(1) = (n 1) + d = O(n) i=2 73
74 Rekurzivni algoritmi 5 24 Fatorial(5) Fatorial(4) Fatorial(3) Fatorial(2) 4 6 3 2 2 1 T(n 1) Fatorial(1) 1 T(1) Slika 81: Rekurzivno stablo izvršenja algoritma gdje su, d > 0 konstante odabrane kako bi zamijenile asimptotsku oznaku O(1) Problemi koji se na prvi pogled čine da su prirodno definirani rekurzivno, ne podrazumjevaju i efikasan algoritam po toj definiiji Pogledajmo na primjeru Fibonaijeva niza Primjer 81 Fibonaijev niz je niz brojeva 1, 1, 2, 3, 5, 8 koji se može rekurzivno zapisati u sljedećem obliku: Neka je s F(n) označen n-ti Fibonaijev broj, sljedeća rekurzija nam daje način nalaženja n-tog Fibonaijevog broja: 1 n 2 F(n) = (81) F(n 1) + F(n 2) inače Sljedeći algoritmi nalaze n-ti Finonaijev broj preko rekurzivnih poziva ili top-down pristupa Rješenje Fib(n) 1 if n 2 2 then return 1; 3 else return Fib(n 1) + Fib(n 2) Analizirajmo vremensku složenost algoritma Primjetimo da vremensku složenost možemo izraziti rekurzijom O(1) n 2 T(n 1) + T(n 2) + O(1) inače Raspisujući rekurzivno stablo u Slii 82 gdje, d zamjenjuju O(1) možemo vremensku složenost omeđiti odozdo i odozgo na sljedeći način: s ograničenjima na T L, T R : T L (n) T(n) T U (n) T L (n) n/2 1 i=0 2 i = (2 n/2 1) T U (n) n 1 i=0 2i = 2 n za odabran tako d Drugim riječima, T(n) kao funkija u ovisnosti o n raste eksponenijalno, te impliira da ovaj algoritam nije efikasan za nalaženje n-tog Fibonaijevog broja
82 "Podijeli-pa-vladaj" strategija 75 T (n) T (n 1) T (n 2) T (n 2) T (n 3) T (n 3) T (n 4) T L T U T (1) T (1) Slika 82: Rekurzivno stablo za Fibonaijev algoritam Jednostavniji i efekasniji način bi bio izgraditi Fibonaijev niz odozdo-prema-gore u O(n) vremenu: Fib(n) n-ti Fibonaijev broj u O(n) vremenu 1 F 1 1 2 F 2 1 3 for k 3 to n 4 do F F 1 + F 2 5 F 1 F 2 6 F 2 F 7 return F 82 "Podijeli-pa-vladaj" strategija Rekurzivni način rješavanja problema je temelj podijeli-pa-vladaj (engl devide-and-onquer) strategije u dizajniranju algoritama Strategija se sastoji od sljedećih koraka: 1 podijeli: podijeli problem u potprobleme koji su manje instane istog tipa originalnog problema 2 vladaj: rekurzivno riječi potprobleme 3 spoji: na odgovarajući način spoji rješenja potproblema
76 Rekurzivni algoritmi Promotrimo ovu strategiju na sljedećem primjeru: Primjer 82 Napišite naivni algoritam koji računa n-tu poteniju broja a Dajte O(lg n) algoritam koji će nalaziti n-tu poteniju broja a Rješenje Po matematičkoj definiiji možemo napisati rekurzivni (ali i iterativni) algoritam u O(n) vremenu za poteniranje broja a > 0 Sljedeća 2 algoritma nam nalaze a n, n N, a > 0 Power(a, n) 1 if n = 1 2 then return a 3 else return a Power(a, n 1) Pokušajmo na brži način naći a n : Power(a, n) 1 if n = 1 2 then return a 3 else if n even 4 then return (Power(a, n/2)) 2 5 else return a (Power(a, (n 1)/2)) 2 Primjetite da je vrijeme izvršenja prvog algoritma O(n) (analogno zaključivanje kao i kod faktorijela) Tvrdnja: Vrijeme izvršenja alternativnog algoritma za poteniranje broja je O(lg n) Kako bi ovo pokazali koristiti ćemo rekurzivno stablo Uzmimo da je n prava potenija broja 2 Vrijeme izvršenja algoritma je onda O(1), n = 1 T(n/2) + O(1), inače Postoji nekoliko načina kako je moguće utvrditi zatvorenu formulu za rekurziju 82, detaljnije se može naći u [2] Mi ćemo se ovdje poslužiti metodom rekurzivnog stabla i radi jednostavnosti uzmimo O(1) = (82) T(n) T(n/2) T(n/4) T(1) lg n 1 i=0 + T(1) lg n + d = O(lg n)
82 "Podijeli-pa-vladaj" strategija 77 Dakle, problem smo podijelili na manje instane veličine n/2, i zatim spojili rješenje kvadriranjem člana a n/2 Kvadriranje u RAM modelu je O(1) vrijeme U slučaju da je n nije prava potenija broja 2, osim kvadriranja imali bi dodatno množenje s a što daje najviše 3 množenja: T( n/2 ) + O(1) što se matematičkom indukijom može pokazati da vrijedi O(lg n) Podijeli-pa-vladaj strategija nam može ubrzati sortiranje Primjer 83 Neka je dano polje A od n elemenata Proedura MergeSort opisana u koraima: 1 podijeli polje veličine n u 2 podpolja veličine n/2 2 rekurzivno sortiraj podpolja veličine n/2 3 rješenje kombiniraj tako da spojiš 2 sortirana podpolja u novo sortirano podpolje ili u pseudokodu: MergeSort(A, p, r) 1 if p < r 2 then q p+r 2 3 MergeSort(A, p, q) 4 MergeSort(A, q + 1, r) 5 Merge(A, p, q, r) sortira polje A u O(n lg n) vremenu Učinite sljedeće: napišite proeduru Merge(A, p, q, r) koja će u O(n) vremenu spojiti 2 sortirana potpolja A[p q], A[q + 1 r] u sortirano polje A[p r] Primjenite algoritam na polju A = 5, 2, 4, 7, 1, 3, 2, 6, 11 Pokažite da je vremenska složenost algoritma O(n lg n) Rješenje Riječ je o podijeli-pa-vladaj algoritmu koji dijeli problem u O(1) vremenu, rekurzivno rješava 2 potproblema veličine n/2 i spaja rekurzivna rješenja u O(n) vremenu Definirajmo sada proeduru spajanja:
78 Rekurzivni algoritmi Merge(A, p, q, r) 1 n 1 q p + 1 2 n 2 r q 3 stvori pomoćna polja L[1 n 1 + 1] and L[1 n 2 + 1] 4 for i 1 to n 1 5 do L[i] A[p + i 1] 6 for j 1 to n 2 7 do R[j] A[q + j] 8 L[n 1 + 1] 9 L[n 2 + 1] 10 i 1 11 j 1 12 for k p to r 13 do if L[i] R[j] 14 then A[k] L[i] 15 i i + 1 16 else A[k] R[j] 17 j j + 1 Ilustraija rada MergeSort algoritma na primjeru A = 5, 2, 4, 7, 1, 3, 2, 6, 11 prikazano je na Slii 83 5 2 4 7 1 3 2 6 11 1 9 5 2 4 7 1 3 2 6 11 1 4 5 9 5 2 4 7 1 3 2 6 11 1 2 3 4 5 6 7 9 5 2 4 7 1 3 2 6 11 8 9 MergeSort(A, p, r) Merge(A, p, q, r) 6 11 6 11 2 5 4 7 1 3 2 6 11 2 5 4 7 1 3 6 7 11 1 2 2 3 4 5 6 7 11 Slika 83: MergeSort algoritam na primjeru A = 5, 2, 4, 7, 1, 3, 2, 6, 11 Indeksi polja označavaju vrijednosti p, r za svaki rekurzivni poziv
82 "Podijeli-pa-vladaj" strategija 79 Vrijeme izvršenja MergeSort alogirtma je opisano rekurzijom: O(1), n = 1 T( n/2 ) + T( n/2 ) + O(n), inače (83) Prethodnu rekurziju možemo pojednostaviti, štoviše, može se pokazati da asimptotska ojena se neće promijeniti ukoliko koristimo (84) umjesto (83) U [1] je dano formalno opravdavanje ove relaksaije O(1), n = 1 (84) 2T(n/2) + O(n), inače i pokazati da je ono O(n lg n) prema rekurzivnom stablu prikazano u Slii 84 (pojednostavljeno, uzeto je = d): lg n 1 i=0 n }} ijena stabla bez listova (n + 1) lg n + (d + 1)n = O(n lg n) + 2 lg n T(1) }} ijena u listovima Primjer 84 Neka su x, y dva n-bitna ijela broja Zbog jednostavnosti pretpostavite neka je n neka potenija broja 2 Pokažite kako možemo pomnožiti 2 n-bitna ijela broja metodom "podijeli pa vladaj" Primjetite da svaki ijeli broj zapisan u n bitova možemo zapisati na sljedeći način: x = x L x R = 2 n/2 x L + x R y = y L y R = 2 n/2 y L + y R i definirati množenje dva ijela broja rekurzivno Npr broj x u binarnom zapisu je 10110110 2 se može zapisati kao x = 1011 2 2 4 + 0110 2 a) Napišite rekurzivni(devide-and-onquer) algoritam u O(n 2 ) vremenu za množenje 2 ijela broja od n bita b) Pokažite kako se algoritam može ubrzati u O(n lg 3 ) vremenu na način da se broj rekurzivnih poziva smanji s 4 na 3 ) Napišite pseudokod za algoritam Rješenje ad a) Napisati ćemo prvo deskriptivno devide-and-onquer algoritam Uočimo: x y = (2 n/2 x L + x R )(2 n/2 y L + y R ) = 2 n x L y L + 2 n/2 (x L y R + x R y L ) + x R y R i ako označimo vidimo da P 1 = x L y L, P 2 = x L y R, P 3 = x R y L, P 4 = x R y R (85) xy = 2 n P 1 + 2 n/2 (P 2 + P 3 ) + P 4 (86)
80 Rekurzivni algoritmi Slika 84: raspis rekurzije vremenske složenosti u koraima (a),(b),(),(d) za Merge-Sort algoritam gdje su P 1, P 2, P 3, P 4 definirani rekurzivno kao (85) i predstavljaju množenje n/2 bitna broja Utvrdimo vremensku složenost algoritma T(n) Problem smo podijelili na instane veličine n/2 u svakom rekurzivnom pozivu, i rješenje kombiniramo po formuli (86) Korak kombiniranja rješenja je O(n) zato što umnožak binarnog broja x s 2 k je implementirano kao bitovni pomak za k mjesta ulijevo u registrima zapisa broja x
82 "Podijeli-pa-vladaj" strategija 81 Vrijeme izvršenja algoritma je izraženo rekurzijom: O(1), n = 1 4T(n/2) + O(n), inače (87) Uz pomoć rekurzivnog stabla utvrdimo asimptotsku ojenu na rekurziju Izdvojimo zadnju sliku: Ideja je sumirati ijenu svih operaija po svim nivoima (od korijena stabla do 1n 2n 4n log 2 n 4 lg n = n lg 4 listova) U našem primjeru imamo lg n 1 (4/2) i n + 4 lg n T(1) }} i=0 }} ijena u listovima ijena stabla bez listova (2 lg n 1)n + 4 lg n d = (2 2 lg n 1)n + 4 4 lg n d n 2 + n 2 d = O(n 2 ) ad b) Primjetite kako član n lg 4 dolazi od svojstva da imamo 4 rekurzivna množenja i u svakom novom rekruzivnom pozivu smanjujemo instanu problema za pola Pokušajmo umjesto 4 operaije množenja pronaći način da koristimo samo 3 množenja! Ideja: x, y C, xy = (a + bi)( + di) = a bd + (ad + b)i ima 4 množenja, uz supstituiju ad + b = (a + b)( + d) a bd pišemo xy = a bd + ((a + b)( + d) a bd)i što zahtjeva 3 množenja!
82 Rekurzivni algoritmi Primjenimo ideju na naš primjer: x L y R + x R y L uvrstimo u (86) imamo: = (x L + x R )(y L + y R ) x L y L x R y R i x y = (2 n/2 x L + x R )(2 n/2 y L + y R ) = 2 n x L y L + 2 n/2 [(x L + x R )(y L + y R ) x L y L x R y R ] + x R y R i uz P 1 = x L y L, P 2 = x R y R, P 3 = (x L + x R )(y L + y R ) možemo sada zapisati xy = 2 n P 1 + 2 n/2 (P 3 P 1 P 2 ) + P 2 što sadrži 3 množenja! Vremensku složenost možemo sada zapisati kao rekurziju O(1), n = 1 3T(n/2) + O(n), inače (88) Tvrdnja: Vrijeme izvršenja devide-and-onquer algoritma s 3 množenja je O(n lg 3 ) Vrijeme izvršenja algoritma možemo analizirati na rekurzivnom stablu Sumiramo ijene n n n/2 n/2 n/2 3/2n h = lg n n/4 n/4 9/4n T (1) T (1) w T (1) w = 3 h = 3 lg n = n lg 3 ; po svim nivoima+ijena u listovima (radi jednostavnosti, O(1) = d, O(n) = n): lg n 1 (3/2) i n + 3 lg n T(1) }} i=0 }} ijena u listovima ijena stabla bez listova ((3/2) lg n 1)n + n lg 3 d (3/2) lg n n + n lg 3 d = n lg 3/2 + n lg 3 d = O(n lg 3 )
82 "Podijeli-pa-vladaj" strategija 83 ad ) Zapišimo sada drugi algoritam u pseudokodu: Multiply(x, y) 1 n = maxsize[x], size[y]} u slučaju da x, y nisu jednake duljine 2 if n = 1 3 then return xy 4 x L, x R leftmost n/2, rightmost n/2 5 y L, y R leftmost n/2, rightmost n/2 6 P 1 Multiply(x L, y L ) 7 P 2 Multiply(x R, y R ) 8 P 3 Multiply(x L + x R, y L + y R ) 9 return P 1 2 n + (P 3 P 1 P 2 ) 2 n/2 + P 2 S time je ovaj primjer završen