Deli in vladaj

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.