Documente Academic
Documente Profesional
Documente Cultură
Algoritmi Fundamentali O Perspectiva C++ PDF
Algoritmi Fundamentali O Perspectiva C++ PDF
FUNDAMENTALI
O PERSPECTIV C++
RZVAN ANDONIE
ILIE GRBACEA
ALGORITMI
FUNDAMENTALI
O PERSPECTIV C++
Editura Libris
Cluj-Napoca, 1995
Cuvnt nainte
Evoluia rapid i spectaculoas a informaticii n ultimile decenii se reflect att
n apariia a numeroase limbaje de programare, ct i n metodele de elaborare i
redactare a unor algoritmi performani.
Un concept nou, care s-a dovedit foarte eficient, este cel al programrii orientate
pe obiect, prin obiect nelegndu-se o entitate ce cuprinde att datele, ct i
procedurile ce opereaz cu ele. Dintre limbajele orientate pe obiect, limbajul C++
prezint printre multe altele avantajul unei exprimri concise, fapt ce uureaz
transcrierea n acest limbaj a algoritmilor redactai n pseudo-cod i motiveaz
folosirea lui n cartea de fa. Cu toate c nu este descris n detaliu, este demn de
menionat faptul c descrierea din Capitolul 2, mpreun cu completrile din
celelalte capitole, constituie o prezentare aproape integral a limbajului C++.
O preocupare meritorie a acestei lucrri este problema analizei eficienei
algoritmilor. Prezentarea acestei probleme ncepe n Capitolul 1 i continu n
Capitolul 5. Tehnicile de analiz expuse se bazeaz pe diferite metode, prezentate
ntr-un mod riguros i accesibil. Subliniem contribuia autorilor n expunerea
detailat a induciei constructive i a tehnicilor de rezolvare a recurenelor liniare.
Diferitele metode clasice de elaborare a algoritmilor sunt descrise n Capitolele
68 prin probleme ce ilustreaz foarte clar ideile de baz i detaliile metodelor
expuse. Pentru majoritatea problemelor tratate, este analizat i eficiena
algoritmului folosit. Capitolul 9 este consacrat tehnicilor de explorri n grafuri.
n primele seciuni sunt prezentate diferite probleme privind parcurgerea
grafurilor. Partea final a capitolului este dedicat jocurilor i cuprinde algoritmi
ce reprezint de fapt soluii ale unor probleme de inteligen artificial.
Cartea este redactat clar i riguros, tratnd o arie larg de probleme din domeniul
elaborrii i analizei algoritmilor. Exerciiile din ncheierea fiecrui capitol sunt
foarte bine alese, multe din ele fiind nsoite de soluii. De asemenea, merit
menionate referirile interesante la istoria algoritmilor i a gndirii algoritmice.
Considerm c aceast carte va fi apreciat i cutat de ctre toi cei ce lucreaz
n domeniul abordat i doresc s-l cunoasc mai bine.
Leon Livovschi
Prefa
Cartea noastr i propune n primul rnd s fie un curs i nu o enciclopedie de
algoritmi. Pornind de la structurile de date cele mai uzuale i de la analiza
eficienei algoritmilor, cartea se concentreaz pe principiile fundamentale de
elaborare a algoritmilor: greedy, divide et impera, programare dinamic,
backtracking. Interesul nostru pentru inteligena artificial a fcut ca penultimul
capitol s fie, de fapt, o introducere din punct de vedere al algoritmilor n
acest domeniu.
Majoritatea algoritmilor selectai au o conotaie estetic. Efortul necesar pentru
nelegerea elementelor mai subtile este uneori considerabil. Ce este ns un
algoritm estetic? Putem rspunde foarte simplu: un algoritm este estetic dac
exprim mult n cuvinte puine. Un algoritm estetic este oare n mod necesar i
eficient? Cartea rspunde i acestor ntrebri.
n al doilea rnd, cartea prezint mecanismele interne eseniale ale limbajului C++
(moteniri, legturi dinamice, clase parametrice, excepii) i trateaz
implementarea algoritmilor n conceptul programrii orientate pe obiect. Totui,
aceast carte nu este un curs complet de C++.
Algoritmii nu sunt pur i simplu transcrii din pseudo-cod n limbajul C++, ci
sunt regndii din punct de vedere al programrii orientate pe obiect. Sperm c,
dup citirea crii, vei dezvolta aplicaii de programare orientat pe obiect i vei
elabora implementri ale altor structuri de date. Programele * au fost scrise pentru
limbajul C++ descris de Ellis i Stroustrup n The Annotated C++ Reference
Manual. Acest limbaj se caracterizeaz, n principal, prin introducerea claselor
parametrice i a unui mecanism de tratare a excepiilor foarte avansat, faciliti
deosebit de importante pentru dezvoltarea de biblioteci C++. Compilatoarele
GNU C++ 2.5.8 (UNIX/Linux) i Borland C++ 3.1 (DOS) suport destul de bine
clasele parametrice. Pentru tratarea excepiilor se pot utiliza compilatoarele
Borland C++ 4.0 i, n viitorul apropiat, GNU C++ 2.7.1.
Fr a face concesii rigorii matematice, prezentarea este intuitiv, cu numeroase
exemple. Am evitat, pe ct posibil, situaia n care o carte de informatic ncepe
*
Fiierele surs ale tuturor exemplelor aproximativ 3400 de linii n 50 de fiiere pot fi obinute
pe o dischet MS-DOS, printr-o comand adresat editurii.
vii
viii
Cuprins
Autorii pot fi contactai prin pot, la adresa: Universitatea Transilvania, Catedra de electronic i
calculatoare, Politehnicii 1-3, 2200 Braov, sau prin E-mail, la adresa: algoritmi&c++@lbvi.sfos.ro
Cuprins
1. PRELIMINARII
1.1 Ce este un algoritm?
1
1
1.6 Exemple
10
1.6.1 Sortare
10
1.6.2 Calculul determinanilor
10
1.6.3 Cel mai mare divizor comun
11
1.6.4 Numerele lui Fibonacci
12
1.7 Exerciii
13
14
14
16
34
40
42
45
49
53
ix
37
Cuprins
56
4.1 Tablouri
56
4.1.1 Alocarea dinamic a memoriei
60
4.1.2 Clasa tablou
4.1.3 Clasa parametric tablou<T>
57
63
69
78
84
89
94
105
6. ALGORITMI GREEDY
6.1 Tehnica greedy
113
113
115
116
119
122
130
Cuprins
6.8 Cele mai scurte drumuri care pleac din acelai punct
6.9 Implementarea algoritmului lui Dijkstra
6.10 Euristica greedy
143
6.10.1 Colorarea unui graf
143
6.10.2 Problema comis-voiajorului
6.11 Exerciii
134
137
144
145
149
149
151
153
161
164
169
172
154
174
177
185
187
189
193
204
198
200
185
219
xi
xii
Cuprins
9. EXPLORRI N GRAFURI
9.1 Parcurgerea arborilor
227
227
229
231
235
239
237
247
249
251
255
255
266
269
EPILOG
271
BIBLIOGRAFIE SELECTIV
273
Lista de notaii
#T
i .. j
interval de ntregi: {k N | i k j}
m mod n
div
mprirea ntreag
|x|
mrimea cazului x
log
lg
logaritm n baza 2
ln
logaritm natural
lg
n
k
R
N ,R
(x) | [P(x)]
(x) | [P(x)]
O, ,
atribuire
(a, b)
{a, b}
xiii
1. Preliminarii
1.1
Ce este un algoritm?
Abu Ja`far Mohammed ibn Mus al-Khowrizm (autor persan, sec. VIII-IX), a
scris o carte de matematic cunoscut n traducere latin ca Algorithmi de
numero indorum, iar apoi ca Liber algorithmi, unde algorithm provine de la
al-Khowrizm, ceea ce literal nseamn din oraul Khowrizm. n prezent,
acest ora se numete Khiva i se afl n Uzbechistan. Att al-Khowrizm, ct i
ali matematicieni din Evul Mediu, nelegeau prin algoritm o regul pe baza
creia se efectuau calcule aritmetice. Astfel, n timpul lui Adam Riese (sec. XVI),
algoritmii foloseau la: dublri, njumtiri, nmuliri de numere. Ali algoritmi
apar n lucrrile lui Stifer (Arithmetica integra, Nrnberg, 1544) i Cardano
(Ars magna sive de reguli algebraicis, Nrnberg, 1545). Chiar i Leibniz
vorbete de algoritmi de nmulire. Termenul a rmas totui mult vreme cu o
ntrebuinare destul de restrns, chiar i n domeniul matematicii.
Kronecker (n 1886) i Dedekind (n 1888) semneaz actul de natere al teoriei
funciilor recursive. Conceptul de recursivitate devine indisolubil legat de cel de
algoritm. Dar abia n deceniile al treilea i al patrulea ale secolului nostru, teoria
recursivitii i algoritmilor ncepe s se constituie ca atare, prin lucrrile lui
Skolem, Ackermann, Sudan, Gdel, Church, Kleene, Turing, Peter i alii.
Este surprinztoare transformarea gndirii algoritmice, dintr-un instrument
matematic particular, ntr-o modalitate fundamental de abordare a problemelor n
domenii care aparent nu au nimic comun cu matematica. Aceast universalitate a
gndirii algoritmice este rezultatul conexiunii dintre algoritm i calculator. Astzi,
nelegem prin algoritm o metod general de rezolvare a unui anumit tip de
problem, metod care se poate implementa pe calculator. n acest context, un
algoritm este esena absolut a unei rutine.
Cel mai faimos algoritm este desigur algoritmul lui Euclid pentru aflarea celui mai
mare divizor comun a dou numere ntregi. Alte exemple de algoritmi sunt
metodele nvate n coal pentru a nmuli/mpri dou numere. Ceea ce d ns
generalitate noiunii de algoritm este faptul c el poate opera nu numai cu numere.
Exist astfel algoritmi algebrici i algoritmi logici. Pn i o reet culinar este
n esen un algoritm. Practic, s-a constatat c nu exist nici un domeniu, orict ar
prea el de imprecis i de fluctuant, n care s nu putem descoperi sectoare
funcionnd algoritmic.
Preliminarii
Capitolul 1
Un algoritm este compus dintr-o mulime finit de pai, fiecare necesitnd una sau
mai multe operaii. Pentru a fi implementabile pe calculator, aceste operaii
trebuie s fie n primul rnd definite, adic s fie foarte clar ce anume trebuie
executat. n al doilea rnd, operaiile trebuie s fie efective, ceea ce nseamn c
n principiu, cel puin o persoan dotat cu creion i hrtie trebuie s poat
efectua orice pas ntr-un timp finit. De exemplu, aritmetica cu numere ntregi este
efectiv. Aritmetica cu numere reale nu este ns efectiv, deoarece unele numere
sunt exprimabile prin secvene infinite. Vom considera c un algoritm trebuie s
se termine dup un numr finit de operaii, ntr-un timp rezonabil de lung.
Programul este exprimarea unui algoritm ntr-un limbaj de programare. Este bine
ca nainte de a nva concepte generale, s fi acumulat deja o anumit experien
practic n domeniul respectiv. Presupunnd c ai scris deja programe ntr-un
limbaj de nivel nalt, probabil c ai avut uneori dificulti n a formula soluia
pentru o problem. Alteori, poate c nu ai putut decide care dintre algoritmii care
rezolvau aceeai problem este mai bun. Aceast carte v va nva cum s evitai
aceste situaii nedorite.
Studiul algoritmilor cuprinde mai multe aspecte:
i)
Seciunea 1.1
v)
Ce este un algoritm?
19
19
22
38
11
76
76
152
152
304
608
608
855
Preliminarii
Capitolul 1
Seciunea 1.1
Ce este un algoritm?
function russe(A, B)
arrays X, Y
{iniializare}
X[1] A; Y[1] B
i 1 {se construiesc cele dou coloane}
while X[i] > 1 do
X[i+1] X[i] div 2 {div reprezint mprirea ntreag}
Y[i+1] Y[i]+Y[i]
i i+1
{adun numerele Y[i] corespunztoare numerelor X[i] impare}
prod 0
while i > 0 do
if X[i] este impar then prod prod+Y[i]
i i1
return prod
Un programator cu experien va observa desigur c tablourile X i Y nu sunt de
fapt necesare i c programul poate fi simplificat cu uurin. Acest algoritm
poate fi programat deci n mai multe feluri, chiar folosind acelai limbaj de
programare.
Pe lng algoritmul de nmulire nvat n coal, iat c mai avem un algoritm
care face acelai lucru. Exist mai muli algoritmi care rezolv o problem, dar i
mai multe programe care pot descrie un algoritm.
Acest algoritm poate fi folosit nu doar pentru a nmuli pe 45 cu 19, dar i pentru
a nmuli orice numere ntregi pozitive. Vom numi (45, 19) un caz (instance).
Pentru fiecare algoritm exist un domeniu de definiie al cazurilor pentru care
algoritmul funcioneaz corect. Orice calculator limiteaz mrimea cazurilor cu
care poate opera. Aceast limitare nu poate fi ns atribuit algoritmului respectiv.
nc o dat, observm c exist o diferen esenial ntre programe i algoritmi.
1.2
Eficiena algoritmilor
Ideal este ca, pentru o problem dat, s gsim mai muli algoritmi, iar apoi s-l
alegem dintre acetia pe cel optim. Care este ns criteriul de comparaie?
Eficiena unui algoritm poate fi exprimat n mai multe moduri. Putem analiza a
posteriori (empiric) comportarea algoritmului dup implementare, prin rularea pe
calculator a unor cazuri diferite. Sau, putem analiza a priori (teoretic) algoritmul,
naintea programrii lui, prin determinarea cantitativ a resurselor (timp, memorie
etc) necesare ca o funcie de mrimea cazului considerat.
Mrimea unui caz x, notat cu | x |, corespunde formal numrului de bii necesari
pentru reprezentarea lui x, folosind o codificare precis definit i rezonabil de
Preliminarii
Capitolul 1
Seciunea 1.2
Eficiena algoritmilor
Dac un algoritm necesit timp n ordinul lui n, vom spune c necesit timp liniar,
iar algoritmul respectiv putem s-l numim algoritm liniar. Similar, un algoritm
este ptratic, cubic, polinomial, sau exponenial dac necesit timp n ordinul lui
2
3
k
n
n , n , n , respectiv c , unde k i c sunt constante.
Un obiectiv major al acestei cri este analiza teoretic a eficienei algoritmilor.
Ne vom concentra asupra criteriului timpului de execuie. Alte resurse necesare
(cum ar fi memoria) pot fi estimate teoretic ntr-un mod similar. Se pot pune i
probleme de compromis memorie - timp de execuie.
1.3
Timpul de execuie al unui algoritm poate varia considerabil chiar i pentru cazuri
de mrime identic. Pentru a ilustra aceasta, vom considera doi algoritmi
elementari de sortare a unui tablou T de n elemente:
procedure insert(T[1 .. n])
for i 2 to n do
x T[i]; j i1
while j > 0 and x < T[ j] do
T[ j+1] T[ j]
j j1
T[ j+1] x
procedure select (T[1 .. n])
for i 1 to n1 do
minj i; minx T[i]
for j i+1 to n do
if T[ j] < minx then minj j
minx T[ j]
T[minj] T[i]
T[i] minx
Ideea general a sortrii prin inserie este s considerm pe rnd fiecare element
al irului i s l inserm n subirul ordonat creat anterior din elementele
precedente. Operaia de inserare implic deplasarea spre dreapta a unei secvene.
Sortarea prin selecie lucreaz altfel, plasnd la fiecare pas cte un element direct
pe poziia lui final.
Fie U i V dou tablouri de n elemente, unde U este deja sortat cresctor, iar V
este sortat descresctor. Din punct de vedere al timpului de execuie, V reprezint
cazul cel mai nefavorabil iar U cazul cel mai favorabil.
Preliminarii
Capitolul 1
Vom vedea mai trziu c timpul de execuie pentru sortarea prin selecie este
ptratic, independent de ordonarea iniial a elementelor. Testul if T[ j] < minx
este executat de tot attea ori pentru oricare dintre cazuri. Relativ micile variaii
ale timpului de execuie se datoreaz doar numrului de executri ale atribuirilor
din ramura then a testului.
La sortarea prin inserie, situaia este diferit. Pe de o parte, insert(U) este foarte
rapid, deoarece condiia care controleaz bucla while este mereu fals. Timpul
necesar este liniar. Pe de alt parte, insert(V) necesit timp ptratic, deoarece
bucla while este executat de i1 ori pentru fiecare valoare a lui i. (Vom analiza
acest lucru n Capitolul 5).
Dac apar astfel de variaii mari, atunci cum putem vorbi de un timp de execuie
care s depind doar de mrimea cazului considerat? De obicei considerm
analiza pentru cel mai nefavorabil caz. Acest tip de analiz este bun atunci cnd
timpul de execuie al unui algoritm este critic (de exemplu, la controlul unei
centrale nucleare). Pe de alt parte ns, este bine uneori s cunoatem timpul
mediu de execuie al unui algoritm, atunci cnd el este folosit foarte des pentru
cazuri diferite. Vom vedea c timpul mediu pentru sortarea prin inserie este tot
ptratic. n anumite cazuri ns, acest algoritm poate fi mai rapid. Exist un
algoritm de sortare (quicksort) cu timp ptratic pentru cel mai nefavorabil caz, dar
cu timpul mediu n ordinul lui n log n. (Prin log notm logaritmul ntr-o baz
oarecare, lg este logaritmul n baza 2, iar ln este logaritmul natural). Deci, pentru
cazul mediu, quicksort este foarte rapid.
Analiza comportrii n medie a unui algoritm presupune cunoaterea a priori a
distribuiei probabiliste a cazurilor considerate. Din aceast cauz, analiza pentru
cazul mediu este, n general, mai greu de efecuat dect pentru cazul cel mai
nefavorabil.
Atunci cnd nu vom specifica pentru ce caz analizm un algoritm, nseamn c
eficiena algoritmului nu depinde de acest aspect (ci doar de mrimea cazului).
1.4
Operaie elementar
Seciunea 1.4
Operaie elementar
function Wilson(n)
{returneaz true dac i numai dac n este prim}
if n divide ((n1)! + 1) then return true
else return false
Dac considerm calculul factorialului i testul de divizibilitate ca operaii
elementare, atunci eficiena testului de primalitate este foarte mare. Dac
considerm c factorialul se calculeaz n funcie de mrimea lui n, atunci
eficiena testului este mai slab. La fel i cu testul de divizibilitate.
Deci, este foarte important ce anume definim ca operaie elementar. Este oare
adunarea o operaie elementar? n teorie, nu, deoarece i ea depinde de lungimea
operanzilor. Practic, pentru operanzi de lungime rezonabil (determinat de modul
de reprezentare intern), putem s considerm c adunarea este o operaie
elementar. Vom considera n continuare c adunrile, scderile, nmulirile,
mpririle, operaiile modulo (restul mpririi ntregi), operaiile booleene,
comparaiile i atribuirile sunt operaii elementare.
1.5
10
Preliminarii
Capitolul 1
Seciunea 1.5
11
nou main, nu i ntr-un nou algoritm. Astfel, pentru n = 10, pe maina veche,
algoritmul nou necesit 10 secunde, adic de o sut de ori mai mult dect
algoritmul vechi. Pe vechiul calculator, algoritmul nou devine mai performant
doar pentru cazuri mai mari sau egale cu 20.
1.6
Exemple
1.6.1
Sortare
Algoritmii de sortare prin inserie i prin selecie necesit timp ptratic, att n
cazul mediu, ct i n cazul cel mai nefavorabil. Cu toate c aceti algoritmi sunt
exceleni pentru cazuri mici, pentru cazuri mari avem algoritmi mai eficieni. n
capitolele urmtoare vom analiza i ali algoritmi de sortare: heapsort, mergesort,
quicksort. Toi acetia necesit un timp mediu n ordinul lui n log n, iar heapsort
i mergesort necesit timp n ordinul lui n log n i n cazul cel mai nefavorabil.
Pentru a ne face o idee asupra diferenei dintre un timp ptratic i un timp n
ordinul lui n log n, vom meniona c, pe un anumit calculator, quicksort a reuit
s sorteze n 30 de secunde 100.000 de elemente, n timp ce sortarea prin inserie
ar fi durat, pentru acelai caz, peste nou ore. Pentru un numr mic de elemente
ns, eficiena celor dou sortri este asemntoare.
1.6.2
Calculul determinanilor
det( M ) = ( 1) j +1 a1 j det( M 1 j )
j =1
12
Preliminarii
Capitolul 1
1.6.3
Un prim algoritm pentru aflarea celui mai mare divizor comun al ntregilor
pozitivi m i n, notat cu cmmdc(m, n), se bazeaz pe definiie:
function cmmdc-def (m, n)
i min(m, n) + 1
repeat i i 1 until i divide pe m i n
return i
Timpul este n ordinul diferenei dintre min(m, n) i cmmdc(m, n).
Exist, din fericire, un algoritm mult mai eficient, care nu este altul dect celebrul
algoritm al lui Euclid.
function Euclid(m, n)
if n = 0 then return m
else return Euclid(n, m mod n)
Prin m mod n notm restul mpririi ntregi a lui m la n. Algoritmul funcioneaz
pentru orice ntregi nenuli m i n, avnd la baz cunoscuta proprietate
cmmdc(m, n) = cmmdc(n, m mod n)
Timpul este n ordinul logaritmului lui min(m, n), chiar i n cazul cel mai
nefavorabil, ceea ce reprezint o mbuntire substanial fa de algoritmul
precedent. Pentru a fi exaci, trebuie s menionm c algoritmul originar al lui
Euclid (descris n Elemente, aprox. 300 a.Ch.) opereaz prin scderi succesive,
i nu prin mprire. Interesant este faptul c acest algoritm se pare c provine
dintr-un algoritm i mai vechi, datorat lui Eudoxus (aprox. 375 a.Ch.).
1.6.4
Seciunea 1.6
Exemple
f 0 = 0; f 1 = 1
f n = f n 1 + f n 2
pentru
13
n2
Preliminarii
14
1.7
Capitolul 1
Exerciii
1.1
Aplicai algoritmii insert i select pentru cazurile T = [1, 2, 3, 4, 5, 6] i
U = [6, 5, 4, 3, 2, 1]. Asigurai-v c ai neles cum funcioneaz.
1.2
nmulirea a la russe este cunoscut nc din timpul Egiptului antic,
fiind probabil un algoritm mai vechi dect cel al lui Euclid. ncercai s nelegei
raionamentul care st la baza acestui algoritm de nmulire.
Indicaie: Facei legtura cu reprezentarea binar.
1.3
1.4
Elaborai un algoritm care s returneze cel mai mare divizor comun a trei
ntregi nenuli.
Soluie:
function Euclid-trei(m, n, p)
return Euclid(m, Euclid(n, p))
1.5
Programai algoritmul fib1 n dou limbaje diferite i rulai comparativ
cele dou programe, pe mai multe cazuri. Verificai dac este valabil principiul
invarianei.
1.6
Elaborai un algoritm care returneaz cel mai mare divizor comun a doi
termeni de rang oarecare din irul lui Fibonacci.
Indicaie: Un algoritm eficient se obine folosind urmtoarea proprietate *,
valabil pentru oricare doi termeni ai irului lui Fibonacci:
cmmdc( f m , f n ) = f cmmdc(m, n)
1.7
0 1
Fie matricea M =
. Calculai produsul vectorului ( f n1 , f n ) cu
1 1
m
2. Programare
orientat pe obiect
Dei aceast carte este dedicat n primul rnd analizei i elaborrii algoritmilor,
am considerat util s folosim numeroii algoritmi care sunt studiai ca un pretext
pentru introducerea elementelor de baz ale programrii orientate pe obiect n
limbajul C++. Vom prezenta n capitolul de fa noiuni fundamentale legate de
obiecte, limbajul C++ i de abstractizarea datelor n C++, urmnd ca, pe baza
unor exemple detaliate, s conturm n capitolele urmtoare din ce n ce mai clar
tehnica programrii orientate pe obiect. Scopul urmrit este de a surprinde acele
aspecte strict necesare formrii unei impresii juste asupra programrii orientate pe
obiect n limbajul C++, i nu de a substitui cartea de fa unui curs complet de
C++.
2.1
Conceptul de obiect
14
Seciunea 2.1
Conceptul de obiect
15
2.2
Limbajul C++
Toate limbajele de nivel nalt, de la FORTRAN la LISP, permit adaptarea unui stil
de programare orientat pe obiect, dar numai cteva ofer mecanismele pentru
16
Capitolul 2
utilizarea direct a obiectelor. Din acest punct de vedere, menionm dou mari
categorii de limbaje:
Limbaje care ofer doar faciliti de abstractizarea datelor i ncapsulare, cum
sunt Ada i Modula-2. De exemplu, n Ada, datele i procedurile care le
manipuleaz pot fi grupate ntr-un pachet (package).
Limbaje orientate pe obiect, care adaug abstractizrii datelor noiunea de
motenire.
Dei definiiile de mai sus restrng mult mulimea limbajelor calificabile ca
orientate pe obiect, aceste limbaje rmn totui foarte diverse, att din punct de
vedere al conceptelor folosite, ct i datorit modului de implementare. S-au
conturat trei mari familii, fiecare accentund un anumit aspect al noiunii de
obiect: limbaje de clase, limbaje de cadre (frames) i limbaje de tip actor.
Limbajul C++ * aparine familiei limbajelor de clase. O clas este un tip de date
care descrie un ansamblu de obiecte cu aceeai structur i acelai comportament.
Clasele pot fi mbogite i completate pentru a defini alte familii de obiecte. n
acest mod se obin ierarhii de clase din ce n ce mai specializate, care motenesc
datele i metodele claselor din care au fost create. Din punct de vedere istoric
primele limbaje de clase au fost Simula (1973) i Smalltalk-80 (1983). Limbajul
Simula a servit ca model pentru o ntreg linie de limbaje caracterizate printr-o
organizare static a tipurilor de date.
S vedem acum care sunt principalele deosebiri dintre limbajele C i C++, precum
i modul n care s-au implementat intrrile/ieirile n limbajul C++.
Limbaj dezvoltat de Bjarne Stroustrup la nceputul anilor 80, n cadrul laboratoarelor Bell de la
AT&T, ca o extindere orientat pe obiect a limbajului C.
Seciunea 2.2
Limbajul C++
17
n acest exemplu, funcia maxim() este declarat ca o funcie de tip float cu doi
parametri tot de tip float, motiv pentru care constanta ntreag 3 este convertit
n momentul apelului la tipul float. Declaraia unei funcii const n prototipul
funciei, care conine tipul valorii returnate, numele funciei, numrul i tipul
parametrilor. Diferena dintre definiie i declaraie noiuni valabile i pentru
variabile const n faptul c definiia este o declaraie care provoac i
rezervare de spaiu sau generare de cod. Declararea unei variabile se face prin
precedarea obligatorie a definiiei de cuvntul cheie extern. i o declaraie de
funcie poate fi precedat de cuvntul cheie extern, accentund astfel c funcia
este definit altundeva.
Definirea unor funcii foarte mici, pentru care procedura de apel tinde s dureze
mai mult dect executarea propriu-zis, se realizeaz n limbajul C++ prin
funciile inline.
inline float maxim( float x, float y ) {
putchar( 'r' ); return x > y? x: y;
}
(Prin apelarea funciei putchar(), putem afla care din cele dou funcii maxim()
este efectiv invocat).
n limbajul C++ nu este obligatorie definirea variabilelor locale strict la nceputul
blocului de instruciuni. n exemplul de mai jos, tabloul buf i ntregul i pot fi
utilizate din momentul definirii i pn la sfritul blocului n care au fost
definite.
18
Capitolul 2
#define DIM 5
void f( ) {
int buf[ DIM ];
for ( int i = 0; i < DIM; )
buf[ i++ ] = maxim( i, DIM - i );
while ( --i )
printf( "%3d ", buf[ i ] );
}
Seciunea 2.2
Limbajul C++
19
f( )" ); f( );
g( )" ); g( );
20
Capitolul 2
suprind prin faptul c funcia float maxim( float, float ) este invocat
naintea funciei main(). Acest lucru este normal, deoarece variabila x trebuie
iniializat naintea lansrii n execuie a funciei main().
Seciunea 2.2
Limbajul C++
21
#include <iostream.h>
long fib2( int n ) {
long i = 1, j = 0;
for ( int k = 0; k++ < n; j = i + j, i = j - i );
return j;
}
int main( ) {
cout << "\nTermenul sirului lui Fibonacci de rang ... ";
int n;
cin >> n;
cout << " este " << fib2( n );
cout << '\n';
return 0;
}
Biblioteca standard C++ conine definiiile unor clase care reprezint diferite
tipuri de fluxuri de comunicaie (stream-uri). Fiecare flux poate fi de intrare, de
ieire, sau de intrare/ieire. Operaia primar pentru fluxul de ieire este
inserarea de date, iar pentru cel de ieire este extragerea de date. Fiierul prefix
(header) iostream.h conine declaraiile fluxului de intrare (clasa istream), ale
fluxului de ieire (clasa ostream), precum i declaraiile obiectelor cin i cout:
extern istream cin;
extern ostream cout;
Operaiile de inserare i extragere sunt realizate prin funciile membre ale claselor
ostream i istream. Deoarece limbajul C++ permite existena unor funcii care
suprancarc o parte din operatorii predefinii, s-a convenit ca inserarea s se fac
prin suprancarcarea operatorului de decalare la stnga <<, iar extragerea prin
suprancrcarea celui de decalare la dreapta >>. Semnificaia secvenei de
instruciuni
cin >> n;
cout << " este " << fib2( n );
este deci urmtoarea: se citete valoarea lui n, apoi se afieaz irul " este "
urmat de valoarea returnat de funcia fib2().
Fluxurile de comunicaie cin i cout lucreaz n mod implicit cu terminalul
utilizatorului. Ca i pentru programele scrise n C, este posibil redirectarea lor
spre alte dispozitive sau n diferite fiiere, n funcie de dorina utilizatorului.
Pentru sistemele de operare UNIX i DOS, redirectrile se indic adugnd
22
Capitolul 2
comenzii de lansare n execuie a programului, argumente de forma >numefisier-iesire, sau <nume-fisier-intrare. n iostream.h mai este definit
nc un flux de ieire numit cerr, utilizabil pentru semnalarea unor condiii de
excepie. Fluxul cerr este legat de terminalul utilizatorului i nu poate fi
redirectat.
Operatorii de inserare (<<) i extragere (>>) sunt, la rndul lor, suprancrcai
astfel nct operandul drept s poat fi de orice tip predefinit. De exemplu, n
instruciunea
cout << " este " << fib2( n );
2.3
Rulnd programul pentru determinarea termenilor din irul lui Fibonacci cu valori
din ce n ce mai mari ale lui n, se observ c rezultatele nu mai pot fi reprezentate
ntr-un int, long sau unsigned long. Soluia care se impune este de a limita
rangul n la valori rezonabile reprezentrii alese. Cu alte cuvinte, n nu mai este de
tip int, ci de un tip care limiteaz valorile ntregi la un anumit interval. Vom
elabora o clas corespunztoare acestui tip de ntregi, clas util multor programe
n care se cere meninerea unei valori ntre anumite limite.
Clasa se numete intErval, i va fi implementat n dou variante. Prima variant
este realizat n limbajul C. Nu este o clas propriu-zis, ci o structur care
confirm faptul c orice limbaj permite adaptarea unui stil de programare orientat
pe obiect i scoate n eviden inconvenientele generate de lipsa mecanismelor de
manipulare a obiectelor. A doua variant este scris n limbajul C++. Este un
adevrat tip abstract ale crui caliti sunt i mai bine conturate prin comparaia
cu (pseudo) tipul elaborat n C.
Seciunea 2.3
23
Efectul acestor definiii const n rezervarea de spaiu pentru fiecare din datele
membre ale obiectelor numar, indice i limita. n plus, datele membre din
numar sunt iniializate cu valorile 80 (min), 32 (max) i 64 (v). Iniializarea, dei
corect din punct de vedere sintactic, face imposibl funcionarea tipului
intErval, deoarece marginea inferioar nu este mai mic dect cea superioar.
Deocamdat nu avem nici un mecanism pentru a evita astfel de situaii.
Pentru manipularea obiectelor de tip intErval, putem folosi atribuiri la nivel de
structur:
limita = numar;
24
Capitolul 2
funcia de citire val() pur i simplu returneaz valoarea v. Practic, aceste dou
funcii implementeaz o form de ncapsulare, izolnd reprezentarea intern a
obiectului de restul programului.
int atr( intErval *pn, int i ) {
return pn->v = verDom( *pn, i );
}
int val( intErval n ) {
return n.v;
}
Utiliznd consecvent cele dou metode ale tipului intErval, obinem obiecte ale
cror valori sunt cu certitudine ntre limitele admisibile. De exemplu, utiliznd
metodele atr() i val(), instruciunea
indice.v = numar.v + 1;
devine
atr( &indice, val( numar ) + 1 );
Deoarece numar are valoarea 64, iar domeniul indice-lui este 32, ..., 64,
instruciunea de mai sus semnaleaz depirea domeniului variabilei indice i
provoac terminarea executrii programului.
Aceast implementare este departe de a fi complet i comod de utilizat. Nu ne
referim acum la aspecte cum ar fi citirea (sau scrierea) obiectelor de tip
intErval, operaie rezolvabil printr-o funcie de genul
void cit( intErval *pn ) {
int i;
scanf( "%d", &i );
atr( pn, i );
}
Seciunea 2.3
25
26
Capitolul 2
intErval numar;
intErval indice, limita;
Aceste obiecte pot fi atribuite ntre ele (fiind structuri atribuirea se va face
membru cu membru):
limita = numar;
care, dei corect din punct de vedere sintactic, este incorect semantic, deoarece
v este un membru private, deci inaccesibil prin intermediul obiectelor indice i
numar.
Dup cum se observ, au disprut argumentele de tip intErval* i intErval ale
funciilor atr(), respectiv val(). Cauza este faptul c funciile membre au un
argument implicit, concretizat n obiectul invocator, adic obiectul care
selecteaz funcia. Este o convenie care ntrete i mai mult atributul de funcie
membr (metod) deoarece permite invocarea unei astfel de funcii numai prin
obiectul respectiv.
Definirea funciilor membre se poate face fie n corpul clasei, fie n exteriorul
acestuia. Funciile definite n corpul clasei sunt considerate implicit inline, iar
pentru cele definite n exteriorul corpului se impune precizarea statutului de
funcie membr. nainte de a defini funciile atr() i verDom(), s observm c
funcia val(), definit n corpul clasei intErval, ncalc de dou ori cele
precizate pn aici. n primul rnd, nu selecteaz membrul v prin intermediul unui
obiect, iar n al doilea rnd, v este privat! Dac funcia val() ar fi fost o funcie
obinuit, atunci observaia ar fi fost ct se poate de corect. Dar val() este
funcie membr i atunci:
Nu poate fi apelat dect prin intermediul unui obiect invocator i toi membrii
utilizai sunt membrii obiectului invocator.
Seciunea 2.3
27
28
Capitolul 2
class intErval {
public:
// operatorul de atribuire corespunzator functiei atr()
int operator =( int i ) { return v = verDom( i ); }
// operatorul de conversie corespunzator functiei val()
operator int( ) { return v; }
private:
int verDom( int );
int min, max;
int v;
};
sau direct
indice = numar + 1;
Sunt dou ntrebri la care trebuie s rspundem referitor la funcia de mai sus:
Care este semnificaia testului if ( is >> i )?
De ce se returneaz istream-ul?
Seciunea 2.3
29
30
Capitolul 2
definiia
intErval indice;
Seciunea 2.3
31
Se observ apariia unei noi funcii membre, numit ~intErval(), al crui corp
este vid. Ea se numete destructor, nu are tip i nici argumente, iar numele ei este
obinut prin precedarea numelui clasei de caracterul ~. Rolul destructorului este
opus celui al constructorului, n sensul c realizeaz operaiile necesare
distrugerii corecte a obiectului. Destructorul este invocat automat, nainte de a
elibera spaiul alocat datelor membre ale obiectului care nceteaz s mai existe.
Un obiect nceteaz s mai existe n urmtoarele situaii:
Obiectele definite ntr-o funcie sau bloc de instruciuni (obiecte cu existen
local) nceteaz s mai existe la terminarea executrii funciei sau blocului
respectiv.
Obiectele definite global, n exteriorul oricrei funcii, sau cele definite
static (obiecte cu existen static) nceteaz s mai existe la terminarea
programului.
Obiectele alocate dinamic prin operatorul new (obiecte cu existen dinamic)
nceteaz s mai existe la invocarea operatorului delete.
Ca i n cazul constructorilor, prezena destructorului ntr-o clas este opional,
fiind lsat la latitudinea proiectantului clasei.
Pentru a putea fi inclus n toate fiierele surs n care este utilizat, definiia unei
clase se introduce ntr-un fiier header (prefix). n scopul evitrii includerii de
mai multe ori a aceluiai fiier (includeri multiple), se recomand ca fiierele
header s aib structura
32
Capitolul 2
#ifndef simbol
#define simbol
// continutul fisierului
#endif
unde simbol este un identificator unic n program. Dac fiierul a fost deja inclus,
atunci identificatorul simbol este deja definit, i deci, toate liniile situate ntre
#ifndef i #endif vor fi ignorate. De exemplu, n fiierul intErval.h, care
conine definiia clasei intErval, identificatorul simbol ar putea fi
__INTeRVAL_H. Iat coninutul acestui fiier:
#ifndef __INTeRVAL_H
#define __INTeRVAL_H
#include <iostream.h>
class intErval {
public:
intErval( int = 1, int = 0 );
~intErval( ) { }
int operator =( int i ) { return v = verDom( i ); }
operator int( ) { return v; }
private:
int verDom( int );
int min, max;
int v;
};
istream& operator >>( istream&, intErval& );
#endif
Funciile membre se introduc ntr-un fiier surs obinuit, care este legat dup
compilare de programul executabil. Pentru clasa intErval, acest fiier este:
#include "intErval.h"
#include <stdlib.h>
intErval::intErval( int sup, int inf ) {
if ( inf >= sup ) {
cerr << "\n\nintErval -- domeniu incorect specificat [ "
<< inf << ", " << (sup - 1) << " ].\n\n";
Seciunea 2.3
33
exit( 1 );
}
min = v = inf;
max =
sup;
}
int intErval::verDom( int i ) {
if ( i < min || i >= max ) {
cerr << "\n\nintErval -- "
<< i << ": valoare exterioara domeniului [ "
<< min << ", " << (max - 1) << " ].\n\n";
exit( 1 );
}
return i;
}
istream& operator >>( istream& is, intErval& n ) {
int i;
if ( is >> i ) // se citeste valoarea
n = i;
// se invoca operatorul de atribuire
return is;
}
34
Capitolul 2
2.4
Exerciii *
2.1
Scriei un program care determin termenul de rang n al irului lui
Fibonacci prin algoritmii fib1 i fib3.
2.2
Care sunt valorile maxime ale lui n pentru care algoritmii fib1, fib2 i fib3
returneaz valori corecte? Cum pot fi mrite aceste valori?
Soluie: Presupunnd c un long este reprezentat pe 4 octei, atunci cel mai mare
numr Fibonacci reprezentabil pe long este cel cu rangul 46. Lucrnd pe
unsigned long, se poate ajunge pn la termenul de rang 47. Pentru aceste
ranguri, timpii de execuie ai algoritmului fib1 difer semnificativ de cei ai
algoritmilor fib2 i fib3.
2.3
Introducei n clasa intErval nc dou date membre prin care s
contorizai numrul de apeluri ale celor doi operatori definii. Completai
Chiar dac nu se precizeaz explicit, toate implementrile se vor realiza n limbajul C++.
Seciunea 2.4
Exerciii
35
2.5
Scriei un program pentru calculul recursiv al coeficienilor binomiali
dup formula dat de triunghiul lui Pascal:
n 1 n 1
+
n
k 1 k
=
k
1
n
n!
=
m m !(n m)!
Soluie: Utilizarea definiiei pentru calculul combinrilor este o idee total
neinspirat, nu numai n ceea ce privete eficiena, ci i pentru faptul c nu poate
fi aplicat dect pentru valori foarte mici ale lui n. De exemplu, ntr-un long de 4
octei, valoarea 13! nu mai poate fi calculat. Funcia recursiv este simpl:
int C( int n, int m) {
return m == 0 ||
m == n?
1: C( n - 1, m - 1 ) + C( n - 1, m );
}
dar i ineficient, deoarece numrul apelurilor recursive este foarte mare (vezi
Exerciiul 8.1). Programul complet este:
#include <iostream.h>
const int N = 16, M = 17;
int r[N][M];
long tr;
36
Capitolul 2
ori astfel:
1
1
1
1
1
1
0
0
0
0
0
0
0
0
0
0
0
0
0
...
Se observ c C(1,1) a fost invocat de 210 ori, iar C(2,2) de 126 de ori!
3. Structuri elementare
de date
nainte de a elabora un algoritm, trebuie s ne gndim la modul n care
reprezentm datele. n acest capitol vom trece n revist structurile fundamentale
de date cu care vom opera. Presupunem n continuare c suntei deja familiarizai
cu noiunile de fiier, tablou, list, graf, arbore i ne vom concentra mai ales pe
prezentarea unor concepte mai particulare: heap-uri i structuri de mulimi
disjuncte.
3.1
Liste
alpha
beta
gamma
delta
coada
listei
O list circular este o list n care, dup ultimul nod, urmeaz primul, deci
fiecare nod are succesor i predecesor.
Operaii curente care se fac n liste sunt: inserarea unui nod, tergerea
(extragerea) unui nod, concatenarea unor liste, numrarea elementelor unei liste
etc. Implementarea unei liste se poate face n principal n dou moduri:
Implementarea secvenial, n locaii succesive de memorie, conform ordinii
nodurilor n list. Avantajele acestei tehnici sunt accesul rapid la
predecesorul/succesorul unui nod i gsirea rapid a primului/ultimului nod.
Dezavantajele sunt inserarea/tergerea relativ complicat a unui nod i faptul
c, n general, nu se folosete ntreaga memorie alocat listei.
Implementarea nlnuit. n acest caz, fiecare nod conine dou pri:
informaia propriu-zis i adresa nodului succesor. Alocarea memoriei fiecrui
nod se poate face n mod dinamic, n timpul rulrii programului. Accesul la un
nod necesit parcurgerea tuturor predecesorilor si, ceea ce poate lua ceva mai
37
38
Capitolul 3
mult timp. Inserarea/tergerea unui nod este n schimb foarte rapid. Se pot
folosi dou adrese n loc de una, astfel nct un nod s conin pe lng adresa
nodului succesor i adresa nodului predecesor. Obinem astfel o list dublu
nlnuit, care poate fi traversat n ambele direcii.
Listele implementate nlnuit pot fi reprezentate cel mai simplu prin tablouri. n
acest caz, adresele sunt de fapt indici de tablou. O alternativ este s folosim
tablouri paralele: s memorm informaia fiecrui nod (valoarea) ntr-o locaie
VAL[i] a tabloului VAL[1 .. n], iar adresa (indicele) nodului su succesor ntr-o
locaie LINK[i] a tabloului LINK[1 .. n]. Indicele de tablou al locaiei primului
nod este memorat n variabila head. Vom conveni ca, pentru cazul listei vide, s
avem head = 0. Convenim de asemenea ca LINK[ultimul nod din list] = 0.
Atunci, VAL[head] va conine informaia primului nod al listei, LINK[head]
adresa celui de-al doilea nod, VAL[LINK[head]] informaia din al doilea nod,
LINK[LINK[head]] adresa celui de-al treilea nod etc.
Acest mod de reprezentare este simplu dar, la o analiz mai atent, apare o
problem esenial: cea a gestionrii locaiilor libere. O soluie elegant este s
reprezentm locaiile libere tot sub forma unei liste nlnuite. Atunci, tergerea
unui nod din lista iniial implic inserarea sa n lista cu locaii libere, iar
inserarea unui nod n lista iniial implic tergerea sa din lista cu locaii libere.
Aspectul cel mai interesant este c, pentru implementarea listei de locaii libere,
putem folosi aceleai tablouri. Avem nevoie de o alt variabil, freehead, care va
conine indicele primei locaii libere din VAL i LINK. Folosim aceleai convenii:
dac freehead = 0 nseamn c nu mai avem locaii libere, iar LINK[ultima locaie
liber] = 0.
Vom descrie n continuare dou tipuri de liste particulare foarte des folosite.
3.1.1
Stive
Seciunea 3.1
Liste
39
3.1.2
Cozi
O coad (queue) este o list liniar n care inserrile se fac doar n capul listei,
iar extragerile doar din coada listei. Cozile se numesc i liste FIFO (First In First
Out).
O reprezentare secvenial interesant pentru o coad se obine prin utilizarea
unui tablou C[0 .. n1], pe care l tratm ca i cum ar fi circular: dup locaia
C[n1] urmeaz locaia C[0]. Fie tail variabila care conine indicele locaiei
40
Capitolul 3
predecesoare primei locaii ocupate i fie head variabila care conine indicele
locaiei ocupate ultima oar. Variabilele head i tail au aceeai valoare atunci i
numai atunci cnd coada este vid. Iniial, avem head = tail = 0. Inserarea i
tergerea (extragerea) unui nod necesit timp constant.
function insert-queue(x, C[0 .. n1])
{adaug nodul x n capul cozii}
head (head+1) mod n
if head = tail then return coad plin
C[head] x
return succes
function delete-queue(C[0 .. n1])
{terge nodul din coada listei i l returneaz}
if head = tail then return coad vid
tail (tail+1) mod n
x C[tail]
return x
Este surprinztor faptul c testul de coad vid este acelai cu testul de coad
plin. Dac am folosi toate cele n locaii, atunci nu am putea distinge ntre situaia
de coad plin i cea de coad vid, deoarece n ambele situaii am avea
head = tail. n consecin, se folosesc efectiv numai n1 locaii din cele n ale
tabloului C, deci se pot implementa astfel cozi cu cel mult n1 noduri.
3.2
Grafuri
Un graf este o pereche G = <V, M>, unde V este o mulime de vrfuri, iar
M V V este o mulime de muchii. O muchie de la vrful a la vrful b este
notat cu perechea ordonat (a, b), dac graful este orientat, i cu mulimea
{a, b}, dac graful este neorientat. n cele ce urmeaz vom presupune c vrfurile
a i b sunt diferite. Dou vrfuri unite printr-o muchie se numesc adiacente. Un
drum este o succesiune de muchii de forma
(a 1 , a 2 ), (a 2 , a 3 ), , (a n1 , a n )
sau de forma
{a 1 , a 2 }, {a 2 , a 3 }, , {a n1 , a n }
dup cum graful este orientat sau neorientat. Lungimea drumului este egal cu
numrul muchiilor care l constituie. Un drum simplu este un drum n care nici un
vrf nu se repet. Un ciclu este un drum care este simplu, cu excepia primului i
ultimului vrf, care coincid. Un graf aciclic este un graf fr cicluri. Un subgraf
al lui G este un graf <V', M'>, unde V' V, iar M' este format din muchiile din M
care unesc vrfuri din V'. Un graf parial este un graf <V, M">, unde M" M.
Seciunea 3.2
Grafuri
41
Un graf neorientat este conex, dac ntre oricare dou vrfuri exist un drum.
Pentru grafuri orientate, aceast noiune este ntrit: un graf orientat este tare
conex, dac ntre oricare dou vrfuri i i j exist un drum de la i la j i un drum
de la j la i.
n cazul unui graf neconex, se pune problema determinrii componentelor sale
conexe. O component conex este un subgraf conex maximal, adic un subgraf
conex n care nici un vrf din subgraf nu este unit cu unul din afar printr-o
muchie a grafului iniial. mprirea unui graf G = <V, M> n componentele sale
conexe determin o partiie a lui V i una a lui M.
Un arbore este un graf neorientat aciclic conex. Sau, echivalent, un arbore este un
graf neorientat n care exist exact un drum ntre oricare dou vrfuri *. Un graf
parial care este arbore se numete arbore parial.
Vrfurilor unui graf li se pot ataa informaii numite uneori valori, iar muchiilor li
se pot ataa informaii numite uneori lungimi sau costuri.
Exist cel puin trei moduri evidente de reprezentare ale unui graf:
Printr-o matrice de adiacen A, n care A[i, j] = true dac vrfurile i i j sunt
adiacente, iar A[i, j] = false n caz contrar. O variant alternativ este s-i dm
lui A[i, j] valoarea lungimii muchiei dintre vrfurile i i j, considernd
A[i, j] = + atunci cnd cele dou vrfuri nu sunt adiacente. Memoria necesar
2
este n ordinul lui n . Cu aceast reprezentare, putem verifica uor dac dou
vrfuri sunt adiacente. Pe de alt parte, dac dorim s aflm toate vrfurile
adiacente unui vrf dat, trebuie s analizm o ntreag linie din matrice.
Aceasta necesit n operaii (unde n este numrul de vrfuri n graf),
independent de numrul de muchii care conecteaz vrful respectiv.
Prin liste de adiacen, adic prin ataarea la fiecare vrf i a listei de vrfuri
adiacente lui (pentru grafuri orientate, este necesar ca muchia s plece din i).
ntr-un graf cu m muchii, suma lungimilor listelor de adiacen este 2m, dac
graful este neorientat, respectiv m, dac graful este orientat. Dac numrul
muchiilor n graf este mic, aceast reprezentare este preferabil din punct de
vedere al memoriei necesare. Este posibil s examinm toi vecinii unui vrf
dat, n medie, n mai puin de n operaii. Pe de alt parte, pentru a determina
dac dou vrfuri i i j sunt adiacente, trebuie s analizm lista de adiacen a
lui i (i, posibil, lista de adiacen a lui j), ceea ce este mai puin eficient dect
consultarea unei valori logice n matricea de adiacen.
Printr-o list de muchii. Aceast reprezentare este eficient atunci cnd avem
de examinat toate muchiile grafului.
n Exerciiul 3.2 sunt date i alte propoziii echivalente care caracterizeaz un arbore.
42
Capitolul 3
nivelul adncimea
2
alpha
gamma
beta
delta
omega
zeta
3.3
Arbori cu rdcin
Seciunea 3.3
Arbori cu rdcin
43
valoarea vrfului
adresa fiului stng
adresa fiului drept
a
44
Capitolul 3
1
2
4
8
5
9
10
6
11
12
7
13
14
15
T [1]
T [2]
T [4]
T [8]
T [3]
T [5] T [6]
T [9] T [10]
T [7]
Seciunea 3.3
Arbori cu rdcin
i)
ii)
n/2 + n/2 = n
pentru orice n ntreg
45
3.4
Heap-uri
10
7
4
2
7
1
46
Capitolul 3
scade, astfel nct devine mai mic dect valoarea cel puin a unui fiu, este
suficient s schimbm ntre ele valoarea modificat cu cea mai mare valoare a
fiiilor, apoi s continum procesul n mod descendent, pn cnd proprietatea de
heap este restabilit. Vom spune c valoarea modificat a fost cernut (sifted
down) ctre noua sa poziie. Urmtoarele proceduri descriu formal operaiunea de
modificare a valorii unui vrf ntr-un heap.
procedure alter-heap(T[1 .. n], i, v)
{T[1 .. n] este un heap; lui T[i], 1 i n, i se atribuie
valoarea v i proprietatea de heap este restabilit}
x T[i]
T[i] v
if v < x then sift-down(T, i)
else percolate(T, i)
procedure sift-down(T[1 .. n], i)
{se cerne valoarea din T[i]}
ki
repeat
jk
{gsete fiul cu valoarea cea mai mare}
if 2j n and T[2j] > T[k] then k 2j
if 2j < n and T[2j+1] > T[k] then k 2j+1
interschimb T[ j] i T[k]
until j = k
procedure percolate(T[1 .. n], i)
{se filtreaz valoarea din T[i]}
ki
repeat
jk
if j > 1 and T[ j div 2] < T[k] then k j div 2
interschimbT[ j] i T[k]
until j = k
Heap-ul este structura de date ideal pentru determinarea i extragerea maximului
dintr-o mulime, pentru inserarea unui vrf, pentru modificarea valorii unui vrf.
Sunt exact operaiile de care avem nevoie pentru a implementa o list dinamic de
prioriti: valoarea unui vrf va da prioritatea evenimentului corespunztor.
Evenimentul cu prioritatea cea mai mare se va afla mereu la rdcina heap-ului,
iar prioritatea unui eveniment poate fi modificat n mod dinamic. Algoritmii care
efectueaz aceste operaii sunt:
function find-max(T[1 .. n])
{returneaz elementul cel mai mare din heap-ul T}
return T[1]
Seciunea 3.4
Heap-uri
47
10
2
7
7
4
10
48
Capitolul 3
7
2
10
4
10
10
se transform succesiv n:
10
10
7
2
6
4
7
4
Subarborele de nivel 2 din dreapta este deja heap. Dup acest pas, tabloul T
devine:
1
10
Seciunea 3.4
Heap-uri
49
Chiar dac heap-ul este o structur de date foarte atractiv, exist totui i
operaii care nu pot fi efectuate eficient ntr-un heap. O astfel de operaie este, de
exemplu, gsirea unui vrf avnd o anumit valoare dat.
Conceptul de heap poate fi mbuntit n mai multe feluri. Astfel, pentru aplicaii
n care se folosete mai des procedura percolate dect procedura sift-down,
renteaz ca un vrf neterminal s aib mai mult de doi fii. Aceasta accelereaz
procedura percolate. i un astfel de heap poate fi implementat secvenial.
Heap-ul este o structur de date cu numeroase aplicaii, inclusiv o remarcabil
tehnic de sortare, numit heapsort.
procedure heapsort(T[1 .. n])
{sorteaz tabloul T}
make-heap(T)
for i n downto 2 do
interschimb T[1] i T[i]
sift-down(T[1 .. i1], 1)
Structura de heap a fost introdus (Williams, 1964) tocmai ca instrument pentru
acest algoritm de sortare.
3.5
ii)
50
Capitolul 3
function find1(x)
{returneaz eticheta mulimii care l conine pe x}
return set[x]
procedure merge1(a, b)
{fuzioneaz mulimile etichetate cu a i b}
i a; j b
if i > j then interschimb i i j
for k j to N do
if set[k] = j then set[k] i
Dac consultarea sau modificarea unui element dintr-un tablou conteaz ca o
operaie elementar, atunci se poate demonstra (Exerciiul 3.7) c o serie de n
operaii merge1 i find1 necesit, pentru cazul cel mai nefavorabil i pornind de la
2
starea iniial, un timp n ordinul lui n .
ncercm s mbuntim aceti algoritmi. Folosind n continuare acelai tablou,
vom reprezenta fiecare mulime ca un arbore cu rdcin inversat. Adoptm
urmtoarea tehnic: dac set[i] = i, atunci i este att eticheta unei mulimi, ct i
rdcina arborelui corespunztor; dac set[i] = j i, atunci j este tatl lui i ntr-un
arbore. De exemplu, tabloul:
1
2
set[1]
set[2]
reprezint arborii:
4
set[10]
3
6
4
7
10
Seciunea 3.5
51
procedure merge2(a, b)
{fuzioneaz mulimile etichetate cu a i b}
if a < b then set[b] a
else set[a] b
O serie de n operaii find2 i merge2 necesit, pentru cazul cel mai nefavorabil i
2
pornind de la starea iniial, un timp tot n ordinul lui n (Exerciiul 3.7). Deci,
deocamdat, nu am ctigat nimic fa de prima variant a acestor algoritmi.
Aceasta deoarece dup k apeluri ale lui merge2, se poate s ajungem la un arbore
de nlime k, astfel nct un apel ulterior al lui find2 s ne pun n situaia de a
parcurge k muchii pn la rdcin.
Pn acum am ales (arbitrar) ca elementul minim s fie eticheta unei mulimi.
Cnd fuzionm doi arbori de nlime h 1 i respectiv h 2 , este bine s facem astfel
nct rdcina arborelui de nlime mai mic s devin fiu al celeilalte rdcini.
Atunci, nlimea arborelui rezultat va fi max(h 1 , h 2 ), dac h 1 h 2 , sau h 1 +1, dac
h 1 = h 2 . Vom numi aceast tehnic regul de ponderare. Aplicarea ei implic
renunarea la convenia ca elementul minim s fie eticheta mulimii respective.
Avantajul este c nlimea arborilor nu mai crete att de rapid. Putem demonstra
(Exerciiul 3.9) c folosind regula de ponderare, dup un numr arbitrar de
fuzionri, pornind de la starea iniial, un arbore avnd k vrfuri va avea nlimea
maxim lg k.
nlimea arborilor poate fi memorat ntr-un tablou H[1 .. N], astfel nct H[i] s
conin nlimea vrfului i n arborele su curent. n particular, dac a este
eticheta unei mulimi, H[a] va conine nlimea arborelui corespunztor. Iniial,
H[i] = 0 pentru 1 i N. Algoritmul find2 rmne valabil, dar vom modifica
algoritmul de fuzionare.
procedure merge3(a, b)
{fuzioneaz mulimile etichetate cu a i b;
presupunem c a b}
if H[a] = H[b]
then H[a] H[a]+1
set[b] a
else if H[a] > H[b]
then set[b] a
else set[a] b
O serie de n operaii find2 i merge3 necesit, pentru cazul cel mai nefavorabil i
pornind de la starea iniial, un timp n ordinul lui n log n.
Continum cu mbuntirile, modificnd algoritmul find2. Vom folosi tehnica
comprimrii drumului, care const n urmtoarele. Presupunnd c avem de
determinat mulimea care l conine pe x, traversm (conform cu find2) muchiile
care conduc spre rdcina arborelui. Cunoscnd rdcina, traversm aceleai
52
Capitolul 3
4
11
9
1
10
12
11
20
10
21
16
12
20
21
16
(a)
(b)
Figura 3.6 Comprimarea drumului.
muchii din nou, modificnd acum fiecare vrf ntlnit n cale astfel nct s
conin direct adresa rdcinii. Folosind tehnica comprimrii drumului, nu mai
este adevrat c nlimea unui arbore cu rdcina a este dat de H[a]. Totui,
H[a] reprezint n acest caz o limit superioar a nlimii i procedura merge3
rmne, cu aceast observaie, valabil. Algoritmul find2 devine:
function find3(x)
{returneaz eticheta mulimii care l conine pe x}
rx
while set[r] r do r set[r]
{r este rdcina arborelui}
ix
while i r do
j set[i]
set[i] r
ij
return r
De exemplu, executnd operaia find3(20) asupra arborelui din Figura 3.6a,
obinem arborele din Figura 3.6b.
Algoritmii find3 i merge3 sunt o variant considerabil mbuntit a
procedurilor de tip find i merge. O serie de n operaii find3 i merge3 necesit,
pentru cazul cel mai nefavorabil i pornind de la starea iniial, un timp n ordinul
lg N = min{k | lg lg ... lg N 0}
14243
de k ori
Seciunea 3.5
53
3.6
Exerciii
3.1
Scriei algoritmii de inserare i de tergere a unui nod pentru o stiv
implementat prin tehnica tablourilor paralele.
3.2
Fie G un graf neorientat cu n vrfuri, n 2. Demonstrai echivalena
urmtoarelor propoziii care caracterizeaz un arbore:
i)
ii)
iii)
iv)
54
Capitolul 3
3.6
Implementai un model de simulare a unei liste dinamice de prioriti
folosind structura de heap.
3.7
n situaia n care, consultarea sau modificarea unui element din tablou
conteaz ca o operaie elementar, demonstrai c timpul de execuie necesar
pentru o secven de n operaii find1 i merge1, pornind din starea iniial i
2
pentru cazul cel mai nefavorabil, este n ordinul lui n . Demonstrai aceeai
proprietate pentru find2 i merge2.
Soluie: find1 necesit un timp constant i cel mai nefavorabil caz l reprezint
secvena:
merge1(N, N1); find1(N)
merge1(N1, N2); find1(N)
3.9
Demonstrai prin inducie c, folosind regula de ponderare (procedura
merge3), un arbore cu k vrfuri va avea dup un numr arbitrar de fuzionri i
pornind de la starea iniial, nlimea maxim lg k.
Soluie: Proprietatea este adevrat pentru k = 1. Presupunem c proprietatea este
adevrat pentru i k1 i demonstrm c este adevrat i pentru k.
Fie T arborele (cu k vrfuri i de nlime h) rezultat din aplicarea procedurii
merge3 asupra arborilor T 1 (cu m vrfuri i de nlime h 1 ) i T 2 (cu km vrfuri i
de nlime h 2 ). Se observ c cel puin unul din arborii T 1 i T 2 are cel mult k/2
vrfuri, deoarece este imposibil s avem m > k/2 i km > k/2. Presupunnd c T 1
are cel mult k/2 vrfuri, avem dou posibiliti:
i)
h 1 h 2 h lg (km) lg k
Seciunea 3.6
ii)
Exerciii
55
h 1 = h 2 h = h 1 +1 lg m+1 lg (k/2)+1 = lg k
3.10
Demonstrai c o serie de n operaii find2 i merge3 necesit, pentru cazul
cel mai nefavorabil i pornind de la starea iniial, un timp n ordinul lui n log n.
Indicaie: inei cont de Exerciiul 3.9 i artai c timpul este n ordinul lui
n lg n. Artai apoi c baza logaritmului poate fi oarecare, ordinul timpului fiind
n log n.
3.11
n locul regulii de ponderare, putem adopta urmtoarea tactic de
fuzionare: rdcina arborelui cu mai puine vrfuri devine fiu al rdcinii celuilalt
arbore. Comprimarea drumului nu modific numrul de vrfuri ntr-un arbore,
astfel nct este uor s memorm aceast valoare n mod exact (n cazul folosirii
regulii de ponderare, dup comprimarea drumului, nu se pstreaz nlimea
exact a unui arbore).
Scriei o procedur merge4 care urmeaz aceast tactic i demonstrai un rezultat
corespunztor Exerciiului 3.9.
3.12
Gsii un algoritm pentru a determina dac un graf neorientat este conex.
Folosii o structur de mulimi disjuncte.
Indicaie: Presupunem c graful este reprezentat printr-o list de muchii.
Considerm iniial c fiecare vrf formeaz o submulime (n acest caz, o
component conex a grafului). Dup fiecare citire a unei muchii {a, b} operm
fuzionarea merge3(find3(a), find3(b)), obinnd astfel o nou component conex.
Procedeul se repet, pn cnd terminm de citit toate muchiile grafului. Graful
este conex, dac i numai dac tabloul set devine constant. Analizai eficiena
algoritmului.
n general, prin acest algoritm obinem o partiionare a vrfurilor grafului n
submulimi dou cte dou disjuncte, fiecare submulime coninnd exact vrfurile
cte unei componente conexe a grafului.
3.13
ntr-o structur de mulimi disjuncte, un element x este canonic, dac nu
are tat. n procedurile find3 i merge3 observm urmtoarele:
i)
ii)
Dac x este un element canonic, atunci informaia din set[x] este folosit doar
pentru a preciza c x este canonic.
Dac elementul x nu este canonic, atunci informaia din H[x] nu este folosit.
4. Tipuri abstracte de
date
n acest capitol, vom implementa cteva din structurile de date prezentate n
Capitolul 3. Utilitatea acestor implementri este dubl. n primul rnd, le vom
folosi pentru a exemplifica programarea orientat pe obiect prin elaborarea unor
noi tipuri abstracte. n al doilea rnd, ne vor fi utile ca suport puternic i foarte
flexibil pentru implementarea algoritmilor studiai n Capitolele 6-9. Utiliznd
tipuri abstracte pentru principalele structuri de date, ne vom putea concentra
exclusiv asupra algoritmilor pe care dorim s i programm, fr a mai fi necesar
s ne preocupm de implementarea structurilor necesare.
Elaborarea fiecrei clase cuprinde dou etape, nu neaprat distincte. n prima,
vom stabili facilitile clasei, adic funciile i operatorii prin care se realizeaz
principalele operaii asociate tipului abstract. De asemenea, vom stabili structura
intern a clasei, adic datele membre i funciile nepublice. Etapa a doua cuprinde
programarea, testarea i depanarea clasei, astfel nct, n final, s avem garania
bunei sale funcionri. ntregul proces de elaborare cuprinde numeroase reveniri
asupra unor aspecte deja stabilite, iar fiecare modificare atrage dup sine o
ntreag serie de alte modificri. Nu vom prezenta toate aceste iteraii, dei ele au
fost destul de numeroase, ci doar rezultatele finale, comentnd pe larg, att
facilitile clasei, ct i detaliile de implementare. Vom explica astfel i cteva
aspecte ale programrii orientate pe obiect n limbajul C++, cum sunt clasele
parametrice i motenirea (derivarea). Dorim ca prin aceast manier de
prezentare s oferim posibilitatea de a nelege modul de funcionare i utilizare al
claselor descrise, chiar dac anumite aspecte, legate n special de implementare,
nu sunt suficient aprofundate.
4.1
Tablouri
56
Seciunea 4.1
Tablouri
57
4.1.1
Diferena fundamental dintre tipul abstract pe care l vom elabora i tipul tablou
predefinit const n alocarea dinamic, n timpul execuiei programului, a
spaiului de memorie necesar stocrii elementelor sale. n limbajul C, alocarea
dinamic se realizeaz prin diversele variante ale funciei malloc(), iar
eliberarea zonelor alocate se face prin funcia mfree(). Limbajul C++ a introdus
alocarea dinamic n structura limbajului. Astfel, pentru alocare avem operatorul
new. Acest operator returneaz adresa * zonei de memorie alocat, sau valoarea 0
dac alocarea nu s-a putut face. Pentru eliberarea memoriei alocate prin
intermediul operatorului new, se folosete un alt operator numit delete.
Programul urmtor exemplific detaliat funcionarea acestor doi operatori.
#include <iostream.h>
#include "intErval.h"
int main( ) {
// Operatorul new are ca argumente numele unui tip T
// (predefinit sau definit de utilizator) si dimensiunea
// zonei care va fi alocata. Valoarea returnata este de
// tip "pointer la T". Operatorul new returneaza 0 in
// cazul in care alocarea nu a fost posibila.
// se aloca o zona de 2048 de intregi
int *pi = new int [ 2048 ];
// se aloca o zona de 64 de elemente de tip
// intErval cu domeniul implicit
intErval *pi_m = new intErval [ 64 ];
// se aloca o zona de 8192 de elemente de tip float
float *pf = new float [ 8192 ];
n limbajul C++, tipul de dat care conine adrese este numit pointer. n continuare, vom folosi
termenul pointer, doar atunci cnd ne referim la tipul de dat. Termenul adres va fi folosit
pentru a ne referi la valoarea datelor de tip pointer.
58
//
//
//
//
Capitolul 4
[ ] pf;
[ ] pi;
i;
f;
[ ] pi_m;
m;
return 0;
}
Seciunea 4.1
Tablouri
59
#include <iostream.h>
class X {
public:
X( ) { cout << '*'; }
~X( ) { cout << '~'; }
private:
int x;
};
int main( ) {
cout << '\n';
X *p =new X [ 4 ];
delete p;
p = new X [ 2 ];
delete [ ] p;
cout << '\n';
return 0;
}
60
Capitolul 4
void no_mem( ) {
cerr << "\n\n no mem. \n\n";
exit( 1 );
}
4.1.2
Clasa tablou
Noul tip, numit tablou, va avea ca date membre numrul de elemente i adresa
zonei de memorie n care sunt memorate acestea. Datele membre fiind private,
adic inaccesibile din exteriorul clasei, oferim posibilitatea obinerii numrului
elementelor tabloului prin intermediul unei funcii membre publice numit
size(). Iat definiia complet a clasei tablou.
class tablou {
public:
// constructorii si destructorul
tablou( int = 0 );
// constructor (numarul de elemente)
tablou( const tablou& ); // constructor de copiere
~tablou( ) { delete a; } // elibereaza memoria alocata
// operatori de atribuire si indexare
tablou& operator =( const tablou& );
int& operator []( int );
// returneaza numarul elementelor
size( ) { return d; }
private:
int d; // numarul elementelor (dimensiunea) tabloului
int *a; // adresa zonei alocate
// functie auxiliara de initializare
void init( const tablou& );
};
Seciunea 4.1
Tablouri
61
// valori implicite
// verificarea dimensiunii
// alocarea memoriei
=( const tablou& t ) {
este o atribuire inefectiva x = x?
eliberarea memoriei alocate
initializarea cu t
se returneaza obiectul invocator
62
Capitolul 4
S vedem ce trebuie s facem ca, prin noul operator de atribuire definit, s putem
scrie
iT = jT = kT;
// iT, jT si kT sunt obiecte de tip tablou
Seciunea 4.1
Tablouri
63
return *this;
4.1.3
Utilitatea clasei tablou este strict limitat la tablourile de ntregi, dei un tablou
de float, char, sau de orice alt tip T, se manipuleaz la fel, funciile i datele
membre fiind practic identice. Pentru astfel de situaii, limbajul C++ ofer
posibilitatea generrii automate de clase i funcii pe baza unor abloane
(template). Aceste abloane, numite i clase parametrice, respectiv funcii
parametrice, depind de unul sau mai muli parametri care, de cele mai multe ori,
sunt tipuri predefinite sau definite de utilizator.
ablonul este o declaraie prin care se specific forma general a unei clase sau
funcii. Iat un exemplul simplu: o funcie care returneaz maximul a dou valori
de tip T.
template <class T>
T max( T a, T b ) {
return a > b? a: b;
}
Acest ablon se citete astfel: max() este o funcie cu dou argumente de tip T,
care returneaz maximul celor dou argumente, adic o valoare de tip T. Tipul T
poate fi orice tip predefinit, sau definit de utilizator, cu conditia s aib definit
operatorul de comparare >, fr de care funcia max() nu poate funciona.
64
Capitolul 4
n prezent sunt utilizate dou modele generale pentru instanierea (generarea) abloanelor, fiecare
cu anumite avantaje i dezavantaje. Reprezentative pentru aceste modele sunt compilatoarele
Borland C++ i translatoarele Cfront de la AT&T. Ambele modele sunt compatibile cu plasarea
abloanelor n fiiere header.
Seciunea 4.1
Tablouri
65
tablou<float> y( 16 );
tablou<int> x( 32 );
tablou<unsigned char> z( 64 );
66
Capitolul 4
Seciunea 4.1
Tablouri
67
68
Capitolul 4
Aceti operatori sunt utilizabili doar dac obiectelor de tip T li se pot aplica
operatorii de extragere/inserare >>, respectiv <<. n caz contrar, orice ncercare de
a aplica obiectelor de tip tablou<T> operatorii mai sus definii, va fi semnalata ca
eroare la compilarea programului.
Operatorul de extragere (citire) >> prezint o anumit particularitate fa de
celelalte funcii care opereaz asupra tablourilor: trebuie s modifice chiar
dimensiunea tabloului. Dou variante de a realiza aceast operaie, dintre care una
prin intermediul funciei newsize( ), sunt discutate n Exerciiile 4.2 i 4.3.
Marcarea erorilor la citire se realizeaz prin modificarea corespunztoare a strii
istream-ului prin
is.clear( ios::failbit );
Dup cum am precizat n Seciunea 2.3.2, starea unui istream se poate testa
printr-un simplu if ( cin >> ... ). Odat ce un istream a ajuns ntr-o stare
de eroare, nu mai rspunde la operatorii respectivi, dect dup ce este readus la
starea normal de utilizare prin instruciunea
is.clear();
4.2
Stivele, cozile i heap-urile sunt, n esen, tablouri manipulate altfel dect prin
operatorul de indexare. Acesat afirmaie contrazice aparent definiiile date n
Capitolul 3. Aici se precizeaz c stivele i cozile sunt liste liniare n care
inserrile/extragerile se fac conform unor algoritmi particulari, iar heap-urile sunt
arbori binari complei. Tot n Capitolul 3 am artat c reprezentarea cea mai
comod pentru toate aceste structuri este cea secvenial, bazat pe tablouri.
n terminologia specific programrii orientate pe obiect, spunem c tipurile
stiva<T>, coada<T> i heap<T> sunt derivate din tipul tablou<T>, sau c
motenesc tipul tablou<T>. Tipul tablou<T> se numete tip de baz pentru
Seciunea 4.2
69
4.2.1
Clasa stiva<T> este un tip nou, derivat din clasa tablou<T>. n limbajul C++,
derivarea se indic prin specificarea claselor de baz (pot fi mai multe!), imediat
dup numele clasei.
template <class T>
class stiva: private tablou<T> {
// ....
};
Fiecare clas de baz este precedat de atributul public sau private, prin care
se specific modalitatea de motenire. O clas derivat public este un subtip al
clasei de baz, iar una derivat private este un tip nou, distinct fa de tipul de
baz.
Clasa derivat motenete toi membrii clasei de baz, cu excepia constructorilor
i destructorilor, dar nu are acces la membrii private ai clasei de baz. Atunci
cnd este necesar, acest incovenient poate fi evitat prin utilizarea n clasa de baz
a nivelului de acces protected n locul celui private. Membrii protected sunt
membri privai, dar accesibili claselor derivate. Nivelul de acces al membrilor
motenii se modific prin derivare astfel:
Membrii neprivai dintr-o clas de baz public i pstreaz nivelele de acces
i n clasa derivat.
Membrii neprivai dintr-o clas de baz privat devin membri private n clasa
derivat.
Revenind la clasa stiva<T>, putem spune c motenete de la clasa de baz
tablou<T> membrii
int d;
T *a;
70
Capitolul 4
Seciunea 4.2
71
intereseaz doar algoritmul. Dar, cum implementm efectiv aceast funcie, astfel
nct s cuprindem ambele situaii? ntrebarea poate fi formulat n contextul mult
mai general al tratrii excepiilor. Rezolvarea unor cazuri particulare, a
excepiilor de la anumite reguli, problem care nu este strict de domeniul
programrii, poate da mai puine dureri de cap prin aplicarea unor principii foarte
simple. Iat, de exemplu, un astfel de principiu formulat de Winston Churchill:
Nu m intrerupei n timp ce ntrerup.
Tratarea excepiilor devine o chestiune foarte complicat, mai ales n cazul
utilizrii unor funcii sau obiecte dintr-o bibliotec. Autorul unei biblioteci de
funcii (obiecte) poate detecta excepiile din timpul execuiei dar, n general, nu
are nici o idee cum s le trateze. Pe de alt parte, utilizatorul bibliotecii tie ce s
fac n cazul apariiei unor excepii, dar nu le poate detecta. Noiunea de excepie,
noiune acceptat de Comitetul de standardizare ANSI C++, introduce un
mecanism consistent de rezolvare a unor astfel de situaii. Ideea este ca, n
momentul cnd o funcie detecteaz o situaie pe care nu o poate rezolva, s
semnaleze (throw) o excepie, cu sperana c una din funciile (direct sau
indirect) invocatoare va rezolva apoi problema. O funcie care este pregtit
pentru acest tip de evenimente i va anuna n prealabil disponibilitatea de a trata
(catch) excepii.
Mecanismul schiat mai sus este o alternativ la tehnicile tradiionale, atunci cnd
acestea se dovedesc a fi inadecvate. El ofer o cale de separare explicit a
secvenelor pentru tratarea erorilor de codul propriu-zis, programul devenind
astfel mai clar i mult mai uor de ntreinut. Din pcate, la nivelul anului 1994,
foarte puine compilatoare C++ implementeaz complet mecanismul throw
catch. Revenim de aceea la stilul clasic, stil independent de limbajul de
programare folosit. Uzual, la ntlnirea unor erori se acioneaz n unul din
urmtoarele moduri:
Se
Se
Se
Se
de
termin programul.
returneaz o valoare reprezentnd eroare.
returneaz o valoare legal, programul fiind lsat ntr-o stare ilegal.
invoc o funcie special construit de programator pentru a fi apelat n caz
eroare.
Terminarea programului se realizeaz prin revenirea din funcia main(), sau prin
invocarea unei funcii de bibliotec numit exit(). Valoarea returnat de main(),
precum i argumentul ntreg al funciei exit(), este interpretat de sistemul de
operare ca un cod de retur al programului. Un cod de retur nul (zero) semnific
executarea corect a programului.
Pn n prezent, am utilizat tratarea excepiilor prin terminarea programului n
clasa intErval. Un alt exemplu de tratare a excepiilor se poate remarca la
operatorul de indexare din clasa tablou<T>. Aici am utilizat penultima alternativ
72
Capitolul 4
din cele patru enunate mai sus: valoarea returnat este legal, dar programul nu a
avut posibilitatea de a trata eroarea.
Pentru stiv i, de fapt, pentru multe din structurile implementate aici i
susceptibile la situaii de excepie, am ales varianta a doua: returnarea unei valori
reprezentnd eroare. Pentru a putea distinge ct mai simplu situaiile normale de
cazurile de excepie, am convenit ca funcia pop() s transmit elementul din
vrful stivei prin intermediul unui argument de tip referin, valoarea returnat
efectiv de funcie indicnd existena sau inexistena acestui element. Astfel,
secvena
while( s.pop( v ) ) {
// ...
}
se execut att timp ct n stiva s mai sunt elemente, variabila v avnd de fiecare
dat valoarea elementului din vrful stivei. Funcia push() are un comportament
asemntor, secvena
while( s.push( v ) ) {
// ...
}
Seciunea 4.2
73
4.2.2
Clasa heap<T>
Vom utiliza structura de heap descris n Seciunea 3.4 pentru implementarea unei
clase definit prin operaiile de inserare a unei valori i de extragere a maximului.
Clasa parametric heap<T> seamn foarte mult cu clasele stiva<T> i
coada<T>. Diferenele apar doar la implementarea operaiilor de inserare n heap
i de extragere a maximului. Definiia clasei heap<T> este:
#ifndef __HEAP_H
#define __HEAP_H
#include <iostream.h>
#include <stdlib.h>
#include "tablou.h"
74
Capitolul 4
Seciunea 4.2
template <class T>
void heap<T>::sift_down(
T *A = a - 1; // a[ 0
// a[ n
int n = h + 1, k = i +
75
int i ) {
]
este A[ 1 ], ...,
- 1 ] este A[ n ]
1, j;
do {
j = k;
if ( 2*j <= n && A[ 2*j
] > A[ k ] ) k = 2*j;
if ( 2*j < n && A[ 2*j+1 ] > A[ k ] ) k = 2*j+1;
T tmp = A[ j ]; A[ j ] = A[ k ]; A[ k ] = tmp;
} while ( j != k );
}
#endif
76
Capitolul 4
cout << "Inserare in heap (^Z/#" << (SIZE - 1) << ")\n... ";
while ( cin >> v ) {
hi.insert( v );
cout << "... ";
}
cin.clear( );
cout << "Extragere din heap\n";
while ( hi.delete_max( v ) ) cout << v << '\n';
return 0;
}
a, const intErval& b ) {
Dei nu este invocat explicit, operatorul de conversie la int este aplicat variabilei
constante x2. nainte de a discuta motivul acestei invocri, s ne oprim puin
asupra manipulrii obiectelor constante. Pentru acest tip de variabile (variabile
constante!), aa cum este x2, se invoc doar funciile membre declarate explicit
const, funcii care nu modific obiectul invocator. O astfel de funcie fiind i
Seciunea 4.2
77
Acelai efect l are i definirea non-const a obiectului x2, dar scopul nu este de a
elimina mesajul, ci de a nelege (i de a elimina) cauza lui.
Atribuirea x1 = x2 ar trebui rezolvat de operatorul de atribuire generat automat
de compilator, pentru fiecare clas. n cazul nostru, acest operator nu se invoc,
deoarece atribuirea poate fi rezolvat numai prin intermediul funciilor membre
explicit definite:
x2 este convertit la int prin operator int( ), conversie care genereaz i
mesajul discutat mai sus
Rezultatul conversiei este atribuit lui x1 prin operator =(int).
Din pcate, rezultatul atribuirii este incorect. n loc ca x2 s fie copiat n x1, va fi
actualizat doar valoarea v a lui x1 cu valoarea v lui x2. Evident c, n exemplul
de mai sus, x1 va semnala depirea domeniului su.
Soluia pentru eliminarea acestei aparente anomalii, generate de interferena
dintre operator int( ) i operator =(int), const n definirea explicit a
operatorului de atribuire pentru obiecte de tip intErval:
intErval& intErval::operator =( const intErval& s ) {
min = s.min; v = s.v; max = s.max;
return *this;
}
Particularitatea acestuia const doar n tipul valorii returnate, const T&, valoare
imposibil de modificat. Consistena declaraiei const, asociat operatorului de
indexare, este dat de ctre proiectantul clasei i nu poate fi verificat semantic
de ctre compilator. O astfel de declaraie poate fi ataat chiar i operatorului de
78
Capitolul 4
indexare obinuit (cel non-const), cci el nu modific nici una din datele membre
ale clasei tablou<T>. Ar fi ns absurd, deoarece tabloul se modific de fapt prin
modificarea elementelor sale.
4.3
Clasa lista<E>
Seciunea 4.3
Clasa lista<E>
79
nod<E> *a;
// adresa nodului dupa care se face inserarea
nod<E> *pn;
// adresa nodului de inserat
// ...
pn->next = a->next;
a->next = pn;
tergerea unui nod din list (operaie care necesit cunoaterea nu numai a
adresei elementului de eliminat, ci i a celui anterior):
nod<E> *a;
// adresa nodului de sters
nod<E> *pp;
// adresa nodului anterior lui a
// ...
pp->next = a->next; // stergerea propriu-zisa
// ...
// eliberarea spatiului de memorie alocat nodului de
// adresa a, nod tocmai eliminat din lista
80
Capitolul 4
listei i, desigur, a nodurilor, fiind invizibil din exterior. Conteaz doar tipul
informaiilor din list i nimic altceva. Iat de ce clasa nod<E> poate fi n
ntregime nepublic:
template <class E>
class nod {
friend class lista<E>;
// ...
protected:
nod( const E& v ): val( v ) { next = 0; }
E
val; // informatia propriu-zisa
nod<E> *next; // adresa nodului urmator
};
n lipsa declaraiei friend, obiectele de tip nod<E> nici mcar nu pot fi definite,
datorit lipsei unui constructor public. Prin declaraia friend se permite accesul
clasei lista<E> la toi membrii privai ai clasei nod<E>. Singurul loc n care
putem utiliza obiectele de tip nod<E> este deci domeniul clasei lista<E>.
nainte de a trece la definirea funciilor de manipulare a listelor, s remarcm un
aspect interesant la constructorul clasei nod<E>. Iniializarea membrului val cu
argumentul v nu a fost realizat printr-o atribuire val = v, ci invocnd
constructorul clasei E prin lista de iniializare a membrilor:
nod( const E& v ): val( v ) { // ... }
n cazul listelor, locul indicelui este luat de elementul curent. Ca i indicele, care
nu este memorat n clasa tablou, acest element curent nu are de ce s fac parte
din structura clasei lista<T>. Putem avea oricte elemente curente,
corespunztoare orictor parcurgeri, tot aa cum un tablou poate fi adresat prin
orici indici. Analogia tablou-list se sfrete aici. Locul operatorului de
indexare [] nu este luat de o funcie membr, ci de o clas special numit
iterator<E>.
ntr-o variant minim, datele membre din clasa iterator<E> sunt:
Seciunea 4.3
Clasa lista<E>
81
adic adresa nodului actual (curent) i adresa adresei primului element al listei.
De ce adresa adresei? Pentru ca iteratorul s rmn funcional i n situaia
eliminrii primului element din list. Operatorul (), numit n terminologia
specific limbajului C++ iterator, este cel care implementeaz efectiv operaia de
parcurgere
template <class E>
iterator<E>::operator ()( E& v ) {
if( a ) { v = a->val; a = a->next; return 1; }
else
{ if( *phead ) a = *phead; return 0; }
}
82
Capitolul 4
Seciunea 4.3
Clasa lista<E>
83
84
Capitolul 4
4.4
Exerciii
4.1
n cazul alocrii dinamice, este mai rentabil ca memoria s se aloce n
blocuri mici sau n blocuri mari?
Soluie: Rulai urmtorul program. Atenie, stiva programului trebuie s fie
suficient de mare pentru a rezista apelurilor recursive ale funciei
alocareDinmica().
#include <iostream.h>
static int nivel;
static int raport;
Seciunea 4.4
Exerciii
85
: " << y;
: " << y;
86
Capitolul 4
return *this;
}
4.4
Seciunea 4.4
Exerciii
87
4.5
Testai funcionarea claselor stiva<T> i coada<T>, folosind elemente de
tip int.
Soluie: Dac programul urmtor furnizeaz rezultate corecte, atunci putem avea
certitudinea c cele dou clase sunt corect implementate.
#include <iostream.h>
#include "stiva.h"
#include "coada.h"
void main( ) {
int n, i = 0;
cout << "Numarul elementelor ... "; cin >> n;
stiva<int> st( n );
coada<int> cd( n );
cout << "\nStiva push ... ";
while ( st.push( i ) ) cout << i++ << ' ';
cout << "\nStiva pop ... ";
while ( st.pop( i ) )
cout << i
88
Capitolul 4
4.6
Testai funcionarea clasei parametrice lista<E> cu noduri de tip adrese
de tablou i apoi cu noduri de tip tablou<T>.
Soluie (incorect): Programul urmtor nu funcioneaz corect dect dup ce a
fost modificat pentru noduri de tip tablou<T>. Pentru a-l corecta, nu uitai c
toate variabilele din ciclul for sunt locale.
#include <iostream.h>
#include "tablou.h"
#include "lista.h"
typedef tablou<int> *PTI;
main( ) {
lista<PTI> tablist;
for ( int n = 0, i = 0; i < 4; i++ )
{ tablou<int> t( i + 1 );
for ( int j = t.size( ); j--; t[ j ] = n++ );
cout << "tablou " << i << ' '; cout << t << '\n';
tablist.insert( &t );
}
cout << "\nLista "; cout << tablist << "\n";
PTI t; iterator<PTI> it = tablist;
while( it( t ) )
cout << "Tablou din lista" << *t << '\n';
return 1;
}
4.7
Destructorul clasei lista<T> distruge nodurile, invocnd procedura
iterativ reset(). Implementai un destructor n variant recursiv.
Indicaie: Dac fiecare element de tip nod<E> are un destructor de forma
~nod( ) { delete next; }, atunci destructorul clasei lista<E> poate fi
~lista( ) { delete head; }.
5. Analiza eficienei
algoritmilor
Vom dezvolta n acest capitol aparatul matematic necesar pentru analiza eficienei
algoritmilor, ncercnd ca aceast incursiune matematic s nu fie excesiv de
formal. Apoi, vom arta, pe baza unor exemple, cum poate fi analizat un
algoritm. O atenie special o vom acorda tehnicilor de analiz a algoritmilor
recursivi.
5.1
Notaia asimptotic
5.1.1
O( f ) = {t : N R | (c R ) (n 0 N) (n n 0 ) [t(n) cf (n)]}
Cu alte cuvinte, O( f ) (se citete ordinul lui f ) este mulimea tuturor funciilor t
mrginite superior de un multiplu real pozitiv al lui f, pentru valori suficient de
mari ale argumentului. Vom conveni s spunem c t este n ordinul lui f (sau,
echivalent, t este n O( f ), sau t O( f )) chiar i atunci cnd valoarea f (n) este
negativ sau nedefinit pentru anumite valori n < n 0 . n mod similar, vom vorbi
despre ordinul lui f chiar i atunci cnd valoarea t(n) este negativ sau nedefinit
pentru un numr finit de valori ale lui n; n acest caz, vom alege n 0 suficient de
mare, astfel nct, pentru n n 0 , acest lucru s nu mai apar. De exemplu, vom
vorbi despre ordinul lui n/log n, chiar dac pentru n = 0 i n = 1 funcia nu este
definit. n loc de t O( f ), uneori este mai convenabil s folosim notaia
t(n) O( f (n)), subnelegnd aici c t(n) i f (n) sunt funcii.
89
90
Capitolul 5
chiar dac pentru 0 n 6 polinomul este negativ. Exerciiul 5.8 trateaz cazul
unui polinom oarecare.
Seciunea 5.1
Notaia asimptotic
91
Notaia O( f ) este folosit pentru a limita superior timpul necesar unui algoritm,
msurnd eficiena algoritmului respectiv. Uneori este util s estimm i o limit
inferioar a acestui timp. n acest scop, definim mulimea
( f ) = {t : N R | (c R ) (n 0 N) (n n 0 ) [t(n) cf (n)]}
Exist o anumit dualitate ntre notaiile O( f ) i ( f ). i anume, pentru dou
O( f ) = {t : N N R | (c R ) (m 0 , n 0 N) (m m 0 ) (n n 0 )
[t(m, n) cf (m, n)]}
Similar, se obin i celelalte generalizri.
5.1.2
O( f | P) = {t : N R (c R ) (n 0 N) (n n 0 )
[P(n) t(n) cf (n)]}
Notaia O( f ) este echivalent cu O( f | P), unde P este predicatul a crui valoare
este mereu true. Similar, se obin notaiile ( f | P) i ( f | P).
92
Capitolul 5
a
t ( n) =
t ( n / 2) + t ( n / 2) + bn
pentru n = 1
pentru n 1
a
t ( n) =
2t (n / 2) + bn
pentru n = 1
pentru n > 1 o putere a lui 2
Prin tehnicile pe care le vom nva la sfritul acestui capitol, ajungem la relaia
t(n) (n log n | n este o putere a lui 2)
Pentru a arta acum c t (n log n), mai trebuie doar s verificm dac t este
eventual nedescresctoare i dac n log n este neted.
Prin inducie, vom demonstra c (n 1) [t(n) t(n+1)]. Pentru nceput, s notm
c
t(1) = a 2(a+b) = t(2)
Fie n > 1. Presupunem c pentru orice m < n avem t(m) t(m+1). n particular,
t(n/2) t((n+1)/2)
t(n/2) t((n+1)/2)
Seciunea 5.1
Notaia asimptotic
93
Atunci,
t(n) = t(n/2)+t(n/2)+bn t((n+1)/2)+t((n+1)/2)+b(n+1) = t(n+1)
n fine, mai rmne s artm c n log n este neted. Funcia n log n este eventual
nedescresctoare i
2n log(2n) = 2n(log 2 + log n) = (2 log 2)n + 2n log n
O(n + n log n) = O(max(n, n log n)) = O(n log n)
De multe ori, timpul de execuie al unui algoritm se exprim sub forma unor
inegaliti de forma
t (n)
t ( n) 1
t ( n / 2) + t ( n / 2) + cn
pentru n n0
pentru n > n0
i, simultan
t (n)
t (n) 2
t ( n / 2) + t ( n / 2) + dn
pentru n n0
pentru n > n0
1
f ( n) =
f ( n / 2 ) + f ( n / 2 ) + n
pentru n = 1
pentru n 1
94
Capitolul 5
5.2
Nu exist o formul general pentru analiza eficienei unui algoritm. Este mai
curnd o chestiune de raionament, intuiie i experien. Vom arta, pe baza
exemplelor, cum se poate efectua o astfel de analiz.
5.2.1
Considerm algoritmul select din Seciunea 1.3. Timpul pentru o singur execuie
a buclei interioare poate fi mrginit superior de o constant a. n total, pentru un i
dat, bucla interioar necesit un timp de cel mult b+a(ni) uniti, unde b este o
constant reprezentnd timpul necesar pentru iniializarea buclei. O singur
execuie a buclei exterioare are loc n cel mult c+b+a(ni) uniti de timp, unde c
este o alt constant. Algoritmul dureaz n total cel mult
n 1
d + ( c + b + a( n i ))
i =1
Seciunea 5.2
5.2.2
95
(i 1) = n(n 1) / 2 (n 2 )
i =1
Vom estima acum timpul mediu necesar pentru un caz oarecare. Presupunem c
elementele tabloului T sunt distincte i c orice permutare a lor are aceeai
probabilitate de apariie. Atunci, dac 1 k i, probabilitatea ca T[i] s fie cel
de-al k-lea cel mai mare element dintre elementele T[1], T[2], , T[i] este 1/i.
Pentru un i fixat, condiia T[i] < T[i1] este fals cu probabilitatea 1/i, deci
probabilitatea ca s se execute comparaia x < T[ j], o singur dat nainte de
ieirea din bucla while, este 1/i. Comparaia x < T[ j] se execut de exact dou
ori tot cu probabilitatea 1/i etc. Probabilitatea ca s se execute comparaia de
exact i1 ori este 2/i, deoarece aceasta se ntmpl att cnd x < T[1], ct i cnd
T[1] x < T[2]. Pentru un i fixat, numrul mediu de comparaii este
c i = 11/i + 21/i ++ (i2)1/i + (i1)2/i = (i+1)/2 1/i
n
ci
i= 2
(n +3n)/4 H n (n )
2
(Exerciiul 5.17).
Se observ c algoritmul insert efectueaz pentru cazul mediu de dou ori mai
puine comparaii dect pentru cazul cel mai nefavorabil. Totui, n ambele
2
situaii, numrul comparaiilor este n (n ).
Algoritmul necesit un timp n (n ), att pentru cazul mediu, ct i pentru cel
mai nefavorabil. Cu toate acestea, pentru cazul cel mai favorabil, cnd iniial
tabloul este ordonat cresctor, timpul este n O(n). De fapt, n acest caz, timpul
este i n (n), deci este n (n).
2
96
5.2.3
Capitolul 5
Heapsort
Vom analiza, pentru nceput, algoritmul make-heap din Seciunea 3.4. Definim ca
barometru instruciunile din bucla repeat a algoritmului sift-down. Fie m numrul
maxim de repetri al acestei bucle, cauzat de apelul lui sift-down(T, i), unde i este
fixat. Notm cu j t valoarea lui j dup ce se execut atribuirea j k la a t-a
repetare a buclei. Evident, j 1 = i. Dac 1 < t m, la sfritul celei de-a (t1)-a
repetri a buclei, avem j k i k 2j. n general, j t 2j t1 pentru 1 < t m.
Atunci,
n j m 2j m1 4j m2 2
Rezult 2
m1
m1
Numrul total de executri ale buclei repeat la formarea unui heap este mrginit
superior de
a
(1 + lg(n / i )) ,
unde a = n/2
(*)
i =1
lg(n / i ) 2 k lg(n / 2 k ) ,
unde b = 2
i c = 2
k+1
i =b
i =1
k =0
lg(n / i ) 2 k lg (n / 2 k ) 2 d +1 lg (n / 2 d 1 )
Demonstraia ultimei inegaliti rezult din Exerciiul 5.26. Dar d = lg(n/2)
implic d+1 lg n i d1 lg(n/8). Deci,
a
lg (n / i ) 3n
i =1
Din (*) deducem c n/2+3n repetri ale buclei repeat sunt suficiente pentru a
construi un heap, deci make-heap necesit un timp t O(n). Pe de alt parte,
deoarece orice algoritm pentru formarea unui heap trebuie s utilizeze fiecare
element din tablou cel puin o dat, t (n). Deci, t (n). Putei compara acest
timp cu timpul necesar algoritmului slow-make-heap (Exerciiul 5.28).
Pentru cel mai nefavorabil caz, sift-down(T[1 .. i1], 1) necesit un timp n
O(log n) (Exerciiul 5.27). innd cont i de faptul c algoritmul make-heap este
Seciunea 5.2
97
liniar, rezult c timpul pentru algoritmul heapsort pentru cazul cel mai
nefavorabil este n O(n log n). Mai mult, timpul de execuie pentru heapsort este
de fapt n (n log n), att pentru cazul cel mai nefavorabil, ct i pentru cazul
mediu.
Algoritmii de sortare prezentai pn acum au o caracteristic comun: se bazeaz
numai pe comparaii ntre elementele tabloului T. Din aceast cauz, i vom numi
algoritmi de sortare prin comparaie. Vom cunoate i ali algoritmi de acest tip:
bubblesort, quicksort, mergesort. S observm c, pentru cel mai nefavorabil caz,
orice algoritm de sortare prin comparaie necesit un timp n (n log n)
(Exerciiul 5.30). Pentru cel mai nefavorabil caz, algoritmul heapsort este deci
optim (n limitele unei constante multiplicative). Acelai lucru se ntmpl i cu
mergesort.
5.2.4
98
Capitolul 5
pentru n = 1
1
t ( n) =
2t (n 1) + 1
pentru n > 1
5.3
5.3.1
Metoda iteraiei
n1
n 2
t(1) + 2i
i= 0
Seciunea 5.3
5.3.2
99
Inducia constructiv
pentru n = 0
0
f ( n) =
f (n 1) + n
pentru n > 0
f ( n) =
i =0
i =0
i n = n2
100
5.3.3
Capitolul 5
Exist, din fericire, i tehnici care pot fi folosite aproape automat pentru a rezolva
anumite clase de recurene. Vom ncepe prin a considera ecuaii recurente liniare
omogene, adic de forma
a 0 t n + a 1 t n1 + + a k t n k = 0
(*)
n1
+ + akx
nk
=0
Soluiile acestei ecuaii sunt fie soluia trivial x = 0, care nu ne intereseaz, fie
soluiile ecuaiei
a0x + a1x
k
k1
+ + ak = 0
t n = ci rin
i =1
n2
Seciunea 5.3
101
t n = c1r1n + c2r2n
Impunnd condiiile iniiale, obinem
=0
c1 + c2
r1c1 + r2c2 = 1
n=0
n=1
de unde determinm
c 1,2 = 1 / 5
Deci, t n = 1 / 5 ( r1n r2n ) . Observm c r 1 = = (1 + 5 )/2, r 2 =
i obinem
t n = 1 / 5 ( () )
n
n3
5.3.4
102
Capitolul 5
a 0 t n + a 1 t n1 + + a k t n k = b p(n)
n
(**)
unde b este o constant, iar p(n) este un polinom n n de grad d. Ideea general
este ca, prin manipulri convenabile, s reducem un astfel de caz la o form
omogen.
De exemplu, o astfel de recuren poate fi:
t n 2t n1 = 3
n+1
n+1
adic (x2)(x3) = 0.
Intuitiv, observm c factorul (x2) corespunde prii stngi a recurenei iniiale,
n timp ce factorul (x3) a aprut ca rezultat al manipulrilor efectuate, pentru a
scpa de parte dreapt.
Generaliznd acest procedeu, se poate arta c, pentru a rezolva (**), este
suficient s lum urmtoarea ecuaie caracteristic:
(a 0 x + a 1 x
k
k1
+ + a k )(xb)
d+1
=0
n1
Seciunea 5.3
103
Avem nevoie de dou condiii iniiale. tim c t 0 = 0; pentru a gsi cea de-a doua
condiie calculm
t 1 = 2t 0 + 1
Din condiiile iniiale, obinem
tn = 2 1
n
i deci, t n (2 ). Putem obine chiar ceva mai mult. Substituind soluia general
napoi n recurena iniial, gsim
n
1 = t n 2t n1 = c 1 + c 2 2 2(c 1 + c 2 2
n
n1
) = c 1
5.3.5
Schimbarea variabilei
n>1
104
Capitolul 5
T(n) = c 1 n + c 2 n
2
Rezult
T(n) O(n | n este o putere a lui 2)
2
n>1
cu ecuaia caracteristic
2
(x4) = 0
i soluia general t k = c 1 4 + c 2 k4 . Atunci,
2
T(n) = c 1 n + c 2 n lg n
2
i obinem
T(n) O(n log n | n este o putere a lui 2)
2
n>1
T(2 ) = 3T(2
k1
) + c2
t k = 3t k1 + c2
cu ecuaia caracteristic
(x3)(x2) = 0
tk = c13 + c22
k
T(n) = c 1 3
lg n
+ c2n
i, deoarece
a
lg b
=b
lg a
Seciunea 5.3
105
obinem
T(n) = c 1 n
lg 3
+ c2n
deci,
T(n) O(n
lg 3
n > n0
(n k )
T (n) (n k log n)
(n logb a )
pentru a < b k
pentru a = b k
pentru a > b k
5.4
Exerciii
5.1
i)
n O(n )
ii)
n O(n )
2
3
3
2
iii) 2
O(2 )
iv) (n+1)! O(n!)
n+1
v)
O(n )]
2
106
Capitolul 5
5.2
Presupunnd c f este strict pozitiv pe N, demonstrai c definiia lui
O( f ) este echivalent cu urmtoarea definiie:
+
O( f ) = {t : N R | (c R ) (n N) [t(n) cf (n)]}
*
5.3
Demonstrai c relaia O este tranzitiv: dac f O(g) i g O(h),
atunci f O(h). Deducei de aici c dac g O(h), atunci O(g) O(h).
5.4
i)
ii)
5.5
O( f ) = O(g)
O( f ) O(g)
f O(g) i g O( f )
f O(g) i g O( f )
1+sin n
5.6
Pentru oricare dou funcii f, g : N R definim urmtoarea relaie
binar: f g dac O( f ) O(g). Demonstrai c relaia este o relaie de
*
ordine parial n mulimea funciilor definite pe N i cu valori n R .
*
O( f + g) = O(max( f, g))
unde suma i maximul se iau punctual.
5.8
f O(n ).
m
5.9
Seciunea 5.4
Exerciii
107
5.11
i)
Fie f , g : N R . Demonstrai c:
O( f ) = O(g)
O( f ) O(g)
ii)
dar
n O(log n)
+
f O(g) g ( f )
5.14
f (g)
ii)
108
5.17
i)
ii)
Capitolul 5
ik
(n
k+1
) pentru oricare k N
i =1
n
iii)
i 1
(log n)
i =1
i 1 = ln n + + 1/2n 1/12n 2 +
i =1
(ni)(i+1) n
Deoarece
(n!) = (n1) ((n1)2) ((n2)3)(2(n1)) (1n) n
2
n ! 2 n (n / e) n (1 + (1 / n))
unde e = 1,71828 .
5.18
Artai c timpul de execuie al unui algoritm este n (g), g : N R ,
dac i numai dac: timpul este n O(g) pentru cazul cel mai nefavorabil i n (g)
pentru cazul cel mai favorabil.
*
5.19
Seciunea 5.4
Exerciii
109
5.20
Demonstrai Proprietatea 5.1. Artai pe baza unor contraexemple c cele
dou condiii t(n) este eventual nedescresctoare i f (bn) O( f (n)) sunt
necesare.
5.21
for i 1 to n do
for j 1 to i+1 do
{operaie elementar}
for i 1 to n do
for j 1 to 6 do
for k 1 to n do
{operaie elementar}
for i 1 to n do
for j 1 to i do
for k 1 to n do
{operaie elementar}
5.22
5.23
110
Capitolul 5
n
5.25
2 k lg(n / 2 k ) = 2 d +1 lg(n / 2 d 1 ) 2 lg n
k =0
Soluie:
d
k =0
k =0
2 k lg(n / 2 k ) = (2 d +1 1) lg n (2 k k )
(2 k k ) = (d 1)2 d +1 + 2
k =0
Seciunea 5.4
Exerciii
111
5.27
Analizai algoritmii percolate i sift-down pentru cel mai nefavorabil caz,
presupunnd c opereaz asupra unui heap cu n elemente.
Indicaie: n cazul cel mai nefavorabil, algoritmii percolate i sift-down necesit
un timp n ordinul exact al nlimii arborelui complet care reprezint heap-ul,
adic n (lg n) = (log n).
5.28
Soluie: Pentru slow-make-heap, cazul cel mai nefavorabil este atunci cnd,
iniial, T este ordonat cresctor. La pasul i, se apeleaz percolate(T[1 .. i], i), care
efectueaz lg i comparaii ntre elemente ale lui T. Numrul total de comparaii
este atunci
C(n) (n1) lg n O(n log n)
Pe de alt parte, avem
n
C(n) =
i =2
lg i >
(lg i 1) = lg n! (n1)
i=2
112
Capitolul 5
Fiecare din cele n! permutri a celor n elemente trebuie s apar ca vrf terminal
n arborele de decizie. Vom lua ca barometru comparaia ntre dou elemente ale
tabloului T. nlimea h a arborelui de decizie corespunde numrului de
comparaii pentru cel mai nefavorabil caz. Deoarece cutm limita inferioar a
timpului, ne intereseaz doar algoritmii cei mai performani de sortare, deci putem
h
presupune c numrul de vrfuri este minim, adic n!. Avem: n! 2 (demonstrai
acest lucru!), adic h lg n!. Considernd i relaia log n! (n log n) (vezi
Exerciiul 5.17), rezult c timpul de execuie pentru orice algoritm de sortare
prin comparaie este, n cazul cel mai nefavorabil, n (n log n).
5.31
Analizai algoritmul heapsort pentru cel mai favorabil caz. Care este cel
mai favorabil caz?
5.32
Soluie:
i)
ii) Pentru a analiza algoritmul fib3, lum ca barometru instruciunile din bucla
while. Fie n t valoarea lui n la sfritul executrii celei de-a t-a bucle. n
particular, n 1 = n/2. Dac 2 t m, atunci
n t = n t1 /2 n t1 /2
Deci,
n t n t1 /2 n/2
Fie m = 1 + lg n. Deducem:
n m n/2 < 1
m
Seciunea 5.4
Exerciii
113
5.34
Care este ordinul timpului de execuie pentru un algoritm recursiv cu
recurena t n = 2t n1 + n.
2
5.35
Scriei o variant recursiv a algoritmului de sortare prin inserie i
determinai ordinul timpului de execuie pentru cel mai nefavorabil caz.
Indicaie: Pentru a sorta T[1 .. n], sortm recursiv T[1 .. n1] i inserm T[n] n
tabloul sortat T[1 .. n1].
5.36
Determinai prin schimbare de variabil ordinul timpului de execuie
pentru un algoritm cu recurena T(n) = 2T(n/2) + n lg n, unde n > 1 este o putere a
lui 2.
Indicaie: T(n) O(n log n | n este o putere a lui 2)
2
5.37
6. Algoritmi greedy
Pui n faa unei probleme pentru care trebuie s elaborm un algoritm, de multe
ori nu tim cum s ncepem. Ca i n orice alt activitate, exist cteva principii
generale care ne pot ajuta n aceast situaie. Ne propunem s prezentm n
urmtoarele capitole tehnicile fundamentale de elaborare a algoritmilor. Cteva
din aceste metode sunt att de generale, nct le folosim frecvent, chiar dac
numai intuitiv, ca reguli elementare n gndire.
6.1
Tehnica greedy
113
114
Algoritmi greedy
Capitolul 6
Seciunea 6.1
Tehnica greedy
115
6.2
T =
i =1
ceea ce este acelai lucru cu a minimiza timpul mediu de ateptare, care este T/n.
De exemplu, dac avem trei clieni cu t 1 = 5, t 2 = 10, t 3 = 3, sunt posibile ase
ordini de servire. n primul caz, clientul 1 este servit primul, clientul 2 ateapt
Ordinea
1 2 3
5+(5+10)+(5+10+3) = 38
1 3 2
5+(5+3)+(5+3+10)
= 31
2 1 3
10+(10+5)+(10+5+3) = 43
2 3 1
10+(10+3)+(10+3+5) = 41
3 1 2
3+(3+5)+(3+5+10)
= 29 optim
3 2 1
3+(3+10)+(3+10+5) = 34
pn este servit clientul 1 i apoi este servit, clientul 3 ateapt pn sunt servii
clienii 1, 2 i apoi este servit. Timpul total de ateptare a celor trei clieni este
38.
Algoritmul greedy este foarte simplu: la fiecare pas se selecteaz clientul cu
timpul minim de servire din mulimea de clieni rmas. Vom demonstra c acest
algoritm este optim. Fie
I = (i 1 i 2 i n )
o permutare oarecare a ntregilor {1, 2, , n}. Dac servirea are loc n ordinea I,
avem
116
Algoritmi greedy
Capitolul 6
(n k + 1) t i
k =1
Presupunem acum c I este astfel nct putem gsi doi ntregi a < b cu
t ia > t ib
Interschimbm pe i a cu i b n I; cu alte cuvinte, clientul care a fost servit al b-lea
va fi servit acum al a-lea i invers. Obinem o nou ordine de servire J, care este
de preferat deoarece
T ( J ) = (n a + 1) tib + (n b + 1) tia +
(n k + 1) ti
k =1
k a ,b
6.3
Seciunea 6.3
150
140
130
110
30
150
10
90
10
40
20
80
20
10
30
117
60
50
30
30
20
10
50
(a)
(b)
11
11
10
9
8
4
10
7
1
7
2
9
5
3
6
5
(a)
(b)
118
Algoritmi greedy
Capitolul 6
L( A) = ai qi
i =1
q1 + q2
q1
q2
Seciunea 6.3
119
6.4
Algoritmi greedy
120
Capitolul 6
#ifndef __VP_H
#define __VP_H
#include <iostream.h>
class vp {
public:
vp( int vf = 0, float pd = 0 ) { v = vf; p = pd; }
operator int ( ) const { return v; }
operator float( ) const { return p; }
int v; float p;
};
inline operator > ( const vp& a, const vp& b) {
return a.p < b.p;
}
inline istream& operator >>( istream& is, vp& element ) {
is >> element.v >> element.p; element.v--;
return is;
}
inline ostream& operator <<( ostream& os, vp& element ) {
os << "{ " << (element.v+1) << "; " << element.p << " }";
return os;
}
#endif
Seciunea 6.4
class nod
public:
int lu;
int st;
int dr;
};
121
{
// lungimea
// fiul stang
// fiul drept
Algoritmi greedy
122
Capitolul 6
este:
Arborele de interclasare: [11]: <-1< 30 >-1> <-1< 10 >-1>
<-1< 20 >-1>
<-1< 30 >-1>
<-1< 50 >-1>
<-1< 10 >-1>
<1< 20 >5>
<2< 40 >6>
<3< 60 >0>
<7< 90 >4>
<8< 150 >9>
Valoarea fiecrui nod este precedat de indicele fiului stng i urmat de cel al
fiului drept, indicele -1 reprezentnd legtura inexistent. Formatele de citire i
scriere ale tablourilor sunt cele stabilite n Seciunea 4.1.3.
6.5
Coduri Huffman
Seciunea 6.5
Coduri Huffman
123
Fie un text compus din urmtoarele litere (n paranteze figureaz frecvenele lor
de apariie):
S (10), I (29), P (4), O (9), T (5)
Conform metodei greedy, construim un arbore binar fuzionnd cele dou litere cu
frecvenele cele mai mici. Valoarea fiecrui vrf este dat de frecvena pe care o
reprezint.
4+5
18
1
1
n final, ajungem la arborele din Figura 6.3, n care fiecare vrf terminal
corespunde unei litere din text.
Pentru a obine codificarea binar a literei P, nu avem dect s scriem secvena de
0-uri i 1-uri n ordinea apariiei lor pe drumul de la rdcin ctre vrful
corespunztor lui P: 1011. Procedm similar i pentru restul literelor:
S (11), I (0), P (1011), O (100), T (1010)
Pentru un text format din n litere care apar cu frecvenele f 1 , f 2 , , f n , un arbore
de codificare este un arbore binar cu vrfurile terminale avnd valorile
f 1 , f 2 , , f n , prin care se obine o codificare binar a textului. Un arbore de
codificare nu trebuie n mod necesar s fie construit dup metoda greedy a lui
Huffman, alegerea vrfurilor care sunt fuzionate la fiecare pas putndu-se face
124
Algoritmi greedy
Capitolul 6
57 0
1
1
S
10
29 I
0
18 0
1
1
28
0
5
ai f i
i =1
6.6
Fie G = <V, M> un graf neorientat conex, unde V este mulimea vrfurilor i M
este mulimea muchiilor. Fiecare muchie are un cost nenegativ (sau o lungime
nenegativ). Problema este s gsim o submulime A M, astfel nct toate
vrfurile din V s rmn conectate atunci cnd sunt folosite doar muchii din A,
Seciunea 6.6
125
iar suma lungimilor muchiilor din A s fie ct mai mic. Cutm deci o
submulime A de cost total minim. Aceast problem se mai numete i problema
conectrii oraelor cu cost minim, avnd numeroase aplicaii.
Graful parial <V, A> este un arbore (Exerciiul 6.11) i este numit arborele
parial de cost minim al grafului G (minimal spanning tree). Un graf poate avea
mai muli arbori pariali de cost minim i acest lucru se poate verifica pe un
exemplu.
Vom prezenta doi algoritmi greedy care determin arborele parial de cost minim
al unui graf. n terminologia metodei greedy, vom spune c o mulime de muchii
este o soluie, dac constituie un arbore parial al grafului G, i este fezabil, dac
nu conine cicluri. O mulime fezabil de muchii este promitoare, dac poate fi
completat pentru a forma soluia optim. O muchie atinge o mulime dat de
vrfuri, dac exact un capt al muchiei este n mulime. Urmtoarea proprietate va
fi folosit pentru a demonstra corectitudinea celor doi algoritmi.
Proprietatea 6.2 Fie G = <V, M> un graf neorientat conex n care fiecare muchie
are un cost nenegativ. Fie W V o submulime strict a vrfurilor lui G i fie
A M o mulime promitoare de muchii, astfel nct nici o muchie din A nu
atinge W. Fie m muchia de cost minim care atinge W. Atunci, A {m} este
promitoare.
Demonstraie: Fie B un arbore parial de cost minim al lui G, astfel nct A B
(adic, muchiile din A sunt coninute n arborele B). Un astfel de B trebuie s
existe, deoarece A este promitoare. Dac m B, nu mai rmne nimic de
demonstrat. Presupunem c m B. Adugndu-l pe m la B, obinem exact un ciclu
(Exerciiul 3.2). n acest ciclu, deoarece m atinge W, trebuie s mai existe cel
puin o muchie m' care atinge i ea pe W (altfel, ciclul nu se nchide). Eliminndul pe m', ciclul dispare i obinem un nou arbore parial B' al lui G. Costul lui m
este mai mic sau egal cu costul lui m', deci costul total al lui B' este mai mic sau
egal cu costul total al lui B. De aceea, B' este i el un arbore parial de cost minim
al lui G, care include pe m. Observm c A B' deoarece muchia m', care atinge
W, nu poate fi n A. Deci, A {m} este promitoare.
Mulimea iniial a candidailor este M. Cei doi algoritmi greedy aleg muchiile
una cte una ntr-o anumit ordine, aceast ordine fiind specific fiecrui
algoritm.
6.6.1
126
Algoritmi greedy
1
4
2
4
Capitolul 6
3
6
7
4
(a)
(b)
Muchia considerat
ini ializare
{1, 2}
{2, 3}
{4, 5}
{6, 7}
{1, 4}
{2, 5}
{4, 7}
{1, 2, 3, 4, 5, 6, 7}
Tabelul 6.1 Algoritmul lui Kruskal aplicat grafului din Figura 6.4a.
Seciunea 6.6
127
128
Algoritmi greedy
Capitolul 6
Pasul
iniializare
Muchia considerat
U
{1}
{2, 1}
{1, 2}
{3, 2}
{1, 2, 3}
{4, 1}
{1, 2, 3, 4}
{5, 4}
{1, 2, 3, 4, 5}
{7, 4}
{1, 2, 3, 4, 5, 6}
{6, 7}
{1, 2, 3, 4, 5, 6, 7}
Tabelul 6.2 Algoritmul lui Prim aplicat grafului din Figura 6.4a.
O(m log m) pentru a sorta muchiile. Deoarece m n(n1)/2, rezult
O(m log m) O(m log n). Mai mult, graful fiind conex, din n1 m rezult i
O(m log n) O(m log m), deci O(m log m) = O(m log n).
O(n) pentru a iniializa cele n mulimi disjuncte.
Cele cel mult 2m operaii find3 i n1 operaii merge3 necesit un timp n
O((2m+n1) lg* n), dup cum am specificat n Capitolul 3. Deoarece
O(lg* n) O(log n) i n 1 m, acest timp este i n O(m log n).
O(m) pentru restul operaiilor.
Deci, pentru cazul cel mai nefavorabil, algoritmul lui Kruskal necesit un timp n
O(m log n).
O alta variant este s pstrm muchiile ntr-un min-heap. Obinem astfel un nou
algoritm, n care iniializarea se face ntr-un timp n O(m), iar fiecare din cele n1
extrageri ale unei muchii minime se face ntr-un timp n O(log m) = O(log n).
Pentru cazul cel mai nefavorabil, ordinul timpului rmne acelai cu cel al
vechiului algoritm. Avantajul folosirii min-heap-ului apare atunci cnd arborele
parial de cost minim este gsit destul de repede i un numr considerabil de
muchii rmn netestate. n astfel de situaii, algoritmul vechi pierde timp, sortnd
n mod inutil i aceste muchii.
6.6.2
Cel de-al doilea algoritm greedy pentru determinarea arborelui parial de cost
minim al unui graf se datoreaz lui Prim (1957). n acest algoritm, la fiecare pas,
mulimea A de muchii alese mpreun cu mulimea U a vrfurilor pe care le
conecteaz formeaz un arbore parial de cost minim pentru subgraful <U, A> al
lui G. Iniial, mulimea U a vrfurilor acestui arbore conine un singur vrf
oarecare din V, care va fi rdcina, iar mulimea A a muchiilor este vid. La
fiecare pas, se alege o muchie de cost minim, care se adaug la arborele
precedent, dnd natere unui nou arbore parial de cost minim (deci, exact una
Seciunea 6.6
129
Proprietatea 6.4 n algoritmul lui Prim, la fiecare pas, <U, A> formeaz un
arbore parial de cost minim pentru subgraful <U, A> al lui G. n final, se obine
arborele parial de cost minim al grafului G.
Algoritmi greedy
130
Capitolul 6
6.7
Funcia care implementeaz algoritmul lui Kruskal n limbajul C++ este aproape
identic cu procedura Kruskal din Seciunea 6.6.1.
tablou<muchie> Kruskal( int n, const tablou<muchie>& M ) {
heap<muchie> h( M );
tablou<muchie> A( n - 1 ); int nA = 0;
set
s( n );
do {
muchie m;
if ( !h.delete_max( m ) )
{ cerr << "\n\nKruskal -- heap vid.\n\n"; return A = 0; }
Seciunea 6.7
131
Diferenele care apar sunt mai curnd precizri suplimentare, absolut necesare n
trecerea de la descrierea unui algoritm la implementarea lui. Astfel, graful este
transmis ca parametru, prin precizarea numrului de vrfuri i a muchiilor. Pentru
muchii, reprezentate prin cele dou vrfuri i costul asociat, am preferat n locul
listei, structura simpl de tablou M, structur folosit i la returnarea arborelui de
cost minim A.
Operaia principal efectuat asupra muchiilor este alegerea muchiei de cost
minim care nc nu a fost considerat. Pentru implementarea acestei operaii,
folosim un min-heap. La fiecare iteraie, se extrage din heap muchia de cost
minim i se ncearc inserarea ei n arborele A.
Rulnd programul
main( ) {
int n;
cout << "\nVarfuri... ";
cin >> n;
tablou<muchie> M;
cout << "\nMuchiile si costurile lor... ";
cin >> M;
cout << "\nArborele de cost minim Kruskal:\n";
cout << Kruskal( n, M ) << '\n';
return 1;
}
Algoritmi greedy
132
Capitolul 6
Seciunea 6.7
133
Seciunea 3.5, elementele canonice sunt difereniate prin faptul c set[i] are
valoarea i. Avnd n vedere c set[i] este indicele n tabloul set al tatlui
elementului i, putem asocia elementelor canonice proprietatea set[i] < 0. Prin
aceast convenie, valoarea absolut a elementelor canonice poate fi oarecare.
Atunci, de ce s nu fie chiar nlimea arborelui?
n concluzie, pentru reprezentarea structurii de mulimi disjuncte, este necesar un
singur tablou, numit set, cu tot attea elemente cte are i mulimea. Valorile
iniiale ale elemetelor tabloului set sunt -1. Aceste iniializri vor fi realizate
prin constructor. Interfaa public a clasei set trebuie s conin funciile
merge3() i find3(), adaptate corepunztor. Tratarea situaiilor de excepie care
pot s apar la invocarea acestor funcii (indici de mulimi n afara intervalului
permis) se realizeaz prin activarea procedurii de verificare a indicilor n tabloul
set.
Aceste considerente au condus la urmtoarele definiii ale funciilor membre din
clasa set.
#include "set.h"
set::set( int n ): set( n ) {
set.vOn( );
for ( int i = 0; i < n; i++ )
set[ i ] = -1;
}
void
//
if
if
Algoritmi greedy
134
Capitolul 6
6.8
Fie G = <V, M> un graf orientat, unde V este mulimea vrfurilor i M este
mulimea muchiilor. Fiecare muchie are o lungime nenegativ. Unul din vrfuri
este desemnat ca vrf surs. Problema este s determinm lungimea celui mai
scurt drum de la surs ctre fiecare vrf din graf.
Vom folosi un algoritm greedy, datorat lui Dijkstra (1959). Notm cu C mulimea
vrfurilor disponibile (candidaii) i cu S mulimea vrfurilor deja selectate. n
fiecare moment, S conine acele vrfuri a cror distan minim de la surs este
deja cunoscut, n timp ce mulimea C conine toate celelalte vrfuri. La nceput,
S conine doar vrful surs, iar n final S conine toate vrfurile grafului. La
fiecare pas, adugm n S acel vrf din C a crui distan de la surs este cea mai
mic.
Seciunea 6.8
135
Spunem c un drum de la surs ctre un alt vrf este special, dac toate vrfurile
intermediare de-a lungul drumului aparin lui S. Algoritmul lui Dijkstra lucreaz
n felul urmtor. La fiecare pas al algoritmului, un tablou D conine lungimea
celui mai scurt drum special ctre fiecare vrf al grafului. Dup ce adugm un
nou vrf v la S, cel mai scurt drum special ctre v va fi, de asemenea, cel mai scurt
dintre toate drumurile ctre v. Cnd algoritmul se termin, toate vrfurile din graf
sunt n S, deci toate drumurile de la surs ctre celelalte vrfuri sunt speciale i
valorile din D reprezint soluia problemei.
Presupunem, pentru simplificare, c vrfurile sunt numerotate, V = {1, 2, , n},
vrful 1 fiind sursa, i c matricea L d lungimea fiecrei muchii, cu L[i, j] = +,
dac muchia (i, j) nu exist. Soluia se va construi n tabloul D[2 .. n]. Algoritmul
este:
function Dijkstra(L[1 .. n, 1 .. n])
{iniializare}
C {2, 3, , n}
{S = V \C exist doar implicit}
for i 2 to n do D[i] L[1, i]
{bucla greedy}
repeat n2 times
v vrful din C care minimizeaz D[v]
C C \ {v}
{i, implicit, S S {v}}
for fiecare w C do
D[w] min(D[w], D[v]+L[v, w])
return D
Pentru graful din Figura 6.5, paii algoritmului sunt prezentai n Tabelul 6.3.
Observm c D nu se schimb dac mai efectum o iteraie pentru a-l scoate i pe
{2} din C. De aceea, bucla greedy se repet de doar n2 ori.
Se poate demonstra urmtoarea proprietate:
50
1
10
30
100
5
20
10
50
136
Algoritmi greedy
Capitolul 6
Pasul
iniializare
{2, 3, 4, 5}
{2, 3, 4}
{2, 3}
{2}
Tabelul 6.3 Algoritmul lui Dijkstra aplicat grafului din Figura 6.5.
este n S, atunci D[i] d lungimea celui mai scurt drum de la surs ctre i;
nu este n S, atunci D[i] d lungimea celui mai scurt drum special de la surs
ctre i.
Seciunea 6.8
137
dac nu reuim s scdem i ordinul timpului necesar pentru alegerea lui v din
bucla repeat. De aceea, vom ine vrfurile v din C ntr-un min-heap, n care
fiecare element este de forma (v, D[v]), proprietatea de min-heap referindu-se la
valoarea lui D[v]. Numim algoritmul astfel obinut Dijkstra-modificat. S l
analizm n cele ce urmeaz.
Iniializarea min-heap-ului necesit un timp n O(n). Instruciunea C C \ {v}
const n extragerea rdcinii min-heap-ului i necesit un timp n O(log n).
Pentru cele n2 extrageri este nevoie de un timp n O(n log n).
Pentru a testa dac D[w] > D[v]+L[v, w], bucla for interioar const acum n
inspectarea fiecrui vrf w din C adiacent lui v. Fiecare vrf v din C este introdus
n S exact o dat i cu acest prilej sunt testate exact muchiile adiacente lui; rezult
c numrul total de astfel de testri este de cel mult m. Dac testul este adevrat,
trebuie s l modificm pe D[w] i s operm un percolate cu w n min-heap, ceea
ce necesit din nou un timp n O(log n). Timpul total pentru operaiile percolate
este deci n O(m log n).
n
concluzie,
algoritmul
Dijkstra-modificat
necesit
un
timp
n
O(max(n, m) log n). Dac graful este conex, atunci m n i timpul este n
O(m log n). Pentru un graf rar este preferabil s folosim algoritmul
Dijkstra-modificat, iar pentru un graf dens algoritmul Dijkstra este mai eficient.
Este uor de observat c, ntr-un graf G neorientat conex, muchiile celor mai
scurte drumuri de la un vrf i la celelalte vrfuri formeaz un arbore parial al
celor mai scurte drumuri pentru G. Desigur, acest arbore depinde de alegerea
rdcinii i i el difer, n general, de arborele parial de cost minim al lui G.
Problema gsirii celor mai scurte drumuri care pleac din acelai punct se poate
pune i n cazul unui graf neorientat.
6.9
138
Algoritmi greedy
Capitolul 6
{
{
{
{
{
{
}
{
{
{
5; 10 } { 4; 100 } { 3; 30 } { 2; 50 } }
4; 50 } { 2; 5 } }
2; 20 } }
4; 10 } }
Seciunea 6.9
139
// #varfuri si #muchii
= 0; s < n; s++ ) {
"\nCele mai scurte drumuri de la varful "
(s + 1) << " sunt:\n";
Dijkstra( G, m, s ) << '\n';
genereaz rezultatele:
Cele mai scurte drumuri de la varful 1 sunt:
[5]: { 1; 0 } { 3; 35 } { 1; 30 } { 5; 20 }
{ 1; 10 }
Cele mai scurte drumuri de la varful 2 sunt:
[5]: { 2; 3.37e+38 } { 1; 0 } { 2; 3.37e+38 }
{ 2; 3.37e+38 } { 2; 3.37e+38 }
Cele mai scurte drumuri de la varful 3 sunt:
[5]: { 3; 3.37e+38 } { 3; 5 } { 1; 0 } { 3; 50 }
{ 3; 3.37e+38 }
140
Algoritmi greedy
Capitolul 6
sau cu un operator
vp& vp::operator ( )( int varf, float pondere )
{ v = varf; p = pondere; return *this; }
Dei era mai natural s folosim operatorul de atribuire =, nu l-am putut folosi
deoarece este operator binar, iar aici avem nevoie de 3 operanzi: n membrul stng
obiectul invocator i n membrul drept vrful, mpreun cu ponderea. Folosind
noul operator (), secvena de iniializare devine mai scurt i mai eficient:
Seciunea 6.9
141
vp w;
// initializare
for ( int i = 0; i < n; i++ )
P[ i ]( s, MAXFLOAT );
for ( iterator<vp> g = G[ s ]; g( w ); )
{ C.insert( w ); P[ w ]( s, w ); }
P[ s ]( 0, 0 );
Algoritmi greedy
142
Capitolul 6
#include <iostream.h>
#include <values.h>
#include
#include
#include
#include
#include
"tablou.h"
"heap.h"
"muchie.h"
"lista.h"
"vp.h"
// muchii
// distanta
// initializare
for ( int i = 0; i < n; i++ )
P[ i ]( s, MAXFLOAT );
for ( iterator<vp> g = G[ s ]; g( w ); )
C.insert( w ); P[ w ]( s, w );
P[ s ]( 0, 0 );
// bucla greedy
for ( i = 1; i < n - 1; i++ ) {
C.delete_max( v ); g = G[ v ];
while ( g( w ) )
if ( (float)P[ w ] > ( dw = (float)P[ v ] + (float)w ) )
C.insert( vp( w, P[ w ]( v, dw ) ) );
}
return P;
}
main( ) {
int n, m = 0;
muchie M;
// #varfuri si #muchii
Seciunea 6.9
143
s = 0; s < n; s++ ) {
"\nCele mai scurte drumuri de la varful "
(s + 1) << " sunt:\n";
Dijkstra( G, m, s ) << '\n';
return 0;
}
144
Algoritmi greedy
Capitolul 6
schimba culoarea. Cnd nici un vrf nu mai poate fi colorat, schimbm culoarea i
vrful de start, repetnd procedeul.
Dac n graful din Figura 6.6 pornim cu vrful 1 i l colorm n rou, mai putem
colora tot n rou vrfurile 3 i 4. Apoi, schimbm culoarea i pornim cu vrful 2,
colorndu-l n albastru. Mai putem colora cu albastru i vrful 5. Deci, ne-au fost
suficiente dou culori. Dac colorm vrfurile n ordinea 1, 5, 2, 3, 4, atunci se
obine o colorare cu trei culori.
Rezult c, prin metoda greedy, nu obinem dect o soluie euristic, care nu este
n mod necesar soluia optim a problemei. De ce suntem atunci interesai ntr-o
astfel de rezolvare? Toi algoritmii cunoscui, care rezolv optim aceast
problem, sunt exponeniali, deci, practic, nu pot fi folosii pentru cazuri mari.
Algoritmul greedy euristic propus furnizeaz doar o soluie acceptabil, dar este
simplu i eficient.
Un caz particular al problemei colorrii unui graf corespunde celebrei probleme a
colorrii hrilor: o hart oarecare trebuie colorat cu un numr minim de culori,
astfel nct dou ri cu frontier comun s fie colorate diferit. Dac fiecrui vrf
i corespunde o ar, iar dou vrfuri adiacente reprezint ri cu frontier
comun, atunci hrii i corespunde un graf planar, adic un graf care poate fi
desenat n plan fr ca dou muchii s se intersecteze. Celebritatea problemei
const n faptul c, n toate exemplele ntlnite, colorarea s-a putut face cu cel
mult 4 culori. Aceasta n timp ce, teoretic, se putea demonstra c pentru o hart
oarecare este nevoie de cel mult 5 culori. Recent * s-a demonstrat pe calculator
faptul c orice hart poate fi colorat cu cel mult 4 culori. Este prima demonstrare
pe calculator a unei teoreme importante.
Problema colorrii unui graf poate fi interpretat i n contextul planificrii unor
activiti. De exemplu, s presupunem c dorim s executm simultan o mulime
de activiti, n cadrul unor sli de clas. n acest caz, vrfurile grafului reprezint
activiti, iar muchiile unesc activitile incompatibile. Numrul minim de culori
necesare pentru a colora graful corespunde numrului minim de sli necesare.
3
1
5
4
Seciunea 6.10
Euristica greedy
La:
10
11
25
12
26
20
15
145
De la:
1
2
3
4
5
18
6.11 Exerciii
6.1
146
i)
ii)
6.2
Algoritmi greedy
Capitolul 6
k , k , , k
n1
6.7
Ce se ntmpl cnd facem o codificare Huffman a unui text binar? Ce se
ntmpl cnd facem o codificare Huffman a unui text format din litere care au
aceeai frecven?
6.8
6.9
Elaborai algoritmul de decompactare a unui ir de caractere codificat
prin codul Huffman, presupunnd c se cunosc caracterele i codificarea lor.
Folosii proprietatea c acest cod este de tip prefix.
Seciunea 6.11
Exerciii
147
6.10
Pe lng codul Huffman, vom considera aici i un alt cod celebru, care nu
se obine ns printr-o metod greedy, ci printr-un algoritm recursiv.
n
148
Algoritmi greedy
Capitolul 6
6.16
n graful din Figura 6.5, gsii pe unde trec cele mai scurte drumuri de la
vrful 1 ctre toate celelalte vrfuri.
6.17
lui.
6.18
Ce se ntmpl cu algoritmul greedy din problema comis-voiajorului dac
admitem c pot exista dou orae fr legtur direct ntre ele?
6.19
Scriei algoritmul greedy pentru problema comis-voiajorului i analizai
eficiena lui.
6.20
ntr-un graf orientat, un drum este hamiltonian dac trece exact o dat
prin fiecare vrf al grafului, fr s se ntoarc n vrful iniial. Fie G un graf
orientat, cu proprietatea c ntre oricare dou vrfuri exist cel puin o muchie.
Artai c n G exist un drum hamiltonian i elaborai algoritmul care gsete
acest drum.
6.21
Este cunoscut c orice numr natural i poate fi descompus n mod unic
ntr-o sum de termeni ai irului lui Fibonacci (teorema lui Zeckendorf). Dac
prin k >> m notm k m+2, atunci
i = f k1 + f k2 + ... f kr
unde
k 1 >> k 2 >> >> k r >> 0
n acest reprezentare Fibonacci a numerelor, singura valoare posibil pentru f k
este cel mai mare termen din irul lui Fibonacci pentru care f k1 i ; singura
valoare posibil pentru f k este cel mai mare termen pentru care f k2 i f k1 etc.
2
i=
n 1
bj f j
j =2
Seciunea 6.11
Exerciii
149
7. Algoritmi divide et
impera
7.1
t (n)
t C ( n) = A
3t C ( n / 2) + t (n)
pentru n n 0
pentru n > n 0
lg 3
Conform rezultatelor din Seciunea 5.3.5, t C (n) este n ordinul lui n . Deoarece
lg 3 1,59, nseamn c de aceast dat am reuit s mbuntim ordinul
timpului.
Iat o descriere general a metodei divide et impera:
149
150
Capitolul 7
function divimp(x)
{returneaz o soluie pentru cazul x}
if x este suficient de mic then return adhoc(x)
{descompune x n subcazurile x 1 , x 2 , , x k }
for i 1 to k do y i divimp(x i )
{recompune y 1 , y 2 , , y k n scopul obinerii soluiei y pentru x}
return y
unde adhoc este subalgoritmul de baz folosit pentru rezolvarea micilor subcazuri
ale problemei n cauz (n exemplul nostru, acest subalgoritm este A).
Un algoritm divide et impera trebuie s evite descompunerea recursiv a
subcazurilor suficient de mici, deoarece, pentru acestea, este mai eficient
aplicarea direct a subalgoritmului de baz. Ce nseamn ns suficient de mic?
n exemplul precedent, cu toate c valoarea lui n 0 nu influeneaz ordinul
lg 3
Seciunea 7.1
151
7.2
Cutarea binar
Cutarea binar este cea mai simpl aplicaie a metodei divide et impera, fiind
cunoscut nc nainte de apariia calculatoarelor. n esen, este algoritmul dup
care se caut un cuvnt ntr-un dicionar, sau un nume n cartea de telefon.
Fie T[1 .. n] un tablou ordonat cresctor i x un element oarecare. Problema
const n a-l gsi pe x n T, iar dac nu se afl acolo n a gsi poziia unde poate fi
inserat. Cutm deci indicele i astfel nct 1 i n i T[i] x < T[i+1], cu
convenia T[0] = , T[n+1] = +. Cea mai evident metod este cutarea
secvenial:
function sequential(T[1 .. n], x)
{caut secvenial pe x n tabloul T }
for i 1 to n do
if T[i] > x then return i1
return n
Algoritmul necesit un timp n (1+r), unde r este indicele returnat; aceasta
nseamn (1) pentru cazul cel mai favorabil i (n) pentru cazul cel mai
nefavorabil. Dac presupunem c elementele lui T sunt distincte, c x este un
element al lui T i c se afl cu probabilitate egal n oricare poziie din T, atunci
2
bucla for se execut n medie de (n +3n2)/2n ori. Timpul este deci n (n) i
pentru cazul mediu.
Pentru a mri viteza de cutare, metoda divide et impera sugereaz s-l cutm pe
x fie n prima jumtate a lui T, fie n cea de-a doua. Comparndu-l pe x cu
elementul din mijlocul tabloului, putem decide n care dintre jumti s cutm.
Repetnd recursiv procedeul, obinem urmtorul algoritm de cutare binar:
function binsearch(T[1 .. n], x)
{caut binar pe x n tabloul T}
if n = 0 or x < T[1] then return 0
return binrec(T[1 .. n], x)
152
Capitolul 7
Seciunea 7.2
Cutarea binar
153
Care din aceti doi algoritmi este oare mai eficient? Pentru cazul cel mai
favorabil, iterbin2 este, evident, mai bun. Pentru cazul cel mai nefavorabil,
ordinul timpului este acelai, numrul de executri ale buclei while este acelai,
dar durata unei bucle while pentru iterbin2 este ceva mai mare; deci iterbin1 este
preferabil, avnd constanta multiplicativ mai mic. Pentru cazul mediu,
compararea celor doi algoritmi este mai dificil: ordinul timpului este acelai, o
bucl while n iterbin1 dureaz n medie mai puin dect n iterbin2, n schimb
iterbin1 execut n medie mai multe bucle while dect iterbin2.
7.3
Fie T[1 .. n] un tablou pe care dorim s-l sortm cresctor. Prin tehnica divide et
impera putem proceda astfel: separm tabloul T n dou pri de mrimi ct mai
apropiate, sortm aceste pri prin apeluri recursive, apoi interclasm soluiile
pentru fiecare parte, fiind ateni s pstrm ordonarea cresctoare a elementelor.
Obinem urmtorul algoritm:
procedure mergesort(T[1 .. n])
{sorteaz n ordine cresctoare tabloul T}
if n este mic
then insert(T)
else arrays U[1 .. n div 2], V[1 .. (n+1) div 2]
U T[1 .. n div 2]
V T[1 + (n div 2) .. n]
mergesort(U); mergesort(V)
merge(T, U, V)
unde insert(T) este algoritmul de sortare prin inserie cunoscut, iar merge(T, U, V)
interclaseaz ntr-un singur tablou sortat T cele dou tablouri deja sortate U i V.
Algoritmul mergesort ilustreaz perfect principiul divide et impera: pentru n
avnd o valoare mic, nu este rentabil s apelm recursiv procedura mergesort, ci
este mai bine s efectum sortarea prin inserie. Algoritmul insert lucreaz foarte
bine pentru n 16, cu toate c, pentru o valoare mai mare a lui n, devine
neconvenabil. Evident, se poate concepe un algoritm mai puin eficient, care s
mearg pn la descompunerea total; n acest caz, mrimea stivei este n
(log n).
Spaiul de memorie necesar pentru tablourile auxiliare U i V este n (n). Mai
k
precis, pentru a sorta un tablou de n = 2 elemente, presupunnd c
descompunerea este total, acest spaiu este de
2 (2 k 1 + 2 k 2 + ... + 2 + 1) = 2 2 k = 2n
154
Capitolul 7
elemente.
Putem considera (conform Exerciiului 7.7) c algoritmul merge(T, U, V) are
timpul de execuie n (#U + #V), indiferent de ordonarea elementelor din U i V.
Separarea lui T n U i V necesit tot un timp n (#U + #V). Timpul necesar
algoritmului mergesort pentru a sorta orice tablou de n elemente este atunci
t(n) t(n/2)+t(n/2)+(n). Aceast ecuaie, pe care am analizat-o n Seciunea
5.1.2, ne permite s conchidem c timpul pentru mergesort este n (n log n). S
reamintim timpii celorlali algoritmi de sortare, algoritmi analizai n Capitolul 5:
pentru cazul mediu i pentru cazul cel mai nefavorabil insert i select necesit un
2
timp n (n ), iar heapsort un timp n (n log n).
n algoritmul mergesort, suma mrimilor subcazurilor este egal cu mrimea
cazului iniial. Aceast proprietate nu este n mod necesar valabil pentru
algoritmii divide et impera. Oare de ce este ns important ca subcazurile s fie de
mrimi ct mai egale? Dac n mergesort l separm pe T n tabloul U avnd n1
elemente i tabloul V avnd un singur element, se obine (Exerciiul 7.9) un nou
2
timp de execuie, care este n (n ). Deducem de aici c este esenial ca
subcazurile s fie de mrimi ct mai apropiate (sau, alfel spus, subcazurile s fie
ct mai echilibrate).
7.4
7.4.1
O soluie neinspirat
Seciunea 7.4
155
156
Capitolul 7
Seciunea 7.4
157
158
Capitolul 7
Seciunea 7.4
159
160
7.4.2
Capitolul 7
Sortarea, de fapt transformarea tabloului t ntr-un tablou sortat, este realizat prin
constructorul
Seciunea 7.4
161
): a( t.a ) {
alocarea zonei de interclasare
sortarea
eliberarea zonei alocate
162
Capitolul 7
7.5
Seciunea 7.5
163
164
Capitolul 7
Putem considera c exist o constant real pozitiv c, astfel nct t(i) ci +c/2
pentru 0 i n 0 . Prin ipoteza induciei specificate parial, presupunem c
2
d fiind o alt constant. Expresia i +(ni1) i atinge maximul atunci cnd i este
0 sau n1. Deci,
2
Dac lum c 2d, obinem t(n) cn +c/2. Am artat c, dac c este suficient de
2
2
mare, atunci t(n) cn +c/2 pentru orice n 0, adic, t O(n ). Analog se arat c
2
t (n ).
2
Am artat, totodat, care este cel mai nefavorabil caz: atunci cnd, la fiecare nivel
de recursivitate, procedura pivot este apelat o singur dat. Dac elementele lui T
sunt distincte, cazul cel mai nefavorabil este atunci cnd iniial tabloul este
ordonat cresctor sau descresctor, fiecare partiionare fiind total neechilibrat.
Pentru acest cel mai nefavorabil caz, am artat c algoritmul quicksort necesit un
2
timp n (n ).
Ce se ntmpl ns n cazul mediu? Intuim faptul c, n acest caz, subcazurile
sunt suficient de echilibrate. Pentru a demonstra aceast proprietate, vom arta c
timpul necesar este n ordinul lui n log n, ca i n cazul cel mai favorabil.
Presupunem c avem de sortat n elemente distincte i c iniial ele pot s apar cu
probabilitate egal n oricare din cele n! permutri posibile. Operaia de pivotare
necesit un timp liniar. Apelarea procedurii pivot poate poziiona primul element
cu probabilitatea 1/n n oricare din cele n poziii. Timpul mediu pentru quicksort
verific relaia
n
Mai precis, fie n 0 i d dou constante astfel nct pentru orice n > n 0 , avem
n
n 1
l =1
i=0
Seciunea 7.5
n 1
i lg i
i = n0 +1
x = n0 +1
x2
lg e 2
x lg x dx =
lg x
x
4
2
x =n
0 +1
165
n2
lg e 2
lg n
n
2
4
pentru n 0 1.
innd cont de aceast margine superioar pentru
n 1
i lg i
i = n0 +1
c =
2d
4
+
lg e
(n 0 + 1) 2 lg e
n0
t ( i)
i =0
Rezult c timpul mediu pentru quicksort este n O(n log n). Pe lng ordinul
timpului, un rol foarte important l are constanta multiplicativ. Practic, constanta
multiplicativ pentru quicksort este mai mic dect pentru heapsort sau
mergesort. Dac pentru cazul cel mai nefavorabil se accept o execuie ceva mai
lent, atunci, dintre tehnicile de sortare prezentate, quicksort este algoritmul
preferabil.
Pentru a minimiza ansa unui timp de execuie n (n ), putem alege ca pivot
mediana irului T[i], T[(i+j) div 2], T[ j]. Preul pltit pentru aceast modificare
este o uoar cretere a constantei multiplicative.
2
7.6
Putem gsi cu uurin elementul maxim sau minim al unui tablou T. Cum putem
determina ns eficient mediana lui T ? Pentru nceput, s definim formal mediana
unui tablou.
Un element m al tabloului T[1 .. n] este mediana lui T, dac i numai dac sunt
verificate urmtoarele dou relaii:
#{i {1, , n} | T[i] < m} < n/2
#{i {1, , n} | T[i] m} n/2
Aceast definiie ine cont de faptul c n poate fi par sau impar i c elementele
din T pot s nu fie distincte. Prima relaie este mai uor de neles dac observm
c
166
Capitolul 7
Seciunea 7.6
167
168
Capitolul 7
Algoritmul adhocmed5 este elaborat special pentru a gsi mediana a exact cinci
elemente. S notm c adhocmed5 necesit un timp n O(1).
Fie m aproximarea medianei tabloului T, gsit prin algoritmul pseudomed.
Deoarece m este mediana tabloului S, avem
#{i {1, , s} | S[i] m} s/2
Fiecare element din S este mediana a cinci elemente din T. n consecin, pentru
fiecare i, astfel nct S[i] m, exist i 1 , i 2 , i 3 ntre 5i4 i 5i, astfel ca
T[i 1 ] T[i 2 ] T[i 3 ] = S[i] m
Deci,
#{i {1, , n} | T[i] m} 3s/2 = 3n/5/2
= 3(n4)/5/2 = 3(n4)/10 (3n12)/10
Similar, din relaia
#{i {1, , s} | S[i] < m} < s/2
care este echivalent cu
#{i {1, , s} | S[i] m} > s/2
deducem
#{i {1, , n} | T[i] m} > 3n/5/2
= 3n/10 = 3(n9)/10 (3n27)/10
Deci,
#{i {1, , n} | T[i] < m} < (7n+27)/10
n concluzie, m aproximeaz mediana lui T, fiind al k-lea cel mai mic element al
lui T, unde k este aproximativ ntre 3n/10 i 7n/10. O interpretare grafic ne va
ajuta s nelegem mai bine aceste relaii. S ne imaginm elementele lui T
dispuse pe cinci linii, cu posibila excepie a cel mult patru elemente (Figura 7.1).
Presupunem c fiecare din cele n/5 coloane este ordonat nedescresctor, de sus
n jos. De asemenea, presupunem c linia din mijloc (corespunztoare tabloului S
din algoritm) este ordonat nedescresctor, de la stnga la dreapta. Elementul
subliniat corespunde atunci medianei lui S, deci lui m. Elementele din interiorul
dreptunghiului sunt mai mici sau egale cu m. Dreptunghiul conine aproximativ
3/5 din jumtatea elementelor lui T, deci n jur de 3n/10 elemente.
Seciunea 7.6
169
(*)
(**)
Dac 1/q + 3/4 < 1, adic dac numrul de elemente asupra crora opereaz cele
dou apeluri recursive din (**) este n scdere, deducem, ntr-un mod similar cu
situaia cnd q = 5, c timpul este tot liniar. Deoarece pentru orice q 5
inegalitatea precedent este verificat, rmne deschis problema alegerii unui q
pentru care s obinem o constant multiplicativ ct mai mic.
170
Capitolul 7
n particular, putem determina mediana unui tablou n timp liniar, att pentru
cazul mediu ct i pentru cazul cel mai nefavorabil. Fa de algoritmul naiv, al
crui timp este n ordinul lui n log n, mbuntirea este substanial.
7.7
O problem de criptologie
O prim soluie a acestei probleme a fost dat n 1976 de W. Diffie i M. E. Hellman. ntre timp sau mai propus i alte protocoale.
Seciunea 7.7
O problem de criptologie
171
function dlog(g, a, p)
A 0; k 1
repeat
A A+1
k kg
until (a = k mod p) or (A = p)
return A
Dac logaritmul nu exist, funcia dlog va returna valoarea p. De exemplu, nu
A
exist un ntreg A, astfel nct 3 = 2 mod 7. Algoritmul de mai sus este ns
extrem de ineficient. Dac p este un numr prim impar, atunci este nevoie n
medie de p/2 repetri ale buclei repeat pentru a ajunge la soluie (presupunnd c
aceasta exist). Dac pentru efecuarea unei bucle este necesar o microsecund,
atunci timpul de execuie al algoritmului poate fi mai mare dect vrsta
Pmntului! Iar aceasta se ntmpl chiar i pentru un numr zecimal p cu doar 24
de cifre.
Cu toate c exist i algoritmi mai rapizi pentru calcularea logaritmilor discrei,
nici unul nu este suficient de eficient dac p este un numr prim cu cteva sute de
cifre. Pe de alt parte, nu se cunoate pn n prezent un alt mod de a-l obine pe x
din p, g, a i b, dect prin calcularea logaritmului discret.
Desigur, Alice i Bob trebuie s poat calcula rapid exponenierile de forma
A
a = g mod p, cci altfel ar fi i ei pui n situaia Evei. Urmtorul algoritm pentru
calcularea exponenierii nu este cu nimic mai subtil sau eficient dect cel pentru
logaritmul discret.
function dexpo1(g, A, p)
a1
for i 1 to A do a ag
return a mod p
Faptul c x y z mod p = ((x y mod p) z) mod p pentru orice x, y, z i p, ne permite
s evitm memorarea unor numere extrem de mari. Obinem astfel o prim
mbuntire:
function dexpo2(g, A, p)
a1
for i 1 to A do a ag mod p
return a
Din fericire pentru Alice i Bob, exist un algoritm eficient pentru calcularea
exponenierii i care folosete reprezentarea binar a lui A. S considerm pentru
nceput urmtorul exemplu
x
25
2 2 2
= (((x x) ) ) x
172
25
Capitolul 7
25
= (((x x) 1) 1) x
h( A) = 1 + h( A 1)
1 + h( A / 2)
pentru A = 0
pentru A impar
altfel
Dac M(p) este limita superioar a timpului necesar nmulirii modulo p a dou
numere naturale mai mici dect p, atunci calcularea lui dexpo(g, A, p) necesit un
timp n O(M(p) h(A)). Mai mult, se poate demonstra c timpul este n
O(M(p) log A), ceea ce este rezonabil. Ca i n cazul cutrii binare, algoritmul
dexpo este mai curnd un exemplu de simplificare dect de tehnic divide et
impera.
Vom nelege mai bine acest algoritm, dac considerm i o versiune iterativ a
lui.
function dexpoiter1(g, A, p)
c 0; a 1
{fie Ak Ak 1 ... A0 reprezentarea binar a lui A}
for i k downto 0 do
c 2c
a aa mod p
if A i = 1 then c c + 1
a ag mod p
return a
Fiecare iteraie folosete una din identitile
Seciunea 7.7
O problem de criptologie
g
g
2c
2c+1
173
c 2
mod p = (g ) mod p
c 2
7.8
nmulirea matricilor
174
Capitolul 7
A11
A21
A12 B11
A22 B21
Seciunea 7.8
nmulirea matricilor
175
unde
Pentru n = 2, nmulirile i adunrile din relaiile de mai sus sunt scalare; pentru
n > 2, aceste operaii sunt ntre matrici de n/2 n/2 elemente. Operaia de adunare
matricial este cea clasic. n schimb, pentru fiecare nmulire matricial, aplicm
recursiv aceste partiionri, pn cnd ajungem la submatrici de 2 2 elemente.
Pentru a obine matricea C, este nevoie de opt nmuliri i patru adunri de matrici
de n/2 n/2 elemente. Dou matrici de n/2 n/2 elemente se pot aduna ntr-un
2
timp n (n ). Timpul total pentru algoritmul divide et impera rezultat este
t(n) 8t(n/2) + (n )
2
Definim funcia
1
f ( n) =
2
8 f (n / 2) + n
pentru n = 1
pentru n 1
176
Capitolul 7
C11 = P + S T + V
C12 = R + T
C21 = Q + S
C22 = P + R Q + U
Metoda lui Strassen nu este unic: s-a demonstrat c exist exact 36 de moduri
diferite de calcul a submatricilor C ij , fiecare din aceste metode utiliznd 7
nmuliri.
2,81
Limita O(n ) poate fi i mai mult redus dac gsim un algoritm de nmulire a
matricilor de 2 2 elemente cu mai puin de apte nmuliri. S-a demonstrat ns
c acest lucru nu este posibil. O alt metod este de a gsi algoritmi mai eficieni
pentru nmulirea matricilor de dimensiuni mai mari dect 2 2 i de a
descompune recursiv pn la nivelul acestor submatrici. Datorit constantelor
multiplicative implicate, exceptnd algoritmul lui Strassen, nici unul din aceti
algoritmi nu are o valoare practic semnificativ.
Pe calculator, s-a putut observa c, pentru n 40, algoritmul lui Strassen este mai
eficient dect metoda clasic. n schimb, algoritmul lui Strassen folosete
memorie suplimentar.
Poate c este momentul s ne ntrebm de unde provine acest interes pentru
nmulirea matricilor. Importana acestor algoritmi * deriv din faptul c operaii
frecvente cu matrici (cum ar fi inversarea sau calculul determinantului) se bazeaz
pe nmuliri de matrici. Astfel, dac notm cu f (n) timpul necesar pentru a nmuli
dou matrici de n n elemente i cu g(n) timpul necesar pentru a inversa o
matrice nesingular de n n elemente, se poate arta c f (g).
7.9
Pentru anumite aplicaii, trebuie s considerm numere ntregi foarte mari. Dac
ai implementat algoritmii pentru generarea numerelor lui Fibonacci, probabil c
S-au propus i metode complet diferite. Astfel, D. Coppersmith i S. Winograd au gsit n 1987 un
2,376
algoritm cu timpul n O(n
).
Seciunea 7.9
177
v-ai confruntat deja cu aceast problem. Acelai lucru s-a ntmplat n 1987,
atunci cnd s-au calculat primele 134 de milioane de cifre ale lui . n criptologie,
numerele ntregi mari sunt de asemenea extrem de importante (am vzut acest
lucru n Seciunea 7.7). Operaiile aritmetice cu operanzi ntregi foarte mari nu
mai pot fi efectuate direct prin hardware, deci nu mai putem presupune, ca pn
acum, c operaiile necesit un timp constant. Reprezentarea operanzilor n
virgul flotant ar duce la aproximri nedorite. Suntem nevoii deci s
implementm prin software operaiile aritmetice respective.
n cele ce urmeaz, vom da un algoritm divide et impera pentru nmulirea
ntregilor foarte mari. Fie u i v doi ntregi foarte mari, fiecare de n cifre zecimale
j
j1
(convenim s spunem c un ntreg k are j cifre dac k < 10 , chiar dac k < 10 ).
Dac s = n/2, reprezentm pe u i v astfel:
u = 10 w + x,
s
v = 10 y + z,
s
n
u
n / 2
n / 2
ntregii w i y au cte n/2 cifre, iar ntregii x i z au cte n/2 cifre. Din relaia
uv = 10 wy + 10 (wz+xy) + xz
2s
178
Capitolul 7
v mod 10 = v 10 y
s
Notm cu t d (n) timpul necesar acestui algoritm, n cazul cel mai nefavorabil,
pentru a nmuli doi ntregi de n cifre. Avem
t d (n) 3t d (n/2) + t d (n/2) + (n)
Dac n este o putere a lui 2, aceast relaie devine
t d (n) 4t d (n/2) + (n)
Folosind Proprietatea 5.2, obinem relaia t d (n ). (Se observ c am rentlnit
un exemplu din Seciunea 5.3.5). nmulirea clasic necesit ns tot un timp
ptratic (Exerciiul 5.29). Nu am ctigat astfel nimic; dimpotriv, am reuit s
mrim constanta multiplicativ!
2
Ideea care ne va ajuta am mai folosit-o la metoda lui Strassen (Seciunea 7.8).
Deoarece nmulirea ntregilor mari este mult mai lent dect adunarea, ncercm
s reducem numrul nmulirilor, chiar dac prin aceasta mrim numrul
adunrilor. Adic, ncercm s calculm wy, wz+xy i xz prin mai puin de patru
nmuliri. Considernd produsul
r = (w+x)(y+z) = wy + (wz+xy) + xz
observm c putem nlocui ultima linie din algoritm cu
r nmul(w+x, y+z)
p nmul(w, y); q nmul(x, z)
2s
s
return 10 p + 10 (rpq) + q
Fie t(n) timpul necesar algoritmului modificat pentru a nmuli doi ntregi, fiecare
cu cel mult n cifre. innd cont c w+x i y+z pot avea cel mult 1+n/2 cifre,
obinem
t(n) t(n/2) + t(n/2) + t(1+n/2) + O(n)
Prin definiie, funcia t este nedescresctoare. Deci,
t(n) 3t(1+n/2) + O(n)
Notnd T(n) = t(n+2) i presupunnd c n este o putere a lui 2, obinem
T(n) 3T(n/2) + O(n)
Prin metoda iteraiei (ca n Exerciiul 7.24), putei arta c
T O(n
lg 3
Seciunea 7.9
179
lg 3
7.10 Exerciii
7.1
Demonstrai c procedura binsearch se termin ntr-un numr finit de pai
(nu cicleaz).
Indicaie: Artai c binrec(T[i .. j], x) este apelat ntotdeauna cu i j i c
binrec(T[i .. j], x) apeleaz binrec(T[u .. v], x) ntotdeauna astfel nct
vu < ji
7.2
180
Capitolul 7
7.6
Rdcina ptrat ntreag a lui n N este prin definiie acel p N
pentru care p n < p +1. Presupunnd c nu avem o funcie radical, elaborai un
algoritm care l gsete pe p ntr-un timp n O (log n ).
Soluie: Se apeleaz p trat (0, n +1, n ), p trat fiind funcia
function p trat ( a , b , n )
if a = b 1 then return a
m ( a + b ) div 2
2
if m n then p trat ( m , b , n )
else p trat ( a , m , n )
7.7
Fie tablourile U [1 .. N ] i V [1 .. M ], ordonate cresctor. Elaborai un
algoritm cu timpul de execuie n ( N + M ), care s interclaseze cele dou tablouri.
Rezultatul va fi trecut n tabloul T [1 .. N + M ].
Soluie: Iat o prim variant a acestui algoritm:
i, j, k 1
while i N and j M do
if U [ i ] V [ j ] then T [ k ] U[ i ]
i i +1
else T [ k ] V [ j ]
j j +1
k k +1
if i > N then for h j to M do
T[k] V[h]
k k +1
else for h i to N do
T[k] U[h]
k k +1
Seciunea 7.10
Exerciii
181
182
Capitolul 7
C (n) = 1
C( n / 2) + C( n / 2) + 2
pentru n = 1
pentru n = 2
pentru n > 2
C (n) = 2C (n / 2) + 2 = K = 2 k 1 C (2) +
k 1
2i = 2 k 1 + 2 k 2 = 3n / 2 2
i =1
Seciunea 7.10
Exerciii
183
7.17
Dac presupunem c gsirea medianei este o operaie elementar, am
vzut c timpul pentru selection, n cazul cel mai nefavorabil, este
t m (n) O(n) + max{t m (i) | i n/2}
Demonstrai c t m O(n).
184
Capitolul 7
a = g mod p = g mod p
B
15
2 1
= (((x ) ) ) x
15
Seciunea 7.10
Exerciii
185
7.23
Gsii un algoritm divide et impera pentru a calcula un termen oarecare
din irul lui Fibonacci. Folosii proprietatea din Exerciiul 1.7. V ajut aceasta la
nelegerea algoritmului fib3 din Seciunea 1.6.4?
( n 1)
( n 1)
Indicaie: Din Exerciiul 1.7, deducem c f n = m 22
, unde m 22
este elementul
n1
lg 7
),
Soluie: Fie dou constante pozitive a i c, astfel nct timpul pentru algoritmul
lui Strassen este
t(n) 7t(n/2) + cn
cn (7/4)
2
= cn
lg n
+ a7
lg 4+lg 7lg 4
k2
) + a7
k1
lg n
+ an
lg 7
O(n
lg 7
7.25
Cum ai modifica algoritmul lui Strassen pentru a nmuli matrici de n n
elemente, unde n nu este o putere a lui doi? Artai c timpul algoritmului rezultat
lg 7
este tot n (n ).
Indicaie: l majorm pe n pn la cea mai mic putere a lui 2, completnd
corespunztor matricile A i B cu elemente nule.
7.26
S presupunem c avem o primitiv grafic box(x, y, r), care deseneaz un
ptrat 2r 2r centrat n (x, y), tergnd zona din interior. Care este desenul
realizat prin apelul star(a, b, c), unde star este algoritmul
procedure star(x, y, r)
if r > 0 then star(xr, y+r,
star(x+r, y+r,
star(xr, yr,
star(x+r, yr,
box(x, y, r)
r
r
r
r
div
div
div
div
2)
2)
2)
2)
Care este rezultatul, dac box(x, y, r) apare naintea celor patru apeluri recursive?
2
Artai c timpul de execuie pentru un apel star(a, b, c) este n (c ).
186
Capitolul 7
7.27
Demonstrai c pentru orice ntregi m i n sunt adevrate urmtoarele
proprieti:
i) dac m i n sunt pare, atunci cmmdc(m, n) = 2cmmdc(m/2, n/2)
ii) dac m este impar i n este par, atunci cmmdc(m, n) = cmmdc(m, n/2)
iii) dac m i n sunt impare, atunci cmmdc(m, n) = cmmdc((mn)/2, n)
Pe majoritatea calculatoarelor, operaiile de scdere, testare a paritii unui ntreg
i mprire la doi sunt mai rapide dect calcularea restului mpririi ntregi.
Elaborai un algoritm divide et impera pentru a calcula cel mai mare divizor
comun a doi ntregi, evitnd calcularea restului mpririi ntregi. Folosii
proprietile de mai sus.
7.28
Gsii o structur de date adecvat, pentru a reprezenta numere ntregi
mari pe calculator. Pentru un ntreg cu n cifre zecimale, numrul de bii folosii
trebuie s fie n ordinul lui n. nmulirea i mprirea cu o putere pozitiv a lui 10
(sau alt baz, dac preferai) trebuie s poat fi efectuate ntr-un timp liniar.
Adunarea i scderea a dou numere de n, respectiv m cifre trebuie s poat fi
efectuate ntr-un timp n (n+m). Permitei numerelor s fie i negative.
7.29
Fie u i v doi ntregi mari cu n, respectiv m cifre. Presupunnd c folosii
structura de date din Exerciiul 7.28, artai c algoritmul de nmulire clasic (i
cel a la russe) a lui u cu v necesit un timp n (nm).
8. Algoritmi de
programare dinamic
8.1
n 1 n 1
+
n k 1 k
=
k
1
n mod direct:
function C(n, k)
if k = 0 or k = n
then return 1
else return C(n1, k1) + C(n1, k)
Multe din valorile C(i, j), i < n, j < k, sunt calculate n mod repetat (vezi
n
Exerciiul 2.5). Deoarece rezultatul final este obinut prin adunarea a de 1,
k
n
rezult c timpul de execuie pentru un apel C(n, k) este n ( ).
k
185
186
Capitolul 8
...
k1
n 1
k 1
n 1
M
n1
n
k
(acesta este desigur triunghiul lui Pascal), obinem un algoritm mai eficient. De
fapt, este suficient s memorm un vector de lungime k, reprezentnd linia curent
din triunghiul lui Pascal, pe care s-l reactualizm de la dreapta la stnga. Noul
algoritm necesit un timp n O(nk). Pe aceast idee se bazeaz i algoritmul fib2
(Capitolul 1). Am ajuns astfel la primul principiu de baz al programrii
dinamice: evitarea calculrii de mai multe ori a aceluiai subcaz, prin memorarea
rezultatelor intermediare.
Putem spune c metoda divide et impera opereaz de sus n jos (top-down),
descompunnd un caz n subcazuri din ce n ce mai mici, pe care le rezolv apoi
separat. Al doilea principiu fundamental al programrii dinamice este faptul c ea
opereaz de jos n sus (bottom-up). Se pornete de obicei de la cele mai mici
subcazuri. Combinnd soluiile lor, se obin soluii pentru subcazuri din ce n ce
mai mari, pn se ajunge, n final, la soluia cazului iniial.
Programarea dinamic este folosit de obicei n probleme de optimizare. n acest
context, conform celui de-al treilea principiu fundamental, programarea dinamic
este utilizat pentru a optimiza o problem care satisface principiul optimalitii:
ntr-o secven optim de decizii sau alegeri, fiecare subsecven trebuie s fie de
asemenea optim. Cu toate c pare evident, acest principiu nu este ntotdeauna
valabil i aceasta se ntmpl atunci cnd subsecvenele nu sunt independente,
adic atunci cnd optimizarea unei secvene intr n conflict cu optimizarea
celorlalte subsecvene.
Pe lng programarea dinamic, o posibil metod de rezolvare a unei probleme
care satisface principiul optimalitii este i tehnica greedy. n Seciunea 8.6 vom
ilustra comparativ aceste dou tehnici.
Seciunea 8.1
187
8.2
O competiie
188
Capitolul 8
nc k partide de jucat
P (i, j)
P (i1, j)
P (i2, j)
nc k1 partide de jucat
P (i, j1)
P (i1, j1)
P (i1, j2)
nc k2 partide de jucat
Figura 8.1 Apelurile recursive efectuate dup un apel al func iei P(i, j).
Fie t(k) timpul necesar, n cazul cel mai nefavorabil, pentru a calcula
probabilitatea P(i, j), unde k = i+j.
Avem:
t(1) a
t(k) 2t(k1) + c,
k>1
a i c fiind dou constante. Prin metoda iteraiei, obinem t O(2 ), iar dac
n
i = j = n, atunci t O(4 ). Dac urmrim modul n care sunt generate apelurile
recursive (Figura 8.1), observm c este identic cu cel pentru calculul ineficient al
coeficienilor binomiali:
k
i + j
2
2
j
2n
Timpul de execuie pentru un apel P(n, n) este deci n ( ). innd cont i de
n
Exerciiul 8.3, obinem c timpul pentru calculul lui P(n, n) este n
n
n
O(4 ) (4 /n). Aceasta nseamn c, pentru valori mari ale lui n, algoritmul
este ineficient.
Pentru a mbunti algoritmul, vom proceda ca n cazul triunghiului lui Pascal.
Tabloul n care memorm rezultatele intermediare nu l vom completa, ns, linie
cu linie, ci pe diagonal. Probabilitatea P(n, n) poate fi calculat printr-un apel
serie(n, p) al algoritmului
Seciunea 8.2
O competiie
189
function serie(n, p)
array P[0..n, 0..n]
q 1p
for s 1 to n do
P[0, s] 1; P[s, 0] 0
for k 1 to s1 do
P[k, sk] pP[k1, sk] + qP[k, sk1]
for s 1 to n do
for k 0 to ns do
P[s+k, nk] pP[s+k1, nk] + qP[s+k, nk1]
return P[n, n]
Deoarece n esen se completeaz un tablou de n n elemente, timpul de
2
execuie pentru un apel serie(n, p) este n (n ). Ca i n cazul coeficienilor
binomiali, nu este nevoie s memorm ntregul tablou P. Este suficient s
memorm diagonala curent din P, ntr-un vector de n elemente.
8.3
10582
54201
2856
4055
26418
nmuliri
nmuliri
nmuliri
nmuliri
nmuliri
Cea mai eficient metod este de aproape 19 ori mai rapid dect cea mai
ineficient. n concluzie, ordinea de efectuare a nmulirilor matriciale poate avea
un impact dramatic asupra eficienei.
190
Capitolul 8
n general, vom spune c un produs de matrici este complet parantezat, dac este:
i) o singur matrice, sau ii) produsul a dou produse de matrici complet
parantezate, nconjurat de paranteze. Pentru a afla n mod direct care este ordinea
optim de efectuare a nmulirilor matriciale, ar trebui s parantezm expresia lui
M n toate modurile posibile i s calculm de fiecare dat care este numrul de
nmuliri scalare necesare.
S notm cu T(n) numrul de moduri n care se poate paranteza complet un produs
de n matrici. S presupunem c decidem s facem prima tietur ntre a i-a i a
(i+1)-a matrice a produsului
M = (M 1 M 2 M i )(M i+1 M i+2 M n )
Sunt acum T(i) moduri de a paranteza termenul stng i T(ni) moduri de a
paranteza termenul drept. Deoarece i poate lua orice valoare ntre 1 i n1,
obinem recurena
n 1
T (n) = T (i ) T (n i )
i =1
cu T(1) = 1. De aici, putem calcula toate valorile lui T(n). De exemplu, T(5) = 14,
T(10) = 4862, T(15) = 2674440. Valorile lui T(n) sunt cunoscute ca numerele
catalane. Se poate demonstra c
T ( n) =
1 2 n 2
n n 1
Seciunea 8.3
s=0
s=1
1<s<n
191
: m[i, i] = 0, i=1, 2, , n
: m[i, i+1] = d[i1] d[i] d[i+1], i=1, 2, , n1
: m[i, i+s] = min (m[i, k] + m[k+1, i+s] + d[i1] d[k] d[i+s]),
i k <i + s
i = 1, 2, , ns
A treia situaie reprezint faptul c, pentru a calcula M i M i+1 M i+s , ncercm
toate posibilitile
(M i M i+1 M k ) (M k+1 M k+2 M i+s )
i o alegem pe cea optim, pentru i k < i+s. A doua situaie este de fapt o
particularizare a celei de-a treia situaii, cu s = 1.
Pentru matricile A, B, C, D, din exemplul precedent, avem
d = (13, 5, 89, 3, 34)
Pentru s = 1, gsim m[1, 2] = 5785, m[2, 3] = 1335, m[3, 4] = 9078. Pentru s = 2,
obinem
m[1, 3] = min(m[1, 1] + m[2, 3] + 1353, m[1, 2] + m[3, 3] + 13893)
= min(1530, 9256) = 1530
m[2, 4] = min(m[2, 2] + m[3, 4] + 58934, m[2, 3] + m[4, 4] + 5334)
= min(24208, 1845) = 1845
Pentru s = 3,
m[1, 4] = min( {k = 1} m[1, 1] + m[2, 4] + 13534,
{k = 2} m[1, 2] + m[3, 4] + 138934,
{k = 3} m[1, 3] + m[4, 4] + 13334)
= min(4055, 54201, 2856) = 2856
Tabloul m este dat n Figura 8.2.
S calculm acum eficiena acestei metode. Pentru s > 0, sunt ns elemente de
calculat pe diagonala s; pentru fiecare, trebuie s alegem ntre s posibiliti
(diferite valori posibile ale lui k). Timpul de execuie este atunci n ordinul exact
al lui
n 1
n 1
n 1
s =1
s =1
s =1
(n s) s = n s s 2 = n 2 (n 1) / 2 n(n 1)(2n 1) / 6 = (n 3 n) / 6
192
i=1
Capitolul 8
j=1
5785
1530
2856
s=3
1335
1845
s=2
9078
s=1
0
s=0
Prin aceast metod, l putem afla pe m[1, n]. Pentru a determina i cum s
calculm produsul M n cel mai eficient mod, vom mai construi un tablou
r[1 .. n, 1 .. n], astfel nct r[i, j] s conin valoarea lui k pentru care este
obinut valoarea minim a lui m[i, j]. Urmtorul algoritm construiete tablourile
globale m i r.
procedure minscal(d[0 .. n])
for i 1 to n do m[i, i] 0
for s 1 to n 1 do
for i 1 to n s do
m[i, i+s] +
for k i to i+s 1 do
q m[i, k] + m[k+1, i+s] + d[i 1] d[k] d[i+s]
if q < m[i, i+s] then m[i, i+s] q
r[i, i+s] k
Produsul M poate fi obinut printr-un apel minmat(1, n) al algoritmului recursiv
Problema nmulirii nlnuite optime a matricilor poate fi rezolvat i prin algoritmi mai eficieni.
Astfel, T. C. Hu i M. R. Shing au propus, (n 1982 i 1984), un algoritm cu timpul de execuie n
O(n log n).
Seciunea 8.3
193
function minmat(i, j)
{returneaz produsul matricial M i M i+1 M j
calculat prin m[i, j] nmuliri scalare;
se presupune c i r[i, j] j}
if i = j then return M i
arrays U, V
U minmat(i, r[i, j])
V minmat(r[i, j]+1, j)
return produs(U, V)
unde funcia produs(U, V) calculeaz n mod clasic produsul matricilor U i V. n
exemplul nostru, produsul ABCD se va calcula n mod optim cu 2856 nmuliri
scalare, corespunztor parantezrii: ((A(BC))D).
8.4
Tablouri multidimensionale
194
00
01
02
Capitolul 8
03
10
11
12
13
a
b
// adica b[ 0 ] = &a[ 0 ][ 0 ]
// adica b[ 1 ] = &a[ 1 ][ 0 ]
};
Seciunea 8.4
Tablouri multidimensionale
195
#include <iostream.h>
main( ) {
int a[ 2 ][ 5 ];
int *b[ 2 ] = { a[ 0 ], a[ 1 ] };
cout << ( a + 1 ) << ' ' << *( a + 1 ) << '\n';
cout << ( b + 1 ) << ' ' << *( b + 1 ) << '\n';
return 1;
}
atunci x este adresa unei zone de memorie care conine textul algoritm, iar y
este adresa unei zone de memorie care conine adresa irului eficient.
Expresiile x[1], *(x+1) i expresiile y[1], *(y+1) sunt corecte, valoarea lor
fiind al doilea caracter din irurile algoritm i, respectiv, eficient. n schimb,
dintre cele dou expresii *(++x) i *(++y), doar a doua este corect, deoarece
valoarea lui x nu poate fi modificat.
Prin introducerea claselor i prin posibilitatea de suprancrcare a operatorului
[], echivalena dintre operatorul de indirectare * i cel de indexare [] nu mai este
valabil. Pe baza definiiei
int D = 8192;
// ...
tablou<int> x( D );
dar nu i
for ( i = 0; i < D; i++ ) *( x + i ) = i;
196
Capitolul 8
Seciunea 8.4
Tablouri multidimensionale
197
8.5
Fie G = <V, M> un graf orientat, unde V este mulimea vrfurilor i M este
mulimea muchiilor. Fiecrei muchii i se asociaz o lungime nenegativ. Dorim s
calculm lungimea celui mai scurt drum ntre fiecare pereche de vrfuri.
*
Diagonala principal este diagonala care unete colul din stnga sus cu cel din dreapta jos.
198
Capitolul 8
Seciunea 8.5
199
50
D0 = L =
30
15
15
15
obinem succesiv
50
D1 =
30
15
0
45
D3 =
30
15
15
35
20
20
15
35
20
15
50
D2 =
30
15
10
15
20
D4 =
30
15
20
15
35
20
15
10
35
20
10
15
0
10
15
De obicei, dorim s aflm nu numai lungimea celui mai scurt drum, dar i traseul
su. n acest situaie, vom construi o a doua matrice P, iniializat cu zero. Bucla
cea mai interioar a algoritmului devine
if D[i, k]+D[k, j] < D[i, j] then
Cnd algoritmul se oprete, P[i, j] va conine vrful din ultima iteraie care a
cauzat o modificare n D[i, j]. Pentru a afla prin ce vrfuri trece cel mai scurt
drum de la i la j, consultm elementul P[i, j]. Dac P[i, j] = 0, atunci cel mai scurt
drum este chiar muchia (i, j). Dac P[i, j] = k, atunci cel mai scurt drum de la i la
200
Capitolul 8
C
A
E
B
D
E
F
4
P=
0
8.6
Un arbore binar n care fiecare vrf conine o valoare (numit cheie) este un
arbore de cutare, dac cheia fiecrui vrf neterminal este mai mare sau egal cu
cheile descendenilor si stngi i mai mic sau egal cu cheile descendenilor si
drepi. Dac cheile arborelui sunt distincte, aceste inegaliti sunt, n mod evident,
stricte.
Figura 8.4 este un exemplu de arbore de cutare *, coninnd cheile A, B, C, , H.
Vrfurile pot conine i alte informaii (n afar de chei), la care s avem acces
prin intermediul cheilor.
Seciunea 8.6
201
E
C
B
G
D
F
E
202
Capitolul 8
p[i ](d i + 1)
i =1
(*)
Seciunea 8.6
p[1] = 0,30
p[4] = 0,45
p[2] = 0,05
p[5] = 0,12
p[3] = 0,08
203
204
Capitolul 8
0,30
m=
0,35
0,43
0,88
0,05
0,13
0,58
0,08
0,53
0,45
1,00
0,70
0,65
0,57
0,12
C[3, 4] = 0,61
C[4, 5] = 0,69
Apoi,
C[1, 3] = m[1, 3] + min(C[1, 0]+C[2, 3], C[1, 1]+C[3, 3], C[1, 2]+C[4, 3])
= 0,43 + min(0,18, 0,38, 0,40) = 0,61
C[2, 4] = 0,76
C[3, 5] = 0,85
C[1, 4] = 1,49
C[2, 5] = 1,00
C[1, 5] = m[1, 5] + min(C[1, 0]+C[2, 5], C[1, 1]+C[3, 5], C[1, 2]+C[4, 5],
C[1, 3]+C[5, 5], C[1, 4]+C[6, 5]) = 1,73
Arborele optim necesit deci n medie 1,73 comparaii pentru a gsi o cheie.
n acest algoritm, calculm valorile C[i, j] n primul rnd pentru j i = 1, apoi
pentru j i = 2 etc. Cnd j i = q, avem de calculat n q valori ale lui C[i, j], fiecare
implicnd o alegere ntre q+1 posibiliti. Timpul necesar * este deci n
n 1
( (n q )(q + 1) ) = (n 3 )
q =1
tim acum cum s calculm numrul minim de comparaii necesare pentru a gsi o
cheie n arborele optim. Mai rmne s construim efectiv arborele optim. n
Seciunea 8.6
205
c4
c1
c5
c3
c2
8.7
ntr-o prim aproximare, arborele binar este un tip de dat similar tipului list.
Vrfurile sunt compuse din informaie (cheie) i legturi, iar arborele propiu-zis
este complet precizat prin adresa vrfului rdcin. n privina organizrii
memoriei, putem opta fie pentru tablouri paralele, ca n Exerciiul 8.10, fie pentru
alocarea dinamic a elementelor. Alegnd alocarea dinamic, vom utiliza n
ntregime modelul oferit de clasa lista<E> elaborat n Seciunea 4.3. Astfel,
clasa parametric arbore<E>, cu o structur intern de forma:
206
Capitolul 8
are la baz o clas privat varf<E> prin intermediul creia vom implementa
majoritatea operaiilor efectuate asupra arborilor. Vom cuta s izolm, ori de
cte ori va fi posibil, operaiile direct aplicabile vrfurilor, astfel nct interfaa
dintre cele dou clase s fie foarte clar precizat printr-o serie de operaii
elementare.
Nu vom implementa n aceast seciune arbori binari n toat generalitatea lor, ci
doar arborii de cutare. Obiectivul urmrit n prezentarea listelor a fost structura
de date n sine, mpreun cu procedurile generale de manipulare. n cazul
arborelui de cutare, nu mai este necesar o astfel de generalitate, deoarece vom
implementa direct operaiile specifice. n mare, aceste operaii pot fi mprite n
trei categorii:
Cutri. Localizarea vrfului cu o anumit cheie, a succesorului sau
predecesorului lui, precum i a vrfurilor cu cheile de valoare maxim,
respectiv minim.
Modificri. Arborele se modific prin inserarea sau tergerea unor vrfuri.
Organizri. Arborele nu este construit prin inserarea elementelor, ci global,
stabilind ntr-o singur trecere legturile dintre vrfuri. Frecvent, organizarea
se face conform unor criterii pentru optimizarea cutrilor. Un caz particular al
acestei operaii este reorganizarea arborelui dup o perioad suficient de mare
de utilizare. Este vorba de reconstruirea arborelui ntr-o structur optim, pe
baza statisticilor de utilizare.
Datorit operaiilor de cutare i modificare, elementele de tip E trebuie s fie
comparabile prin operatorii uzuali ==, !=, >. n finalul Seciunii 7.4.1, am artat
c o asemenea pretenie nu este totdeauna justificat. Desigur c, n cazul unor
structuri bazate pe relaia de ordine, aa cum sunt heap-ul i arborele de cutare,
este absolut normal ca elementele s poat fi comparate.
Principalul punct de interes pentru noi este optimizarea, conform algoritmului de
programare dinamic. Nu vom ignora nici cutrile, nici operaiile de modificare
(tratate n Seciunea 8.7.2).
Seciunea 8.7
8.7.1
207
Arborele optim
Vom rezolva problema obinerii arborelui optim n cel mai simplu caz posibil (din
punct de vedere al utilizrii, dar nu i n privina programrii): arborele deja
exist i trebuie reorganizat ntr-un arbore de cutare optim. Avnd n vedere
specificul diferit al operaiilor de organizare fa de celelalte operaii efectuate
asupra grafurilor, am considerat util s ncapsulm optimizarea ntr-o clas pe
care o vom numi structur pentru optimizarea arborilor sau, pe scurt, s8a.
Clasa s8a este o clas parametric privat, asociat clasei arbore<E>.
Funcionalitatea ei const n:
i)
ii)
Principalul motiv pentru care a fost aleas aceast implementare este c sunt
necesare doar operaii modificare a legturilor. Deplasarea unui vrf (de exemplu,
pentru sortare) nseamn nu numai deplasarea cheii, ci i a informaiei asociate.
Cum fiecare din aceste elemente pot fi orict de mari, clasa s8a realizeaz o
economie semnificativ de timp i (mai ales) de memorie.
Pentru optimizarea propriu-zis, am implementat att algoritmul de programare
dinamic, ct i pe cel greedy prezentat n Exerciiul 8.12. Dei algoritmul greedy
nu garanteaz obinerea arborelui optim, el are totui avantajul c este mai
eficient dect algoritmul de programare dinamic din punct de vedere al timpului
de execuie i al memoriei utilizate. Invocarea optimizrii se realizeaz din clasa
arbore<E>, prin secvene de genul
arbore<float> af;
// arborele af se creeaza prin inserarea cheilor
// arborele af se utilizeaza
// pe baza probabilitatilor predefinite si actualizate
// prin utilizarea arborelui se invoca optimizarea
af.re_prodin( ); // sau af.re_greedy( );
208
Capitolul 8
Seciunea 8.7
209
n stabilirea valorilor tablourilor pvarf i r se pot distinge foarte clar cele dou
etape ale execuiei constructorului clasei s8a, etape menionate n Seciunea
4.2.1. Este vorba de etapa de iniializare (implementat prin lista de iniializare a
membrilor) i de etapa de atribuire (implementat prin corpul constructorului).
Lista de iniializare asociat constructorului clasei s8a conine parametrul necesar
dimensionrii tabloului pvarf pentru cele n elemente ale arborelui. Cum este ns
iniializat tabloul r care nu apare n lista de iniializare? n astfel de cazuri, se
invoc automat constructorul implicit (apelabil fr nici un argument) al clasei
respective. Pentru clasa tablou<T>, constructorul implicit doar iniializeaz cu 0
datele membre.
Etapa de atribuire a constructorului clasei s8a, implementat prin invocarea
funciei setvarf(), const n parcurgerea arborelui i memorarea adreselor
vrfurilor vizitate n tabloul pvarf. Funcia setvarf() parcurge pentru fiecare
vrf subarborele stng, apoi memoreaz adresa vrfului curent i, n final,
parcurge subarborele drept. Dup cum vom vedea n Exerciiul 9.1, acest mod de
parcurgere are proprietatea c elementele arborelui sunt parcurse n ordine
cresctoare. De fapt, este vorba de o metod de sortare similar quicksort-ului,
vrful rdcin avnd acelai rol ca i elementul pivot din quicksort.
template <class E>
void s8a<E>::setvarf( int& poz, varf<E>* x ) {
if ( x ) {
setvarf( poz, x->st );
pvarf[ poz++ ] = x;
setvarf( poz, x->dr );
// anulam toate legaturile elementului x
x->st = x->dr = x->tata = 0;
}
}
n aceast funcie, x->st, x->dr i x->tata sunt legturile vrfului curent x ctre
fiul stng, ctre cel drept i, respectiv, ctre vrful tat. n plus fa de aceste
legturi, obiectele de tip varf<E> mai conin cheia (informaia) propriu-zis i un
cmp auxiliar pentru probabilitatea vrfului (elementului). n consecin, clasa
varf<E> are urmtoarea structur:
210
Capitolul 8
key; // cheia
p; // frecventa utilizarii cheii curente
};
Seciunea 8.7
211
}
// subarborele curent este gata; se returneaza adresa lui
return actual;
}
212
Capitolul 8
8.7.2
Cutarea n arbore
Seciunea 8.7
213
214
Capitolul 8
Succesorul unui vrf X este vrful cu cea mai mic cheie mai mare dect cheia vrfului X (vezi i
Exerciiul 8.10).
Seciunea 8.7
215
S remarcm asemnarea dintre funciile C++ de mai sus i funciile analoage din
Exerciiul 8.10.
Pentru a demonstra corectitudinea funciilor _serarch() i _min(), nu avem
dect s ne reamintim c, prin definiie, ntr-un arbore binar de cutare fiecare
vrf K verific relaiile X K i K Y pentru orice vrf X din subarborele stng
i orice vrf Y din subarborele drept.
Demonstrarea corectitudinii funciei _succ() este de asemenea foarte simpl. Fie
K vrful al crui succesor S trebuie determinat. Vrfurile K i S pot fi situate
astfel:
Vrful S este n subarborele drept al vrfului K. Deoarece aici sunt numai
vrfuri Y cu proprietatea K Y (vezi Figura 8.7a) rezult c S este valoarea
minim din acest subarbore. n plus, avnd n vedere procedura pentru
determinarea minimului, vrful S nu are fiul stng.
Vrful K este n subarborele stng al vrfului S. Deoarece fiecare vrf X de aici
verific inegalitatea X S (vezi Figura 8.7b), deducem c maximul din acest
subarbore este chiar K. Dar maximul se determin parcurgnd fiii din dreapta
pn la un vrf fr fiul drept. Deci, vrful K nu are fiul drept, iar S este
primul ascendent din stnga al vrfului K.
n consecin, cele dou situaii se exclud reciproc, deci funcia _succ() este
corect.
S
(a) Vrful succesor S este
minim n subarborele drept
al vrfului K.
K
(b) Vrful K este maxim n
subarborele stng al
vrfului succesor S.
216
8.7.3
Capitolul 8
Modificarea arborelui
Valoarea returnat este true, dac cheia k a putut fi inserat n arbore, sau false,
n cazul n care deja exist n arbore un vrf cu cheia k. Inserarea propriu-zis
const n cutarea cheii k prin intermediul adreselor x i y, y fiind adresa tatlui
lui x. Atunci cnd am terminat procesul de cutare, valoarea lui x devine 0 i noul
vrf se va insera la stnga sau la dreapta lui y, n funcie de relaia dintre cheia k
i cheia lui y.
Procedura de tergere ncepe prin a determina adresa z a vrfului de ters, pe baza
cheii k. Dac procesul de cutare se finalizeaz cu succes, cheia k se va actualiza
(n scopul unor prelucrri ulterioare) cu informaia din vrful z, iar apoi se
demareaz procesul de tergere efectiv a vrfului z. Dac z este un vrf terminal,
nu avem dect s anulm legtura corespunztoare din vrful tat. Chiar i atunci
cnd z are un singur fiu, tergerea este direct. Adresa lui z din vrful tat se
nlocuiete cu adresa fiului lui z. A treia i cea mai complicat situaie apare
Seciunea 8.7
R
C
217
H
E
N
P
N
L
(a)
(b)
// se cauta cheia k
// nu a fost gasita
n--;
// in arbore va fi cu un varf mai putin
k = z->key; // k va retine intreaga informatie din z
// - y
//
// - x
varf<E>
Predecesorul unui vrf X este vrful care are cea mai mare cheie mai mic dect cheia vrfului X.
218
Capitolul 8
spre x
x;
x;
x;
Seciunea 8.7
g.inord( );
g.inord( );
219
220
Capitolul 8
g.re_prodin( );
cout << "Arborele Greedy re-ProgDin:\n"; g.inord( );
return 1;
}
8.8
(
(
(
(
(
(
key
key
key
key
key
key
C,
H,
M,
N,
P,
R,
f
f
f
f
f
f
0,
0,
0,
0,
0,
0,
st
st
st
st
st
st
0x0000,
0x166c,
0x0000,
0x169c,
0x0000,
0x168c,
dr
dr
dr
dr
dr
dr
0x0000,
0x165c,
0x0000,
0x16ac,
0x0000,
0x0000,
tata
tata
tata
tata
tata
tata
0x163c
0x0000
0x168c
0x165c
0x168c
0x163c
)
)
)
)
)
)
f ( x ) = vi x i
i =1
Seciunea 8.8
g i xi
221
i =1
222
Capitolul 8
8.9
Exerciii
8.1
Soluie: Notm cu r(n, k) numrul de apeluri recursive necesare pentru a-l calcula
pe C(n, k). Procedm prin inducie, n funcie de n. Dac n este 0, proprietatea
este adevrat. Presupunem proprietatea adevrat pentru n 1 i demonstrm
pentru n.
Presupunem, pentru nceput, c 0 < k < n. Atunci, avem recurena
r(n, k) = r(n 1, k 1) + r(n 1, k) + 2
Din relaia precedent, obinem
n 1
n 1
n
r(n, k) = 2
2 + 2
2 + 2 = 2 2
k 1
k
k
n
Dac k este 0 sau n, atunci r(n, k) = 0 i, deoarece n acest caz avem = 1,
k
rezult c proprietatea este adevrat. Acest rezultat poate fi verificat practic,
rulnd programul din Exerciiul 2.5.
8.2
i)
ii)
8.3
2n
n
Demonstrai c 4 /(2n+1).
k
8.4
Folosind algoritmul serie, calculai probabilitatea ca juctorul A s
ctige, presupunnd n = 4 i p = 0,45.
Seciunea 8.9
Exerciii
223
8.5
Problema nmulirii nlnuite optime a matricilor se poate rezolva i prin
urmtorul algoritm recursiv:
function rminscal(i, j)
{returneaz numrul minim de nmuliri scalare
pentru a calcula produsul matricial M i M i+1 M j }
if i = j then return 0
q +
for k i to j 1 do
q min(q, rminscal(i, k)+rminscal(k+1, j)+d[i 1]d[k]d[ j])
return q
unde tabloul d[0 .. n] este global. Gsii o limit inferioar a timpului. Explicai
ineficiena acestui algoritm.
Soluie: Notm cu r( j i+1) numrul de apeluri recursive necesare pentru a-l
calcula pe rminscal(i, j). Pentru n > 2 avem
n 1
n 1
k =1
k =1
r (n) = r ( k ) + r (n k ) = 2 r ( k ) 2r (n 1)
iar r(2) = 2. Prin metoda iteraiei, deducei c r(n) 2
n
pentru un apel rminscal(1, n) este atunci n (2 ).
n1
8.6
Elaborai un algoritm eficient care s afieze parantezarea optim a unui
produs matricial M(1), , M(n). Folosii pentru aceasta matricea r, calculat de
algoritmul minscal. Analizai algoritmul obinut.
Soluie: Se apeleaz cu paran(1, n) urmtorul algoritm:
function paran(i, j)
if i = j then write M(, i, )
else write (
parant(i, r[i, j])
write *
parant(r[i, j]+1, j)
write )
Artai prin inducie c o parantezare complet unei expresii de n elemente are
exact n 1 perechi de paranteze. Deducei de aici care este eficiena algoritmului.
8.7
Presupunnd matricea P din algoritmul lui Floyd cunoscut, elaborai un
algoritm care s afieze prin ce vrfuri trece cel mai scurt drum dintre dou
vrfuri oarecare.
224
Capitolul 8
8.8
ntr-un graf orientat, s presupunem c ne intereseaz doar existena, nu i
lungimea drumurilor, ntre fiecare pereche de vrfuri. Iniial, L[i, j] = true dac
muchia (i, j) exist i L[i, j] = false n caz contrar. Modificai algoritmul lui Floyd
astfel nct, n final, s avem D[i, j] = true dac exist cel puin un drum de la i la
j i D[i, j] = false n caz contrar.
Soluie: Se nlocuiete bucla cea mai interioar cu:
D[i, j] D[i, j] or (D[i, k] and D[k, j])
obinndu-se algoritmul lui Warshall (1962). Matricea boolean L se numete
nchiderea tranzitiv a grafului.
8.9
Artai cu ajutorul unui contraexemplu c urmtoarea propoziie nu este,
n general, adevrat: Un arbore binar este un arbore de cutare dac cheia
fiecrui vrf neterminal este mai mare sau egal cu cheia fiului su stng i mai
mic sau egal cu cheia fiului su drept.
8.10
Fie un arbore binar de cutare reprezentat prin adrese, astfel nct vrful i
(adic vrful a crui adres este i) este memorat n patru locaii diferite coninnd
:
KEY[i]
ST[i]
DR[i]
TATA[i]
=
=
=
=
cheia vrfului
adresa fiului stng
adresa fiului drept
adresa tatlui
Seciunea 8.9
Exerciii
225
function tree-search(i, v)
if i = 0 or v = KEY[i] then return i
if v < KEY[i] then return tree-search(ST[i], v)
else return tree-search(DR[i], v)
Iat i o versiune iterativ a acestui algoritm:
function iter-tree-search(i, v)
while i 0 and v KEY[i] do
if i < KEY[i] then i ST[i]
else i DR[i]
return i
ii)
8.13
S se gseasc
226
Capitolul 8
V (l , j , X ) = max
vi xi
l i j
g i xi X
l i j
x i {0, 1},
lij
Seciunea 8.9
Exerciii
227
8.15
Fie u i v dou secvene de caractere. Dorim s transformm pe u n v, cu
un numr minim de operaii de urmtoarele tipuri:
terge un caracter
adaug un caracter
schimb un caracter
De exemplu, putem s transformm abbac n abcbc n trei etape:
abbac
abac
ababc
abcbc
(terge b)
(adaug b)
(schimb a cu c)
stng
9. Explorri n grafuri
Am vzut deja c o mare varietate de probleme se formuleaz n termeni de
grafuri. Pentru a le rezolva, de multe ori trebuie s explorm un graf, adic s
consultm (vizitm) vrfurile sau muchiile grafului respectiv. Uneori trebuie s
consultm toate vrfurile sau muchiile, alteori trebuie s consultm doar o parte
din ele. Am presupus, pn acum, c exist o anumit ordine a acestor consultri:
cel mai apropiat vrf, cea mai scurt muchie etc. n acest capitol, introducem
cteva tehnici care pot fi folosite atunci cnd nu este specificat o anumit ordine
a consultrilor.
Vom folosi termenul de graf n dou ipostaze. Un graf va fi uneori, ca i pn
acum, o structur de date implementat n memoria calculatorului. Acest mod
explicit de reprezentare nu este ns indicat atunci cnd graful conine foarte
multe vrfuri.
S presupunem, de exemplu, c folosim vrfurile unui graf pentru a reprezenta
configuraii n jocul de ah, fiecare muchie corespunznd unei mutri legale ntre
120
dou configuraii. Acest graf are aproximativ 10 vrfuri. Presupunnd c un
11
calculator ar fi capabil s genereze 10 vrfuri pe secund, generarea complet a
80
grafului asociat jocului de ah s-ar face n mai mult de 10 ani! Un graf att de
mare nu poate s aib dect o existen implicit, abstract.
Un graf implicit este un graf reprezentat printr-o descriere a vrfurilor i
muchiilor sale, el neexistnd integral n memoria calculatorului. Poriuni relevante
ale grafului pot fi construite pe msur ce explorarea progreseaz. De exemplu,
putem avea n memorie doar o reprezentare a vrfului curent i a muchiilor
adiacente lui; pe msur ce naintm n graf, vom actualiza aceast reprezentare.
Tehnicile de explorare pentru cele dou concepte de graf (grafuri construite
explicit i grafuri implicite) sunt, n esen, identice. Indiferent de obiectivul
urmrit, explorarea se realizeaz pe baza unor algoritmi de parcurgere, care
asigur consultarea sistematic a vrfurilor sau muchiilor grafului respectiv.
9.1
Parcurgerea arborilor
Pentru parcurgerea arborilor binari exist trei tehnici de baz. Dac pentru fiecare
vrf din arbore vizitm prima dat vrful respectiv, apoi vrfurile din subarborele
stng i, n final, subarborele drept, nseamn c parcurgem arborele n preordine.
Dac vizitm subarborele stng, vrful respectiv i apoi subarborele drept, atunci
227
228
Parcurgerea arborilor
Capitolul 9
parcurgem arborele n inordine, iar dac vizitm prima dat subarborele stng,
apoi cel drept, apoi vrful respectiv, parcurgerea este n postordine. Toate aceste
tehnici parcurg arborele de la stnga spre dreapta. Putem parcurge ns arborele i
de la dreapta spre stnga, obinnd astfel nc trei moduri de parcurgere.
Proprietatea 9.1 Pentru fiecare din aceste ase tehnici de parcurgere, timpul
necesar pentru a explora un arbore binar cu n vrfuri este n (n).
Demonstraie: Fie t(n) timpul necesar pentru parcurgerea unui arbore binar cu n
vrfuri. Putem presupune c exist constanta real pozitiv c, astfel nct t(n) c
pentru 0 n 1. Timpul necesar pentru parcurgerea unui arbore cu n vrfuri,
n > 1, n care un vrf este rdcina, i vrfuri sunt situate n subarborele stng i
n i 1 vrfuri n subarborele drept, este
t(n) c + max {t(i)+t(n i 1) | 0 i n 1}
Vom arta, prin inducie constructiv, c t(n) dn+c, unde d este o alt constant.
Pentru n = 0, proprietatea este adevrat. Prin ipoteza induciei specificate parial,
presupunem c t(i) di+c, pentru orice 0 i < n. Demonstrm c proprietatea este
adevrat i pentru n. Avem
t(n) c+2c+d(n 1) = dn+c+2c d
Lund d 2c, obinem t(n) dn+c. Deci, pentru d suficient de mare, t(n) dn+c,
pentru orice n 0, adic t O(n). Pe de alt parte, t (n), deoarece fiecare din
cele n vrfuri trebuie vizitat. n consecin, t (n).
Seciunea 9.2
9.2
229
Tipul abstract arbore este imposibil de conceput n lipsa unor metode sistematice
de explorare. Iat cteva situaii n care le-am folosit, sau va trebui s le folosim:
Reorganizarea ntr-un arbore de cutare optim. Este vorba de procedura
setvarf() din clasa s8a (Seciunea 8.7.1), procedur prin care s-a iniializat
un tablou cu adresele tuturor vrfurilor din arbore. Acum este clar c am
folosit o parcurgere n inordine, prilej cu care am ajuns i la o procedur de
sortare similar quicksort-ului.
Copierea, vrf cu vrf, a unui arbore ntr-un alt arbore. Procedura este
necesar constructorului i operatorului de atribuire.
Implementarea destructorului clasei, adic eliberarea spaiului ocupat de
fiecare din vrfurile arborelui.
Afiarea unor instantanee ale structurii arborilor pentru a verifica
corectitudinea diverselor operaii.
Operaia de copiere este implementat prin funcia _copy() din clasa varf<E>.
Este vorba de o funcie care copiaz recursiv arborele al crui vrf rdcin este
dat ca argument, iar apoi returneaz adresa arborelui construit prin copiere.
template <class E>
varf<E>* _copy( varf<E>* x ) {
varf<E> *z = 0;
if ( x ) {
// varful nou alocat se initializeaza cu x
z = new varf<E>( x->key, x->p );
// se copiaza
// fiecare se
if ( (z->st =
z->st->tata
if ( (z->dr =
z->dr->tata
}
return z;
}
230
Explorri n grafuri
Capitolul 9
Efectul instruciunii delete root ar trebui s fie tergerea tuturor vrfurilor din
arborele cu rdcina root. Pentru a ajunge la acest rezultat, avem nevoie de
implementarea corespunztoare a destructorului clasei varf<E>, destructor
invocat, dup cum se tie, nainte ca operatorul delete s elibereze spaiul alocat.
Forma acestui destructor este foarte simpl:
~varf( ) { delete st; delete dr; }
Seciunea 9.2
cout <<
<<
<<
<<
<<
<<
<<
231
x
" ( key " << x->key
", f "
<< x->p
", st "
<< x->st
", dr "
<< x->dr
", tata " << x->tata
" )";
_inord( x->dr );
}
este exact ceea ce ne trebuie pentru a afia ntreaga structur intern a arborelui.
9.3
Fie G = <V, M> un graf orientat sau neorientat, ale crui vrfuri dorim s le
consultm. Presupunem c avem posibilitatea s marcm vrfurile deja vizitate n
tabloul global marca. Iniial, nici un vrf nu este marcat.
Pentru a efectua o parcurgere n adncime, alegem un vrf oarecare, v V, ca
punct de plecare i l marcm. Dac exist un vrf w adiacent lui v (adic, dac
exist muchia (v, w) n graful orientat G, sau muchia {v, w} n graful neorientat
G) care nu a fost vizitat, alegem vrful w ca noul punct de plecare i apelm
recursiv procedura de parcurgere n adncime. La ntoarcerea din apelul recursiv,
dac exist un alt vrf adiacent lui v care nu a fost vizitat, apelm din nou
procedura etc. Cnd toate vrfurile adiacente lui v au fost marcate, se ncheie
consultarea nceput n v. Dac au rmas vrfuri n V care nu au fost vizitate,
alegem unul din aceste vrfuri i apelm procedura de parurgere. Continum
astfel, pn cnd toate vrfurile din V au fost marcate. Iat algoritmul:
procedure parcurge(G)
for fiecare v V do marca[v] nevizitat
for fiecare v V do
if marca[v] = nevizitat then ad(v)
procedure ad(v)
{vrful v nu a fost vizitat}
marca[v] vizitat
for fiecare vrf w adiacent lui v do
if marca[w] = nevizitat then ad(w)
232
Explorri n grafuri
Capitolul 9
(a)
(b)
Seciunea 9.3
233
Cu alte cuvinte, se parcurg n preordine vrfurile arborelui parial din Figura 9.1b.
Se poate observa c parcurgerea n adncime a unui arbore, pornind din rdcin,
are ca efect parcurgerea n preordine a arborelui.
9.3.1
Puncte de articulare
Explorri n grafuri
234
Capitolul 9
preord[v]
preord[w] pentru fiecare vrf w pentru care exist o muchie {v, w} n G
care nu are o muchie corespunztoare n A (n Figura 9.1b, o muchie
punctat)
minim[x] pentru fiecare fiu x al lui v n A
3. Punctele de articulare se determin acum astfel:
a. rdcina lui A este un punct de articulare al lui G, dac i numai dac are
mai mult de un fiu;
Alternativa 3a din algoritm rezult imediat, deoarece este evident c rdcina lui
A este un punct de articulare al lui G, dac i numai dac are mai mult de un fiu.
S presupunem acum c v nu este rdcina lui A. Dac x este un fiu al lui v i
minim[x] < preord[v], rezult c exist o succesiune de muchii care l conectez
pe x cu celelalte vrfuri ale grafului, chiar i dup eliminarea lui v. Pe de alt
parte, nu exist nici o succesiune de muchii care s l conecteze pe x cu tatl lui v,
dac minim[x] preord[v]. Se deduce c i alternativa 3b este corect.
9.3.2
Sortarea topologic
n aceast seciune, vom arta cum putem aplica parcurgerea n adncime a unui
graf, ntr-un procedeu de sortare esenial diferit fa de sortrile ntlnite pn
acum.
Seciunea 9.3
235
C
but
cafea
preparat
cafea
A
trezire
duul
mbrcare
plecare
9.4
236
Explorri n grafuri
Capitolul 9
Pentru a putea compara aceste dou tehnici de parcurgere, vom da pentru nceput
o versiune nerecursiv pentru procedura ad. Versiunea se bazeaz pe utilizarea
unei stive. Presupunem c avem funcia ftop care returneaz ultimul vrf inserat n
stiv, fr s l tearg. Folosim i funciile push i pop din Seciunea 3.1.1.
procedure iterad(v)
S stiv vid
marca[v] vizitat
push(v, S)
while S nu este vid do
while exist un vrf w adiacent lui ftop(S)
astfel nct marca[w] = nevizitat do
marca[w] vizitat
push(w, S)
pop(S)
Pentru parcurgerea n lime, vom utiliza o coad i funciile insert-queue,
delete-queue din Seciunea 3.1.2. Iat acum algoritmul de parcurgere n lime:
procedure lat(v)
C coad vid
marca[v] vizitat
insert-queue(v, C)
while C nu este vid do
u delete-queue(C)
for fiecare vrf w adiacent lui u do
if marca[w] = nevizitat then marca[w] vizitat
insert-queue(w, C)
Procedurile iterad i lat trebuie apelate din procedura
procedure parcurge(G)
for fiecare v V do marca[v] nevizitat
for fiecare v V do
if marca[v] = nevizitat then {iterad sau lat} (v)
De exemplu, pentru graful din Figura 9.1, ordinea de parcurgere n lime a
vrfurilor este: 1, 2, 3, 4, 5, 6, 7, 8.
Ca i n cazul parcurgerii n adncime, parcurgerea n lime a unui graf G conex
asociaz lui G un arbore parial. Dac G nu este conex, atunci obinem o pdure
de arbori, cte unul pentru fiecare component conex.
Analiza eficienei algoritmului de parcurgere n lime se face la fel ca pentru
parcurgerea n adncime. Pentru a parcurge un graf cu n vrfuri i m muchii
Seciunea 9.4
237
timpul este n: i) (n+m), dac reprezentm graful prin liste de adiacen; ii)
2
(n ), dac reprezentm graful printr-o matrice de adiacen.
Parcurgerea n lime este folosit de obicei atunci cnd se exploreaz parial
anumite grafuri infinite, sau cnd se caut cel mai scurt drum dintre dou vrfuri.
9.5
Explorri n grafuri
238
Capitolul 9
Seciunea 9.5
239
9.6
Backtracking
240
Explorri n grafuri
Capitolul 9
posibile de plasare a celor opt regine, verificnd de fiecare dat dac nu s-a
obinut o soluie. Deoarece n total exist
64
. .368
= 4.426165
8
combinaii posibile, este evident c acest mod de abordare nu este practic. O
prim mbuntire ar fi s nu plasm niciodat mai mult de o regin pe o linie.
Aceast restricie reduce reprezentarea pe calculator a unei configuraii pe tabla
de ah la un simplu vector, posibil[1 .. 8]: regina de pe linia i, 1 i 8, se afl pe
coloana posibil[i], adic n poziia (i, posibil[i]). De exemplu, vectorul
(3, 1, 6, 2, 8, 6, 4, 7) nu reprezint o soluie, deoarece reginele de pe liniile trei i
ase sunt pe aceeai coloan i, de asemenea, exist dou perechi de regine situate
pe aceeai diagonal. Folosind acest reprezentare, putem scrie n mod direct
algoritmul care gsete o soluie a problemei:
procedure regine1
for i 1 1 to 8 do
for i 2 1 to 8 do
M
for i 8 1 to 8 do
posibil (i 1 , i 2 , , i 8 )
if soluie(posibil) then write posibil
stop
write nu exist soluie
8
Seciunea 9.6
Backtracking
241
procedure perm(i)
if i = n then utilizeaz(T) {T este o nou permutare}
else for j i to n do interschimb T[i] i T[ j]
perm(i+1)
interschimb T[i] i T[ j]
n algoritmul de generare a permutrilor, T[1 .. n] este un tablou global iniializat
cu [1, 2, , n], iar primul apel al procedurii este perm(1). Dac utilizeaz(T)
necesit un timp constant, atunci perm(1) necesit un timp n (n!).
Aceast abordare reduce numrul de configuraii posibile la 8! = 40.320. Dac se
folosete algoritmul perm, atunci pn la prima soluie sunt generate 2830
permutri. Mecanismul de generare a permutrilor este mai complicat dect cel de
generare a vectorilor de opt ntregi ntre 1 i 8. n schimb, verificarea faptului
dac o configuraie este soluie se face mai uor: trebuie doar verificat dac nu
exist dou regine pe aceeai diagonal.
Chiar i cu aceste mbuntiri, nu am reuit nc s eliminm o deficien comun
a algoritmilor de mai sus: verificarea unei configuraii prin if soluie(posibil) se
face doar dup ce toate reginele au fost deja plasate pe tabl. Este clar c se
pierde astfel foarte mult timp.
Vom reui s eliminm aceast deficien aplicnd principiul backtracking. Pentru
nceput, reformulm problema celor opt regine ca o problem de cutare ntr-un
arbore. Spunem c vectorul P[1 .. k] de ntregi ntre 1 i 8 este k-promitor,
pentru 0 k 8, dac zonele controlate de cele k regine plasate n poziiile
(1, P[1]), (2, P[2]), , (k, P[k]) sunt disjuncte. Matematic, un vector P este
k-promitor dac:
P[i] P[ j] {i j, 0, j i},
pentru orice 0 i, j k, i j
Pentru k 1, orice vector P este k-promitor. Soluiile problemei celor opt regine
corespund vectorilor 8-promitori.
Fie V mulimea vectorilor k-promitori, 0 k 8. Definim graful orientat
G = <V, M> astfel: (P, Q) M, dac i numai dac exist un ntreg k, 0 k 8,
astfel nct P este k-promitor, Q este (k+1)-promitor i P[i] = Q[i] pentru
fiecare 0 i k. Acest graf este un arbore cu rdcina n vectorul vid (k = 0).
Vrfurile terminale sunt fie soluii (k = 8), fie vrfuri moarte (k < 8), n care
este imposibil de plasat o regin pe urmtoarea linie fr ca ea s nu intre n zona
controlat de reginele deja plasate. Soluiile problemei celor opt regine se pot
obine prin explorarea acestui arbore. Pentru aceasta, nu este necesar s generm
n mod explicit arborele: vrfurile vor fi generate i abandonate pe parcursul
explorrii. Vom parcurge arborele G n adncime, ceea ce este echivalent aici cu o
parcurgere n preordine, cobornd n arbore numai dac exist anse de a ajunge
la o soluie.
242
Explorri n grafuri
Capitolul 9
Seciunea 9.6
Backtracking
243
9.7
Grafuri i jocuri
Cele mai multe jocuri strategice pot fi reprezentate sub forma grafurilor orientate
n care vrfurile sunt poziii n joc, iar muchiile sunt mutri legale ntre dou
poziii. Dac numrul poziiilor nu este limitat a priori, atunci graful este infinit.
Vom considera n cele ce urmeaz doar jocuri cu doi parteneri, fiecare avnd pe
rnd dreptul la o mutare. Presupunem, de asemenea, c jocurile sunt simetrice
244
Explorri n grafuri
Capitolul 9
(regulile sunt aceleai pentru cei doi parteneri) i deterministe (nu exist un factor
aleator).
Pentru a determina o strategie de ctig ntr-un astfel de joc, vom ataa fiecrui
vrf al grafului o etichet care poate fi de ctig, pierdere, sau remiz. Eticheta
corespunde situaiei unui juctor care se afl n poziia respectiv i trebuie s
mute. Presupunem c nici unul din juctori nu greete, fiecare alegnd mereu
mutarea care este pentru el optim. n particular, din anumite poziii ale jocului
nu se poate efectua nici o mutare, astfel de poziii terminale neavnd poziii
succesoare n graf. Etichetele vor fi ataate n mod sistematic astfel:
Etichetele ataate unei poziii terminale depind de jocul n cauz. De obicei,
juctorul care se afl ntr-o poziie terminal a pierdut.
O poziie neterminal este o poziie de ctig, dac cel puin una din poziiile
ei succesoare n graf este o poziie de pierdere.
O poziie neterminal este o poziie de pierdere, dac toate poziiile ei
succesoare n graf sunt poziii de ctig.
Orice poziie care a rmas neetichetat este o poziie de remiz.
Dac jocul este reprezentat printr-un graf finit aciclic, aceast metod eticheteaz
vrfurile n ordine topologic invers.
9.7.1
Jocul nim
Vom ilustra aceste idei printr-o variant a jocului nim. Iniial, pe mas se afl cel
puin dou bee de chibrit. Primul juctor ridic cel puin un b, lsnd pe mas
cel puin un b. n continuare, pe rnd, fiecare juctor ridic cel puin un b i
cel mult de dou ori numrul de bee ridicate de ctre partenerul de joc la mutarea
anterioar. Ctig juctorul care ridic ultimul b. Nu exist remize.
O poziie n acest joc este specificat att de numrul de bee de pe tabl, ct i de
numrul maxim de bee care pot fi ridicate la urmtoarea mutare. Vrfurile
grafului asociat jocului sunt perechi <i, j>, 1 j i, indicnd c pot fi ridicate cel
mult j bee din cele i bee de pe mas. Din vrful <i, j> pleac j muchii ctre
vrfurile <i k, min(2k, i k)>, 1 k j. Vrful corespunztor poziiei iniiale
ntr-un joc cu n bee, n 2, este <n, n 1>. Toate vrfurile pentru care a dou
component este zero corespund unor poziii terminale, dar numai vrful <0, 0>
este interesant: vrfurile <i, 0>, pentru i > 0, sunt inaccesibile. n mod similar,
vrfurile <i, j>, cu j impar i j < i 1, sunt inaccesibile. Vrful <0, 0> corespunde
unei poziii de pierdere.
Seciunea 9.7
Grafuri i jocuri
<5,4>
<3,3>
<2,2>
<0,0>
245
<4,2>
<4,3>
<1,1>
<3,2>
<2,1>
246
Explorri n grafuri
Capitolul 9
Seciunea 9.7
Grafuri i jocuri
247
248
9.7.2
Explorri n grafuri
Capitolul 9
ahul este, desigur, un joc mult mai complex dect jocul nim. La prima vedere,
graful asociat ahului conine cicluri. Exist ns reglementri ale Federaiei
Internaionale de ah care previn intrarea ntr-un ciclu. De exemplu, se declar
remiz o partid dup 50 de mutri n care nu are loc nici o aciune ireversibil
(mutarea unui pion, sau eliminarea unei piese). Datorit acestor reguli, putem
considera c graful asociat ahului nu are cicluri.
Vom eticheta fiecare vrf ca poziie de ctig pentru Alb, poziie de ctig pentru
Negru, sau remiz. Odat construit, acest graf ne permite s jucm perfect ah,
adic s ctigm mereu, cnd este posibil, i s pierdem doar cnd este
inevitabil. Din nefericire (din fericire pentru juctorii de ah), acest graf conine
attea vrfuri, nct nu poate fi explorat complet nici cu cel mai puternic
calculator existent.
Deoarece o cutare complet n graful asociat jocului de ah este imposibil, nu
putem folosi tehnica programrii dinamice. Se impune atunci, n mod natural,
aplicarea unei tehnici recursive, care s modeleze raionamentul de sus n jos.
Aceast tehnic (numit minimax) este de tip euristic, i nu ne ofer certitudinea
ctigrii unei partide. Ideea de baz este urmtoarea: fiind ntr-o poziie
oarecare, se alege una din cele mai bune mutri posibile, explornd doar o parte a
grafului. Este de fapt o modelare a raionamentului unui juctor uman care
gndete doar cu un mic numr de mutri n avans.
Primul pas este s definim o funcie de evaluare static eval, care atribuie o
anumit valoare fiecrei poziii posibile. n mod ideal, eval(u) va crete atunci
cnd poziia u devine mai favorabil Albului. Aceast funcie trebuie s in cont
de mai muli factori: numrul i calitatea pieselor existente de ambele pri,
controlul centrului tablei, libertatea de micare etc. Trebuie s facem un
compromis ntre acurateea acestei funcii i timpul necesar calculrii ei. Cnd se
aplic unei poziii terminale, funcia de evaluare trebuie s returneze + dac a
ctigat Albul, dac a ctigat Negrul i 0 dac a fost remiz.
Dac funcia de evaluare static ar fi perfect, ar fi foarte uor s determinm care
este cea mai bun mutare dintr-o anumit poziie. S presupunem c este rndul
Albului s mute din poziia u. Cea mai bun mutare este cea care l duce n poziia
v, pentru care
eval(v) = max{eval(w) | w este succesor al lui u}
Aceast poziie se determin astfel:
val
for fiecare w succesor al lui u do
if eval(w) val then val eval(w)
vw
Seciunea 9.7
Grafuri i jocuri
249
Explorri n grafuri
250
Capitolul 9
cine mut:
regula:
Albul
Negrul
Albul
max
a
b
min
d
5
e
7
Negrul
f
i
6
g
j
2
k
1
l
3
max
...
min
9.8
Grafuri AND/OR
Seciunea 9.8
Grafuri AND/OR
251
C
E
D
F
if v este rezolvabil
then return true
else return false
for fiecare vrf w adiacent lui v do
if not sol(w) then return false
return true
for fiecare vrf w adiacent lui v do
if sol(w) then return true
return false
252
9.9
Explorri n grafuri
Capitolul 9
Exerciii
9.1
ntr-un arbore binar de cutare, care este modul de parcurgere a vrfurilor
pentru a obine lista ordonat cresctor a cheilor?
9.2
Fiecrei expresii aritmetice n care apar numai operatori binari i se poate
ataa n mod natural un arbore binar. Dai exemple de parcurgere n inordine,
preordine i postordine a unui astfel de arbore. Se obin diferite moduri de scriere
a expresiilor aritmetice. Astfel, parcurgerea n postordine genereaz scrierea
postfixat menionat n Seciunea 3.1.1.
9.3
Fie un arbore binar reprezentat prin adrese, astfel nct vrful i (adic
vrful a crui adres este i) este memorat n trei locaii diferite coninnd:
VAL[i]
ST[i]
DR[i]
= valoarea vrfului
= adresa fiului stng
= adresa fiului drept
Seciunea 9.9
Exerciii
253
9.6
Revedei Exerciiul 8.8. Scriei un algoritm care gsete nchiderea
tranzitiv a unui graf orientat. Folosii parcurgerea n adncime sau lime.
Comparai algoritmul obinut cu algoritmul lui Warshall.
9.7
ntr-un arbore cu rdcin, elaborai un algoritm care verific pentru dou
vrfuri oarecare v i w, dac w este un descendent al lui v. (Pentru ca problema s
nu devin trivial, presupunem c vrfurile nu conin adresa tatlui).
Indicaie: Orice soluie direct necesit un timp n (n), n cazul cel mai
nefavorabil, unde n este numrul vrfurilor subarborelui cu rdcina n v.
Iat un mod indirect de rezolvare a problemei, care este n principiu avantajos
atunci cnd trebuie s verificm mai multe cazuri (perechi de vrfuri) pentru
acelai arbore. Fie preord[1 .. n] i postord[1 .. n] tablourile care conin ordinea
de parcurgere a vrfurilor n preordine, respectiv n postordine. Pentru oricare
dou vrfuri v i w avem:
preord[v] < preord[w]
Dup ce calculm valorile preord i postord ntr-un timp n (n), orice caz
particular se poate rezolva ntr-un timp n (1). Acest mod indirect de rezolvare
ilustreaz metoda precondiionrii.
9.8
Fie A arborele parial generat de parcurgerea n adncime a grafului
neorientat conex G. Demonstrai c, pentru orice muchie {v, w} din G, este
adevrat urmtoarea proprietate: v este un descendent sau un ascendent al lui w
n A.
Soluie: Dac muchiei {v, w} i corespunde o muchie n A, atunci proprietatea
este evident adevrat. Putem presupune deci c vrfurile v i w nu sunt adiacente
n A. Fr a pierde din generalitate, putem considera c v este vizitat naintea lui
w. Parcurgerea n adncime a grafului G nseamn, prin definiie, c explorarea
vrfului v nu se ncheie dect dup ce a fost vizitat i vrful w (innd cont de
existena muchiei {v, w}). Deci, v este un ascendent al lui w n A. 9.9
Dac
v
este un vrf al unui graf conex, demonstrai c v este un punct de articulare, dac
i numai dac exist dou vrfuri a i b diferite de v, astfel nct orice drum care
l conecteaz pe a cu b trece n mod necesar prin v.
254
Explorri n grafuri
Capitolul 9
9.10
Fie G un graf neorientat conex, dar nu i biconex. Elaborai un algoritm
pentru gsirea mulimii minime de muchii care s fie adugat lui G, astfel nct G
s devin biconex. Analizai algoritmul obinut.
9.11
Fie M[1 .. n, 1 .. n] o matrice boolean care reprezint un labirint n
forma unei table de ah. n general, pornind dintr-un punct dat, este permis s
mergei ctre punctele adiacente de pe aceeai linie sau coloan. Prin punctul (i, j)
se poate trece dac i numai dac M(i, j) este true. Elaborai un algoritm
backtracking care gsete un drum ntre colurile (1, 1) i (n, n), dac un astfel de
drum exist.
9.12
n algoritmul perm de generare a permutrilor, nlocuii utilizeaz(T) cu
write T i scriei rezultatul afiat de perm(1), pentru n = 3. Facei apoi acelai
lucru, presupunnd c tabloul T este iniializat cu [n, n 1, , 1].
9.13
(Problema submulimilor de sum dat). Fie mulimea de numere pozitive
W = {w 1 , , w n } i fie M un numr pozitiv. Elaborai un algoritm backtracking
care gsete toate submulimile lui W pentru care suma elementelor este M.
Indicaie: Fie W = {11, 13, 24, 7} i M = 31. Cel mai important lucru este cum
reprezentm vectorii care vor fi vrfurile arborelui generat. Iat dou moduri de
reprezentare pentru soluia (11, 13, 7):
i)
ii)
9.14
Un cal este plasat n poziia arbitrar (i, j), pe o tabl de ah de n n
2
ptrate. Concepei un algoritm backtracking care determin n 1 mutri ale
calului, astfel nct fiecare poziie de pe tabl este vizitat exact o dat
(presupunnd c o astfel de secven de mutri exist).
9.15
Gsii un algoritm backtracking capabil s transforme un ntreg n ntr-un
ntreg m, aplicnd ct mai puine transformri de forma f(i) = 3i i g(i) = i/2. De
exemplu, 15 poate fi transformat n 4 folosind patru transformri: 4 = gfgg(15).
Cum se comport algoritmul dac este imposibil de transformat astfel n n m ?
Seciunea 9.9
Exerciii
255
9.16
Modificai algoritmul rec pentru jocul nim, astfel nct s returneze un
ntreg k:
i)
ii)
9.17
Jocul lui Grundy seamn foarte mult cu jocul nim. Iniial, pe mas se afl
o singur grmad de n bee. Cei doi juctori au alternativ dreptul la o mutare. O
mutare const din mprirea uneia din grmezile existente n dou grmezi de
mrimi diferite (dac acest lucru nu este posibil, adic dac toate grmezile
constau din unul sau dou bee, juctorul pierde partida). Ca i la nim, remiza este
exclus. Gsii un algoritm care s determine dac o poziie este de ctig sau de
pierdere.
9.18
Tehnica minimax modeleaz eficient, dar n acelai timp i simplist,
comportamentul unui juctor uman. Una din presupunerile noastre a fost c nici
unul dintre juctori nu greete. n ce msur rmne valabil aceast tehnic dac
admitem c: i) juctorii pot s greeasc, ii) fiecare juctor nu exclude
posibilitatea ca partenerul s fac greeli.
9.19
Dac graful obinut prin reducerea unei probleme are i vrfuri care nu
sunt de tip AND sau de tip OR, artai c prin adugarea unor vrfuri fictive
putem transforma acest graf ntr-un graf AND/OR.
9.20
Modificai algoritmul sol pentru a-l putea aplica grafurilor AND/OR
oarecare.
Implementarea este preluat din R. Sethi, Programming Languages. Concepts and Constructs,
Seciunea 6.7.
255
256
Capitolul 10
contor (2)
contor (3)
contor (4)
ciur
sita (2)
sita (2)
ciur
sita (3)
ciur
Acest element este un prototip, deoarece lista conine trei tipuri diferite de
elemente, difereniate prin funcia de cernere:
Seciunea 10.1
257
Clasa contor este definit complet. Primul element generat (cernut) este stabilit
prin v, parametrul constructorului. Pentru clasa sita, funcia de cernere este mult
mai selectiv. Ea solicit de la surs valori, pn cnd primete o valoare care nu
este multiplu al propriei valori.
int sita::cerne( ) {
while ( 1 ) {
int n = sursa->cerne( );
if ( n % factor ) return n;
}
}
258
Capitolul 10
int ciur::cerne( ) {
int n = sursa->cerne( );
sursa = new sita( sursa, n );
return n;
}
Se observ c noua sit este creat i inserat n ciur printr-o singur instruciune:
sursa = new sita( sursa, n );
al crei efect poate fi exprimat astfel: sursa nodului ciur este o nou sita (cu
valoarea n) a crei surs va fi sursa actual a ciurului. O astfel de operaie de
inserare este una dintre cele mai uzuale n lucrul cu liste.
Prin programul urmtor, ciurul descris poate fi pus n funciune pentru
determinarea numerelor prime mai mici dect o anumit valoare.
#include <iostream.h>
#include <stdlib.h>
#include <new.h>
// definitiile claselor element, contor, ciur, sita
void no_mem( ) { cerr << "no_mem"; exit( 1 ); }
int main( ) {
_new_handler = no_mem;
int max;
cout << "max ... "; cin >> max;
ciur s( &contor( 2 ) );
int prim;
do {
prim = s.cerne( );
cout << prim << ' ';
} while ( prim < max );
cout << '\n';
return 0;
}
nainte de a introduce valori max prea mari, este bine s v asigurai c stiva
programului este suficient de mare i c avei suficient de mult memorie liber
pentru a fi alocat dinamic.
Folosind cunotinele expuse pn acum, aceast ciudat implementare a
algoritmului lui Eratostene nu are cum s funcioneze. Iat cel puin dou motive:
Seciunea 10.1
259
Prin derivarea public, tipurile ciur, sita i contor sunt subtipuri ale tipului de
baz element. Conversia de la un subtip (tip derivat public) la tipul de baz este
o conversie sigur, bine definit. Membrii tipului de baz vor fi totdeauna corect
iniializai cu valorile membrilor respectivi din subtip, iar valorile membrilor din
subtip, care nu se regsesc n tipul de baz, se vor pierde. Din aceleai motive,
conversia subtip-tip de baz se extinde i asupra pointerilor sau referinelor.
Altfel spus, n orice situaie, un obiect, un pointer sau o referin la un obiect
dintr-un tip de baz, poate fi nlocuit cu un obiect, un pointer sau o referin la un
obiect dintr-un tip derivat public.
Declaraia virtual din funcia cerne() permite implementarea legturilor
dinamice. Prin redefinirea funciei cerne() n fiecare din subtipurile derivate din
element, se permite invocarea difereniat a funciilor cerne() printr-o sintax
unic:
sursa->cerne( )
Dac sursa este de tip pointer la element, atunci, dup cum am precizat mai sus,
oricnd este posibil ca obiectul sursa s fie dintr-un tip derivat din element.
Funcia cerne() fiind virtual n tipul de baz, funcia efectiv invocat nu va fi
cea din tipul de baz, ci cea din tipul actual al obiectului invocator. Acest
mecanism implic stabilirea unei legturi dinamice, n timpul execuiei
programului, ntre tipul actual al obiectului invocator i funcia virtual.
Datorit faptului c definiia din clasa de baz a funciei cerne(), practic nu are
importan, este posibil s o lsm nedefinit:
virtual int cerne( ) = 0;
260
Capitolul 10
Consecina acestei aciuni este c nu mai putem defini obiecte de tip element,
clasa putnd servi doar ca baz pentru derivarea unor subtipuri. Astfel de funcii
se numesc virtuale pure, iar clasele respective sunt numite clase abstracte.
Ideea acestei structuri este sugerat n A. V. Aho, J. E. Hopcroft i J. D. Ullman, The Design and
Analysis of Computer Algorithms.
Seciunea 10.2
50
70
40
261
4
sb
10.2.1 Structura
ntreaga construcie se bazeaz pe o idee similar cu mai sus menionata list a
elementelor modificate. Este o idee ct se poate de natural, completat cu o
ingenioas modalitate de evitare a parcurgerii secveniale a listei.
Tabloului iniializat virtual a i se asociaz un tablou b, n care sunt memorate
poziiile elementelor modificate. De exemplu, dac s-au modificat elementele
a[3], a[0] i a[4], atunci primele trei poziii din b au valorile 3, 0 i 4. Se
observa c b este o list implementat secvenial, sub forma unui tablou.
Dac nici o poziie din b nu a fost ocupat, atunci orice element din a are valoarea
implicit (stabilit apriori). Dac b este complet ocupat, atunci toate elementele
din a au fost modificate, deci valorile lor pot fi obinute direct din locaiile
corespunztoare din a. Pentru evidena locaiilor ocupate din lista b (i implicit a
celor libere), vom folosi variabila sb, variabil iniializat cu -1.
Cum procedm n cazul general, cnd doar o parte din elemente au fost
modificate? Nu vom parcurge locaiile ocupate din b, ci va trebui s decidem n
timp constant dac un anumit element, de exemplu a[1], a fost sau nu a fost
modificat. Pentru a atinge aceast performan, vom completa structura cu un
alt tablou, tabloul p, n care sunt memorate poziiile din b ale fiecrui element
modificat (Figura 10.2).
Tabloul p, tablou paralel cu tabloul a, se iniializeaz pe msur ce elementele din
a sunt modificate. n exemplul nostru, p[3] este 0 deoarece elementul a[3] a fost
modificat primul, ocupnd astfel prima poziie n b. Apoi, p[0] este 1 pentru c
a[0] este al doilea element modificat, iar p[4] are valoarea 2 deoarece al treilea
element modificat a fost a[4]. Valoarea lui a[1], valoare nemodificat, se obine
prin intermediul tablourilor p i b astfel:
262
Capitolul 10
Dac p[1] nu este o poziie valid n b, adic 0 > p[1] sau p[1] > sb, atunci
a[1] nu a fost modificat i are valoarea implicit.
Dac p[1] este o poziie valid n b, atunci, pentru ca a[1] s nu aib valoarea
implicit, b[p[1]] trebuie s fie 1. Deoarece a[1] nu a fost modificat, nici o
poziie ocupat din b nu are ns valoarea 1. Deci, a[1] are valoarea implicit.
S vedem acum ce se ntmpl pentru un element deja modificat, cum este a[0].
Valoarea lui p[0] corespunde unei poziii ocupate din b, iar b[p[0]] este 0, deci
a[0] nu are valoarea implicit.
Seciunea 10.2
private:
T vi;
263
// valoarea implicita
264
Capitolul 10
template<class T>
tablouVI<T>& tablouVI<T>::operator =( T v ) {
vi = v; sb = -1;
return *this;
}
virtual
prin
intermediul
template<class T>
tablouVI<T>::tablouVI( int n, T v ):
tablou<T>( n ), vi( v ), p( n ), b( n ), sb( -1 ) {
}
Operaiile de iniializare a obiectelor prin constructori constituie una din cele mai
bine fundamentate pri ale limbajului C++. Pentru clasa tablou<T>, iniializarea
elementelor tabloului este ascuns n constructorul
template<class T>
tablou<T>::tablou( int dim ) {
a = 0; v = 0; d = 0;
// valori implicite
if ( dim > 0 )
// verificarea dimensiunii
a = new T [ d = dim ]; // alocarea memoriei
}
Seciunea 10.2
265
care invoc destructorul clasei T (n caz c T are destructor) pentru fiecare obiect
alocat la adresa a. Efectele destructorului asupra obiectelor care nu au fost
niciodat iniializate sunt greu de prevzut. Rezult c prezena destructorului n
clasa T este chiar periculoas, spre deosebire de prezena constructorului care va
genera doar pierderea timpului constant de iniializare.
Continund analiza deficienelor clasei tablouIV<T>, ajungem la banala operaie
de atribuire a[ i ] = vi din operatorul de indexare. Dac tipul T are un operator
de atribuire, atunci acest operator consider obiectul invocator (cel din membrul
drept) deja iniializat i va ncerca s-l distrug n aceeai manier n care
procedeaz i destructorul. n cazul nostru, premisa este contrar: a[ i ] nu este
iniializat, deci nu ne trebuie o operaie de atribuire, ci una de iniializare a
obiectului din locaia a[ i ] cu valoarea vi. Iat un nou argument n favoarea
utilizrii unui tablou de adrese i nu a unui tablou de obiecte.
Fr a mai conta la nota acordat, s observm c operaiile de iniializare i de
atribuire ntre obiecte de tip tablouVI<T> sunt nu numai generatoare de surprize
(neplcute), ci i foarte ineficiente. Surprizele sunt datorate constructorilor i
destructorilor clasei T i au fost analizate mai sus. Ineficiena se datoreaz
faptului c nu este necesar parcurgerea n ntregime a tablourilor implicate n
transfer, ci doar parcurgerea elementelor purttoare de informaie (iniializate).
Cauza acestor probleme este operarea membru cu membru n clasa tablouVI<T>,
prin intermediul constructorului de copiere i al operatorului de atribuire din clasa
tablou<T>.
Concluzia este c tabloul iniializat virtual genereaz o mulime de probleme.
Aceasta, deoarece procesul de iniializare i cel opus, de distrugere, sunt tocmai
elementele imposibil de ocolit n semantica structurilor de tip clas din limbajul
C++. Implementarea prezentat, chiar dac este doar de nota ase, poate fi
266
Capitolul 10
Acest program este corect, dar valorile afiate nu sunt cele care ar trebui s fie.
Cauza este operatorul de indexare [] din tablou<T>, operator invocat n funcia
Seciunea 10.2
267
prin intermediul argumentului t. Noi dorim ca, atunci cnd t este de tip
tablouVI<T>, operatorul de indexare [] invocat s fie cel din clasa
tablouVI<T>. De fapt, ceea ce urmrim este o legare (selectare) dinamic, n
timpul rulrii programului, a operatorului [] de tipul actual al obiectului
invocator. Putem obine acest lucru declarnd virtual operatorul de indexare din
clasa tablou<T>:
template <class T>
class tablou {
// ..
public:
// ...
virtual T& operator []( int );
// ...
};
268
Capitolul 10
Seciunea 10.3
Exerciii
269
10.3 Exerciii
10.1
Dac toate elementele unui tablou iniializat virtual au fost modificate,
atunci testarea strii fiecrui element prin tablourile p i b este inutil. Mai mult,
spaiul alocat tablourilor p i b poate fi eliberat.
Modificai operatorul de indexare al clasei tablouVI<T> astfel nct s trateze i
situaia de mai sus.
10.2
Operatorul de indexare al clasei tablouVI<T> iniializeaz fiecare
element cu valoarea implicit, chiar la primul acces al elementului respectiv.
Procedeul este oarecum ineficient, deoarece memorarea valorii unui element are
sens doar dac aceast valoare este diferit de valoarea implicit.
Completai clasa tablouVI<T> astfel nct s fie memorate efectiv doar valorile
diferite de valoarea implicit.
10.3
Ceea ce difereniaz operatorul de indexare din clasa tablouVI<T> fa
de cel din clasa tablou<T> este, n cele din urm, verificarea indicelui:
n clasa tablou<T> este o vorba de o simpl ncadrare ntre 0 i d
n clasa tablouVI<T> este un algoritm care necesit verificarea corelaiilor
dintre tablourile p i b.
Implementai precedura virtual check( int ) pentru verificarea indicelui n
cele dou clase i modificai operatorul de indexare din clasa tablou<T> astfel
nct operatorul de indexare din clasa tablouVI<T> s nu mai fie necesar.
10.4
Implementai constructorul de copiere, operatorul de atribuire i funcia
de redimensionare pentru obiecte de tip tablouVI<T>.
Epilog
De la nmulirea a la russe pn la grafurile AND/OR am parcurs de fapt o mic
istorie a gndirii algoritmice. Am pornit de la regulile aritmetice din Antichitate i
am ajuns la modelarea raionamentului uman prin inteligena artificial. Acest
evoluie spectaculoas reflect, de fapt, evoluia noastr ca fiine raionale.
S-ar putea ca paii fcui s fi fost uneori prea mari. La aceasta a dus dorina
noastr de a acoperi o arie suficient de larg. Pe de alt parte, este i efectul
obiectului studiat: elegana acestor algoritmi impune o exprimare concis. Mai
mult, limbajul C este cunoscut ca un limbaj elegant, iar limbajul C++ accentuaz
aceast caracteristic. Interesant acest fenomen prin care limbajul ia forma
obiectului pe care l descrie. Cartea noastr este, n mod ideal, ea nsi un
algoritm, sau un program C++.
Este acum momentul s dezvluim obiectivul nostru secret: am urmrit ca, la un
anumit nivel, implementarea s fie ct mai apropiat de pseudo-cod. Detaliile
legate de programarea orientat pe obiect devin, n acest caz, neimportante,
utilizarea obiectelor fiind tot att de simpl ca invocarea unor funcii de
bibliotec. Pentru a ajunge la aceast simplitate este necesar ca cineva s
construiasc bine clasele respective. Cartea noastr reprezint un prim ghid pentru
acel cineva.
Nu putem ncheia dect amintind cuvintele lui Wiston Churchill referitoare la
btlia pentru Egipt:
Acesta nu este sfritul.
Nu este nici mcar nceputul.
Dar este, poate, sfritul
nceputului.
271
Bibliografie selectiv
Brassard, G., Bratley, P. Algorithmics Theory and Practice, Prentice-Hall,
Englewood Cliffs, 1988.
Cormen, T.H., Leiserson, C.E., Rivest, R.L. Introduction to Algorithms, The
MIT Press, Cambridge, Massachusetts, 1992 (eighth printing).
Ellis, M., Stroustrup, B. The
Addison-Wesley, Reading, 1991.
Graham, R.L., Knuth, D.E.,
Addison-Wesley, Reading, 1989.
Annotated
Patashnik,
O.
C++
Reference
Concrete
Manual,
Mathematics,
273