RAČUNALNIŠTVO IN INFORMACIJSKE TEHNOLOGIJE OSNOVE ALGORITMOV NIKOLA GUID Fakulteta za elektrotehniko, računalništvo in informatiko Maribor, 2011 Kazalo 1 Deli-in-vladaj 1.1 Splošna strategija . . . . . . . . . 1.2 Hitro uredi . . . . . . . . . . . . 1.3 Uredi z zlivanjem . . . . . . . . . 1.4 Dvojiško iskanje . . . . . . . . . . 1.5 Množenje matrik z deli in vladaj 1.6 Strassenovo množenje matrik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1-1 1-1 1-6 1-16 1-21 1-24 1-25 Poglavje 1 Deli-in-vladaj Mnogo algoritmov uporablja pristop deli-in-vladaj, ki omogoča uporabo rekurzije. V literaturi često namesto besede pristop (approach) uporabljamo besedi paradigma (paradigm) ali strategija (strategy). Najprej razložimo obravnavani pristop: Če je problem zahteven, ga delimo na podprobleme toliko časa, da znamo njihovo rešitev preprosto poiskati. Omejimo se najprej na probleme, ki jih razcepimo v podprobleme, podobne izvirnemu problemu. Tedaj lahko uporabimo rekurzijo. Nekateri problemi zahtevajo po rešitvi podproblemov še posebno proceduro, ki združi rešitve podproblemov, drugi pa ne. 1.1 Splošna strategija Brez izgube splošnosti predpostavimo, da problem razdelimo na dva podproblema. Zapišimo pristop deli-in-vladaj v obliki procedure DELI-IN-VLADAJ: DELI-IN-VLADAJ(A, dno, vrh) 1 if PROBLEM-MAJHEN(dno, vrh) % ugotavljanje majhnosti problema 2 then RESI(A, dno, vrh) % reševanje majhnega problema 3 else s ← DELI(A, dno, vrh) % poišči indeks delilnega elementa 4 DELI-IN-VLADAJ(A, dno, s) % reši levo podzaporedje 5 DELI-IN-VLADAJ(A, s + 1, vrh) % reši desno podzaporedje 6 ZLIJ(A, dno, s, vrh) % združi dobljeni rešitvi dno pomeni najmanjši indeks v nekem podzaporedju zaporedja A, vrh pa najvišji indeks v istem podzaporedju. Če hočemo nekaj opraviti na celotnem zaporedju A dolžine n, je začetni klic procedure DELI-IN-VLADAJ(A, 1, n). 1-2 1.1 Splošna strategija Znotraj procedure DELI-IN-VLADAJ, ki ima dva rekurzivna klica, imamo štiri druge procedure: • Procedura PROBLEM-MAJHEN pove, kdaj je problem dovolj razcepljen, da ga enostavno rešimo. To je največkrat tedaj, ko sta dno in vrh kvečjemu za 1 narazen. • Procedura RESI je namenjena reševanju majhnega problema. • Procedura DELI pove, kako razcepimo prevelik problem v dva manjša. Intuitivno sklepamo, da bo najbolje deliti na podprobleme enakih velikosti, vendar to ni splošno pravilo. • Procedura ZLIJ sestavi rešitev obeh podproblemov v globalno rešitev. Včasih je rešitev podproblema že rešitev prvotnega problema, zato združevanje ni potrebno. Časovna zahtevnost splošnega algoritma, ki uporablja pristop deli-invladaj Označimo s T (n) časovno zahtevnost problema velikosti n, s T (s) pa časovno zahtevnost velikosti s. Časovna zahtevnost problema velikosti n je enaka vsoti časovnih zahtevnosti levega podproblema, desnega podproblema, procedure DELI in procedure ZLIJ: T (n) = T (s) + T (n − s) + TDELI + TZLIJ (1.1) Zahtevnost je sorazmerna globini drevesa rekurzivnih klicev. Če delimo probleme na podprobleme enakih velikosti, ima drevo stanj najmanjšo globino in s tem tudi najmanjšo časovno zahtevnost. Pri določanju zahtevnosti algoritmov, ki uporabljajo pristop deli-in-vladaj, nam pomaga naslednji izrek, ki ga bomo tudi dokazali: Izrek 1.1. Naj bodo a, b, c in T1 nenegativne konstante. Naj bo n potenca števila c, tj. n = ck . Tedaj je rešitev rekurzije: { T1 , ( ) n=1 (1.2) T (n) = n r aT c + bn , n > 1 dana z O(nr ), O(nr log2 n), T (n) = O(nlogc a ), a < cr a = cr a > cr (1.3) 1-3 1.1 Splošna strategija V enačbah ( ) 1.2 in 1.3 pomeni T1 = T (1) zahtevnost pri problemu velikostir n = 1, T n c zahtevnost enega podproblema, a število podproblemov in bn vsoto zahtevnosti procedur DELI in ZLIJ. Dokaz: Rekurzija naj ima naslednjo obliko (enačba 1.2): (n) T (n) = aT + bnr . c Ker je n = ck , lahko zapišemo: T (ck ) = aT (ck−1 ) + bckr . (1.4) Določimo vrednost enačbe 1.4 pri različnih vrednosti k: k=1: T (c) = aT (1) + bcr = aT1 + bcr k=2: T (c2 ) = aT (c) + bc2r = a(aT1 + bcr ) + bc2r = a2 T1 + abcr + bc2r ( a) = a2 T1 + bc2r 1 + r c 3 2 T (c ) = aT (c ) + bc3r = a(a2 T1 + abcr + bc2r ) + bc3r ) ( a a2 3 2 r 2r 3r 3 3r = a T1 + a bc + abc + bc = a T1 + bc 1 + r + 2r c c k=3: Iz rešitev pri različnih k lahko, če ck nazaj nadomestimo z n, intuitivno zapišemo splošno formulo, ki je rešitev rekurzivne enačbe 1.4: T (n) = ak T1 + bnr k−1 ( ) ∑ a i i=0 cr . (1.5) ———————————————————————————————————Pri dokazovanju potrebujemo naslednjo enačbo, ki jo bomo dokazali: ak = nlogc a (1.6) Dokaz: Iz pogoja n = ck lahko zapišemo k = logc n. Tako je: ak = alogc n . (1.7) Če enačbo 1.7 logaritmiramo, dobimo: logc ak = logc n · logc a = logc a · logc n = logc nlogc a , (1.8) 1-4 1.1 Splošna strategija potem ko smo zamenjali vrstni red množenja. Osnovi v enačbi 1.8 sta enaki, zato sta enaka tudi logaritmanda: ak = nlogc a . ♢ ———————————————————————————————————- Vstavimo enačbo 1.6 v enačbo 1.5 in dobimo: T (n) = T1 nlogc n + bnr k−1 ( ) ∑ a i i=0 cr . (1.9) 1 , ko gre k → ∞ . Tako a 1− r c imamo: 1 T (n) ≤ T1 nlogc a + bnr (1.10) a. 1− r c Neenakost dobimo, ker je vsota vrste pri k = ∞ večja od vsote vrste pri k < ∞. Ker je a < cr , velja logc a < r. Tako ima prvi sumand manjšo stopnjo potence obsežnosti problema n kot drugi, zato ga izpustimo. Dobimo: a) Za a < cr vsota vrste v enačbi 1.9 konvergira v T (n) = O(nr ). ♢ (1.11) b) Za a = cr velja: logc a = logc cr = r in k−1 ( ) ∑ a i i=0 cr = k−1 ∑ 1i = k. i=0 Tako lahko zapišemo enačbo 1.9 v obliki: T (n) = T1 nr + bnr k. (1.12) Če enačbo n = ck logaritmiramo z dvojiškim logaritmom, dobimo naslednjo vrednost za k: log2 n k= . (1.13) log2 c Iz enačb 1.12 in 1.13 izpeljemo: T (n) = T1 nr + b nr log2 n = O(nr log2 n). ♢ log2 c (1.14) 1-5 1.1 Splošna strategija c) Za a > cr uporabimo formulo: 1+ ( a )2 a + r cr c + ... + ( a )k−1 cr ( a )k = −1 cr . a − 1 cr (1.15) Če vstavimo enačbo 1.15 v enačbo 1.9, dobimo: ( a )k −1 r . T (n) = T1 nlogc a + bnr ca − 1 cr (1.16) Če upoštevamo enačbo 1.6 in pogoj n = ck v enačbi 1.16, lahko zapišemo: ( a )k −1 r ak − ckr T (n) = T1 ak + bckr ca = T 1 ak + b a . − 1 − 1 cr cr (1.17) Drugi sumand v enačbi 1.17 lahko aproksimiramo z O(ak−1 ), tako da lahko celoten izraz na koncu ocenimo kot: T (n) = O(ak ). (1.18) Z upoštevanjem enačbe 1.6 lahko izraz 1.18 zapišemo kot: ( ) T (n) = O nlogc a . ♢ (1.19) 1.2 Hitro uredi 1.2 1-6 Hitro uredi Hitro uredi (quicksort) je algoritem urejanja, ki temelji na pristopu deli-in-vladaj in omogoča uporabo rekurzije. Njegova glavna značilnost je, da ne potrebuje združevanja rešitev podproblemov. Že v naslednjem razdelku bomo spoznali algoritem urejanja (procedura UREDI-Z-ZLIVANJEM), ki pa zahteva združevanje. Problem urejanja (sorting), ki nas zanima, naj bo naslednje vrste: Vhod: zaporedje n števil ⟨A[1], A[2], . . . , A[n]⟩. ′ ′ ′ Izhod: permutacija (ali ponovna razvrstitev) ⟨A [1], A [2], . . . , A [n]⟩ vhodnega ′ ′ ′ zaporedja tako, da velja A [1] ≤ A [2] ≤ . . . ≤ A [n]. Rečemo tudi, da je izhodno zaporedje urejeno nepadajoče. V nepadajočem zaporedju je element z višjim indeksom kvečjemu enak, ne more pa biti manjši od elementa z manjšim indeksom (npr. ⟨2, 4, 4, 7, 15⟩). Vrnimo se k našemu algoritmu HITRO-UREDI, ki neurejeno zaporedje A uredi v nepadajočem vrstnem redu. Poglejmo si osnovni koncept delovanja: poljubno neurejeno zaporedje razdeli na dva dela, tako da izbrani element zaporedja (rečemo mu tudi delilni element (partition element)) postavi na pravo mesto v končni rešitvi. Tako dobimo dve podzaporedji, eno levo od delilnega elementa in drugo desno od delilnega elementa. V levem podzaporedju so vsi elementi manjši ali kvečjemu enaki delilnemu elementu, v desnem podzaporedju pa so vsi elementi večji ali kvečjemu enaki delilnemu elementu. Delitev ponavljamo, dokler v podzaporedju ne dobimo kvečjemu enega elementa. Algoritem hitro uredi izvede naslednja procedura: HITRO-UREDI(A, dno, vrh) 1 if dno < vrh % ali je problem dovolj majhen? 2 then j ← DELI(A, dno, vrh) % poišči indeks delilnega elementa 3 HITRO-UREDI(A, dno, j − 1) % uredi levo podzaporedje 4 HITRO-UREDI(A, j + 1, vrh) % uredi desno podzaporedje dno pomeni najmanjši indeks v zaporedju A, vrh pa najvišji indeks v istem zaporedju. Če hočemo urediti celotno zaporedje A dolžine n, je začetni klic procedure HITRO-UREDI(A, 1, n). Ključno delo v proceduri HITRO-UREDI opravi procedura DELI. 1-7 1.2 Hitro uredi DELI(A, dno, vrh) 1 w ← A[dno] % izberi delilni element 2 i ← dno % postavi spodnji indeks 3 j ← vrh + 1 % postavi zgornji indeks 4 loop 5 repeat j ← j − 1 % pregleduj z desne v levo 6 until A[j] ≤ w 7 repeat i ← i + 1 % pregleduj z leve v desno 8 until A[i] ≥ w 9 if i < j 10 then zamenjaj elementa A[i] in A[j] 11 else zamenjaj elementa A[j] in A[dno] 12 return j % vrni novi indeks delilnega elementa w Zgled 1.1. Radi bi uredili zaporedje A[1..6] na sliki 1.1a. Ker je za razumevanje celotnega algoritma ključna procedura DELI, si poglejmo najprej njeno delovanje. Poglejmo si prvi klic te procedure DELI(A, 1, 6) na zaporedju A[1..6]. 1 a) 2 3 4 5 6 1 2 3 4 5 1 2 2 3 4 3 4 1 d) 6 j 2 3 4 5 6 7 1 6 0 4 8 j 5 5 7 1 6 8 4 0 i 6 7 1 6 0 4 8 i e) b) j i c) 1 7 1 6 8 4 0 j i 6 4 1 6 0 7 8 Slika 1.1: Delovanje procedure DELI na zaporedju ⟨7, 1, 6, 8, 4, 0⟩ 1. Vrstica 1: Najprej izberemo delilni element w = A[1] = 7. 2. Vrstica 2: Postavimo indeks i: i = 1 (slika 1.1a). 3. Vrstica 3: Postavimo indeks j: j = 7 (slika 1.1a). To je v bistvu indeks straže, ki mora imeti vrednost, večjo od vseh elementov v zaporedju. Le-ta je potrebna pri padajočem zaporedju, ko je prvi element največji element zaporedja. Stražo postavimo teoretično na vrednost ∞. 1.2 Hitro uredi 1-8 4. Vrstica 4: Vstopimo v prvo iteracijo zanke loop. Prva zanka v vrstici 5 repeat se ustavi pri j = 6 (slika 1.1b), saj je A[6] < w (0 < 7). 5. Vrstica 7: Druga zanka repeat se ustavi pri i = 4 (slika 1.1b), saj je A[4] > w (8 > 7). 6. Vrstica 9: Ker je i < j (4<6), se zamenjata elementa na položajih A[i] in A[j], tj. A[4] in A[6] ali 8 in 0. Novo stanje prikazuje slika 1.1c. 7. Vrstica 4: Vstopimo v drugo iteracijo zanke loop. Pri tem izhajamo iz trenutnih vrednosti indeksov i in j. Prva zanka v vrstici 5 repeat se ustavi pri j = 5 (slika 1.1d), saj je A[5] < w (4 < 7). 8. Vrstica 7: Druga zanka repeat se ustavi pri i = 6 (slika 1.1d), saj je A[6] > w (8 > 7). 9. Vrstica 9: Ker je i > j (6>5), se zamenjata elementa na položajih A[j] in A[dno], tj. A[5] in A[1] ali 4 in 7, in procedura DELI vrne vrednost j = 5. Novo stanje prikazuje slika 1.1e. Delilni element w je postavljen na končno mesto v urejenem zaporedju, saj je peti najmanjši element vhodnega zaporedja. Delilni element je razdelil vhodno zaporedje na dve podzaporedji. V levem podzaporedju so vsi elementi manjši od 7, v desnem pa vsi večji od 7. Sedaj, ko poznamo delovanje procedure DELI, se vrnimo na proceduro HITROUREDI. Za razlago delovanja si pomagajmo s sliko 1.2, kjer pokažemo vse rekurzivne klice procedure HITRO-UREDI in klice procedure DELI z njenimi rezultati. To sliko smo zreducirali v sliko rekurzivnih klicev procedure HITRO-UREDI (slika 1.3). 1-9 1.2 Hitro uredi HITRO-UREDI(A, 1, 6) 1 2 3 4 5 6 A 7 1 6 8 4 0 DELI(A, 1, 6) HITRO-UREDI(A, 1, 4) delilni element 4 1 6 0 7 8 j=5 HITRO-UREDI(A, 6, 6) A 4 1 6 0 DELI(A, 1, 4) delilni element 0 1 4 6 HITRO-UREDI(A, 1, 2) j=3 HITRO-UREDI(A, 4, 4) A 0 1 DELI(A, 1, 2) delilni element 0 1 HITRO-UREDI(A, 1, 0) j=1 HITRO-UREDI(A, 2, 2) Slika 1.2: Drevo rekurzivnih klicev procedure HITRO-UREDI in DELI na zaporedju ⟨7, 1, 6, 8, 4, 0⟩ 1.2 Hitro uredi Slika 1.3: 1-10 Drevo rekurzivnih klicev procedure HITRO-UREDI na zaporedju ⟨7, 1, 6, 8, 4, 0⟩ in je v bistvu poenostavljena slika 1.2 1-11 1.2 Hitro uredi 1. Začetni klic procedure je HITRO-UREDI(A, 1, 6). Ta klic ustreza korenu drevesa rekurzivnih klicev (slika 1.3). 2. Vrstica 1: Ker je dno < vrh (1<6), v vrstici 2 pokličemo proceduro DELI (A, 1, 6), ki deluje na zaporedju ⟨7, 1, 6, 8, 4, 0⟩. Njeno delovanje kaže slika 1.1. Ta nam vrne levo podzaporedje ⟨4, 1, 6, 0⟩ in desno podzaporedje ⟨8⟩ in indeks j = 5 (delilni element 7 postavi na lokacijo 5). 3. Vrstica 3: Vstopimo v proceduro HITRO-UREDI(A, 1, 4), ki deluje na globini 1 in mora urediti levo podzaporedje ⟨4, 1, 6, 0⟩. To je drugi klic in predstavlja levi sin v drevesu rekurzivnih klicev (slika 1.3). 4. Vrstica 1: Ker je dno < vrh (1<4), v vrstici 2 pokličemo proceduro DELI (A, 1, 4), ki deluje na zaporedju ⟨4, 1, 6, 0⟩. Njeno delovanje kaže slika 1.4. Ta nam vrne levo podzaporedje ⟨0, 1⟩, desno podzaporedje ⟨6⟩ in indeks j = 3 (delilni element 4 postavi na lokacijo 3). 1 a) 2 3 4 i 1 c) 2 2 2 3 4 4 1 6 0 i j 1 3 4 4 1 0 6 1 b) j i j e) 1 4 1 6 0 d) 2 3 4 4 1 0 6 j i 3 4 0 1 4 6 Slika 1.4: Delovanje procedure DELI na zaporedju ⟨4, 1, 6, 0⟩ 5. Vrstica 3: Vstopimo v proceduro HITRO-UREDI(A, 1, 2), ki deluje na globini 2 in mora urediti levo podzaporedje ⟨0, 1⟩. To je že tretji klic (slika 1.3). 6. Vrstica 1: Ker je dno < vrh (1<2), v vrstici 2 pokličemo proceduro DELI (A, 1, 2), ki deluje na zaporedju ⟨0, 1⟩. Njeno delovanje kaže slika 1.5. Ta nam vrne levo podzaporedje ⟨0⟩, desno podzaporedje ⟨⟩ in indeks j = 1 (delilni element 0 postavi na lokacijo 1). 7. Vrstica 3: Vstopimo v proceduro HITRO-UREDI(A, 1, 0) (četrti klic, slika 1.3), ki deluje na globini 3 in mora urediti levo podzaporedje, ki je prazno (glej sliko 1.5b). 1-12 1.2 Hitro uredi 1 a) 1 2 b) 0 1 i j 2 0 1 j i Slika 1.5: Delovanje procedure DELI na zaporedju ⟨0, 1⟩ 8. Vrstica 1: Ker je dno > vrh (1>0), zaključimo izvajanje te procedure na globini 3. Pri tem klicu se ne izvede več delitev, zato ta klic označimo s pravokotnikom (slika 1.3). 9. Vrstica 4: Vstopimo v proceduro HITRO-UREDI(A, 2, 2) (peti klic, slika 1.3), ki deluje na globini 3 in mora urediti desno podzaporedje ⟨1⟩, ki je sestoji iz enega samega elementa, tj. A[2] = 1 (glej sliko 1.5b). 10. Vrstica 1: Ker je dno = vrh (2=2), zaključimo izvajanje te procedure na globini 3. Tudi pri tem klicu se ne izvede več delitev, zato ta klic označimo s pravokotnikom (slika 1.3). 11. Vrstica 4: Vrnimo se na globino 2. Vstopimo v proceduro HITRO-UREDI (A, 4, 4) (šesti klic, slika 1.3). Ta uredi desno podzaporedje, ki je sestoji iz enega samega elementa, tj. A[4] = 6 (glej sliko 1.4e). 12. Vrstica 1: Ker je dno = vrh (4=4), zaključimo izvajanje te procedure na globini 2. Tudi pri tem klicu se ne izvede več delitev, zato ta klic označimo s pravokotnikom (slika 1.3). 13. Vrstica 4: Vrnimo se na globino 1. Vstopimo v proceduro HITRO-UREDI (A, 6, 6) (sedmi klic, slika 1.3), ki mora urediti desno podzaporedje, ki sestoji iz enega samega elementa, tj. A[6] = 8 (glej sliko 1.1e). 14. Vrstica 1: Ker je dno = vrh (6=6), zaključimo izvajanje te procedure na globini 1. Tudi pri tem klicu se ne izvede več delitev, zato ta klic označimo s pravokotnikom (slika 1.3). Na sliki 1.2 vidimo, da je bilo v našem primeru potrebno razdeliti zaporedje trikrat. Torej procedura DELI se je izvedla trikrat (glej preglednico 1.1). Pri prvem klicu DELI(1, 6) dobimo levo podzaporedje ⟨4, 1, 6, 0⟩ in desno podzaporedje ⟨8⟩. Levo podzaporedje še ni urejeno (tudi če bi bilo, o tem algoritem ne ve ničesar), zato ga je potrebno še urediti, desno podzaporedje pa sestoji samo iz enega elementa, zato ga ni potrebno več urejati. Pri drugem klicu DELI(1, 4) dobimo levo podzaporedje ⟨0, 1⟩ in desno podzaporedje ⟨6⟩. Levo podzaporedje še ni urejeno (čeprav je v našem primeru urejeno, 1-13 1.2 Hitro uredi Preglednica 1.1: Rezultati delovanja procedure DELI zap. št. klica 1 parametra DELI 1, 6 vhodno zaporedje 123456 716840 2 1, 4 4160 3 1, 2 01 izhodni zaporedji 1234 5 6 4| 1{z6 0} 7 |{z} 8 0 1 4 |{z} 6 |{z} 0 |{z} 1 tega algoritem ne ve), zato ga je potrebno še urediti, desno podzaporedje pa sestoji samo iz enega elementa, zato ga ni potrebno več urejati. Pri tretjem klicu DELI(1, 2) dobimo levo podzaporedje ⟨⟩ in desno podzaporedje ⟨1⟩. Levo podzaporedje je prazno, zato tu ni kaj za urejati, desno podzaporedje pa sestoji samo iz enega elementa, zato ga tudi ni potrebno več urejati. ♢ Časovna zahtevnost procedure HITRO-UREDI Če bi urejali zaporedje z istimi elementi, toda z drugačno razporeditvijo, bi v splošnem dobili drugačno drevo rekurzivnih klicev. To pomeni, da je časovna zahtevnost procedure HITRO-UREDI odvisna tako od velikosti kot od urejenosti zaporedja. a) Najneugodnejša časovna zahtevnost Poglejmo si najprej najneugodnejšo, tj. zgornjo mejo oz. najslabšo časovno zahtevnost. Pri analizi zahtevnosti upoštevajmo le primerjave elementov, te pa nastopajo le v proceduri DELI. Število primerjav pri vsakem klicu deli je vrh − dno + 1, če pa elementi niso različni pa je teh primerjav vrh − dno + 2. Na vsakem nivoju rekurzije izločimo po en delilni element. Najneugodnejši slučaj nastopi, ko DELI na vsakem koraku vrne eno od podzaporedij prazno (torej j = dno ali j = vrh). V začetku, torej na globini 0, imamo še n elementov in največ O(n) primerjav. Na globini 1 nam ostane zaporedje z n − 1 elementi, zato imamo največ O(n) primerjav itd. Tako je najneugodnejša časovna zahtevnost: TW (n) = O(n) + O(n − 1) + . . . + O(1). (1.20) Ker velja: 1 + 2 + 3 + ... + n = n2 n n(n + 1) = + , 2 2 2 (1.21) 1-14 1.2 Hitro uredi lahko enačbo 1.20 zapišemo kot: T (n) = TW (n) = O(n2 ). (1.22) b) Poprečna časovna zahtevnost Izračun poprečne časovne zahtevnosti zahteva precej matematične spretnosti in znanja, kljub temu pa si ga oglejmo. Postavimo, da ima delilni element w enako verjetnost, da je j-ti najmanjši v zaporedju A[1..n]. Verjetnosti dogodkov, da je j = i so enake: P (j = i) = 1 1 = . vrh − dno + 1 n Število primerjav elementov pri prvem klicu procedure DELI je največ: vrh − dno + 2 = n − 1 + 2 = n + 1. Velja: TA (0) = TA (1) = 0. in naslednja rekurzivna enačba: 1∑ [TA (j − 1) + TA (n − j)] . n n TA (n) = n + 1 + (1.23) j=1 Ker lahko j zavzame vse vrednosti od 1 do n z enako verjetnostjo, moramo upoštevati vseh n delitev na dva podproblema TA (j − 1) in TA (n − j). Zato vzamemo aritmetično sredino vseh n delitev. Enačbo 1.23 množimo z n: nTA (n) = n(n + 1) + 2[TA (0) + TA (1) + . . . + TA (n − 1)]. (1.24) V enačbi 1.24 nadomestimo n z n − 1: (n − 1)TA (n − 1) = n(n − 1) + 2[TA (0) + TA (1) + . . . + TA (n − 2)]. (1.25) Odštejmo enačbo 1.25 od enačbe 1.24: nTA (n) − (n − 1)TA (n − 1) = 2n + 2TA (n − 1). (1.26) Enačbo 1.26 delimo z n(n + 1) in dobimo: TA (n) TA (n − 1) 2 = + . n+1 n n+1 (1.27) 1-15 1.2 Hitro uredi V enačbi 1.27 nadomestimo n z n − 1 in dobimo: TA (n − 1) TA (n − 2) 2 = + . n n−1 n Vstavimo enačbo 1.28 v enačbo 1.27: TA (n) TA (n − 2) 2 2 = + + . n+1 n−1 n n+1 Na podoben način bi dobili: TA (n) n+1 = = TA (n − 3) 2 2 2 + + + n−2 n−1 n n+1 n+1 n+1 ∑ ∑1 1 TA (1) +2 =2 . 2 j j j=3 (1.28) (1.29) (1.30) j=3 Velja naslednja ocena, ki jo lahko hitro grafično potrdimo (slika 1.6): n+1 ∑ 1 ∫ n+1 dx 2 ≤ = ln(n + 1) − ln 2 < ln(n + 1). j x 2 (1.31) j=3 Slika 1.6: Grafična potrditev enačbe 1.31 Iz enačb 1.30 in 1.31 dobimo: TA (n) < 2(n + 1) ln(n + 1) = O(n log2 n) (1.32) Končno lahko zapišemo, da je poprečna časovna zahtevnost HITRO-UREDI: TA (n) = O(n log2 n) (1.33) Vidimo, da je poprečna časovna zahtevnost manjša od najneugodnejše zahtevnosti TW (n), kar nam potrdi uspešnost tega algoritma v praksi. 1.3 Uredi z zlivanjem 1.3 1-16 Uredi z zlivanjem V tem razdelku bomo spoznali algoritem urejanja, imenovan uredi z zlivanjem (merge-sort), ki prav tako uporablja princip deli-in-vladaj. Ta algoritem razdeli problem na približno dva enaka dela. Delitev se izvaja, dokler podproblem ni majhen (velikost enega elementa). Rešitev pa dobimo s postopnim združevanjem dveh rešitev podproblemov; torej, pomagamo si s proceduro ZLIJ. Zapišimo psevdokod procedure UREDI-Z-ZLIVANJEM: UREDI-Z-ZLIVANJEM(A, dno, vrh) 1 if dno < vrh 2 then s ← ⌊(dno + vrh)/2⌋ % poišči indeks sredine 3 UREDI-Z-ZLIVANJEM(A, dno, s) % uredi levo podzaporedje 4 UREDI-Z-ZLIVANJEM(A, s + 1, vrh) % uredi desno podzaporedje 5 ZLIJ(A, dno, s, vrh) % združi rešitvi dveh podproblemov Podobno kot pri proceduri HITRO-UREDI, kjer je glavno delo opravila procedura DELI, opravi pri proceduri UREDI-Z-ZLIVANJEM večino dela procedura ZLIJ: 1.3 Uredi z zlivanjem ZLIJ(A, dno, s, vrh) 1 h ← dno % postavi indeks levega urejenega podzaporedja 2 j ←s+1 % postavi indeks desnega urejenega podzaporedja 3 i ← dno % postavi indeks skupnega urejenega zaporedja 4 while (h ≤ s) and (j ≤ vrh) % dokler je še kaj elementov, jemljemo v novo zaporedje B % manjši element 5 do if A[h] ≤ A[j] 6 then B[i] ← A[h] 7 h←h+1 8 else B[i] ← A[j] 9 j ←j+1 10 i←i+1 % eno od podzaporedij je izčrpano 11 if h > s % izčrpano je levo podzaporedje, zato jemljemo v zaporedje B % preostale elemente iz desnega podzaporedja 12 then for k = j to vrh 13 do B[i] ← A[k] 14 i←i+1 % izčrpano je desno podzaporedje (j > vrh), zato jemljemo v % zaporedje B preostale elemente iz levega podzaporedja 15 else for k = h to s 16 do B[i] ← A[k] 17 i←i+1 % preložimo rezultat iz zaporedja B nazaj v zaporedje A 18 for k = dno to vrh 19 do A[k] ← B[k] 1-17 1-18 1.3 Uredi z zlivanjem Zgled 1.2. Radi bi uredili zaporedje A[1..6] na sliki 1.1a. Drevo rekurzivnih klicev procedure UREDI-Z-ZLIVANJEM prikazuje slika 1.7. Par vrednosti v vozliščih predstavlja dno in vrh podzaporedja, ki ga v danem trenutku urejamo. Notranja vozlišča so eliptične oblike in predstavljajo klice, kjer se izvede procedura ZLIJ. Listi drevesa so pravokotne oblike in pomenijo klice, ki se končajo brez delitve. Številka ob vozlišču pomeni vrstni red izvedbe klica. Pod vozliščem je označena tudi vrednost sredine s. globina 1 1, 6 s=3 0 2 7 1, 3 s=2 4, 6 s=5 6 3 1, 2 s=1 5 2, 2 11 8 3, 3 4 1, 1 1 4, 5 s=4 6, 6 9 2 10 4, 4 5, 5 3 Legenda: zaporedna številka klica klic procedure UREDI-Z-ZLIVANJEM, ki se konča z delitvijo: dno, vrh sredina s klic procedure UREDI-Z-ZLIVANJEM, ki se konča brez delitve: dno, vrh zaporedna številka klica Slika 1.7: Drevo rekurzivnih klicev procedure UREDI-Z-ZLIVANJEM na zaporedju ⟨7, 1, 6, 8, 4, 0⟩ 1-19 1.3 Uredi z zlivanjem Slika 1.8: Drevo klicev procedure ZLIJ na zaporedju ⟨7, 1, 6, 8, 4, 0⟩ Klice procedure ZLIJ kaže drevo na sliki 1.8. V vozliščih so označene vrednosti parametrov: dno, s in vrh. Številka ob vozlišču zunaj predstavlja vrstni red izvedbe procedure ZLIJ. Procedura ZLIJ se izvede petkrat (glej preglednico 1.2). Klic ZLIJ(1,1,2) pomeni prvi klic te procedure, ko se zlijeta levo podzaporedje ⟨A[1]⟩ = ⟨7⟩ in desno podzaporedje ⟨A[2]⟩ = ⟨1⟩. Rezultat je novo urejeno zaporedje, sestavljeno iz dveh elementov, tj. ⟨A[1], A[2]⟩ = ⟨1, 7⟩. Naslednji klic procedure ZLIJ(1,2,3) pomeni drugi klic te procedure, ko se zlijeta levo urejeno podzaporedje ⟨A[1], A[2]⟩ = ⟨1, 7⟩ in desno podzaporedje ⟨A[3]⟩ = ⟨6⟩. Rezultat je novo urejeno zaporedje, sestavljeno iz treh elementov, tj. ⟨A[1], A[2], A[3]⟩ = ⟨1, 6, 7⟩, itd. Preglednica 1.2: Rezultati delovanja procedure ZLIJ zap. št. klica 1 2 3 4 5 parametri ZLIJ 1, 1, 4, 4, 1, 1, 2, 4, 5, 3, 2 3 5 6 6 vhodni podzaporedji 1 2 3 4 5 6 ⟨7⟩⟨1⟩ ⟨1, 7⟩⟨6⟩ ⟨8⟩⟨4⟩ ⟨4, 8⟩⟨0⟩ ⟨1, 6, 7⟩ ⟨0, 4, 8⟩ izhodno zaporedje 1 2 3 4 5 6 ⟨1, 7⟩ ⟨1, 6, 7⟩ ⟨4, 8⟩ ⟨0, 4, 8⟩ ⟨0, 1, 4, 6, 7, 8⟩ 1-20 1.3 Uredi z zlivanjem Časovna zahtevnost algoritma UREDI-Z-ZLIVANJEM Drevo rekurzivnih klicev je odvisno samo od velikosti problema n, ne pa tudi od oblike podatkov. Zato so vse zahtevnosti, tj. najboljša, poprečna in najslabša, enake. Postavimo rekurzivno formulo za izračun časovne zahtevnosti: { 0, (⌊ ⌋) n=1 (⌈ ⌉) T (n) = (1.34) n n T +T + bn, n > 1 2 2 Če je n = 2k , dobimo iz izraza 1.34: (n) T (n) = 2T + bn, 2 za n > 1 (1.35) Po izreku 1.1 velja za a = cr (2 = 21 ), da je časovna zahtevnost: T (n) = O(n log2 n) = Ω(n log2 n) = Θ(n log2 n), (1.36) kar pomeni, da je poprečna časovna zahtevnost: TA (n) = O(n log2 n). (1.37) Tako poprečno časovno zahtevnost ima tudi algoritem HITRO-UREDI, dejansko pa je algoritem UREDI-Z-ZLIVANJEM dvakrat počasnejši od HITRO-UREDI, saj ima očitno dvakrat večjo vodilno konstanto. 1-21 1.4 Dvojiško iskanje 1.4 Dvojiško iskanje Dvojiško iskanje ali bisekcija (binary search) je zelo učinkovit algoritem za iskanje ključa v urejenem seznamu. Deluje tako, da primerja ključ K s srednjim elementom polja A[s]. Če je K = A[s], se algoritem ustavi, sicer se rekurzivno pokliče levo podzaporedje, če je K < A[s], in desno podzaporedje, če je K > A[s]. Iterativna oblika algoritma predstavlja naslednji psevdokod: DVOJISKO-ISKANJE(A, n, K, resitev) 1 dno ← 1 2 vrh ← n 3 while dno ≤ vrh 4 do s ← ⌊(dno + vrh)/2⌋ 5 if K = A[s] 6 then return resitev ← s 7 else if K < A[s] 8 then vrh ← s − 1 9 else dno ← s + 1 10 return resitev ← −1 % ključ smo našli % ključa nismo našli Če je ključ v seznamu, nam algoritem vrne indeks elementa, ki je identičen kjuču. Če ključa v seznamu ni, algoritem vrne vrednost −1. Zgled 1.3. Dano naj bo zaporedje A = ⟨3, 6, 7, 8, 11, 12, 20⟩. Poiščimo ključ K = 11. Delovanje procedure ponazarja preglednica 1.3. Preglednica 1.3: Delovanje procedure DVOJISKO-ISKANJE pri iskanju ključa K = 11 dno 1 5 vrh 7 s 4 resitev št. it. while 1. 6 5 2. 5 5 3. opomba s = ⌊(1 + 7)/2⌋ = 4 Ker je K > A[4], je dno = s + 1 = 5. s = ⌊(5 + 7)/2⌋ = 6 Ker je K < A[6], je vrh = s − 1 = 5. s = ⌊(5 + 5)/2⌋ = 5 Ker je K = A[5], je resitev = 5 Poiščimo ključ K = 4. Delovanje procedure ponazarja preglednica 1.4. To je primer neuspešnega iskanja. Delovanje procedure DVOJISKO-ISKANJE lahko ponazorimo z dvojiškim odločitvenim drevesom. Odločitveno drevo je odvisno samo od velikosti problema n, 1-22 1.4 Dvojiško iskanje Preglednica 1.4: Delovanje procedure DVOJISKO-ISKANJE pri iskanju ključa K=4 dno 1 vrh 7 3 s 4 resitev št. it. while 1. 2 1 2. 1 2 3. −1 opomba s = ⌊(1 + 7)/2⌋ = 4 Ker je K < A[4], je vrh = s − 1 = 3. s = ⌊(1 + 3)/2⌋ = 2 Ker je K < A[2], je vrh = s − 1 = 1. s = ⌊(1 + 1)/2⌋ = 1 Ker je K > A[1], je dno = s + 1 = 2 Ker je dno > vrh, je resitev = −1. nikakor pa ne od podatkov v urejenem seznamu. Na sliki 1.9 je prikazano odločitveno drevo za n = 7. Notranja vozlišča so označena s krogci, zunanja pa s kvadratki. Znotraj krogcev je številka elementa v zaporedju. V oglatem oklepaju ob vozliščih je predstavljen tekoči par ⌊dno, vrh⌋. Krogci predstavljajo uspešna iskanja, medtem ko kvadratki neuspešna iskanja. Ključ K = 11 smo našli s tremi iteracijami zanke while, kar pomeni, da leži na globini drevesa h = 2. Da ključa K = 4 ni v elementu, smo ugotovili tudi s tremi iteracijami zanke while. V drevesu na sliki smo končali pri listu (kvadratku), označenem z [2, 1]. Slednji list leži na globini 3. Od tod lahko sklepamo, da je časovna zahtevnost odvisna od globine vozlišča. ♢ Slika 1.9: Dvojiško odločitveno drevo za n = 7 1-23 1.4 Dvojiško iskanje Časovna zahtevnost procedure DVOJISKO-ISKANJE Najprej proučimo časovno zahtevnost dvojiškega iskanja. V najboljšem primeru lahko najdemo iskani element z eno samo iteracijo: T (n) = Ω(1). V najslabšem primeru končamo v vozlišču z globino h. Za polno dvojiško drevo velja: h = log2 (n + 1) − 1. Za poravnano drevo velja: h = ⌈log2 (n + 1) − 1⌉. Od tod sklepamo, da je zgornja meja časovne zahtevnosti: T (n) = O(log2 n). Tudi pričakovana časovna zahtevnost je istega reda (izpeljava sledi iz povprečnega nivoja za eno vozlišče v drevesu): TA (n) = O(log2 n). Proučimo še neuspešna iskanja, ki se končajo v vozliščih, označenih s kvadratki. Ta ležijo na globini h oziroma h + 1. Zato sklepamo, da je časovna zahtevnost za neuspešna iskanja: T (n) = O(log2 n). 1.5 Množenje matrik z deli in vladaj 1.5 1-24 Množenje matrik z deli in vladaj Matriki A in B naj bosta reda n × n, kjer je n = 2k . Pri množenju matrik bomo uporabili strategijo deli-in-vladaj. Vsako od matrik A in B razbijemo v štiri matrike reda n2 × n2 : [ ] A11 A12 A= . (1.38) A21 A22 Analogno napravimo z matriko B in matriko C, ki je produkt matrik AB: ] ] [ ][ [ C11 C12 B11 B12 A11 A12 = C, (1.39) = AB = C21 C22 B21 B22 A21 A22 pri čemer so: C11 = A11 B11 + A12 B21 , C12 = A11 B12 + A12 B22 , C21 = A21 B11 + A22 B21 , C22 = A21 B12 + A22 B22 . (1.40) Izračun matrike C reda n × n zahteva 8 matričnih množenj matrik reda n2 × n2 in 4 matrična seštevanja matrik reda n2 × n2 . Za matrično seštevanje vemo, da 2 ima n4 elementarnih seštevanj (seštevanj je toliko, kot ima elementov matrika reda n2 × n2 ). Od tod ni težko napisati rekurzivne enačbe za časovno zahtevnost množenja matrik: { T1 , ( ) n≤2 T (n) = (1.41) n 2 8 T 2 + bn , n > 2 pri čemer pomeni b sorazmeren faktor porabljenega časa za seštevanje. Po izreku 1.2 ugotovimo, da je rešitev rekurzivne enačbe naslednja (velja a > cr , ker je a = 8, c = 2 in r = 2): T (n) = O(nlogc a ) = O(n3 ). (1.42) 1.6 Strassenovo množenje matrik 1.6 1-25 Strassenovo množenje matrik Strassen je odkril postopek, s katerim je zmanjšal število množenj podmatrik Cij reda n2 × n2 (enačbe 1.40) na 7. V njegovem postopku se zato poveča število seštevanj oziroma odštevanj na 18. Postopek zahteva izračun 7 specialno definiranih matrik reda n2 × n2 . Pri izračunu vsake od teh matrik uporabimo eno matrično množenje. Formule za izračun teh matrik so: P = (A11 + A22 )(B11 + B22 ), Q = (A21 + A22 )B11 , R = A11 (B12 − B22 ), S = A22 (B21 − B11 ), T = (A11 + A12 )B22 , U = (A21 − A11 )(B11 + B12 ), V = (A12 − A22 )(B21 + B22 ). (1.43) S pomočjo enačb 1.43 izračunamo podmatrike produkta Cij : C11 = P + S − T + V, C12 = R + T, C21 = Q + S, C22 = P + R − Q + U. (1.44) Tudi Strassenovo množenje matrik temelji na strategiji del-in-vladaj. Tudi tukaj predpostavljamo, da sta matriki A in B reda n × n, kjer je n = 2k . Problem razpade na sedem podproblemov istega tipa. Tudi seštevanj je še vedno reda n2 , zato imamo naslednjo rekurzivno enačbo za časovno zahtevnost: { T1 , ( ) n≤2 T (n) = (1.45) n 2 7 T 2 + bn , n > 2 Po izreku 1.2 ugotovimo, da je rešitev rekurzivne enačbe naslednja (velja a > cr , ker je a = 7, c = 2 in r = 2): T (n) = O(nlogc a ) = O(nlog2 7 ) ≈ O(n2.81 ). Pri velikih n predstavlja taka časovna zahtevnost kar velik prihranek. (1.46) Literatura Aho, A. V., Hopcroft, J. E., and Ullman, J. D. (1974). The Design and Analysis of Computer Algorithms. Addison-Wesley, Reading. Cormen, T. H., Leiserson, C. E., and Rivest, R. L. (2007). Introduction to Algorithms. Druga izdaja, MIT Press, Cambridge. Horowitz, E., Sahni, S., and Rajasekaran, S. (1998). Computer Algorithms. Computer Science Press, New York. Kleinberg, J. and Tardos, E. (2006). Algorithm Design. Parson Education, Inc., New York. Kononenko, I. (1996). Načrtovanje podatkovnih struktur in algoritmov. Fakulteta za računalništvo in informatiko, Ljubljana. Kozak, J. (1997). Podatkovne strukture in algoritmi. Društvo matematikov, fizikov in astronomov Slovenije, Ljubljana. Levitin, A. (2007). The Design and Analysis of Algorithms. Druga izdaja, Pearson Education, Inc., Boston. Manber, U. (1989). Introduction to Algorithms. A Creative Approach. AddisonWesley, Reading. Nilsson, N. J. (1980). Principles of Artificial Intelligence. Tioga. Sedgewick, R. (2003). Algorithms in Java. Third Edition. Addison-Wesley, Boston. Vilfan, B. (1998). Osnovni algoritmi. Fakulteta za računalništvo in informatiko, Ljubljana.
© Copyright 2024