Sunteți pe pagina 1din 294

ALGORITMI

FUNDAMENTALI
O PERSPECTIV C++

ALGORITMI
FUNDAMENTALI

O PERSPECTIV C++

RZVAN ANDONIE ILIE GRBACEA
Editura Libris
Cluj-Napoca, 1995
Referent: Leon Livovschi
Coperta: Zoltn Albert
Copyright 1995 Editura Libris
Universitii 8/8, 3400 Cluj-Napoca

ISBN 973-96494-5-9
v
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

vii
n clipa cnd exprimm un lucru, reuim,
n mod bizar, s-l i depreciem.
Maeterlinck
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.
viii Principii de algoritmi i C++ Cuprins

spre disperarea ne-matematicienilor cu celebrul Fie ... , sau cu o definiie. Am
ncercat, pe de alt parte, s evitm situaia cnd totul este evident, sau se
poate demonstra. Fiecare capitol este conceput fluid, ca o mic poveste, cu
puine referine i note. Multe rezultate mai tehnice sunt obinute ca exerciii.
Algoritmii sunt prezentai ntr-un limbaj pseudo-cod compact, fr detalii inutile.
Am adugat la sfritul fiecrui capitol numeroase exerciii, multe din ele cu
soluii.
Presupunem c cititorul are la baz cel puin un curs introductiv n programare,
nefiindu-i strini termeni precum algoritm, recursivitate, funcie, procedur i
pseudo-cod. Exist mai multe modaliti de parcurgere a crii. n funcie de
interesul i pregtirea cititorului, acesta poate alege oricare din prile referitoare
la elaborarea, analiza, sau implementarea algoritmilor. Cu excepia prilor de
analiz a eficienei algoritmilor (unde sunt necesare elemente de matematici
superioare), cartea poate fi parcurs i de ctre un elev de liceu. Pentru
parcurgerea seciunilor de implementare, este recomandabil cunoaterea
limbajului C.
Cartea noastr se bazeaz pe cursurile pe care le inem, ncepnd cu 1991, la
Secia de electronic i calculatoare a Universitii Transilvania din Braov. S-a
dovedit util i experiena noastr de peste zece ani n dezvoltarea produselor
software. Colectivul de procesare a imaginilor din ITC Braov a fost un excelent
mediu n care am putut s ne dezvoltm profesional. Le mulumim pentru aceasta
celor care au fcut parte, alturi de noi, din acest grup: Sorin Cisma, tefan
Jozsa, Eugen Carai. Nu putem s nu ne amintim cu nostalgie de compilatorul C al
firmei DEC (pentru minicalculatoarele din seria PDP-11) pe care l-am
descoperit mpreun, cu zece ani n urm.
Ca de obicei n astfel de situaii, numrul celor care au contribuit ntr-un fel sau
altul la realizarea acestei cri este foarte mare, cuprinznd profesorii notri,
colegii de catedr, studenii pe care am testat cursurile, prietenii. Le mulumim
tuturor. De asemenea, apreciem rbdarea celor care ne-au suportat n cei peste doi
ani de elaborare a crii.
Sperm s citii aceast carte cu aceeai plcere cu care ea a fost scris.

Braov, ianuarie 1995
Rzvan Andonie
Ilie Grbacea
*


*
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
ix
Cuprins
1. PRELIMINARII 1
1.1 Ce este un algoritm? 1
1.2 Eficiena algoritmilor 5
1.3 Cazul mediu i cazul cel mai nefavorabil 6
1.4 Operaie elementar 8
1.5 De ce avem nevoie de algoritmi eficieni? 9
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
2. PROGRAMARE ORIENTAT PE OBIECT 14
2.1 Conceptul de obiect 14
2.2 Limbajul C++ ++ ++ ++ 15
2.2.1 Diferenele dintre limbajele C i C++ 16
2.2.2 Intrri/ieiri n limbajul C++ 20
2.3 Clase n limbajul C++ ++ ++ ++ 22
2.3.1 Tipul intErval n limbajul C 23
2.3.2 Tipul intErval n limbajul C++ 25
2.4 Exerciii 34
3. STRUCTURI ELEMENTARE DE DATE 37
3.1 Liste 37
3.1.1 Stive 38
3.1.2 Cozi 39
3.2 Grafuri 40
3.3 Arbori cu rdcin 42
3.4 Heap-uri 45
3.5 Structuri de mulimi disjuncte 49
3.6 Exerciii 53
x Principii de algoritmi i C++ Cuprins

4. TIPURI ABSTRACTE DE DATE 56
4.1 Tablouri 56
4.1.1 Alocarea dinamic a memoriei 57
4.1.2 Clasa tablou 60
4.1.3 Clasa parametric tablou<T> 63
4.2 Stive, cozi, heap-uri 68
4.2.1 Clasele stiva<T> i coada<T> 69
4.2.2 Clasa heap<T> 73
4.3 Clasa lista<E> 78
4.4 Exerciii 84
5. ANALIZA EFICIENEI ALGORITMILOR 89
5.1 Notaia asimptotic 89
5.1.1 O notaie pentru ordinul lui 89
5.1.2 Notaia asimptotic condiionat 91
5.2 Tehnici de analiz a algoritmilor 94
5.2.1 Sortarea prin selecie 94
5.2.2 Sortarea prin inserie 94
5.2.3 Heapsort 95
5.2.4 Turnurile din Hanoi 97
5.3 Analiza algoritmilor recursivi 98
5.3.1 Metoda iteraiei 98
5.3.2 Inducia constructiv 98
5.3.3 Recurene liniare omogene 99
5.3.4 Recurene liniare neomogene 101
5.3.5 Schimbarea variabilei 103
5.4 Exerciii 105
6. ALGORITMI GREEDY 113
6.1 Tehnica greedy 113
6.2 Minimizarea timpului mediu de ateptare 115
6.3 Interclasarea optim a irurilor ordonate 116
6.4 Implementarea arborilor de interclasare 119
6.5 Coduri Huffman 122
6.6 Arbori pariali de cost minim 124
6.6.1 Algoritmul lui Kruskal 125
6.6.2 Algoritmul lui Prim 128
6.7 Implementarea algoritmului lui Kruskal 130
Cuprins Principii de algoritmi i C++ xi

6.8 Cele mai scurte drumuri care pleac din acelai punct 134
6.9 Implementarea algoritmului lui Dijkstra 137
6.10 Euristica greedy 143
6.10.1 Colorarea unui graf 143
6.10.2 Problema comis-voiajorului 144
6.11 Exerciii 145
7. ALGORITMI DIVIDE ET IMPERA 149
7.1 Tehnica divide et impera 149
7.2 Cutarea binar 151
7.3 Mergesort (sortarea prin interclasare) 153
7.4 Mergesort n clasele tablou<T> i lista<E> 154
7.4.1 O soluie neinspirat 154
7.4.2 Tablouri sortabile i liste sortabile 159
7.5 Quicksort (sortarea rapid) 161
7.6 Selecia unui element dintr-un tablou 164
7.7 O problem de criptologie 169
7.8 nmulirea matricilor 172
7.9 nmulirea numerelor ntregi mari 174
7.10 Exerciii 177
8. ALGORITMI DE PROGRAMARE DINAMIC 185
8.1 Trei principii fundamentale ale programrii dinamice 185
8.2 O competiie 187
8.3 nmulirea nlnuit a matricilor 189
8.4 Tablouri multidimensionale 193
8.5 Determinarea celor mai scurte drumuri ntr-un graf 198
8.6 Arbori binari optimi de cutare 200
8.7 Arborii binari de cutare ca tip de dat 204
8.7.1 Arborele optim 206
8.7.2 Cutarea n arbore 211
8.7.3 Modificarea arborelui 215
8.8 Programarea dinamic comparat cu tehnica greedy 219
8.9 Exerciii 221
xii Principii de algoritmi i C++ Cuprins

9. EXPLORRI N GRAFURI 227
9.1 Parcurgerea arborilor 227
9.2 Operaii de parcurgere n clasa arbore<E> 229
9.3 Parcurgerea grafurilor n adncime 231
9.3.1 Puncte de articulare 233
9.3.2 Sortarea topologic 234
9.4 Parcurgerea grafurilor n lime 235
9.5 Salvarea i restaurarea arborilor binari de cutare 237
9.6 Backtracking 239
9.7 Grafuri i jocuri 243
9.7.1 Jocul nim 243
9.7.2 ahul i tehnica minimax 247
9.8 Grafuri AND/OR 249
9.9 Exerciii 251
10. DERIVARE PUBLIC, FUNCII VIRTUALE 255
10.1 Ciurul lui Eratostene 255
10.2 Tablouri iniializate virtual 260
10.2.1 Structura 261
10.2.2 Implementarea (o variant de nota ase) 262
10.2.3 tablouVI<T> ca subtip al tipului tablou<T> 266
10.3 Exerciii 269
EPILOG 271
BIBLIOGRAFIE SELECTIV 273
xiii
Lista de notaii
#T numrul de elemente din tabloul (sau mulimea) T
i .. j interval de ntregi: {k N | i k j}
m mod n restul mpririi ntregi a lui m la n
div mprirea ntreag
| x | mrimea cazului x
log logaritm ntr-o baz oarecare (n contextul notaiei
asimptotice)
lg logaritm n baza 2
ln logaritm natural
lg

logaritm iterat (vezi Seciunea 3.5)


n
k
|
\

| combinri de n luate cte k


R

mulimea numerelor reale nenegative


N
+
, R
+
mulimea numerelor naturale (reale) strict pozitive
B mulimea constantelor booleene {true, false}
(x) | [P(x)] exist un x astfel nct P(x)
(x) | [P(x)] pentru orice x astfel nct P(x)
x cel mai mare ntreg mai mic sau egal cu x
x( cel mai mic ntreg mai mare sau egal cu x
O, , notaie asimptotic (vezi Seciunea 5.1.1)
atribuire
(a, b) muchia unui graf orientat
{a, b} muchia unui graf neorientat



1
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.
2 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) Elaborarea algoritmilor. Actul de creare a unui algoritm este o art care nu
va putea fi niciodat pe deplin automatizat. Este n fond vorba de
mecanismul universal al creativitii umane, care produce noul printr-o
sintez extrem de complex de tipul:
tehnici de elaborare (reguli) + creativitate (intuiie) = soluie.
Un obiectiv major al acestei cri este de a prezenta diverse tehnici
fundamentale de elaborare a algoritmilor. Utiliznd aceste tehnici, acumulnd
i o anumit experien, vei fi capabili s concepei algoritmi eficieni.
ii) Exprimarea algoritmilor. Forma pe care o ia un algoritm ntr-un program
trebuie s fie clar i concis, ceea ce implic utilizarea unui anumit stil de
programare. Acest stil nu este n mod obligatoriu legat de un anumit limbaj de
programare, ci, mai curnd, de tipul limbajului i de modul de abordare.
Astfel, ncepnd cu anii 80, standardul unanim acceptat este cel de
programare structurat. n prezent, se impune standardul programrii
orientate pe obiect.
iii) Validarea algoritmilor. Un algoritm, dup elaborare, nu trebuie n mod
necesar s fie programat pentru a demonstra c funcioneaz corect n orice
situaie. El poate fi scris iniial ntr-o form precis oarecare. n aceast
form, algoritmul va fi validat, pentru a ne asigura c algoritmul este corect,
independent de limbajul n care va fi apoi programat.
iv) Analiza algoritmilor. Pentru a putea decide care dintre algoritmii ce rezolv
aceeai problem este mai bun, este nevoie s definim un criteriu de apreciere
a valorii unui algoritm. n general, acest criteriu se refer la timpul de calcul
i la memoria necesar unui algoritm. Vom analiza din acest punct de vedere
toi algoritmii prezentai.
Seciunea 1.1 Ce este un algoritm? 3

v) Testarea programelor. Aceasta const din dou faze: depanare (debugging) i
trasare (profiling). Depanarea este procesul executrii unui program pe date
de test i corectarea eventualelor erori. Dup cum afirma ns E. W. Dijkstra,
prin depanare putem evidenia prezena erorilor, dar nu i absena lor. O
demonstrare a faptului c un program este corect este mai valoroas dect o
mie de teste, deoarece garanteaz c programul va funciona corect n orice
situaie. Trasarea este procesul executrii unui program corect pe diferite
date de test, pentru a-i determina timpul de calcul i memoria necesar.
Rezultatele obinute pot fi apoi comparate cu analiza anterioar a
algoritmului.
Aceast enumerare servete fixrii cadrului general pentru problemele abordate n
carte: ne vom concentra pe domeniile i), ii) i iv).
Vom ncepe cu un exemplu de algoritm. Este vorba de o metod, cam ciudat la
prima vedere, de nmulire a dou numere. Se numete nmulirea a la russe.
Vom scrie denmulitul i nmulitorul (de exemplu 45 i 19) unul lng altul,
formnd sub fiecare cte o coloan, conform urmtoarei reguli: se mparte
numrul de sub denmulit la 2, ignornd fraciile, apoi se nmulete cu 2 numrul
de sub nmulitor. Se aplic regula, pn cnd numrul de sub denmulit este 1. n
final, adunm toate numerele din coloana nmulitorului care corespund, pe linie,
unor numere impare n coloana denmulitului. n cazul nostru, obinem:
19 + 76 + 152 + 608 = 855.
Cu toate c pare ciudat, aceasta este tehnica folosit de hardware-ul multor
calculatoare. Ea prezint avantajul c nu este necesar s se memoreze tabla de
nmulire. Totul se rezum la adunri i nmuliri/mpriri cu 2 (acestea din urm
fiind rezolvate printr-o simpl decalare).
Pentru a reprezenta algoritmul, vom utiliza un limbaj simplificat, numit
pseudo-cod, care este un compromis ntre precizia unui limbaj de programare i
uurina n exprimare a unui limbaj natural. Astfel, elementele eseniale ale
algoritmului nu vor fi ascunse de detalii de programare neimportante n aceast
faz. Dac suntei familiarizat cu un limbaj uzual de programare, nu vei avea nici
45 19 19
22 38
11 76 76
5 152 152
2 304
1 608 608
855
4 Preliminarii Capitolul 1

o dificultate n a nelege notaiile folosite i n a scrie programul respectiv.
Cunoatei atunci i diferena dintre o funcie i o procedur. n notaia pe care o
folosim, o funcie va returna uneori un tablou, o mulime, sau un mesaj. Vei
nelege c este vorba de o scriere mai compact i n funcie de context vei putea
alege implementarea convenabil. Vom conveni ca parametrii funciilor
(procedurilor) s fie transmii prin valoare, exceptnd tablourile, care vor fi
transmise prin adresa primului element. Notaia folosit pentru specificarea unui
parametru de tip tablou va fi diferit, de la caz la caz. Uneori vom scrie, de
exemplu:
procedure proc1(T)
atunci cnd tipul i dimensiunile tabloului T sunt neimportante, sau cnd acestea
sunt evidente din context. ntr-un astfel de caz, vom nota cu #T numrul de
elemente din tabloului T. Dac limitele sau tipul tabloului sunt importante, vom
scrie:
procedure proc2(T[1 .. n])
sau, mai general:
procedure proc3(T[a .. b])
n aceste cazuri, n, a i b vor fi considerai parametri formali.
De multe ori, vom atribui unor elemente ale unui tablou T valorile , nelegnd
prin acestea dou valori numerice extreme, astfel nct pentru oricare alt element
T[i] avem < T[i] < +.
Pentru simplitate, vom considera uneori c anumite variabile sunt globale, astfel
nct s le putem folosi n mod direct n proceduri.
Iat acum i primul nostru algoritm, cel al nmulirii a la russe:
Seciunea 1.1 Ce este un algoritm? 5

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
6 Preliminarii Capitolul 1

compact. Astfel, cnd vom vorbi despre sortare, | x | va fi numrul de elemente
de sortat. La un algoritm numeric, | x | poate fi chiar valoarea numeric a cazului
x.
Avantajul analizei teoretice este faptul c ea nu depinde de calculatorul folosit, de
limbajul de programare ales, sau de ndemnarea programatorului. Ea salveaz
timpul pierdut cu programarea i rularea unui algoritm care se dovedete n final
ineficient. Din motive practice, un algoritm nu poate fi testat pe calculator pentru
cazuri orict de mari. Analiza teoretic ne permite ns studiul eficienei
algoritmului pentru cazuri de orice mrime.
Este posibil s analizm un algoritm i printr-o metod hibrid. n acest caz,
forma funciei care descrie eficiena algoritmului este determinat teoretic, iar
valorile numerice ale parametrilor sunt apoi determinate empiric. Aceast metod
permite o predicie asupra comportrii algoritmului pentru cazuri foarte mari, care
nu pot fi testate. O extrapolare doar pe baza testelor empirice este foarte
imprecis.
Este natural s ntrebm ce unitate trebuie folosit pentru a exprima eficiena
teoretic a unui algoritm. Un rspuns la aceast problem este dat de principiul
invarianei, potrivit cruia dou implementri diferite ale aceluiai algoritm nu
difer n eficien cu mai mult de o constant multiplicativ. Adic, presupunnd
c avem dou implementri care necesit t
1
(n) i, respectiv, t
2
(n) secunde pentru a
rezolva un caz de mrime n, atunci exist ntotdeauna o constant pozitiv c,
astfel nct t
1
(n) ct
2
(n) pentru orice n suficient de mare. Acest principiu este
valabil indiferent de calculatorul (de construcie convenional) folosit, indiferent
de limbajul de programare ales i indiferent de ndemnarea programatorului
(presupunnd c acesta nu modific algoritmul!). Deci, schimbarea calculatorului
ne poate permite s rezolvm o problem de 100 de ori mai repede, dar numai
modificarea algoritmului ne poate aduce o mbuntire care s devin din ce n ce
mai marcant pe msur ce mrimea cazului soluionat crete.
Revenind la problema unitii de msur a eficienei teoretice a unui algoritm,
ajungem la concluzia c nici nu avem nevoie de o astfel de unitate: vom exprima
eficiena n limitele unei constante multiplicative. Vom spune c un algoritm
necesit timp n ordinul lui t, pentru o funcie t dat, dac exist o constant
pozitiv c i o implementare a algoritmului capabil s rezolve fiecare caz al
problemei ntr-un timp de cel mult ct(n) secunde, unde n este mrimea cazului
considerat. Utilizarea secundelor n aceast definiie este arbitrar, deoarece
trebuie s modificm doar constanta pentru a mrgini timpul la at(n) ore, sau bt(n)
microsecunde. Datorit principiului invarianei, orice alt implementare a
algoritmului va avea aceeai proprietate, cu toate c de la o implementare la alta
se poate modifica constanta multiplicativ. n Capitolul 5 vom reveni mai riguros
asupra acestui important concept, numit notaie asimptotic.
Seciunea 1.2 Eficiena algoritmilor 7

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
n
2
, n
3
, n
k
, respectiv c
n
, 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 Cazul mediu i cazul cel mai nefavorabil
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.
8 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
O operaie elementar este o operaie al crei timp de execuie poate fi mrginit
superior de o constant depinznd doar de particularitatea implementrii
(calculator, limbaj de programare etc). Deoarece ne intereseaz timpul de execuie
n limita unei constante multiplicative, vom considera doar numrul operaiilor
elementare executate ntr-un algoritm, nu i timpul exact de execuie al operaiilor
respective.
Urmtorul exemplu este testul lui Wilson de primalitate (teorema care st la baza
acestui test a fost formulat iniial de Leibniz n 1682, reluat de Wilson n 1770
i demonstrat imediat dup aceea de Lagrange):
Seciunea 1.4 Operaie elementar 9

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 De ce avem nevoie de algoritmi eficieni?
Performanele hardware-ului se dubleaz la aproximativ doi ani. Mai are sens
atunci s investim n obinerea unor algoritmi eficieni? Nu este oare mai simplu
s ateptm urmtoarea generaie de calculatoare?
10 Preliminarii Capitolul 1

S presupunem c pentru rezolvarea unei anumite probleme avem un algoritm
exponenial i un calculator pe care, pentru cazuri de mrime n, timpul de rulare
este de 10
4
2
n
secunde. Pentru n = 10, este nevoie de 1/10 secunde. Pentru
n = 20, sunt necesare aproape 2 minute. Pentru n = 30, o zi nu este de ajuns, iar
pentru n = 38, chiar i un an ar fi insuficient. Cumprm un calculator de 100 de
ori mai rapid, cu timpul de rulare de 10
6
2
n
secunde. Dar i acum, pentru
n = 45, este nevoie de mai mult de un an! n general, dac n cazul mainii vechi
ntr-un timp anumit se putea rezolva problema pentru cazul n, pe noul calculator,
n acest timp, se poate rezolva cazul n+7.
S presupunem acum c am gsit un algoritm cubic care rezolv, pe calculatorul
vechi, cazul de mrime n n 10
2
n
3
secunde. n Figura 1.1, putem urmri cum
evolueaz timpul de rulare n funcie de mrimea cazului. Pe durata unei zile,
rezolvm acum cazuri mai mari dect 200, iar n aproximativ un an am putea
rezolva chiar cazul n = 1500. Este mai profitabil s investim n noul algoritm
dect ntr-un nou hardware. Desigur, dac ne permitem s investim att n
software ct i n hardware, noul algoritm poate fi rulat i pe noua main. Curba
10
4
n
3
reprezint aceast din urm situaie.
Pentru cazuri de mrime mic, uneori este totui mai rentabil s investim ntr-o

Figura 1.1 Algoritmi sau hardware?
Seciunea 1.5 De ce avem nevoie de algoritmi eficieni? 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
Poate c v ntrebai dac este ntr-adevr posibil s accelerm att de spectaculos
un algoritm. Rspunsul este afirmativ i vom da cteva 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
Fie det( M ) determinantul matricii
M = (a
ij
)
i, j = 1, , n

i fie M
ij
submatricea de (n1) (n1) elemente, obinut din M prin tergerea
celei de-a i-a linii i celei de-a j-a coloane. Avem binecunoscuta definiie
recursiv
det( ) ( ) det( ) M a M
j
j j
j
n
=
+
=

1
1
1 1
1

Dac folosim aceast relaie pentru a evalua determinantul, obinem un algoritm
cu timp n ordinul lui n!, ceea ce este mai ru dect exponenial. O alt metod
clasic, eliminarea Gauss-Jordan, necesit timp cubic. Pentru o anumit
12 Preliminarii Capitolul 1

implementare s-a estimat c, n cazul unei matrici de 20 20 elemente, n timp ce
algoritmul Gauss-Jordan dureaz 1/20 secunde, algoritmul recursiv ar dura mai
mult de 10 milioane de ani!
Nu trebuie tras de aici concluzia c algoritmii recursivi sunt n mod necesar
neperformani. Cu ajutorul algoritmului recursiv al lui Strassen, pe care l vom
studia i noi n Seciunea 7.8, se poate calcula det( M ) ntr-un timp n ordinul lui
n
lg 7
, unde lg 7 2,81, deci mai eficient dect prin eliminarea Gauss-Jordan.
1.6.3 Cel mai mare divizor comun
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 i1 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 Numerele lui Fibonacci
irul lui Fibonacci este definit prin urmtoarea recuren:
Seciunea 1.6 Exemple 13

f f
f f f n
n n n
0 1
1 2
0 1
2
= =
= +


;
pentru

Acest celebru ir a fost descoperit n 1202 de ctre Leonardo Pisano (Leonardo
din Pisa), cunoscut sub numele de Leonardo Fibonacci. Cel de-al n-lea termen al
irului se poate obine direct din definiie:
function fib1(n)
if n < 2 then return n
else return fib1(n1) + fib1(n2)
Aceast metod este foarte ineficient, deoarece recalculeaz de mai multe ori
aceleai valori. Vom arta n Seciunea 5.3.1 c timpul este n ordinul lui
n
, unde
= (1+ 5 )/2 este seciunea de aur, deci este un timp exponenial.
Iat acum o alt metod, mai performant, care rezolv aceeai problem ntr-un
timp liniar.
function fib2(n)
i 1; j 0
for k 1 to n do j i + j
i j i
return j
Mai mult, exist i un algoritm cu timp n ordinul lui log n, algoritm pe care l
vom argumenta ns abia n Capitolul 7:
function fib3(n)
i 1; j 0; k 0; h 1
while n > 0 do
if n este impar then t jh
j ih+jk+t
i ik+t
t h
2

h 2kh+t
k k
2
+t
n n div 2
return j
V recomandm s comparai aceti trei algoritmi, pe calculator, pentru diferite
valori ale lui n.
14 Preliminarii Capitolul 1

1.7 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 n algoritmul Euclid, este important ca n m ?

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 Fie matricea M =
|
\

|
0 1
1 1
. Calculai produsul vectorului ( f
n1
, f
n
) cu
matricea M
m
, unde f
n1
i f
n
sunt doi termeni consecutivi oarecare ai irului lui
Fibonacci.

*
Aceast surprinztoare proprietate a fost descoperit n 1876 de Lucas.
14
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
Activitatea de programare a calculatoarelor a aprut la sfritul anilor 40.
Primele programe au fost scrise n limbaj main i de aceea depindeau n
ntregime de arhitectura calculatorului pentru care erau concepute. Tehnicile de
programare au evoluat apoi n mod natural spre o tot mai net separare ntre
conceptele manipulate de programe i reprezentrile acestor concepte n
calculator.
n faa complexitii crescnde a problemelor care se cereau soluionate,
structurarea programelor a devenit indispensabil. coala de programare Algol a
propus la nceputul anilor 60 o abordare devenit ntre timp clasic. Conform
celebrei ecuaii a lui Niklaus Wirth:
algoritmi + structuri de date = programe
un program este format din dou pri total separate: un ansamblu de proceduri i
un ansamblu de date asupra crora acioneaz procedurile. Procedurile sunt privite
ca i cutii negre, fiecare avnd de rezolvat o anumit sarcin (de fcut anumite
prelucrri). Aceast modalitate de programare se numete programare dirijat de
prelucrri. Evoluia calculatoarelor i a problemelor de programare a fcut ca n
aproximativ zece ani programarea dirijat de prelucrri s devin ineficient.
Astfel, chiar dac un limbaj ca Pascal-ul permite o bun structurare a programului
n proceduri, este posibil ca o schimbare relativ minor n structura datelor s
provoace o dezorganizare major a procedurilor.
Seciunea 2.1 Conceptul de obiect 15

Inconvenientele programrii dirijate de prelucrri sunt eliminate prin
ncapsularea datelor i a procedurilor care le manipuleaz ntr-o singur entitate
numit obiect. Lumea exterioar obiectului are acces la datele sau procedurile lui
doar prin intermediul unor operaii care constituie interfaa obiectului.
Programatorul nu este obligat s cunoasc reprezentarea fizic a datelor i
procedurilor utilizate, motiv pentru care poate trata obiectul ca pe o cutie neagr
cu un comportament bine precizat. Aceast caracteristic permite realizarea unor
tipuri abstracte de date. Este vorba de obiecte nzestrate cu o interfa prin care
se specific interaciunile cu exteriorul, singura modalitate de a comunica cu un
astfel de obiect fiind invocarea interfeei sale. n terminologia specific
programrii orientate pe obiect, procedurile care formeaz interfaa unui obiect se
numesc metode. Obiectul este singurul responsabil de maniera n care se
efectueaz operaiile asupra lui. Apelul unei metode este doar o cerere, un mesaj
al apelantului care solicit executarea unei anumite aciuni. Obiectul poate refuza
s o execute, sau, la fel de bine, o poate transmite unui alt obiect. n acest
context, programarea devine dirijat de date, i nu de prelucrrile care trebuie
realizate.
Utilizarea consecvent a obiectelor confer programrii urmtoarele caliti:
Abstractizarea datelor. Nu este nevoie de a cunoate implementarea i
reprezentarea intern a unui obiect pentru a-i adresa mesaje. Obiectul decide
singur maniera de execuie a operaiei cerute n functie de implementarea
fizic. Este posibil suprancrcarea metodelor, n sensul c la aceleai mesaje,
obiecte diferite rspund n mod diferit. De exemplu, este foarte comod de a
desemna printr-un simbol unic, +, adunarea ntregilor, concatenarea irurilor
de caractere, reuniunea mulimilor etc.
Modularitate. Structura programului este determinat n mare msur de
obiectele utilizate. Schimbarea definiiilor unor obiecte se poate face cu un
minim de implicaii asupra celorlalte obiecte utilizate n program.
Flexibilitate. Un obiect este definit prin comportamentul su graie existenei
unei interfee explicite. El poate fi foarte uor introdus ntr-o bibliotec pentru
a fi utilizat ca atare, sau pentru a construi noi tipuri prin motenire, adic prin
specializare i compunere cu obiecte existente.
Claritate. ncapsularea, posibilitatea de suprancrcare i modularitatea
ntresc claritatea programelor. Detaliile de implementare sunt izolate de
lumea exterioar, numele metodelor pot fi alese ct mai natural posibil, iar
interfeele specific precis i detaliat modul de utilizare al obiectului.
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 Programare orientat pe obiect 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++.
2.2.1 Diferenele dintre limbajele C i C+ ++ ++ ++ +
Limbajul C, foarte lejer n privina verificrii tipurilor de date, las
programatorului o libertate deplin. Aceast libertate este o surs permanent de
erori i de efecte colaterale foarte dificil de depanat. Limbajul C++ a introdus o
verificare foarte strict a tipurilor de date. n particular, apelul oricarei funcii
trebuie precedat de declararea funciei respective. Pe baza declaraiilor, prin care
se specific numrul i tipul parametrilor formali, parametrii efectivi poat fi
verificai n momentul compilrii apelului. n cazul unor nepotriviri de tipuri,
compilatorul ncearc realizarea corespondenei (matching) prin invocarea unor
conversii, semnalnd eroare doar dac nu gsete nici o posibilitate.

float maxim( float, float );
float x = maxim( 3, 2.5 );


*
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;
}

Specificarea inline este doar orientativ i indic compilatorului c este
preferabil de a nlocui fiecare apel cu corpul funciei apelate. Expandarea unei
funcii inline nu este o simpl substituie de text n progamul surs, deoarece se
realizeaz prin pstrarea semanticii apelului, deci inclusiv a verificrii
corespondenei tipurilor parametrilor efectivi.
Mecanismul de verificare a tipului lucreaz ntr-un mod foarte flexibil, permind
att existena funciilor cu un numr variabil de argumente, ct i a celor
suprancrcate. Suprancrcarea permite existena mai multor funcii cu acelai
nume, dar cu paremetri diferii. Eliminarea ambiguitii care apare n momentul
apelului se rezolv pe baza numrului i tipului parametrilor efectivi. Iat, de
exemplu, o alt funcie maxim():

inline int maxim( int x, int y ) {
putchar( 'i' ); 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 Programare orientat pe obiect 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 ] );
}

n legtur cu acest exemplu, s mai notm i faptul c instruciunea for permite
chiar definirea unor variabile (variabila i n cazul nostru). Variabilele definite n
instruciunea for pot fi utilizate la nivelul blocului acestei instruciuni i dup
terminarea executrii ei.
Dei transmiterea parametrilor n limbajul C se face numai prin valoare, limbajul
C++ autorizeaz n egal msur i transmiterea prin referin. Referinele,
indicate prin caracterul &, permit accesarea n scriere a parametrilor efectivi, fr
transmiterea lor prin adrese. Iat un exemplu n care o procedur interschimb
(swap) valorile argumentelor.

void swap( float& a, float& b ) {
float tmp = a; a = b; b = tmp;
}

Referinele evit duplicarea provocat de transmiterea parametrilor prin valoare i
sunt utile mai ales n cazul transmiterii unor structuri. De exemplu, presupunnd
existena unei structuri de tip struct punct,

struct punct {
float x; /* coordonatele unui */
float y; /* punct din plan */
};

urmtoarea funcie transform un punct n simetricul lui fa de cea de a doua
bisectoare.

void sim2( struct punct& p ) {
swap( p.x, p.y ); // p.x si p.y se transmit prin
// referinta si nu prin valoare
p.x = -p.x; p.y = -p.y;
}

Parametrii de tip referin pot fi protejai de modificri accidentale prin
declararea lor const.
Seciunea 2.2 Limbajul C++ 19


void print( const struct punct& p ) {
// compilatorul interzice orice tentativa
// de a modifica variabila p
printf( "(%4.1f, %4.1f) ", p.x, p.y );
}

Caracterele // indic faptul c restul liniei curente este un comentariu. Pe lng
aceast modalitate nou de a introduce comentarii, limbajul C++ a preluat din
limbajul C i posibiliatea ncadrrii lor ntre /* i */.
Atributul const poate fi asociat nu numai parametrilor formali, ci i unor definiii
de variabile, a cror valoare este specificat n momentul compilrii. Aceste
variabile sunt variabile read-only (constante), deoarece nu mai pot fi modificate
ulterior. n limbajul C, constantele pot fi definite doar prin intermediul directivei
#define, care este o surs foarte puternic de erori. Astfel, n exemplul de mai
jos, constanta ntreag dim este o variabil propriu-zis accesibil doar n funcia
g(). Dac ar fi fost definit prin #define (vezi simbolul DIM utilizat n funcia
f() de mai sus) atunci orice identificator dim, care apare dup directiva de
definire i pn la sfritul fiierului surs, este nlocuit cu valoarea respectiv,
fr nici un fel de verificri sintactice.

void g( ) {
const int dim = 5;
struct punct buf[ dim ];

for ( int i = 0; i < dim; i++ ) {
buf[ i ].x = i;
buf[ i ].y = dim / 2. - i;

sim2( buf[ i ] );
print( buf[ i ] );
}
}

Pentru a obine un prim program n C++, nu avem dect s adugm obinuitul

#include <stdio.h>

precum i funcia main()

int main( ) {
puts( "\n main." );

puts( "\n f( )" ); f( );
puts( "\n g( )" ); g( );
20 Programare orientat pe obiect Capitolul 2


puts( "\n ---\n" );
return 0;
}

Rezultatele obinute n urma rulrii acestui program:

r
main.

f( )
iiiii 4 3 3 4
g( )
(-2.5, -0.0) (-1.5, -1.0) (-0.5, -2.0)
( 0.5, -3.0) ( 1.5, -4.0)
---

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().
2.2.2 Intrri/ieiri n limbajul C+ ++ ++ ++ +
Limbajul C++ permite definirea tipurilor abstracte de date prin intermediul
claselor. Clasele nu sunt altceva dect generalizri ale structurilor din limbajul C.
Ele conin date membre, adic variabile de tipuri predefinite sau definite de
utilizator prin intermediul altor clase, precum i funcii membre, reprezentnd
metodele clasei.
Cele mai utilizate clase C++ sunt cele prin care se realizeaz intrrile i ieirile.
Reamintim c n limbajul C, intrrile i ieirile se fac prin intermediul unor funcii
de bibliotec cum sunt scanf() i printf(), funcii care permit citirea sau
scrierea numai a datelor (variabilelor) de tipuri predefinite (char, int, float
etc.). Biblioteca standard asociat oricrui compilator C++, conine ca suport
pentru operaiile de intrare i ieire nu simple funcii, ci un set de clase adaptabile
chiar i unor tipuri noi, definite de utilizator. Aceast bibliotec este un exemplu
tipic pentru avantajele oferite de programarea orientat pe obiect. Pentru fixarea
ideilor, vom folosi un program care determin termenul de rang n al irului lui
Fibonacci prin algoritmul fib2 din Seciunea 1.6.4.

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 Programare orientat pe obiect Capitolul 2

comenzii de lansare n execuie a programului, argumente de forma >nume-
fisier-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 );

se va apela operatorul de inserare cu argumentul drept de tip char*. Acest
operator, ca i toi operatorii de inserare i extragere, returneaz operandul stng,
adic stream-ul. Astfel, invocarea a doua oar a operatorului de inserare are
sens, de acesat dat alegndu-se cel cu argumentul drept de tip long. n prezent,
biblioteca standard de intrare/ieire are n jur de 4000 de linii de cod, i conine
15 alternative pentru fiecare din operatorii << i >>. Programatorul poate
suprancrca n continuare aceti operatori pentru propriile tipuri.
2.3 Clase n limbajul C+ ++ ++ ++ +
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 Clase n limbajul C++ 23

2.3.1 Tipul intErval n limbajul C
Reprezentarea intern a tipului conine trei membri de tip ntreg: marginile
intervalului i valoarea propriu-zis. Le vom grupa ntr-o structur care, prin
intermediul instruciunii typedef, devine sinonim cu intErval.

typedef struct {
int min; /* marginea inferioara a intervalului */
int max; /* marginea superioara a intervalului */
int v; /* valoarea, min <= v, v < max */
} intErval;

Variabilele (obiectele) de tip intErval se definesc folosind sintaxa uzual din
limbajul C.

intErval numar = { 80, 32, 64 };
intErval indice, limita;

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;

Astfel de atribuiri se numesc atribuiri membru cu membru, deoarece sunt realizate
ntre datele membre corespunztoare celor dou obiecte implicate n atribuire.
O alt posibilitate este accesul direct la membri:

indice.min = 32; indice.max = 64;
indice.v = numar.v + 1;

Selectarea direct a membrilor ncalc proprietile fundamentale ale obiectelor.
Reamintim c un obiect este manipulat exclusiv prin interfaa sa, structura lui
intern fiind n general inaccesibil.
Comportamentul obiectelor este realizat printr-un set de metode implementate n
limbajul C ca funcii. Pentru intErval, acestea trebuie s permit n primul rnd
selectarea, att n scriere ct i n citire, a valorii propriu-zise date de membrul v.
Funcia de scriere atr() verific ncadrarea noii valori n domeniul admisibil, iar
24 Programare orientat pe obiect 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;
}

Funcia verDom() verific ncadrarea n domeniul admisibil:

int verDom( intErval n, int i ) {
if ( i < n.min || i >= n.max ) {
fputs( "\n\nintErval -- valoare exterioara.\n\n", stderr);
exit( 1 );
}
return i;
}

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 );
}

ci la altele, mult mai delicate, cum ar fi:
Seciunea 2.3 Clase n limbajul C++ 25

I
1
Evitarea unor iniializri eronate din punct de vedere semantic i interzicerea
utilizrii obiectelor neiniializate:

intErval numar = {80,32,64}; // obiect incorect initializat
intErval indice, limita; // obiecte neinitializate

I
2
Interzicerea modificrii necontrolate a datelor membre:

indice.v = numar.v + 1;

I
3
Sintaxa foarte ncrcat, diferit de sintaxa obinuit n manipularea tipurilor
ntregi predefinite.
n concluzie, aceast implementare, n loc s ne simplifice activitatea de
programare, mai mult a complicat-o. Cauza nu este ns conceperea greit a
tipului intErval, ci lipsa facilitilor de manipulare a obiectelor din limbajul C.
2.3.2 Tipul intErval n limbajul C+ ++ ++ ++ +
Clasele se obin prin completarea structurilor uzuale din limbajul C cu setul de
funcii necesar implementrii interfeei obiectului. n plus, pentru realizarea
izolrii reprezentrii interne de restul programului, fiecrui membru i se asociaz
nivelul de ncapsulare public sau private. Un membru public corespunde, din
punct de vedere al nivelului de accesibilitate, membrilor structurilor din limbajul
C. Membrii private sunt accesibili doar n domeniul clasei, adic n clasa
propriu-zis i n toate funciile membre. n clasa intErval, membrii publici sunt
doar funciile atr() i val(), iar membrii verDom(), min, max i v sunt privai.

class intErval {
public:
int atr( int );
int val( ) { return v; }

private:
int verDom( int );

int min, max;
int v;
};

Obiectele de tip intErval se definesc ca i n limbajul C.

26 Programare orientat pe obiect 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;

i pot fi iniializate (tot membru cu membru) cu un obiect de acelai tip:

intErval cod = numar;

Selectarea membrilor se face prin notaiile utilizate pentru structuri. De exemplu,
dup executarea instruciunii

indice.atr( numar.val( ) + 1 );

valoarea obiectului indice va fi valoarea obiectului numar, incrementat cu 1.
Aceast operaie poate fi descris i prin intruciunea

indice.v = numar.v + 1;

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 Clase n limbajul C++ 27

ncapsularea unui membru funcioneaz doar n exteriorul domeniului clasei.
Funciile membre fac parte din acest domeniu i au acces la toi membrii,
indiferent de nivelul lor de ncapsulare.
Specificarea atributului de funcie membr se face precednd numele funciei de
operatorul domeniu :: i de numele domeniului, care este chiar numele clasei.
Pentru asigurarea consistenei clasei, funciile membre definite n exterior trebuie
obligatoriu declarate n corpul clasei.

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;
}

int intErval::atr( int i ) {
return v = verDom( i );
// verDom(), fiind membru ca si v, se va invoca pentru
// obiectul invocator al functiei atr()
}

Din cele trei inconveniente menionate n finalul Seciunii 2.3.1 am rezolvat, pn
n acest moment, doar inconvenientul I
2
, cel care se refer la ncapsularea datelor.
n continuare ne vom ocupa de I
3
, adic de simplificarea sintaxei.
Limbajul C++ permite nu numai suprancrcarea funciilor, ci i a majoritii
operatorilor predefinii. n general, sunt posibile dou modaliti de
suprancrcare:
Ca funcii membre, caz n care operandul stng este implicit obiect invocator.
Ca funcii nemembre, dar cu condiia ca cel puin un argument (operand) s fie
de tip clas.
Pentru clasa intErval, ne intereseaz n primul rnd operatorul de atribuire
(implementat deocamdat prin funcia atr()) i un operator care s corespund
funciei val(). Dei pare surprinztor, funcia val() nu face altceva dect s
converteasc tipul intErval la tipul int. n consecin, vom implementa aceast
funcie ca operator de conversie la int. n noua sa form, clasa intErval arat
astfel:

28 Programare orientat pe obiect 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;
};

Revenind la obiectele indice i numar, putem scrie acum

indice = (int)numar + 1;

sau direct

indice = numar + 1;

conversia numar-ului la int fiind invocat automat de ctre compilator. Nu este
nimic miraculos n aceast invocare automat, deoarece operatorul + nu este
definit pentru argumente de tip intErval i int, dar este definit pentru int i
int. Altfel spus, expresia numar + 1 poate fi evaluat printr-o simpl conversie a
primului operand de la intErval la int.
O alt funcie util tipului intErval este cea de citire a valorii v, funcie
denumit n paragraful precedent cit(). Ne propunem s o nlocuim cu
operatorul de extragere >>, pentru a putea scrie direct cin >> numar.
Suprancrcarea operatorului >> ca funcie membr nu este posibil, deoarece
argumentul stng este obiectul invocator i atunci ar trebui s scriem n >> cin.
Operatorul de extragere necesar pentru citirea valorii obiectelor de tip intErval
se poate defini astfel:

istream& operator >>( istream& is, intErval& n ) {
int i;
if ( is >> i ) // se citeste valoarea
n = i; // se invoca operatorul de atribuire
return is;
}

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 Clase n limbajul C++ 29

n testul if ( is >> i ) se invoc de fapt operatorul de conversie de la
istream la int, rezultatul fiind valoarea logic true (valoare diferit de zero) sau
false (valoarea zero), dup cum operaia a decurs normal sau nu.
Returnarea istream-ului este o modalitate de a aplica operatorului >> sintaxa de
concatenare, sintax utilizat n expresii de forma i = j = 0. De exemplu,
obiectele numar i indice de tip intErval, pot fi citite printr-o singur
instruciune

cin >> numar >> indice;

De asemenea, remarcm i utilizarea absolut justificat a argumentelor de tip
referin. n lipsa lor, obiectul numar ar fi putut s fie modificat doar dac i-am fi
transmis adresa. n plus, utilizarea sintaxei de concatenare provoac, n lipsa
referinelor, multiplicarea argumentului de tip istream de dou ori pentru fiecare
apel: prima dat ca argument efectiv, iar a doua oar ca valoare returnat.
Clasa intErval a devenit o clas comod de utilizat, foarte bine ncapsulat i cu
un comportament similar ntregilor. ncapsularea este ns att de bun, nct,
practic, nu avem nici o modalitate de a iniializa limitele superioar i inferioar
ale domeniului admisibil. De fapt, am revenit la inconvenientul I
1
menionat n
finalul Seciunii 2.3.1. Problema iniializrii datelor membre n momentul
definirii obiectelor nu este specific doar clasei intErval. Pentru rezolvarea ei,
limbajul C++ ofer o categorie special de funcii membre, numite constructori.
Constructorii nu au tip, au numele identic cu numele clasei i sunt invocai
automat de ctre compilator, dup rezervarea spaiului pentru datele obiectului
definit.
Constructorul necesar clasei intErval are ca argumente limitele domeniului
admisibil. Transmiterea lor se poate face implicit, prin notaia

intErval numar( 80, 32 );

sau explicit, prin specificarea constructorului

intErval numar = intErval( 80, 32 );

Definiia acestui constructor este

30 Programare orientat pe obiect Capitolul 2

intErval::intErval( int sup, int inf ) {
if ( inf >= sup ) {
cerr << "\n\nintErval -- domeniu incorect specificat [ "
<< inf << ", " << (sup - 1) << " ].\n\n";
exit( 1 );
}
min = v = inf;
max = sup;
}

Datorit lipsei unui constructor fr argumente, compilatorul va interzice orice
declaraii n care nu se specific domeniul. De exemplu,

intErval indice;

este o definiie incomplet, semnalat la compilare. Mai mult, definiiile incorecte
semantic cum este

intErval limita( 32, 80 );

sunt i ele detectate, dar nu de ctre compilator, ci de ctre constructor. Acesta,
dup cum se observ, verific dac limita inferioar a domeniului este mai mic
dect cea superioar, semnalnd corespunztor domeniile incorect specificate.
n declaraiile funciilor, limbajul C++ permite specificarea valorilor implicite ale
argumentelor, valori utilizabile n situaiile n care nu se specific toi parametrii
efectivi. Aceast facilitate este util i n cazul constructorului clasei intErval.
Prin declaraia

intErval( int = 1, int = 0 );

definiia

intErval indice;

nu va mai fi respins, ci va provoca invocarea constructorului cu argumentele
implicite 1 i 0. Corespondena dintre argumentele actuale i cele formale se
realizeaz poziional, ceea ce nseamn c primul argument este asociat limitei
superioare, iar cel de-al doilea celei inferioare. Frecvent, limita inferioar are
valoarea implicit zero. Deci la transmiterea argumentelor constructorului, ne
putem limita doar la precizarea limitei superioare.
Constructorul apelabil fr nici un argument se numete constructor implicit.
Altfel spus, constructorul implicit este constructorul care, fie nu are argumente,
fie are toate argumentele implicite. Limbajul C++ nu impune prezena unui
constructor implicit n fiecare clas, dar sunt anumite situaii n care acest
constructor este absolut necesar.
Seciunea 2.3 Clase n limbajul C++ 31

Dup aceste ultime precizri, definiia clasei intErval este:

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;
};

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 Programare orientat pe obiect 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 Clase n limbajul C++ 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;
}

Adaptarea programului pentru determinarea termenilor irului lui Fibonacci
necesit doar includerea fiierului intErval.h, precum i schimbarea definiiei
rangului n din int n intErval.

#include <iostream.h>
#include "intErval.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 ... ";

intErval n = 47; cin >> n;
cout << " este " << fib2( n );
cout << '\n';

return 0;
}

Desigur c, la programul executabil, se va lega i fiierul rezultat n urma
compilrii definiiilor funciilor membre din clasa intErval.
34 Programare orientat pe obiect Capitolul 2

Neconcordana dintre argumentul formal de tip int din fib2() i argumentul
efectiv (actual) de tip intErval se rezolv, de ctre compilator, prin invocarea
operatorului de conversie de la intErval la int.
Programarea orientat pe obiect este deosebit de avantajoas n cazul aplicaiilor
mari, dezvoltate de echipe ntregi de programatori pe parcursul ctorva luni, sau
chiar ani. Aplicaia prezentat aici este mult prea mic pentru a putea fi folosit
ca un argument n favoarea acestei tehnici de programare. Cu toate acestea,
comparnd cele dou implementri ale clasei intErval (n limbajele C, respectiv
C++), sunt deja evidente dou avantaje ale programrii orientate pe obiect:
n primul rnd, este posibilitatea dezvoltrii unor tipuri noi, definite exclusiv
prin comportament i nu prin structur. Codul surs este mai compact, dar n
nici un caz mai rapid dect n situaia n care nu am fi folosit obiecte. S
reinem c programarea orientat pe obiect nu este o modalitate de a micora
timpul de execuie, ci de a spori eficiena activitii de programare.
n al doilea rnd, se remarc posibilitile de a suprancrca operatori, inclusiv
pe cei de conversie. Efectul este foarte spectaculos, deoarece utilizarea noilor
tipuri este la fel de comod ca i utilizarea tipurilor predefinite. Pentru tipul
intErval, aceste avantaje se concretizeaz n faptul c obiectele de tip
intErval se comport exact ca i cele de tip int, ncadrarea lor n limitele
domeniului admisibil fiind absolut garantat.
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

constructorul i destructorul astfel nct s iniializeze, respectiv s afieze, aceste
valori.

2.4 Implementai testul de primalitate al lui Wilson prezentat n Seciunea
1.4.

2.5 Scriei un program pentru calculul recursiv al coeficienilor binomiali
dup formula dat de triunghiul lui Pascal:
n
k
n
k
n
k |
\

| =

|
\

| +
|
\

1
1
1
1
pentru 0 < k < n
altfel

Analizai avantajele i dezavantajele acestui program n raport cu programul care
calculeaz coeficientul conform definiiei:
n
m
n
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]; // contorizeaza numarul de apeluri ale
// functiei C( int, int ) separat,
// pentru toate valorile argumentelor

long tr; // numarul total de apeluri ale
// functiei C( int, int )

36 Programare orientat pe obiect Capitolul 2

int C( int n, int m ) {
r[n][m]++; tr++;
return m == 0 || m == n?
1: C( n - 1, m - 1 ) + C( n - 1, m );
}

void main( ) {
int n, m;
for ( n = 0; n < N; n++ )
for ( m = 0; m < M; m++ ) r[n][m] = 0;
tr = 0;

cout << "\nCombinari de (maxim " << N << ") ... ";
cin >> n;
cout << " luate cate ... ";
cin >> m;
cout << "sunt " << C( n, m ) << '\n';

cout << "\n\nC( int, int ) a fost invocata de "
<< tr << " ori astfel:\n";
for ( int i = 1; i <= n; i++, cout << '\n' )
for ( int j = 0; j <= i; j++ ) {
cout.width( 4 );
cout << r[i][j] << ' ';
}
}

Rezultatele obinute n urma rulrii sunt urmtoarele:

Combinari de (maxim 16) ...12
luate cate ...7
sunt 792


C( int, int ) a fost invocata de 1583 ori astfel:
210 210
84 210 126
28 84 126 70
7 28 56 70 35
1 7 21 35 35 15
0 1 6 15 20 15 5
0 0 1 5 10 10 5 1
0 0 0 1 4 6 4 1 0
0 0 0 0 1 3 3 1 0 0
0 0 0 0 0 1 2 1 0 0 0
0 0 0 0 0 0 1 1 0 0 0 0
0 0 0 0 0 0 0 1 0 0 0 ...

Se observ c C(1,1) a fost invocat de 210 ori, iar C(2,2) de 126 de ori!
37
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
O list este o colecie de elemente de informaie (noduri) aranjate ntr-o anumit
ordine. Lungimea unei liste este numrul de noduri din list. Structura
corespunztoare de date trebuie s ne permit s determinm eficient care este
primul/ultimul nod n structur i care este predecesorul/succesorul (dac exist)
unui nod dat. Iat cum arat cea mai simpl list, lista liniar:
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
alpha beta gamma delta
capul
listei
coada
listei

38 Structuri elementare de date 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
O stiv (stack) este o list liniar cu proprietatea c operaiile de
inserare/extragere a nodurilor se fac n/din coada listei. Dac nodurile A, B, C, D
sunt inserate ntr-o stiv n aceast ordine, atunci primul nod care poate fi extras
este D. n mod echivalent, spunem c ultimul nod inserat va fi i primul ters. Din
acest motiv, stivele se mai numesc i liste LIFO (Last In First Out), sau liste
pushdown.
Cel mai natural mod de reprezentare pentru o stiv este implementarea secvenial
ntr-un tablou S[1 .. n], unde n este numrul maxim de noduri. Primul nod va fi
memorat n S[1], al doilea n S[2], iar ultimul n S[top], unde top este o variabil
care conine adresa (indicele) ultimului nod inserat. Iniial, cnd stiva este vid,
avem top = 0. Iat algoritmii de inserare i de tergere (extragere) a unui nod:
Seciunea 3.1 Liste 39

function push(x, S[1 .. n])
{adaug nodul x n stiv}
if top n then return stiv plin
top top+1
S[top] x
return succes
function pop(S[1 .. n])
{terge ultimul nod inserat din stiv i l returneaz}
if top 0 then return stiv vid
x S[top]
top top1
return x
Cei doi algoritmi necesit timp constant, deci nu depind de mrimea stivei.
Vom da un exemplu elementar de utilizare a unei stive. Dac avem de calculat
expresia aritmetic
5(((9+8)(46))+7)
putem folosi o stiv pentru a memora rezultatele intermediare. ntr-o scriere
simplificat, iat cum se poate calcula expresia de mai sus:
push(5); push(9); push(8); push(pop + pop); push(4); push(6);
push(pop pop); push(pop pop); push(7); push(pop + pop);
push(pop pop); write (pop);
Observm c, pentru a efectua o operaie aritmetic, trebuie ca operanzii s fie
deja n stiv atunci cnd ntlnim operatorul. Orice expresie aritmetic poate fi
transformat astfel nct s ndeplineasc aceast condiie. Prin aceast
transformare se obine binecunoscuta notaie postfixat (sau polonez invers),
care se bucur de o proprietate remarcabil: nu sunt necesare paranteze pentru a
indica ordinea operaiilor. Pentru exemplul de mai sus, notaia postfixat este:
5 9 8 + 4 6 7 +
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 Structuri elementare de date 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
este n ordinul lui n
2
. 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 Structuri elementare de date Capitolul 3

3.3 Arbori cu rdcin
Fie G un graf orientat. G este un arbore cu rdcina r, dac exist n G un vrf r
din care oricare alt vrf poate fi ajuns printr-un drum unic.
Definiia este valabil i pentru cazul unui graf neorientat, alegerea unei rdcini
fiind ns n acest caz arbitrar: orice arbore este un arbore cu rdcin, iar
rdcina poate fi fixat n oricare vrf al su. Aceasta, deoarece dintr-un vrf
oarecare se poate ajunge n oricare alt vrf printr-un drum unic.
Cnd nu va fi pericol de confuzie, vom folosi termenul arbore, n loc de
termenul corect arbore cu rdcin. Cel mai intuitiv este s reprezentm un
arbore cu rdcin, ca pe un arbore propriu-zis. n Figura 3.1, vom spune c beta
este tatl lui delta i fiul lui alpha, c beta i gamma sunt frai, c delta este un
descendent al lui alpha, iar alpha este un ascendent al lui delta. Un vrf terminal
este un vrf fr descendeni. Vrfurile care nu sunt terminale sunt neterminale.
De multe ori, vom considera c exist o ordonare a descendenilor aceluiai
printe: beta este situat la stnga lui gamma, adic beta este fratele mai vrstnic
al lui gamma.
Orice vrf al unui arbore cu rdcin este rdcina unui subarbore constnd din
vrful respectiv i toi descendenii si. O mulime de arbori disjunci formeaz o
pdure.
ntr-un arbore cu rdcin vom adopta urmtoarele notaii. Adncimea unui vrf
este lungimea drumului dintre rdcin i acest vrf; nlimea unui vrf este
lungimea celui mai lung drum dintre acest vrf i un vrf terminal; nlimea
delta omega
adncimea
0
1 1
0 2
gamma
alpha
zeta
beta
nivelul
2

Figura 3.1 Un arbore cu rdcin.
Seciunea 3.3 Arbori cu rdcin 43

arborelui este nlimea rdcinii; nivelul unui vrf este nlimea arborelui,
minus adncimea acestui vrf.
Reprezentarea unui arbore cu rdcin se poate face prin adrese, ca i n cazul
listelor nlnuite. Fiecare vrf va fi memorat n trei locaii diferite, reprezentnd
informaia propriu-zis a vrfului (valoarea vrfului), adresa celui mai vrstnic fiu
i adresa urmtorului frate. Pstrnd analogia cu listele nlnuite, dac se
cunoate de la nceput numrul maxim de vrfuri, atunci implementarea arborilor
cu rdcin se poate face prin tablouri paralele.
Dac fiecare vrf al unui arbore cu rdcin are pn la n fii, arborele respectiv
este n-ar. Un arbore binar poate fi reprezentat prin adrese, ca n Figura 3.2.
Observm c poziiile pe care le ocup cei doi fii ai unui vrf sunt semnificative:
lui a i lipsete fiul drept, iar b este fiul stng al lui a.
ntr-un arbore binar, numrul maxim de vrfuri de adncime k este 2
k
. Un arbore
binar de nlime i are cel mult 2
i+1
1 vrfuri, iar dac are exact 2
i+1
1 vrfuri, se
numete arbore plin. Vrfurile unui arbore plin se numeroteaz n ordinea
adncimii. Pentru aceeai adncime, numerotarea se face n arbore de la stnga la
dreapta (Figura 3.3).
Un arbore binar cu n vrfuri i de nlime i este complet, dac se obine din
arborele binar plin de nlime i, prin eliminarea, dac este cazul, a vrfurilor
numerotate cu n+1, n+2, , 2
i+1
1. Acest tip de arbore se poate reprezenta
secvenial folosind un tablou T, punnd vrfurile de adncime k, de la stnga la
dreapta, n poziiile T[2
k
], T[2
k+1
], , T[2
k+1
1] (cu posibila excepie a nivelului
0, care poate fi incomplet). De exemplu, Figura 3.4 exemplific cum poate fi
a
valoarea vrfului
adresa fiului stng
adresa fiului drept
b
d c

Figura 3.2 Reprezentarea prin adrese a unui arbore binar.
44 Structuri elementare de date Capitolul 3

reprezentat un arbore binar complet cu zece vrfuri, obinut din arborele plin din
Figura 3.3, prin eliminarea vrfurilor 11, 12, 13, 14 i 15. Tatl unui vrf
reprezentat n T[i], i > 1, se afl n T[i div 2]. Fiii unui vrf reprezentat n T[i] se
afl, dac exist, n T[2i] i T[2i+1].

Facem acum o scurt incursiune n matematica elementar, pentru a stabili cteva
rezultate de care vom avea nevoie n capitolele urmtoare. Pentru un numr real
oarecare x, definim
x = max{n n x, n este ntreg} i x = min{n n x, n este ntreg}
Putei demonstra cu uurin urmtoarele proprieti:
4
9 8
5
11 10
6
13 12
7
15 14
2 3
1

Figura 3.3 Numerotarea vrfurilor ntr-un arbore binar de nlime 3.
T [4]
T [9] T [8]
T [5]
T [10]
T [2]
T [6] T [7]
T [3]
T [1]

Figura 3.4 Un arbore binar complet.
Seciunea 3.3 Arbori cu rdcin 45

i) x1 < x x x < x+1
pentru orice x real
ii) n/2 + n/2 = n
pentru orice n ntreg
iii) n/a/b = n/ab i n/a/b = n/ab
pentru orice n, a, b ntregi (a, b 0)
iv) n/m = (nm+1)/m i n/m = (n+m1)/m
pentru orice numere ntregi pozitive n i m
n fine, artai c un arbore binar complet cu n vrfuri are nlimea lg n.
3.4 Heap-uri
Un heap (n traducere aproximativ, grmad ordonat) este un arbore binar
complet, cu urmtoarea proprietate, numit proprietate de heap: valoarea fiecrui
vrf este mai mare sau egal cu valoarea fiecrui fiu al su. Figura 3.5 prezint un
exemplu de heap.
Acelai heap poate fi reprezentat secvenial prin urmtorul tablou:
10 7 9 4 7 5 2 2 1 6
T[1] T[2] T[3] T[4] T[5] T[6] T[7] T[8] T[9] T[10]
Caracteristica de baz a acestei structuri de dat este c modificarea valorii unui
vrf se face foarte eficient, pstrndu-se proprietatea de heap. Dac valoarea unui
vrf crete, astfel nct depete valoarea tatlui, este suficient s schimbm ntre
ele aceste dou valori i s continum procedeul n mod ascendent, pn cnd
proprietatea de heap este restabilit. Vom spune c valoarea modificat a fost
filtrat ( percolated ) ctre noua sa poziie. Dac, dimpotriv, valoarea vrfului
4
1 2
7
6
5 2
7 9
10

Figura 3.5 Un heap.
46 Structuri elementare de date 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]}
k i
repeat
j k
{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]}
k i
repeat
j k
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

procedure delete-max(T[1 .. n])
{terge elementul cel mai mare din heap-ul T}
T[1] T[n]
sift-down(T[1 .. n1], 1)
procedure insert(T[1 .. n], v)
{insereaz un element cu valoarea v n heap-ul T
i restabilete proprietatea de heap}
T[n+1] v
percolate(T[1 .. n+1], n+1)
Rmne de vzut cum putem forma un heap pornind de la tabloul neordonat
T[1 .. n]. O soluie evident este de a porni cu un heap vid i s adugm
elementele unul cte unul.
procedure slow-make-heap(T[1 .. n])
{formeaz, n mod ineficient, din T un heap}
for i 2 to n do percolate(T[1 .. i], i)
Soluia nu este eficient i, n Capitolul 5, vom reveni asupra acestui lucru. Exist
din fericire un algoritm mai inteligent, care lucreaz n timp liniar, dup cum vom
demonstra tot n Capitolul 5.
procedure make-heap(T[1 .. n])
{formeaz din T un heap}
for i (n div 2) downto 1 do sift-down[T, i]
Ne reamintim c n T[n div 2] se afl tatl vrfului din T[n]. Pentru a nelege cum
lucreaz aceast procedur, s presupunem c pornim de la tabloul:
1 6 9 2 7 5 2 7 4 10
care corespunde arborelui:
Mai nti formm heap-uri din subarborii cu rdcina la nivelul 1, aplicnd
procedura sift-down rdcinilor respective:
2
4 7
7
10
5 2
6 9
1

48 Structuri elementare de date Capitolul 3

Dup acest pas, tabloul T devine:
1 6 9 7 10 5 2 2 4 7
Subarborii de la urmtorul nivel sunt apoi transformai i ei n heap-uri. Astfel,
subarborele
se transform succesiv n:
Subarborele de nivel 2 din dreapta este deja heap. Dup acest pas, tabloul T
devine:
1 10 9 7 7 5 2 2 4 6
Urmeaz apoi s repetm procedeul i pentru nivelul 3, obinnd n final heap-ul
din Figura 3.5.
Un min-heap este un heap n care proprietatea de heap este inversat: valoarea
fiecrui vrf este mai mic sau egal cu valoarea fiecrui fiu al su. Evident,
rdcina unui min-heap va conine n acest caz cel mai mic element al heap-ului.
n mod corespunztor, se modific i celelalte proceduri de manipulare a
heap-ului.
7
4 2
10
7
5 2

7
4 2
10
7
6

7
4 2
6
7
10

7
4 2
7
6
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 Structuri de mulimi disjuncte
S presupunem c avem N elemente, numerotate de la 1 la N. Numerele care
identific elementele pot fi, de exemplu, indici ntr-un tablou unde sunt memorate
numele elementelor. Fie o partiie a acestor N elemente, format din submulimi
dou cte dou disjuncte: S
1
, S
2
, . Ne intereseaz s rezolvm dou probleme:
i) Cum s obinem reuniunea a dou submulimi, S
i
S
j
.
ii) Cum s gsim submulimea care conine un element dat.
Avem nevoie de o structur de date care s permit rezolvarea eficient a acestor
probleme.
Deoarece submulimile sunt dou cte dou disjuncte, putem alege ca etichet
pentru o submulime oricare element al ei. Vom conveni pentru nceput ca
elementul minim al unei mulimi s fie eticheta mulimii respective. Astfel,
mulimea {3, 5, 2, 8} va fi numit mulimea 2.
Vom aloca tabloul set[1 .. N], n care fiecrei locaii set[i] i se atribuie eticheta
submulimii care conine elementul i. Avem atunci proprietatea: set[i] i, pentru
1 i N.
Presupunem c, iniial, fiecare element formeaz o submulime, adic set[i] = i,
pentru 1 i N. Problemele i) i ii) se pot rezolva prin urmtorii algoritmi:
50 Structuri elementare de date 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
starea iniial, un timp n ordinul lui n
2
.
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 3 2 1 3 4 3 3 4
set[1] set[2] set[10]
reprezint arborii:
care, la rndul lor, reprezint mulimile {1,5}, {2,4,7,10} i {3,6,8,9}. Pentru a
fuziona dou mulimi, trebuie acum s modificm doar o singur valoare n
tablou; pe de alt parte, este mai dificil s gsim mulimea creia i aparine un
element dat.
function find2(x)
{returneaz eticheta mulimii care l conine pe x}
i x
while set[i] i do i set[i]
return i
1
5
3
7
6 8
10
9 4
2

Seciunea 3.5 Structuri de mulimi disjuncte 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
pornind de la starea iniial, un timp tot n ordinul lui n
2
(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 Structuri elementare de date Capitolul 3

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}
r x
while set[r] r do r set[r]
{r este rdcina arborelui}
i x
while i r do
j set[i]
set[i] r
i j
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
lui n lg

N, unde lg

este definit astfel:


lg min{ | lg lg ... lg }

= N k N
k de ori
1 2 4 3 4
0
11
12
16
1
20
10 8
4 9
6
21
11 1 8 21 16 12
4 9 20 10
6
(a) (b)

Figura 3.6 Comprimarea drumului.
Seciunea 3.5 Structuri de mulimi disjuncte 53

Demonstrarea acestei afirmaii este laborioas i nu o vom prezenta aici. Funcia
lg

crete extrem de ncet: lg

N 5 pentru orice N 65536 i lg

N 6 pentru
orice N 2
65536
. Deoarece numrul atomilor universului observabil este estimat la
aproximativ 10
80
, ceea ce este mult mai puin dect 2
65536
, vom ntlni foarte rar o
valoare a lui N pentru care lg

N > 6.
De acum ncolo, atunci cnd vom aplica procedurile find3 i merge3 asupra unor
mulimi disjuncte de elemente, vom spune c folosim o structur de mulimi
disjuncte.
O important aplicaie practic a structurilor de mulimi disjuncte este verificarea
eficient a conexitii unui graf (Exerciiul 3.12).
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) G este conex i aciclic.
ii) G este aciclic i are n1 muchii.
iii) G este conex i are n1 muchii.
iv) G este aciclic i, adugndu-se o singur muchie ntre oricare dou vrfuri
neadiacente, se creaz exact un ciclu.
v) G este conex i, dac se suprim o muchie oarecare, nu mai este conex.
vi) Oricare dou vrfuri din G sunt unite printr-un drum unic.

3.3 Elaborai i implementai un algoritm de evaluare a expresiilor aritmetice
postfixate.

3.4 De ce procedura percolate este mai eficient dac admitem c un vrf
neterminal poate avea mai mult de doi fii?

3.5 Fie T[1 .. 12] un tablou, astfel nct T[i] = i, pentru i < 12. Determinai
starea tabloului dup fiecare din urmtoarele apeluri de procedur, aplicate
succesiv:
make-heap(T); alter-heap(T, 12, 10); alter-heap(T, 1, 6); alter-heap(T, 5, 6)
54 Structuri elementare de date 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
pentru cazul cel mai nefavorabil, este n ordinul lui n
2
. 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)

merge1(Nn+1, Nn); find1(N)
n aceast secven, merge1(Ni+1, Ni) necesit un timp n ordinul lui i. Timpul
total este n ordinul lui 1+2++n = n(n+1)/2, deci n ordinul lui n
2
. Simetric,
merge2 necesit un timp constant i cel mai nefavorabil caz l reprezint secvena:
merge2(N, N1); find2(N)
merge2(N1, N2); find2(N),

merge2(Nn+1, Nn); find2(N)
n care find2(i) necesit un timp n ordinul lui i etc.

3.8 De ce am presupus n procedura merge3 c a b?

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 Exerciii 55

ii) 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) Dac x este un element canonic, atunci informaia din set[x] este folosit doar
pentru a preciza c x este canonic.
ii) Dac elementul x nu este canonic, atunci informaia din H[x] nu este folosit.
innd cont de i) i ii), modificai procedurile find3 i merge3 astfel nct, n
locul tablourilor set i H, s folosii un singur tablou de N elemente.
Indicaie: Utilizai n noul tablou i valori negative.
56
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
n mod surprinztor, ncepem cu tabloul, structur fundamental, predefinit n
majoritatea limbajelor de programare. Necesitatea de a elabora o nou structur
de acest tip provine din urmtoarele inconveniente ale tablourilor predefinite,
inconveniente care nu sunt proprii numai limbajelor C i C++:
Numrul elementelor unui tablou trebuie s fie o expresie constant, fixat n
momentul compilrii.
Pe parcursul execuiei programului este imposibil ca un tablou s fie mrit sau
micorat dup necesiti.
Seciunea 4.1 Tablouri 57

Nu se verific ncadrarea n limitele admisibile a indicilor elementelor
tablourilor.
Tabloul i numrul elementelor lui sunt dou entiti distincte. Orice operaie
cu tablouri (atribuiri, transmiteri de parametri etc) impune specificarea
explicit a numrului de elemente ale fiecrui tablou.
4.1.1 Alocarea dinamic a memoriei
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 Tipuri abstracte de date Capitolul 4



// De asemenea, operatorul new poate fi folosit pentru
// alocarea unui singur element de un anumit tip T,
// precizand eventual si argumentele constructorului
// tipului respectiv.

// se aloca un intreg initializat cu 8
int *i = new int( 8 );

// se aloca un element de tip intErval
// cu domeniul admisibil -16, ..., 15
intErval *m = new intErval( 16, -16 );

// se aloca un numar real (float) initializat cu 32
float *f = new float( 32 );


// Zonele alocate pot fi eliberate oricand si in orice
// ordine, dar numai prin intermediul pointerului
// returnat de operatorul new.

delete [ ] pf;
delete [ ] pi;
delete i;
delete f;
delete [ ] pi_m;
delete m;

return 0;
}

Operatorul new iniializeaz memoria alocat prin intermediul constructorilor
tipului respectiv. n cazul alocrii unui singur element, se invoc constructorul
corespunztor argumentelor specificate, iar n cazul alocrii unui tablou de
elemente, operatorul new invoc constructorul implicit pentru fiecare din
elementele alocate. Operatorul delete, nainte de eliberarea spaiului alocat, va
invoca destructorul tipului respectiv. Dac zona alocat conine un tablou de
elemente i se dorete invocarea destructorului pentru fiecare element n parte,
operatorul delete va fi invocat astfel:

delete [ ] pointer;

De exemplu, rulnd programul

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;
}

constatm c, n alocarea zonei pentru cele patru elemente de tip X, constructorul
X() a fost invocat de patru ori, iar apoi, la eliberare, destructorul ~X() doar o
singur dat. n cazul zonei de dou elemente, att constructorul ct i
destructorul au fost invocai de cte dou ori. Pentru unele variante mai vechi de
compilatoare C++, este necesar s se specifice explicit numrul elementelor din
zona ce urmeaz a fi eliberat.
n alocarea dinamic, cea mai uzual eroare este generat de imposibilitatea
alocrii memoriei. Pe lng soluia banal, dar extrem de incomod, de testare a
valorii adresei returnate de operatorul new, limbajul C++ ofer i posibilitatea
invocrii, n caz de eroare, a unei funcii definite de utilizator. Rolul acesteia este
de a obine memorie, fie de la sistemul de operare, fie prin eliberarea unor zone
deja ocupate. Mai exact, atunci cnd operatorul new nu poate aloca spaiul
solicitat, el invoc funcia a crei adres este dat de variabila global
_new_handler i apoi ncearc din nou s aloce memorie. Variabila
_new_handler este de tip pointer la funcie de tip void fr nici un argument,
void (*_new_handler)(), valoarea ei implicit fiind 0.
Valoarea 0 a pointerului _new_handler marcheaz lipsa funciei de tratare a
erorii i, n aceast situaie, operatorul new va returna 0 ori de cte ori nu poate
aloca memoria necesar. Programatorul poate modifica valoarea acestui pointer,
fie direct:

_new_handler = no_mem;

unde no_mem este o funcie de tip void fr nici un argument,
60 Tipuri abstracte de date Capitolul 4


void no_mem( ) {
cerr << "\n\n no mem. \n\n";
exit( 1 );
}

fie prin intermediul funciei de bibliotec set_new_handler:

set_new_handler( no_mem );

Toate declaraiile necesare pentru utilizarea pointerului _new_handler se gsesc
n fiierul header new.h.
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& );
};

Definiiile funciilor membre sunt date n continuare.
Seciunea 4.1 Tablouri 61


tablou::tablou( int dim ) {
a = 0; d = 0; // valori implicite
if ( dim > 0 ) // verificarea dimensiunii
a = new int [ d = dim ]; // alocarea memoriei
}

tablou::tablou( const tablou& t ) {
// initializarea obiectului invocator cu t
init( t );
}

tablou& tablou::operator =( const tablou& t ) {
if ( this != &t ) { // este o atribuire inefectiva x = x?
delete a; // eliberarea memoriei alocate
init( t ); // initializarea cu t
}
return *this; // se returneaza obiectul invocator
}

void tablou::init( const tablou& t ) {
a = 0; d = 0; // valori implicite
if ( t.d > 0 ) { // verificarea dimensiunii
a = new int [ d = t.d ]; // alocarea si copierea elem.
memcpy( a, t.a, d * sizeof( int ) );
}
}

int& tablou::operator []( int i ) {
static int z; // "elementul" tablourilor de dimensiune zero
return d? a[ i ]: z;
}

Fr ndoial c cea mai spectaculoas definiie este cea a operatorului de
indexare []. Acesta permite att citirea unui element dintr-un tablou:

tablou x( n );
// ...
cout << x[ i ];

ct i modificarea valorii (scrierea) lui:

cin >> x[ i ];

Facilitile deosebite ale operatorului de indexare [] se datoreaz tipului valorii
returnate. Acest operator nu returneaz elementul i, ci o referin la elementul i,
referin care permite accesul att n scriere, ct i n citire a variabilei de la
adresa respectiv.
Clasa tablou permite utilizarea tablourilor n care nu exist nici un element.
Operatorul de indexare [] este cel mai afectat de aceast posibilitate, deoarece
62 Tipuri abstracte de date Capitolul 4

ntr-un tablou cu zero elemente va fi greu de gsit un element a crui referin s
fie returnat. O soluie posibil const n returnarea unui element fictiv, unic
pentru toate obiectele de tip tablou. n cazul nostru, acest element este variabila
local static int z, variabil alocat static, adic pe toat durata rulrii
programului.
O atenie deosebit merit i operatorul de atribuire =. Dup cum am precizat n
Seciunea 2.3, structurile pot fi atribuite ntre ele, membru cu membru. Pentru
clasa tablou, acest mod de funcionare a operatorului implicit de atribuire este
inacceptabil, deoarece genereaz referiri multiple la aceeai zon de memorie.
Iat un exemplu simplu de ceea ce nseamn referiri multiple la aceeai zon de
memorie.
Fie x i y dou obiecte de tip tablou. n urma atribuirii x = y prin operatorul
predefinit =, ambele obiecte folosesc aceeai zon de memorie pentru memorarea
elementelor. Dac unul dintre ele nceteaz s mai existe, atunci destructorul su
i va elibera zona alocat. n consecin, cellalt va lucra ntr-o zon de memorie
considerat liber, zon care poate fi alocat oricnd altui obiect. Prin definirea
unui nou operator de atribuire specific clasei tablou, obiectele din aceast clas
sunt atribuite corect, fiecare avnd propria zon de memorie n care sunt
memorate elementele.
O alt observaie relativ la operatorul de atribuire se refer la valoarea returnat.
Tipurile predefinite permit concatenarea operatorului de atribuire n expresii de
forma

i = j = k;
// unde i, j si k sunt variabile de orice tip predefinit

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

Operatorul de atribuire predefinit are asociativitate de dreapta (se evalueaz de la
dreapta la stnga) i aceast caracteristic rmne neschimbat la suprancrcare.
Altfel spus, iT = jT = kT nseamn de fapt iT = (jT = kT), sau
operator =( iT, operator =( jT, kT) ). Rezult c operatorul de atribuire
trebuie s returneze operandul stng, sau o referin la acesta. n cazul nostru,
operandul stng este chiar obiectul invocator. Cum n fiecare funcie membr este
implicit definit un pointer la obiectul invocator, pointer numit this (acesta),
operatorul de atribuire va returna o referin la obiectul invocator prin
instruciunea
Seciunea 4.1 Tablouri 63


return *this;

Astfel, sintaxa de concatenare poate fi folosit fr nici o restricie.
n definiia clasei tablou a aprut un nou constructor, constructorul de copiere

tablou( const tablou& )

Este un constructor a crui implementare seamn foarte mult cu cea a
operatorului de atribuire. Rolul su este de a iniializa obiecte de tip tablou cu
obiecte de acelai tip. O astfel de operaie, ilustrat n exemplul de mai jos, este
n mare msur similar unei copieri.

tablou x;
// ...
tablou y = x; // se invoca constructorul de copiere

n lipsa constructorului de copiere, iniializarea se face implicit, adic membru cu
membru. Consecinele negative care decurg de aici au fost discutate mai sus.
4.1.3 Clasa parametric tablou<T>
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 Tipuri abstracte de date Capitolul 4

Compilatorul nu genereaz nici un fel de cod pentru abloane, pn n momentul
n care sunt efectiv folosite. De aceea, abloanele se specific n fiiere header,
fiiere incluse n fiecare program surs C++ n care se utilizeaz clasele sau
funciile parametrice respective
*
. De exemplu, n funcia

void f( int ia, int ib, float fa ) {
int m1 = max( ia, ib );
float m2 = max( ia, fa );
}

se invoc funciile int max(int, int) i float max(float, float), funcii
generate automat, pe baza ablonului de mai sus
Conform specificaiilor din Ellis i Stroustrup, The Annotated C++ Reference
Manual, generarea abloanelor este un proces care nu implic nici un fel de
conversii. n consecin, linia

float m2 = max( ia, fa );

este eronat. Unele compilatoare nu semnaleaz aceast erorare, deoarece invoc
totui conversia lui ia din int n float. Atunci cnd compilatorul semnaleaz
eroarea, putem declara explicit funcia (vezi i Seciunea 10.2.3)

float max( float, float );

declaraie care nu mai necesit referirea la ablonul funciei max(). Aceast
declaraie este, n general, suficient pentru a genera funcia respectiv pe baza
ablonului.
Pn cnd limbajul C++ va deveni suficient de matur pentru a fi standardizat,
artificiile de programare de mai sus sunt deseori indispensabile pentru utilizarea
abloanelor.
Pentru abloanele de clase, lucrurile decurg aproximativ n acelai mod, adic
generarea unei anumite clase este declanat de definiiile ntlnite n program.
Pentru clasa parametric tablou<T> definiiile

*
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 );

provoac generarea clasei tablou<T> pentru tipurile float, int i unsigned
char. Fiierul header (tablou.h) al acestei clase este:

#ifndef __TABLOU_H
#define __TABLOU_H

#include <iostream.h>

template <class T>
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& );
T& operator []( int );

// returneaza numarul elementelor
size( ) { return d; }

// activarea/dezactivarea verificarii indicilor
void vOn ( ) { v = 1; }
void vOff( ) { v = 0; }

protected:
int d; // numarul elementelor (dimensiunea) tabloului
T *a; // adresa zonei alocate
char v; // indicator verificare indice

// functie auxiliara de initializare
void init( const tablou& );
};

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
}

66 Tipuri abstracte de date Capitolul 4

template <class T>
tablou<T>::tablou( const tablou<T>& t ) {
// initializarea obiectului invocator cu t
init( t );
}

template <class T>
tablou<T>& tablou<T>::operator =( const tablou<T>& t ) {
if ( this != &t ) { // este o atribuire inefectiva x = x?
delete [ ] a; // eliberarea memoriei alocate
init( t ); // initializarea cu t
}
return *this; // se returneaza obiectul invocator
}

template<class T>
void tablou<T>::init( const tablou<T>& t ) {
a = 0; v = 0; d = 0; // valori implicite
if ( t.d > 0 ) { // verificarea dimensiunii
a = new T [ d = t.d ]; // alocarea si copierea elem.
for ( int i = 0; i < d; i++ ) a[ i ] = t.a[ i ];
v = t.v; // duplicarea indicatorului
} // pentru verificarea indicilor
}

template< class T >
T& tablou<T>::operator []( int i ) {
static T z; // elementul returnat in caz de eroare

if ( d == 0 ) // tablou de dimensiune zero
return z;

if ( v == 0 || ( 0 <= i && i < d ) )
// verificarea indicilor este dezactivata,
// sau este activata si indicele este corect
return a[ i ];

cerr << "\n\ntablou -- " << i
<< ": indice exterior domeniului [0, "
<< ( d - 1 ) << "].\n\n";

return z;
}

ntr-o prim aproximare, diferenele fa de clasa neparametric tablou sunt
urmtoarele:
Nivelul de incapsulare protected a nlocuit nivelul private. Este o
modificare necesar procesului de derivare al claselor, prezentat n seciunile
urmtoare.
Eliberarea zonei alocate dinamic trebuie s se realizeze prin invocarea
destructorului tipului T pentru fiecare element. Deci, n loc de delete a, este
Seciunea 4.1 Tablouri 67

obligatoriu s scriem delete [] a att n destructor, ct i n operatorul de
atribuire. De asemenea, copierea elementelor n funcia init() nu se mai
poate face global, prin memcpy(), ci element cu element, pentru a invoca astfel
opratorul de atribuire al tipului T.
Prezena definiiilor funciilor membre n fiierul header nu este o greeal. De
fapt, este vorba de abloanele funciilor membre.
Printre inconvenientele tablourilor predefinite am enumerat i imposibilitatea
detectrii indicilor eronai. Dup cum se observ, am completat clasa parametric
tablou<T> cu funciile publice vOn() i vOff(), prin care se activeaz, respectiv
se dezactiveaz, verificarea indicilor. n funcie de valoarea logic a variabilei
private v, valoare stabilit prin funciile vOn() i vOff(), operatorul de indexare
va verifica, sau nu va verifica, corectitudinea indicelui. Operatorul de indexare a
fost modificat corespunztor.
Pentru citirea i scrierea obiectelor de tip tablou<T>, suprancrcm operatorii
respectivi (>> i <<) ca funcii nemembre. Convenim ca, n operaiile de
citire/scriere, s reprezentm tablourile n formatul

[dimensiune] element1 element2 ...

Cei doi operatori pot fi implementai astfel:

template <class T>
istream& operator >>( istream& is, tablou<T>& t ) {
char c;

// citirea dimensiunii tabloului incadrata de '[' si ']'
is >> c;
if ( c != '[' ) { is.clear( ios::failbit ); return is; }
int n; is >> n; is >> c;
if ( c != ']' ) { is.clear( ios::failbit ); return is; }

// modificarea dimensiunii tabloului,
// evitand copierea elementelor existente
t.newsize( 0 ).newsize( n );

// citirea elementelor
for ( int i = 0; i < n; is >> t[ i++ ] );

return is;
}

68 Tipuri abstracte de date Capitolul 4

template <class T>
ostream& operator <<( ostream& os, tablou<T>& t ) {
int n = t.size( );

os << " [" << n << "]: ";
for ( int i = 0; i < n; os << t[ i++ ] << ' ' );

return os;
}

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 Stive, cozi, heap-uri
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 Stive, cozi, heap-uri 69

tipurile stiva<T>, coada<T> i heap<T>. Prin motenire, limbajul C++ permite
att crearea unor subtipuri ale tipului de baz, ct i crearea unor tipuri noi,
diferite de tipul de baz. Stivele, cozile i heap-urile vor fi tipuri noi, diferite de
tipul de baz tablou. Posibilitatea de a crea subtipuri prin derivare, o facilitate
deosebit de puternic a programrii orientate pe obiect i a limbajului C++, va fi
exemplificat n Seciunile 11.1 i 10.2.
4.2.1 Clasele stiva<T> i coada<T>
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;

ca membri private, precum i cei doi operatori (publici n clasa tablou<T>)

70 Tipuri abstracte de date Capitolul 4

tablou& operator =( const tablou& );
T& operator []( int );

tot ca membri private.
Pe baza celor de mai sus, se justific foarte simplu faptul c prin derivarea privat
se obin tipuri noi, total distincte fa de tipul de baz. Astfel, nu este disponibil
nici una din facilitile clasei de baz tablou<T> n exteriorul clasei stiva<T>,
existena clasei de baz fiind total ascuns utilizatorului. n schimb, pentru
implementarea propriilor faciliti, clasa stiva<T> poate folosi din plin toi
membrii clasei tablou<T>. Prin derivarea private, realizm deci o reutilizare a
clasei de baz.
Definirea unei stive derivat din tablou se realizeaz astfel (fiierul stiva.h):

#ifndef __STIVA_H
#define __STIVA_H

#include <iostream.h>
#include "tablou.h"

template <class T>
class stiva: private tablou<T> {
public:
stiva( int d ): tablou<T>( d ) { s = -1; }

push( const T& );
pop ( T& );

private:
int s; // indicele ultimului element inserat
};

template <class T>
stiva<T>::push( const T& v ) {
if ( s >= d - 1 ) return 0;
a[ ++s ] = v; return 1;
}

template <class T>
stiva<T>::pop( T& v ) {
if ( s < 0 ) return 0;
v = a[ s-- ]; return 1;
}

#endif

nainte de a discuta detaliile de implementare, s remarcm o anumit
inconsecven aprut n definiia funciei pop() din Seciunea 3.1.1. Aceast
funcie returneaz fie elementul din vrful stivei, fie un mesaj de eroare (atunci
cnd stiva este vid). Desigur c nu este un detaliu deranjant att timp ct ne
Seciunea 4.2 Stive, cozi, heap-uri 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 termin programul.
Se returneaz o valoare reprezentnd eroare.
Se returneaz o valoare legal, programul fiind lsat ntr-o stare ilegal.
Se invoc o funcie special construit de programator pentru a fi apelat n caz
de 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 Tipuri abstracte de date 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 ) ) {
// ...
}

executndu-se atta timp ct n stiv se mai pot insera elemente.
n continuare, ne propunem s analizm mai amnunit contribuia clasei de baz
tablou<T> n funcionarea clasei stiva<T>. S remarcm mai nti invocarea
constructorului tipului de baz pentru iniializarea datelor membre motenite,
invocare realizat prin lista de iniializare a membrilor:

stiva( int d ): tablou<T>( d ) { s = -1; }

Utilizarea acestei sintaxe speciale se datoreaz faptului c execuia oricrui
constructor se face n dou etape. ntr-o prim etap, etap de iniializare, se
invoc constructorii datelor membre motenite de la clasele de baz, conform
listei de iniializare a membrilor. n a doua etap, numit etap de atribuire, se
execut corpul propriu-zis al constructorului. Necesitatea unei astfel de etapizri
se justific prin faptul c iniializarea membrilor motenii trebuie rezolvat n
mod unitar de constructorii proprii, i nu de cel al clasei derivate. Dac lista de
iniializare a membrilor este incomplet, atunci, pentru membrii rmai
neiniializai, se invoc constructorii implicii. De asemenea, tot n etapa de
iniializare se vor invoca constructorii datelor membre de tip clas i se vor
iniializa datele membre de tip const sau referin.
Continund analiza contribuiei tipului de baz tablou<T>, s remarcm c n
clasa stiva<T> nu s-au definit constructorul de copiere, operatorul de atribuire i
Seciunea 4.2 Stive, cozi, heap-uri 73

destructorul. Iniializarea i atribuirea obiectelor de tip stiv cu obiecte de acelai
tip, precum i distrugerea acestora, se realizeaz totui corect, datele membre
motenite de la tablou<T> fiind manipulate de funciile membre ale acestui tip. n
funcia

void f( ) {
stiva<int> x( 16 );
stiva<int> y = x;
x = y;
}

iniializarea lui y cu x se face membru cu membru, pentru datele proprii clasei
stiva<T> (ntregul top), i prin invocarea constructorului de copiere al clasei
tablou<T>, pentru iniializarea datelor membre motenite (ntregul d i adresa a).
Atribuirea x = y se efectueaz membru cu membru, pentru datele proprii, iar
pentru cele motenite, prin invocarea operatorului de atribuire al clasei
tablou<T>. La terminarea funciei, obiectele x i y vor fi distruse prin invocarea
destructorilor n ordinea invers a invocrii constructorilor, adic destructorul
clasei stiva<T> (care nu a fost precizat pentru c nu are de fcut nimic) i apoi
destructorul clasei de baz tablou<T>.
Implementarea clasei coada<T> se face pe baza precizrilor din Seciunea 3.1.2,
direct prin modificarea definiiei clasei stiva<T>. n locul indicelui top, vom
avea dou date membre, i anume indicii head i tail, iar funciile membre
push() i pop() vor fi nlocuite cu ins_q(), respectiv del_q(). Ca exerciiu, v
propunem s realizai implementarea efectiv a acestei clase.
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 Tipuri abstracte de date Capitolul 4

template <class T>
class heap: private tablou<T> {
public:
heap( int d ): tablou<T>( d ) { h = -1; }
heap( const tablou<T>& t ): tablou<T>( t )
{ h = t.size( ) - 1; make_heap( ); }

insert ( const T& );
delete_max( T& );

protected:
int h; // indicele ultimului element din heap

void percolate( int );
void sift_down( int );
void make_heap( );
};

template <class T>
heap<T>::insert( const T& v ) {
if ( h >= d - 1 ) return 0;
a[ ++h ] = v; percolate( h );
return 1;
}

template <class T>
heap<T>::delete_max( T& v ) {
if ( h < 0 ) return 0;
v = a[ 0 ];
a[ 0 ] = a[ h-- ]; sift_down( 0 );
return 1;
}

template <class T>
void heap<T>::make_heap( ) {
for ( int i = (h + 1) / 2; i >= 1; sift_down( --i ) );
}

template <class T>
void heap<T>::percolate( int i ) {
T *A = a - 1; // a[ 0 ] este A[ 1 ], ...,
// a[ i - 1 ] este A[ i ]
int k = i + 1, j;

do {
j = k;
if ( j > 1 && A[ k ] > A[ j/2 ] ) k = j/2;
T tmp = A[ j ]; A[ j ] = A[ k ]; A[ k ] = tmp;
} while ( j != k );
}

Seciunea 4.2 Stive, cozi, heap-uri 75

template <class T>
void heap<T>::sift_down( int i ) {
T *A = a - 1; // a[ 0 ] este A[ 1 ], ...,
// a[ n - 1 ] este A[ n ]
int n = h + 1, k = i + 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

Procedurile insert() i delete_max() au fost adaptate stilului de tratare a
excepiilor prezentat n seciunea precedent: ele returneaz valorile logice true
sau false, dup cum operaiile respective sunt, sau nu sunt posibile.
Clasa heap<T> permite crearea unor heap-uri cu elemente de cele mai diverse
tipuri: int, float, long, char etc. Dar ncercarea de a defini un heap pentru un
tip nou T, definit de utilizator, poate fi respins chiar n momentul compilrii,
dac acest tip nu are definit operatorul de comparare >. Acest operator, a crui
definire rmne n sarcina proiectantului clasei T, trebuie s returneze true (o
valoare diferit de 0) dac argumentele sale sunt n relaia > i false (adic 0) n
caz contrar. Pentru a nu fi necesar i definirea operatorului <, n implementarea
clasei heap<T> am folosit numai operatorul >.
Vom exemplifica utilizarea clasei heap<T> cu un operator > diferit de cel
predefinit prin intermediul clasei intErval. Dei clasa intErval nu are definit
operatorul >, programul urmtor trece de compilare i se execut (aparent)
corect.

#include "intErval.h"
#include "heap.h"

// dimensiunea heap-ului, margine superioara in intErval
const SIZE = 128;

int main( ) {
heap<intErval> hi( SIZE );
intErval v( SIZE );

76 Tipuri abstracte de date 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;
}

Justificarea corectitudinii sintactice a programului de mai sus const n existena
operatorului de conversie de la intErval la int. Prin aceast conversie,
compilatorul rezolv compararea a dou valori de tip intErval (pentru operatorul
>), sau a unei valori intErval cu valoarea 0 (pentru operatorul !=) folosind
operatorii predefinii pentru argumente de tip ntreg. Utiliznd acelai operator de
conversie de la intErval la int, putem defini foarte comod un operator >, prin
care heap-ul s devin un min-heap. Noul operator > este practic negarea relaiei
uzuale >:

// Operatorul > pentru min-heap
int operator >( const intErval& a, const intErval& b ) {
return a < b;
}

La compilarea programului de mai sus, probabil c ai observat un mesaj relativ la
invocarea funciei non-const intErval::operator int() pentru un obiect
const n funcia heap<T>::insert(). Iat despre ce este vorba. Urmtorul
program genereaz exact acelai mesaj:

#include "intErval.h"

int main( ) {
intErval x1;
const intErval x2( 20, 10 );

x1 = x2;
return 0;
}

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 Stive, cozi, heap-uri 77

operatorul de conversie intErval::operator int(), va trebui s-i completm
definiia din clasa intErval cu atributul const:

operator int( ) const { return v; }

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;
}

Dup ce am clarificat particularitile obiectelor constante, este momentul s
adaptm corespunztor i clasa tablou<T>. Orice clas frecvent utilizat i
tablou<T> este una din ele trebuie s fie proiectat cu grij, astfel nct s
suporte inclusiv lucrul cu obiecte constante. Vom aduga n acest scop atributul
const funciei membre size():

size( ) const { return d; }

n plus, mai adugm i un nou operator de indexare:

const T& operator []( int ) const;

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 Tipuri abstracte de date 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>
Structurile prezentate pn acum sunt de fapt liste implementate secvenial,
difereniate prin particularitile operaiilor de inserare i extragere. n cele ce
urmeaz, ne vom concentra asupra unei implementri nlnuite a listelor, prin
alocarea dinamic a memoriei.
Ordinea nodurilor unei liste se realizez prin completarea informaiei propriu-zise
din fiecare nod, cu informaii privind localizarea nodului urmtor i eventual a
celui precedent. Informaiile de localizare, numite legturi sau adrese, pot fi, n
funcie de modul de implementare ales (vezi Seciunea 3.1), indici ntr-un tablou,
sau adrese de memorie. n cele ce urmeaz, fiecare nod va fi alocat dinamic prin
operatorul new, legturile fiind deci adrese.
Informaia din fiecare nod poate fi de orice tip, de la un numr ntreg sau real la o
structur orict de complex. De exemplu, pentru reprezentarea unui graf prin
lista muchiilor, fiecare nod conine cele dou extremiti ale muchiei i lungimea
(ponderea) ei. Limbajul C++ permite implementarea structurii de nod prin
intermediul claselor parametrice astfel:

template <class E>
class nod {
// ...
E val; // informatia propriu-zisa
nod<E> *next; // adresa nodului urmator
};

Operaiile elementare, cum sunt parcurgerile, inserrile sau tergerile, pot fi
implementate prin intermediul acestei structuri astfel:
Parcurgerea nodurilor listei:

nod<E> *a; // adresa nodului actual
// ...
while ( a ) { // adresa ultimului element are valoarea 0
// ... prelucrarea informatiei a->val
a = a->next; // notatie echivalenta cu a = (*a).next
}

Inserarea unui nou nod n list:
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

Structura de nod este suficient pentru manipularea listelor cu elemente de tip E,
cu condiia s cunoatem primul nod:

nod<E> head; // primul nod din lista

Exist totui o list imposibil de tratat prin intermediul acestei implementri, i
anume lista vid. Problema de rezolvat este oarecum paradoxal, deoarece
variabila head, primul nod din list, trebuie s reprezinte un nod care nu exist.
Se pot gsi diverse soluii particulare, dependente de tipul i natura informaiilor.
De exemplu, dac informaiile sunt valori pozitive, o valoare negativ ar putea
reprezenta un nod inexistent. O alt soluie este adugarea unei noi date membre
pentru validarea existenei nodului curent. Dar este inacceptabil ca pentru un
singur nod i pentru o singur situaie s ncrcm toate celelalte noduri cu nc
un cmp.
Imposibilitatea reprezentrii listelor vide nu este rezultatul unei proiectri
defectuoase a clasei nod<E>, ci al confuziei dintre list i nodurile ei. Identificnd
lista cu adresa primului ei nod i adugnd funciile uzuale de manipulare
(inserri, tergeri etc), obinem tipul abstract lista<E> cu elemente de tip E:

template <class E>
class lista {
// ...
private:
nod<E> *head; // adresa primul nod din lista
};

Conform principiilor de ncapsulare, manipularea obiectelor clasei abstracte
lista<E> se face exclusiv prin intermediul funciilor membre, structura intern a
80 Tipuri abstracte de date 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 acest context, atribuirea este ineficient, deoarece val ar fi iniializat de dou
ori: o dat n faza de iniializare prin constructorul implicit al clasei E, iar apoi,
n faza de atribuire, prin invocarea operatorului de atribuire.
Principalele operaii asupra listelor sunt inserarea i parcurgerea elementelor.
Pentru a implementa parcurgerea, s ne amintim ce nseamn parcurgerea unui
tablou pur i simplu un indice i un operator de indexare:

tablou<int> T( 32 );
T[ 31 ] = 1;

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


template <class E>
class iterator {
// ...
private:
nod<E>* const *phead;
nod<E> *a;
};

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; }
}

Se observ c parcurgerea este circular, adic, odat ce elementul actual a ajuns
la sfritul listei, el este iniializat din nou cu primul element, cu condiia ca lista
s nu fie vid. Atingerea sfritului listei este marcat prin returnarea valorii
false. n caz contrar, valoarea returnat este true, iar elementul curent este
returnat prin argumentul de tip referin la E. Pentru exemplificare, operatorul
de inserare n ostream poate fi implementat prin clasa iterator<E> astfel:

template <class E>
ostream& operator <<( ostream& os, const lista<E>& lista ) {
E v; iterator<E> l = lista;

os << " { ";
while ( l( v ) ) os << v << ' ';
os << "} ";

return os;
}

Iniializarea iteratorului l, realizat prin definiia iterator<E> l = lista, este
implementat de constructorul

82 Tipuri abstracte de date Capitolul 4

template <class E>
iterator<E>::iterator( const lista<E>& l ) {
phead = &l.head;
a = *phead;
}

Declaraia const a argumentului lista<E>& l semnific faptul c l, mpreun cu
datele membre, este o variabil read-only (constant) n acest constructor. n
consecin, *phead trebuie s fie constant, adic definit ca

nod<E>* const *phead;

Aceeai iniializare mai poate fi realizat i printr-o instruciune de atribuire
l = lista, operatorul corespunztor fiind asemntor celui de mai sus:

template <class E>
iterator<E>& iterator<E>::operator =( const lista<E>& l ) {
phead = &l.head;
a = *phead;

return *this;
}

Pentru a putea defini un iterator neiniializat, se va folosi constructorul implicit
(fr nici un argument):

template <class E>
iterator<E>::iterator( ) {
phead = 0;
a = 0;
}

n finalul discuiei despre clasa iterator<E>, vom face o ultim observaie.
Aceast clas trebuie s aib acces la membrii privai din clasele nod<E> i
lista<E>, motiv pentru care va fi declarat friend n ambele.
n sfrit, putem trece acum la definirea complet a clasei lista<E>. Funcia
insert() insereaz un nod naintea primului element al listei.

template <class E>
lista<E>& lista<E>::insert( const E& v ) {
nod<E> *pn = new nod<E>( v );
pn->next = head; head = pn;

return *this;
}

Seciunea 4.3 Clasa lista<E> 83

O alt funcie membr, numit init(), este invocat de ctre constructorul de
copiere i de ctre operatorul de atribuire, pentru inializarea unei liste noi cu o
alta, numit list sursa.

template <class E>
void lista<E>::init( const lista<E>& sursa ) {
E v; iterator<E> s = sursa;

for ( nod<E> *tail = head = 0; s( v ); ) {
nod<E> *pn = new nod<E>( v );
if ( !tail ) head = pn; else tail->next = pn;
tail = pn;
}
}

Funcia reset() elimin rnd pe rnd toate elementele listei:

template <class E>
void lista<E>::reset( ) {
nod<E> *a = head;

while( a ) {
nod<E> *pn = a->next;
delete a;
a = pn;
}
head = 0;
}

Instruciunea head = 0 are, aparent, acelai efect ca ntreaga funcie reset(),
deoarece lista este redus la lista vid. Totui, aceast instruciune nu se poate
substitui ntregii funcii, deoarece elementele listei ar rmne alocate, fr s
existe posibilitatea de a recupera spaiul alocat.
Declaraiile claselor nod<E>, lista<E> i iterator<E>, n forma lor complet,
sunt urmtoarele:

template <class E>
class nod {
friend class lista<E>;
friend class iterator<E>;

protected:
nod( const E& v ): val( v ) { next = 0; }

E val; // informatia propriu-zisa
nod<E> *next; // adresa nodului urmator
};
84 Tipuri abstracte de date Capitolul 4


template <class E>
class lista {
friend class iterator<E>;
public:
lista( ) { head = 0; }
lista( const lista<E>& s ) { init( s ); }
~lista( ) { reset( ); }

lista& operator =( const lista<E>& );
lista& insert( const E& );

private:
nod<E> *head; // adresa primul nod din lista

void init( const lista<E>& );
void reset( );
};

template <class E>
class iterator {
public:
iterator( );
iterator( const lista<E>& );

operator ()( E& );
iterator<E>& operator =( const lista<E>& );

private:
nod<E>* const *phead;
nod<E> *a;
};

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

void alocareDinamica( unsigned n ) {
++nivel;
char *ptr = new char[ n ];
if ( ptr )
alocareDinamica( n );

// memoria libera este epuizata
delete ptr;
if ( !raport++ )
cout << "\nMemoria libera a fost epuizata. "
<< "S-au alocat "
<< (long)nivel * n * sizeof( char ) / 1024 << 'K'
<< ".\nNumarul de apeluri " << nivel
<< "; la fiecare apel s-au alocat "
<< n * sizeof( char ) << " octeti.\n";
}


main( ) {
for ( unsigned i = 1024; i > 32; i /= 2 ) {
nivel = 1; raport = 0;
alocareDinamica( 64 * i - 1 );
}

return 1;
}
Rezultatele obinute sunt clar n favoarea blocurilor mari. Explicaia const n
faptul c fiecrui bloc alocat i se adaug un antet necesar gestionrii zonelor
ocupate i a celor libere, zone organizate n dou liste nlnuite.

4.2 Explicai rezultatele programului de mai jos.

#include <iostream.h>
#include "tablou.h"

int main( ) {
tablou<int> y( 12 );

for ( int i = 0, d = y.size( ); i < d; i++ )
y[ i ] = i;

cout << "\nTabloul y : " << y;
y = 8;
cout << "\nTabloul y : " << y;

cout << '\n';
return 0;
}

Soluie: Elementul surprinztor al acestui program este instruciunea de atribuire
y = 8. Surpinztor, n primul rnd, deoarece ea trece de compilare, dei nu s-a
86 Tipuri abstracte de date Capitolul 4

definit operatorul de atribuire corespunztor. n al doilea rnd, instruciunea
y = 8 surprinde prin efectele execuiei sale: tabloul y are o alt dimensiune i un
alt coninut. Explicaia este dat de o convenie a limbajului C++, prin care un
constructor cu un singur argument este folosit i ca operator de conversie de la
tipul argumentului, la tipul clasei respective. n cazul nostru, tabloului y i se
atribuie un tablou temporar de dimensiune 8, generat prin invocarea
constructorului clasei tablou<T> cu argumentul 8. S-a realizat astfel modificarea
dimensiunii tabloului y, dar cu preul pierderii coninutului iniial.

4.3 Exerciiul de mai sus conine o soluie pentru modificarea dimensiunii
obiectelor de tip tablou<T>. Problema pe care o punem acum este de a rezolva
problema, astfel nct coninutul tabloului s nu se mai piard.
Soluie: Iat una din posibilele implementri:

template< class T >
tablou<T>& tablou<T>::newsize( int dN ) {
T *aN = 0; // noua adresa

if ( dN > 0 ) {
aN = new T [ dN ]; // alocarea dinamica a memoriei
for ( int i = d < dN? d: dN; i--; )
aN[ i ] = a[ i ]; // alocarea dinamica a memoriei
}
else
dN = 0;

delete [ ] a; // eliberarea vechiului spatiu
d = dN; a = aN; // redimensionarea obiectului

return *this;
}


4.4 Implementai clasa parametric coada<T>.
Soluie: Conform celor menionate la sfritul Seciunii 4.2.1, ne vom inspira de
la structura clasei stiva<T>. Una din implementrile posibile este urmtoarea.

template <class T>
class coada: private tablou<T> {
public:
coada( int d ): tablou<T>( d )
{ head = tail = 0; }

Seciunea 4.4 Exerciii 87

ins_q( const T& );
del_q( T& );

private:
int head; // indicele ultimei locatii ocupate
int tail; // indicele locatiei predecesoare primei
// locatii ocupate
};

template <class T>
coada<T>::ins_q( const T& x ) {
int h = ( head + 1 ) % d;
if ( h == tail ) return 0;
a[ head = h ] = x; return 1;
}

template <class T>
coada<T>::del_q( T& x ) {
if ( head == tail ) return 0;
tail = ( tail + 1 ) % d;
x = a[ tail ]; return 1;
}


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 << ' ';

cout << "\nCoada ins_q... ";
while ( cd.ins_q( i ) ) cout << i++ << ' ';

88 Tipuri abstracte de date Capitolul 4

cout << "\nCoada del_q... ";
while ( cd.del_q( i ) ) cout << i << ' ';

cout << '\n';
}


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; }.
89
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
n Capitolul 1 am dat un neles intuitiv situaiei cnd un algoritm necesit un
timp n ordinul unei anumite funcii. Revenim acum cu o definiie riguroas.
5.1.1 O notaie pentru ordinul lui
Fie N mulimea numerelor naturale (pozitive sau zero) i R mulimea numerelor
reale. Notm prin N
+
i R
+
mulimea numerelor naturale, respectiv reale, strict
pozitive, i prin R

mulimea numerelor reale nenegative. Mulimea {true, false}


de constante booleene o notm cu B. Fie f : N R

o funcie arbitrar. Definim


mulimea
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.
90 Analiza eficienei algoritmilor Capitolul 5

Fie un algoritm dat i fie o funcie t : N R

astfel nct o anumit implementare


a algoritmului s necesite cel mult t(n) uniti de timp pentru a rezolva un caz de
mrime n, n N. Principiul invarianei (menionat n Capitolul 1) ne asigur c
orice implementare a algoritmului necesit un timp n ordinul lui t. Mai mult,
acest algoritm necesit un timp n ordinul lui f pentru orice funcie f : N R


pentru care t O( f ). n particular, t O(t). Vom cuta n general s gsim cea
mai simpl funcie f, astfel nct t O( f ).
Proprietile de baz ale lui O( f ) sunt date ca exerciii (Exerciiile 5.15.7) i
este recomandabil s le studiai nainte de a trece mai departe.
Notaia asimptotic definete o relaie de ordine parial ntre funcii i deci, ntre
eficiena relativ a diferiilor algoritmi care rezolv o anumit problem. Vom da
n continuare o interpretare algebric a notaiei asimptotice. Pentru oricare dou
funcii f , g : N R

, definim urmtoarea relaie binar: f g dac O( f ) O(g).


Relaia este o relaie de ordine parial n mulimea funciilor definite pe N i
cu valori n R

(Exerciiul 5.6). Definim i o relaie de echivalen: f g dac


O( f ) = O(g).
n mulimea O( f ) putem nlocui pe f cu orice alt funcie echivalent cu f. De
exemplu, lg n ln n log n i avem O(lg n) = O(ln n) = O(log n). Notnd cu O(1)
ordinul funciilor mrginite superior de o constant, obinem ierarhia:
O(1) O(log n) O(n) O(n log n) O(n
2
) O(n
3
) O(2
n
)
Aceast ierarhie corespunde unei clasificri a algoritmilor dup un criteriu al
performanei. Pentru o problem dat, dorim mereu s obinem un algoritm
corespunztor unui ordin ct mai la stnga. Astfel, este o mare realizare dac n
locul unui algoritm exponenial gsim un algoritm polinomial.
n Exerciiul 5.7 este dat o metod de simplificare a calculelor, n care apare
notaia asimptotic. De exemplu,
n
3
+3n
2
+n+8 O(n
3
+(3n
2
+n+8)) = O(max(n
3
, 3n
2
+n+8)) = O(n
3
)
Ultima egalitate este adevrat, chiar dac max(n
3
, 3n
2
+n+8) n
3
pentru
0 n 3, deoarece notaia asimptotic se aplic doar pentru n suficient de mare.
De asemenea,
n
3
3n
2
n8 O(n
3
/2+(n
3
/23n
2
n8)) = O(max(n
3
/2, n
3
/23n
2
n8))
= O(n
3
/2) = O(n
3
)
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
funcii oarecare f, g : N R

, avem: f O(g), dac i numai dac g ( f ).


O situaie fericit este atunci cnd timpul de execuie al unui algoritm este limitat,
att inferior ct i superior, de cte un multiplu real pozitiv al aceleiai funcii.
Introducem notaia
( f ) = O( f ) ( f )
numit ordinul exact al lui f. Pentru a compara ordinele a dou funcii, notaia
nu este ns mai puternic dect notaia O, n sensul c relaia O( f ) = O(g) este
echivalent cu ( f ) = (g).
Se poate ntmpla ca timpul de execuie al unui algoritm s depind simultan de
mai muli parametri. Aceast situaie este tipic pentru anumii algoritmi care
opereaz cu grafuri i n care timpul depinde att de numrul de vrfuri, ct i de
numrul de muchii. Notaia asimptotic se generalizeaz n mod natural i pentru
funcii cu mai multe variabile. Astfel, pentru o funcie arbitrar f : N N R


definim
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 Notaia asimptotic condiionat
Muli algoritmi sunt mai uor de analizat dac considerm iniial cazuri a cror
mrime satisface anumite condiii, de exemplu s fie puteri ale lui 2. n astfel de
situaii, folosim notaia asimptotic condiionat. Fie f : N R

o funcie
arbitrar i fie P : N B un predicat.
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 Analiza eficienei algoritmilor Capitolul 5

O funcie f : N R

este eventual nedescresctoare, dac exist un n


0
, astfel
nct pentru orice n n
0
avem f (n) f (n+1), ceea ce implic prin inducie c,
pentru orice n n
0
i orice m n, avem f (n) f (m). Fie b 2 un ntreg oarecare.
O funcie eventual nedescresctoare este b-neted dac f (bn) O( f (n)). Orice
funcie care este b-neted pentru un anumit b 2 este, de asemenea, b-neted
pentru orice b 2 (demonstrai acest lucru!); din aceast cauz, vom spune pur i
simplu c aceste funcii sunt netede. Urmtoarea proprietate asambleaz aceste
definiii, demonstrarea ei fiind lsat ca exerciiu.

Proprietatea 5.1 Fie b 2 un ntreg oarecare, f : N R

o funcie neted i
t : N R
*
o funcie eventual nedescresctoare, astfel nct
t(n) X( f (n) | n este o putere a lui b)
unde X poate fi O, , sau . Atunci, t X( f ). Mai mult, dac t ( f ), atunci i
funcia t este neted.

Pentru a nelege utilitatea notaiei asimptotice condiionate, s presupunem c
timpul de execuie al unui algoritm este dat de ecuaia
t n
a n
t n t n bn n
( )
( / ) ( / )
=
=
+ +

pentru
pentru
1
2 2 1

unde a, b R
+
sunt constante arbitrare. Este dificil s analizm direct aceast
ecuaie. Dac considerm doar cazurile cnd n este o putere a lui 2, ecuaia devine
t n
a n
t n bn n
( )
( / )
=
=
+ >

pentru
pentru o putere a lui 2
1
2 2 1

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 n n
t n t n cn n n
( )
( / ) ( / )


+ + >

1 0
0
2 2
( ) pentru
pentru

i, simultan
t n
t n n n
t n t n dn n n
( )
( / ) ( / )


+ + >

2 0
0
2 2
( ) pentru
pentru

pentru anumite constante c, d R
+
, n
0
N i pentru dou funcii t
1
, t
2
: N R
+
.
Notaia asimptotic ne permite s scriem cele dou inegaliti astfel:
t(n) t(n/2) + t(n/2) + O(n)
respectiv
t(n) t(n/2) + t(n/2) + (n)
Aceste dou expresii pot fi scrise i concentrat:
t(n) t(n/2) + t(n/2) + (n)
Definim funcia
f n
n
f n f n n n
( )
( / ) ( / )
=
=
+ +

1 1
2 2 1
pentru
pentru

Am vzut c f (n log n). Ne ntoarcem acum la funcia t care satisface
inegalitile precedente. Prin inducie, se demonstreaz c exist constantele
v d, u c, astfel nct
v t(n)/f (n) u
pentru orice n N
+
. Deducem atunci
t ( f ) = (n log n)
94 Analiza eficienei algoritmilor Capitolul 5

Aceast tehnic de rezolvare a inegalitilor iniiale are dou avantaje. n primul
rnd, nu trebuie s demonstrm independent c t O(n log n) i t (n log n).
Apoi, mai important, ne permite s restrngem analiza la situaia cnd n este o
putere a lui 2, aplicnd apoi Proprietatea 5.1. Deoarece nu tim dac t este
eventual nedescresctoare, nu putem aplica Proprietatea 5.1 direct asupra
inegalitilor iniiale.
5.2 Tehnici de analiz a algoritmilor
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 Sortarea prin selecie
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
d c b a n i
i
n
+ ++ + + ++ + + ++ +
= == =


( ( ))
1
1

uniti de timp, d fiind din nou o constant. Simplificm aceast expresie i
obinem
(a/2)n
2
+ (b+ca/2)n + (dcb)
de unde deducem c algoritmul necesit un timp n O(n
2
). O analiz similar
asupra limitei inferioare arat c timpul este de fapt n (n
2
). Nu este necesar s
considerm cazul cel mai nefavorabil sau cazul mediu, deoarece timpul de
execuie este independent de ordonarea prealabil a elementelor de sortat.
n acest prim exemplu am dat toate detaliile. De obicei, detalii ca iniializarea
buclei nu se vor considera explicit. Pentru cele mai multe situaii, este suficient s
alegem ca barometru o anumit instruciune din algoritm i s numrm de cte
ori se execut aceast instruciune. n cazul nostru, putem alege ca barometru
testul din bucla interioar, acest test executndu-se de n(n1)/2 ori. Exerciiul
5.23 ne sugereaz c astfel de simplificri trebuie fcute cu discernmnt.
Seciunea 5.2 Tehnici de analiz a algoritmilor 95

5.2.2 Sortarea prin inserie
Timpul pentru algoritmul insert (Seciunea 1.3) este dependent de ordonarea
prealabil a elementelor de sortat. Vom folosi comparaia x < T[ j] ca
barometru.
S presupunem c i este fixat i fie x = T[i], ca n algoritm. Cel mai nefavorabil
caz apare atunci cnd x < T[ j] pentru fiecare j ntre 1 i i1, algoritmul fcnd n
aceast situaie i1 comparaii. Acest lucru se ntmpl pentru fiecare valoare a
lui i de la 2 la n, atunci cnd tabloul T este iniial ordonat descresctor. Numrul
total de comparaii pentru cazul cel mai nefavorabil este
( ) ( ) / i n n
i
n
=
=

1 1 2
1
(n
2
)
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
Pentru a sorta n elemente, avem nevoie de c
i
i
n
= == =

2
comparaii, ceea ce este egal cu
(n
2
+3n)/4 H
n
(n
2
)
unde prin H
n
= i
i
n

= == =

1
1
(log n) am notat al n-lea element al seriei armonice
(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
situaii, numrul comparaiilor este n (n
2
).
Algoritmul necesit un timp n (n
2
), 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).
96 Analiza eficienei algoritmilor Capitolul 5

5.2.3 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
m1
i
Rezult 2
m1
n/i, iar de aici obinem relaia m 1 + lg(n/i).
Numrul total de executri ale buclei repeat la formarea unui heap este mrginit
superior de
( lg( / )) 1
1
+
=

n i
i
a
, unde a = n/2 (*)
Pentru a simplifica aceast expresie, s observm c pentru orice k 0
lg( / ) lg( / )
i b
c
k k
n i n
=

2 2 , unde b = 2
k
i c = 2
k+1
1
Descompunem expresia (*) n seciuni corespunztoare puterilor lui 2 i notm
d = lg(n/2) :
lg( / ) lg ( / ) lg ( / ) n i n n
k k d d
k
d
i
a

+
= =

2 2 2 2
1 1
0 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,
lg ( / ) n i n
i
a

3
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 Tehnici de analiz a algoritmilor 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 Turnurile din Hanoi
Matematicianul francez duard Lucas a propus n 1883 o problem care a devenit
apoi celebr, mai ales datorit faptului c a prezentat-o sub forma unei legende.
Se spune c Brahma a fixat pe Pmnt trei tije de diamant i pe una din ele a pus
n ordine cresctoare 64 de discuri de aur de dimensiuni diferite, astfel nct
discul cel mai mare era jos. Brahma a creat i o mnstire, iar sarcina clugrilor
era s mute toate discurile pe o alt tij. Singura operaiune permis era mutarea a
cte unui singur disc de pe o tij pe alta, astfel nct niciodat s nu se pun un
disc mai mare peste unul mai mic. Legenda spune c sfritul lumii va fi atunci
cnd clugrii vor svri lucrarea. Aceasta se dovedete a fi o previziune extrem
de optimist asupra sfritului lumii. Presupunnd c n fiecare secund se mut
un disc i lucrnd fr ntrerupere, cele 64 de discuri nu pot fi mutate nici n 500
de miliarde de ani de la nceputul aciunii!
Observm c pentru a muta cele mai mici n discuri de pe tija i pe tija j (unde
1 i 3, 1 j 3, i j, n 1), transferm cele mai mici n1 discuri de pe tija i
pe tija 6ij, apoi transferm discul n de pe tija i pe tija j, iar apoi retransferm
cele n1 discuri de pe tija 6ij pe tija j. Cu alte cuvinte, reducem problema
mutrii a n discuri la problema mutrii a n1 discuri. Urmtoarea procedur
descrie acest algoritm recursiv.
procedure Hanoi(n, i, j)
{mut cele mai mici n discuri de pe tija i pe tija j}
if n > 0 then Hanoi(n1, i, 6ij)
write i j
Hanoi(n1, 6ij, j)
Pentru rezolvarea problemei iniiale, facem apelul Hanoi(64, 1, 2).
98 Analiza eficienei algoritmilor Capitolul 5

Considerm instruciunea write ca barometru. Timpul necesar algoritmului este
exprimat prin urmtoarea recuren:
t n
n
t n n
( )
( )
=
=
+ >

1 1
2 1 1 1
pentru
pentru

Vom demonstra n Seciunea 5.2 c t(n) = 2
n
1. Rezult t (2
n
).
Acest algoritm este optim, n sensul c este imposibil s mutm n discuri de pe o
tij pe alta cu mai puin de 2
n
1 operaii. Implementarea n oricare limbaj de
programare care admite exprimarea recursiv se poate face aproape n mod direct.
5.3 Analiza algoritmilor recursivi
Am vzut n exemplul precedent ct de puternic i, n acelai timp, ct de
elegant este recursivitatea n elaborarea unui algoritm. Nu vom face o
introducere n recursivitate i nici o prezentare a metodelor de eliminare a ei. Cel
mai important ctig al exprimrii recursive este faptul c ea este natural i
compact, fr s ascund esena algoritmului prin detaliile de implementare. Pe
de alt parte, apelurile recursive trebuie folosite cu discernmnt, deoarece
solicit i ele resursele calculatorului (timp i memorie). Analiza unui algoritm
recursiv implic rezolvarea unui sistem de recurene. Vom vedea n continuare
cum pot fi rezolvate astfel de recurene. ncepem cu tehnica cea mai banal.
5.3.1 Metoda iteraiei
Cu puin experien i intuiie, putem rezolva de multe ori astfel de recurene
prin metoda iteraiei: se execut primii pai, se intuiete forma general, iar apoi
se demonstreaz prin inducie matematic c forma este corect. S considerm de
exemplu recurena problemei turnurilor din Hanoi. Pentru un anumit n > 1
obinem succesiv
t(n) = 2t(n1) + 1 = 2
2
t(n2) + 2 + 1 = = 2
n1
t(1) + 2
0
2
i
i
n
= == =



Rezult t(n) = 2
n
1. Prin inducie matematic se demonstreaz acum cu uurin
c aceast form general este corect.
Seciunea 5.3 Analiza algoritmilor recursivi 99

5.3.2 Inducia constructiv
Inducia matematic este folosit de obicei ca tehnic de demonstrare a unei
aseriuni deja enunate. Vom vedea n aceast seciune c inducia matematic
poate fi utilizat cu succes i n descoperirea enunului aseriunii. Aplicnd
aceast tehnic, putem simultan s demonstrm o aseriune doar parial specificat
i s descoperim specificaiile care lipsesc i datorit crora aseriunea este
corect. Vom vedea c aceast tehnic a induciei constructive este util pentru
rezolvarea anumitor recurene care apar n contextul analizei algoritmilor.
ncepem cu un exemplu.
Fie funcia f : N N, definit prin recurena
f n
n
f n n n
( )
( )
=
=
+ >

0 0
1 0
pentru
pentru

S presupunem pentru moment c nu tim c f (n) = n(n+1)/2 i s cutm o astfel
de formul. Avem
f n i n n
i
n
i
n
( ) = =
= =

0 0
2

i deci, f (n) O(n
2
). Aceasta ne sugereaz s formulm ipoteza induciei
specificate parial IISP(n) conform creia f este de forma f (n) = an
2
+bn+c.
Aceast ipotez este parial, n sensul c a, b i c nu sunt nc cunoscute.
Tehnica induciei constructive const n a demonstra prin inducie matematic
aceast ipotez incomplet i a determina n acelai timp valorile constantelor
necunoscute a, b i c.
Presupunem c IISP(n1) este adevrat pentru un anumit n 1. Atunci,
f (n) = a(n1)
2
+b(n1)+c+n = an
2
+(1+b2a)n+(ab+c)
Dac dorim s artm c IISP(n) este adevrat, trebuie s artm c
f (n) = an
2
+bn+c. Prin identificarea coeficienilor puterilor lui n, obinem
ecuaiile 1+b2a = b i ab+c = c, cu soluia a = b = 1/2, c putnd fi oarecare.
Avem acum o ipotez mai complet, pe care o numim tot IISP(n):
f (n) = n
2
/2+n/2+c. Am artat c, dac IISP(n1) este adevrat pentru un anumit
n 1, atunci este adevrat i IISP(n). Rmne s artm c este adevrat i
IISP(0). Trebuie s artm c f (0) = a0+b0+c = c. tim c f (0) = 0, deci IISP(0)
este adevrat pentru c = 0. n concluzie, am demonstrat c f (n) = n
2
/2+n/2
pentru orice n.
100 Analiza eficienei algoritmilor Capitolul 5

5.3.3 Recurene liniare omogene
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
nk
= 0 (*)
unde t
i
sunt valorile pe care le cutm, iar coeficienii a
i
sunt constante.
Conform intuiiei, vom cuta soluii de forma
t
n
= x
n

unde x este o constant (deocamdat necunoscut). ncercm aceast soluie n (*)
i obinem
a
0
x
n
+ a
1
x
n1
+ + a
k
x
nk
= 0
Soluiile acestei ecuaii sunt fie soluia trivial x = 0, care nu ne intereseaz, fie
soluiile ecuaiei
a
0
x
k
+ a
1
x
k1
+ + a
k
= 0
care este ecuaia caracteristic a recurenei (*).
Presupunnd deocamdat c cele k rdcini r
1
, r
2
, , r
k
ale acestei ecuaii
caracteristice sunt distincte, orice combinaie liniar
t c r
n i i
n
i
k
= == =
= == =

1

este o soluie a recurenei (*), unde constantele c
1
, c
2
, , c
k
sunt determinate de
condiiile iniiale. Este remarcabil c (*) are numai soluii de aceast form.

S exemplificm prin recurena care definete irul lui Fibonacci (din Seciunea
1.6.4):
t
n
= t
n1
+ t
n2
n 2
iar t
0
= 0, t
1
= 1. Putem s rescriem aceast recuren sub forma
t
n
t
n1
t
n2
= 0
care are ecuaia caracteristic
x
2
x 1 = 0
Seciunea 5.3 Analiza algoritmilor recursivi 101

cu rdcinile r
1, 2
= (1 5 )/2. Soluia general are forma
t c r c r
n
n n
= == = + ++ +
1 1 2 2

Impunnd condiiile iniiale, obinem
c
1
+ c
2
= 0 n = 0
r
1
c
1
+ r
2
c
2
= 1 n = 1
de unde determinm
c
1, 2
= 1 5 /
Deci, t r r
n
n n
= == = 1 5
1 2
/ ( ) . Observm c r
1
= = (1 + 5 )/2, r
2
=
1
i obinem
t
n
= == = 1 5 / (
n
()
n
)
care este cunoscuta relaie a lui de Moivre, descoperit la nceputul secolului
XVI. Nu prezint nici o dificultate s artm acum c timpul pentru algoritmul
fib1 (din Seciunea 1.6.4) este n (
n
).

Ce facem ns atunci cnd rdcinile ecuaiei caracteristice nu sunt distincte? Se
poate arta c, dac r este o rdcin de multiplicitate m a ecuaiei caracteristice,
atunci t
n
= r
n
, t
n
= nr
n
, t
n
= n
2
r
n
, , t
n
= n
m1
r
n
sunt soluii pentru (*). Soluia
general pentru o astfel de recuren este atunci o combinaie liniar a acestor
termeni i a termenilor provenii de la celelalte rdcini ale ecuaiei caracteristice.
Din nou, sunt de determinat exact k constante din condiiile iniiale.

Vom da din nou un exemplu. Fie recurena
t
n
= 5t
n1
8t
n2
+ 4t
n3
n 3
iar t
0
= 0, t
1
= 1, t
2
= 2. Ecuaia caracteristic are rdcinile 1 (de multiplicitate 1)
i 2 (de multiplicitate 2). Soluia general este:
t
n
= c
1
1
n
+ c
2
2
n
+ c
3
n2
n

Din condiiile iniiale, obinem c
1
= 2, c
2
= 2, c
3
= 1/2.
5.3.4 Recurene liniare neomogene
Considerm acum recurene de urmtoarea form mai general
102 Analiza eficienei algoritmilor Capitolul 5

a
0
t
n
+ a
1
t
n1
+ + a
k
t
nk
= b
n
p(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

n acest caz, b = 3 i p(n) = 1, un polinom de grad 0. O simpl manipulare ne
permite s reducem acest exemplu la forma (*). nmulim recurena cu 3, obinnd
3t
n
6t
n1
= 3
n+1

nlocuind pe n cu n+1 n recurena iniial, avem
t
n+1
2t
n
= 3
n+1

n fine, scdem aceste dou ecuaii
t
n+1
5t
n
+ 6t
n1
= 0
Am obinut o recuren omogen pe care o putem rezolva ca n seciunea
precedent. Ecuaia caracteristic este:
x
2
5x + 6 = 0
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
k
+ a
1
x
k1
+ + a
k
)(xb)
d+1
= 0
Odat ce s-a obinut aceast ecuaie, se procedeaz ca n cazul omogen.

Vom rezolva acum recurena corespunztoare problemei turnurilor din Hanoi:
t
n
= 2t
n1
+ 1 n 1
iar t
0
= 0. Rescriem recurena astfel
t
n
2t
n1
= 1
Seciunea 5.3 Analiza algoritmilor recursivi 103

care este de forma (**) cu b = 1 i p(n) = 1, un polinom de grad 0. Ecuaia
caracteristic este atunci (x2)(x1) = 0, cu soluiile 1 i 2. Soluia general a
recurenei este:
t
n
= c
1
1
n
+ c
2
2
n

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
t
n
= 2
n
1
Dac ne intereseaz doar ordinul lui t
n
, nu este necesar s calculm efectiv
constantele n soluia general. Dac tim c t
n
= c
1
1
n
+ c
2
2
n
, rezult t
n
O(2
n
).
Din faptul c numrul de mutri a unor discuri nu poate fi negativ sau constant,
deoarece avem n mod evident t
n
n, deducem c c
2
> 0. Avem atunci t
n
(2
n
)
i deci, t
n
(2
n
). Putem obine chiar ceva mai mult. Substituind soluia general
napoi n recurena iniial, gsim
1 = t
n
2t
n1
= c
1
+ c
2
2
n
2(c
1
+ c
2
2
n1
) = c
1

Indiferent de condiia iniial, c
1
este deci 1.
5.3.5 Schimbarea variabilei
Uneori, printr-o schimbare de variabil, putem rezolva recurene mult mai
complicate. n exemplele care urmeaz, vom nota cu T(n) termenul general al
recurenei i cu t
k
termenul noii recurene obinute printr-o schimbare de
variabil. Presupunem pentru nceput c n este o putere a lui 2.

Un prim exemplu este recurena
T(n) = 4T(n/2) + n n > 1
n care nlocuim pe n cu 2
k
, notm t
k
= T(2
k
) = T(n) i obinem
t
k
= 4t
k1
+ 2
k

Ecuaia caracteristic a acestei recurene liniare este
(x4)(x2) = 0
104 Analiza eficienei algoritmilor Capitolul 5

i deci, t
k
= c
1
4
k
+ c
2
2
k
. nlocuim la loc pe k cu lg n
T(n) = c
1
n
2
+ c
2
n
Rezult
T(n) O(n
2
| n este o putere a lui 2)

Un al doilea exemplu l reprezint ecuaia
T(n) = 4T(n/2) + n
2
n > 1
Procednd la fel, ajungem la recurena
t
k
= 4t
k1
+ 4
k
cu ecuaia caracteristic
(x4)
2
= 0
i soluia general t
k
= c
1
4
2
+ c
2
k4
2
. Atunci,
T(n) = c
1
n
2
+ c
2
n
2
lg n
i obinem
T(n) O(n
2
log n | n este o putere a lui 2)

n fine, s considerm i exemplul
T(n) = 3T(n/2) + cn n > 1
c fiind o constant. Obinem succesiv
T(2
k
) = 3T(2
k1
) + c2
k

t
k
= 3t
k1
+ c2
k
cu ecuaia caracteristic
(x3)(x2) = 0
t
k
= c
1
3
k
+ c
2
2
k

T(n) = c
1
3
lg n
+ c
2
n
i, deoarece
a
lg b
= b
lg a
Seciunea 5.3 Analiza algoritmilor recursivi 105

obinem
T(n) = c
1
n
lg 3
+ c
2
n
deci,
T(n) O(n
lg 3
| n este o putere a lui 2)

n toate aceste exemple am folosit notaia asimptotic condiionat. Pentru a arta
c rezultatele obinute sunt adevrate pentru orice n, este suficient s adugm
condiia ca T(n) s fie eventual nedescresctoare. Aceasta, datorit Proprietii
5.1 i a faptului c funciile n
2
, n log n i n
lg 3
sunt netede.
Putem enuna acum o proprietate care este util ca reet pentru analiza
algoritmilor cu recursiviti de forma celor din exemplele precedente.
Proprietatea, a crei demonstrare o lsm ca exerciiu, ne va fi foarte util la
analiza algoritmilor divide et impera din Capitolul 7.

Proprietatea 5.2 Fie T : N R
+
o funcie eventual nedescresctoare
T(n) = aT(n/b) + cn
k
n > n
0

unde: n
0
1, b 2 i k 0 sunt ntregi; a i c sunt numere reale pozitive; n/n
0

este o putere a lui b. Atunci avem
T n
n a b
n n a b
n a b
k k
k k
a k
b
( )
( )
log )
( )
log

<
=
>

pentru
( pentru
pentru


5.4 Exerciii
5.1 Care din urmtoarele afirmaii sunt adevrate?
i) n
2
O(n
3
)
ii) n
3
O(n
2
)
iii) 2
n+1
O(2
n
)
iv) (n+1)! O(n!)
v) pentru orice funcie f : N R
*
, f O(n) [ f
2
O(n
2
)]
vi) pentru orice funcie f : N R
*
, f O(n) [2
f
O(2
n
)]
106 Analiza eficienei algoritmilor 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 Pentru oricare dou funcii f, g : N R
*
, demonstrai c:
i) O( f ) = O(g) f O(g) i g O( f )
ii) O( f ) O(g) f O(g) i g O( f )

5.5 Gsii dou funcii f, g : N R
*
, astfel nct f O(g) i g O( f ).
Indicaie: f (n) = n, g(n) = n
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
*
.
Indicaie: Trebuie artat c relaia este parial, reflexiv, tranzitiv i
antisimetric. inei cont de Exerciiul 5.5.

5.7 Pentru oricare dou funcii f, g : N R
*
demonstrai c
O( f + g) = O(max( f, g))
unde suma i maximul se iau punctual.

5.8 Fie f (n) = a
m
n
m
++a
1
n + a
0
un polinom de grad m, cu a
m
> 0. Artai c
f O(n
m
).

5.9 O(n
2
) = O(n
3
+(n
2
n
3
)) = O(max(n
3
, n
2
n
3
)) = O(n
3
)
Unde este eroarea?

5.10 Gsii eroarea n urmtorul lan de relaii:
Seciunea 5.4 Exerciii 107

i
i
n
= == =

1
= 1+2++n O(1+2++n) = O(max(1, 2, , n)) = O(n)

5.11 Fie f , g : N R
+
. Demonstrai c:
i) lim ( ) / ( )
n
f n g n

R
+
O( f ) = O(g)
ii) lim ( ) / ( )
n
f n g n

= 0 O( f ) O(g)
Observaie: Implicaiile inverse nu sunt n general adevrate, deoarece se poate
ntmpla ca limitele s nu existe.

5.12 Folosind regula lui lHspital i Exerciiile 5.4, 5.11, artai c
log n O( n ), dar n O(log n)
Indicaie: Prelungim domeniile funciilor pe R
+
, pe care sunt derivabile i
aplicm regula lui lHspital pentru log n/ n .

5.13 Pentru oricare f, g : N R
*
, demonstrai c:
f O(g) g ( f )

5.14 Artai c f (g) dac i numai dac
(c, d R
+
) (n
0
N) (n n
0
) [cg(n) f (n) dg(n)]

5.15 Demonstrai c urmtoarele propoziii sunt echivalente, pentru oricare
dou funcii f, g : N R
*
.
i) O( f ) = O(g)
ii) ( f ) = (g)
iii) f (g)

5.16 Continund Exerciiul 5.11, artai c pentru oricare dou funcii
f, g : N R
+
avem:
i) lim ( ) / ( )
n
f n g n

R
+
f (g)
ii) lim ( ) / ( )
n
f n g n

= 0 f O(g) dar f (g)


108 Analiza eficienei algoritmilor Capitolul 5

iii) lim ( ) / ( )
n
f n g n

= + f (g) dar f (g)



5.17 Demonstrai urmtoarele afirmaii:
i) log
a
n (log
b
n) pentru oricare a, b > 1
ii) i
k
i
n
= == =

1
(n
k+1
) pentru oricare k N
iii) i
i
n

= == =

1
1
(log n)
iv) log n! (n log n)
Indicaie: La punctul iii) se ine cont de relaia:
i
i

1
1
= ln n + + 1/2n 1/12n
2
+
unde = 0,5772 este constanta lui Euler.
La punctul iv), din n! < n
n
, rezult log n! O(n log n). S artm acum, c
log n! (n log n). Pentru 0 i n1 este adevrat relaia
(ni)(i+1) n
Deoarece
(n!)
2
= (n1) ((n1)2) ((n2)3)(2(n1)) (1n) n
n

rezult 2 log n! n log n i deci log n! (n log n).
Punctul iv) se poate demonstra i altfel, considernd aproximarea lui Stirling:
n n n e n
n
! ( / ) ( ( / )) + 2 1 1
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 Pentru oricare dou funcii f, g : N R
*
demonstrai c
( f )+ (g) = ( f + g) = (max( f, g)) = max(( f ), (g))
unde suma i maximul se iau punctual.
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 Analizai eficiena urmtorilor patru algoritmi:
for i 1 to n do for i 1 to n do
for j 1 to 5 do for j 1 to i+1 do
{operaie elementar} {operaie elementar}
for i 1 to n do for i 1 to n do
for j 1 to 6 do for j 1 to i do
for k 1 to n do for k 1 to n do
{operaie elementar} {operaie elementar}

5.22 Construii un algoritm cu timpul n (n log n).

5.23 Fie urmtorul algoritm
k 0
for i 1 to n do
for j 1 to T[i] do
k k+T[ j]
unde T este un tablou de n ntregi nenegativi. n ce ordin este timpul de execuie
al algoritmului?
Soluie: Fie s suma elementelor lui T. Dac alegem ca barometru instruciunea
k k+T[ j], calculm c ea se execut de s ori. Deci, am putea deduce c
timpul este n ordinul exact al lui s. Un exemplu simplu ne va convinge c am
greit. Presupunem c T[i] = 1, atunci cnd i este un ptrat perfect, i T[i] = 0, n
rest. n acest caz, s = n . Totui, algoritmul necesit timp n ordinul lui (n),
deoarece fiecare element al lui T este considerat cel puin o dat. Nu am inut cont
de urmtoarea regul simpl: putem neglija timpul necesar iniializrii i
controlului unei bucle, dar cu condiia s includem ceva de fiecare dat cnd se
execut bucla.
Iat acum analiza detailat a algoritmului. Fie a timpul necesar pentru o executare
a buclei interioare, inclusiv partea de control. Executarea complet a buclei
interioare, pentru un i dat, necesit b+aT[i] uniti de timp, unde constanta b
reprezint timpul pentru iniializarea buclei. Acest timp nu este zero, cnd
T[i] = 0. Timpul pentru o executare a buclei exterioare este c+b+aT[i], c fiind o
110 Analiza eficienei algoritmilor Capitolul 5

nou constant. n fine, ntregul algoritm necesit d c b aT i
i
n
+ + +
=

( [ ])
1
uniti de
timp, unde d este o alt constant. Simplificnd, obinem (c+b)n+as+d. Timpul
t(n, s) depinde deci de doi parametri independeni n i s. Avem: t (n+s) sau,
innd cont de Exerciiul 5.19, t (max(n, s)).

5.24 Pentru un tablou T[1 .. n], fie urmtorul algoritm de sortare:
for i n downto 1 do
for j 2 to i do
if T[ j1] > T[ j] then interschimb T[ j1] i T[ j]
Aceast tehnic de sortare se numete metoda bulelor (bubble sort).
i) Analizai eficiena algoritmului, lund ca barometru testul din bucla
interioar.
ii) Modificai algoritmul, astfel nct, dac pentru un anumit i nu are loc nici o
interschimbare, atunci algoritmul se oprete. Analizai eficiena noului
algoritm.

5.25 Fie urmtorul algoritm
for i 0 to n do
j i
while j 0 do j j div 2
Gsii ordinul exact al timpului de execuie.

5.26 Demonstrai c pentru oricare ntregi pozitivi n i d
2 2 2 2 2
0
1 1 k
k
d
k d d
n n n
=
+

= lg( / ) lg( / ) lg
Soluie:
2 2 2 1 2
0
1
0
k
k
d
k d k
k
d
n n k
=
+
=

= lg( / ) ( ) lg ( )
Mai rmne s artai c
( ) ( ) 2 1 2 2
0
1 k
k
d
d
k d
=
+

= +
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 Analizai algoritmul slow-make-heap pentru cel mai nefavorabil caz.
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
C(n) =
i
n
=

2
lg i >
i
n
=

2
(lg i 1) = lg n! (n1)
n Exerciiul 5.17 am artat c lg n! (n log n). Rezult C(n) (n log n) i
timpul este deci n (n log n).

5.29 Artai c, pentru cel mai nefavorabil caz, timpul de execuie al
algoritmului heapsort este i n (n log n), deci n (n log n).

5.30 Demonstrai c, pentru cel mai nefavorabil caz, orice algoritm de sortare
prin comparaie necesit un timp n (n log n). n particular, obinem astfel, pe
alt cale, rezultatul din Exerciiul 5.29.
Soluie: Orice sortare prin comparaie poate fi interpretat ca o parcurgere a unui
arbore binar de decizie, prin care se stabilete ordinea relativ a elementelor de
sortat. ntr-un arbore binar de decizie, fiecare vrf neterminal semnific o
comparaie ntre dou elemente ale tabloului T i fiecare vrf terminal reprezint o
permutare a elementelor lui T. Executarea unui algoritm de sortare corespunde
parcurgerii unui drum de la rdcina arborelui de decizie ctre un vrf terminal.
La fiecare vrf neterminal se efectueaz o comparaie ntre dou elemente T[i] i
T[ j]: dac T[i] T[ j] se continu cu comparaiile din subarborele stng, iar n
caz contrar cu cele din subarborele drept. Cnd se ajunge la un vrf terminal,
nseamn c algoritmul de sortare a reuit s stabileasc ordinea elementelor din
T.
112 Analiza eficienei algoritmilor 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
presupune c numrul de vrfuri este minim, adic n!. Avem: n! 2
h
(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 Analizai algoritmii fib2 i fib3 din Seciunea 1.6.4.
Soluie:
i) Se deduce imediat c timpul pentru fib2 este n (n).
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
t

Fie m = 1 + lg n. Deducem:
n
m
n/2
m
< 1
Dar, n
m
N, i deci, n
m
= 0, care este condiia de ieire din bucl. Cu alte
cuvinte, bucla este executat de cel mult m ori, timpul lui fib3 fiind n O(log n).
Artai c timpul este de fapt n (log n).
La analiza acestor doi algoritmi, am presupus implicit c operaiile efectuate sunt
independente de mrimea operanzilor. Astfel, timpul necesar adunrii a dou
numere este independent de mrimea numerelor i este mrginit superior de o
constant. Dac nu mai considerm aceast ipotez, atunci analiza se complic.

5.33 Rezolvai recurena t
n
3t
n1
4t
n2
= 0, unde n 2, iar t
0
= 0, t
1
= 1.
Seciunea 5.4 Exerciii 113


5.34 Care este ordinul timpului de execuie pentru un algoritm recursiv cu
recurena t
n
= 2t
n1
+ n.
Indicaie: Se ajunge la ecuaia caracteristic (x2)(x1)
2
= 0, iar soluia general
este t
n
= c
1
2
n
+ c
2
1
n
+ c
3
n1
n
. Rezult t O(2
n
).
Substituind soluia general napoi n recuren, obinem c, indiferent de condiia
iniial, c
2
= 2 i c
3
= 1. Atunci, toate soluiile interesante ale recurenei trebuie
s aib c
1
> 0 i ele sunt toate n (2
n
), deci n (2
n
).

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
2
n | n este o putere a lui 2)

5.37 Demonstrai Proprietatea 5.2, folosind tehnica schimbrii de variabil.
113
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
Algoritmii greedy (greedy = lacom) sunt n general simpli i sunt folosii la
probleme de optimizare, cum ar fi: s se gseasc cea mai bun ordine de
executare a unor lucrri pe calculator, s se gseasc cel mai scurt drum ntr-un
graf etc. n cele mai multe situaii de acest fel avem:
o mulime de candidai (lucrri de executat, vrfuri ale grafului etc)
o funcie care verific dac o anumit mulime de candidai constituie o soluie
posibil, nu neaprat optim, a problemei
o funcie care verific dac o mulime de candidai este fezabil, adic dac
este posibil s completm aceast mulime astfel nct s obinem o soluie
posibil, nu neaprat optim, a problemei
o funcie de selecie care indic la orice moment care este cel mai promitor
dintre candidaii nc nefolosii
o funcie obiectiv care d valoarea unei soluii (timpul necesar executrii
tuturor lucrrilor ntr-o anumit ordine, lungimea drumului pe care l-am gsit
etc); aceasta este funcia pe care urmrim s o optimizm
(minimizm/maximizm)
Pentru a rezolva problema noastr de optimizare, cutm o soluie posibil care s
optimizeze valoarea funciei obiectiv. Un algoritm greedy construiete soluia pas
cu pas. Iniial, mulimea candidailor selectai este vid. La fiecare pas, ncercm
s adugm acestei mulimi cel mai promitor candidat, conform funciei de
selecie. Dac, dup o astfel de adugare, mulimea de candidai selectai nu mai
este fezabil, eliminm ultimul candidat adugat; acesta nu va mai fi niciodat
considerat. Dac, dup adugare, mulimea de candidai selectai este fezabil,
ultimul candidat adugat va rmne de acum ncolo n ea. De fiecare dat cnd
lrgim mulimea candidailor selectai, verificm dac aceast mulime nu
constituie o soluie posibil a problemei noastre. Dac algoritmul greedy
114 Algoritmi greedy Capitolul 6

funcioneaz corect, prima soluie gsit va fi totodat o soluie optim a
problemei. Soluia optim nu este n mod necesar unic: se poate ca funcia
obiectiv s aib aceeai valoare optim pentru mai multe soluii posibile.
Descrierea formal a unui algoritm greedy general este:
function greedy(C)
{C este mulimea candidailor}
S {S este mulimea n care construim soluia}
while not soluie(S) and C do
x un element din C care maximizeaz/minimizeaz select(x)
C C \ {x}
if fezabil(S {x}) then S S {x}
if soluie(S) then return S
else return nu exist soluie
Este de neles acum de ce un astfel de algoritm se numete lacom (am putea
s-i spunem i nechibzuit). La fiecare pas, procedura alege cel mai bun candidat
la momentul respectiv, fr s-i pese de viitor i fr s se rzgndeasc. Dac un
candidat este inclus n soluie, el rmne acolo; dac un candidat este exclus din
soluie, el nu va mai fi niciodat reconsiderat. Asemenea unui ntreprinztor
rudimentar care urmrete ctigul imediat n dauna celui de perspectiv, un
algoritm greedy acioneaz simplist. Totui, ca i n afaceri, o astfel de metod
poate da rezultate foarte bune tocmai datorit simplitii ei.
Funcia select este de obicei derivat din funcia obiectiv; uneori aceste dou
funcii sunt chiar identice.
Un exemplu simplu de algoritm greedy este algoritmul folosit pentru rezolvarea
urmtoarei probleme. S presupunem c dorim s dm restul unui client, folosind
un numr ct mai mic de monezi. n acest caz, elementele problemei sunt:
candidaii: mulimea iniial de monezi de 1, 5, i 25 uniti, n care
presupunem c din fiecare tip de moned avem o cantitate nelimitat
o soluie posibil: valoarea total a unei astfel de mulimi de monezi selectate
trebuie s fie exact valoarea pe care trebuie s o dm ca rest
o mulime fezabil: valoarea total a unei astfel de mulimi de monezi selectate
nu este mai mare dect valoarea pe care trebuie s o dm ca rest
funcia de selecie: se alege cea mai mare moned din mulimea de candidai
rmas
funcia obiectiv: numrul de monezi folosite n soluie; se dorete minimizarea
acestui numr
Se poate demonstra c algoritmul greedy va gsi n acest caz mereu soluia optim
(restul cu un numr minim de monezi). Pe de alt parte, presupunnd c exist i
monezi de 12 uniti sau c unele din tipurile de monezi lipsesc din mulimea
Seciunea 6.1 Tehnica greedy 115

iniial de candidai, se pot gsi contraexemple pentru care algoritmul nu gsete
soluia optim, sau nu gsete nici o soluie cu toate c exist soluie.
Evident, soluia optim se poate gsi ncercnd toate combinrile posibile de
monezi. Acest mod de lucru necesit ns foarte mult timp.
Un algoritm greedy nu duce deci ntotdeauna la soluia optim, sau la o soluie.
Este doar un principiu general, urmnd ca pentru fiecare caz n parte s
determinm dac obinem sau nu soluia optim.
6.2 Minimizarea timpului mediu de ateptare
O singur staie de servire (procesor, pomp de benzin etc) trebuie s satisfac
cererile a n clieni. Timpul de servire necesar fiecrui client este cunoscut n
prealabil: pentru clientul i este necesar un timp t
i
, 1 i n. Dorim s minimizm
timpul total de ateptare
T
i
n
=
=

1
(timpul de ateptare pentru clientul i)
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
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
Ordinea T
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
116 Algoritmi greedy Capitolul 6

T I t t t t t t nt n t n k t
i i i i i i i i i
k
n
k
( ) ( ) ( ) ... ( ) ... ( ) = + + + + + + = + + = +
=

1 1 2 1 2 3 1 2
1 1
1

Presupunem acum c I este astfel nct putem gsi doi ntregi a < b cu
t t
i i
a b
>
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 t n b t n k t
T I T J n a t t n b t t b a t t
i i i
k
k a b
n
i i i i i i
b a k
a b b a a b
( ) ( ) ( ) ( )
( ) ( ) ( ) ( ) ( ) ( ) ( ) ( )
,
= + + + + +
= + + + = >
=

1 1 1
1 1 0
1

Prin metoda greedy obinem deci ntotdeauna planificarea optim a clienilor.
Problema poate fi generalizat pentru un sistem cu mai multe staii de servire.
6.3 Interclasarea optim a irurilor ordonate
S presupunem c avem dou iruri S
1
i S
2
ordonate cresctor i c dorim s
obinem prin interclasarea lor irul ordonat cresctor care conine elementele din
cele dou iruri. Dac interclasarea are loc prin deplasarea elementelor din cele
dou iruri n noul ir rezultat, atunci numrul deplasrilor este #S
1
+ #S
2
.
Generaliznd, s considerm acum n iruri S
1
, S
2
, , S
n
, fiecare ir S
i
, 1 i n,
fiind format din q
i
elemente ordonate cresctor (vom numi q
i
lungimea lui S
i
). Ne
propunem s obinem irul S ordonat cresctor, coninnd exact elementele din
cele n iruri. Vom realiza acest lucru prin interclasri succesive de cte dou
iruri. Problema const n determinarea ordinii optime n care trebuie efectuate
aceste interclasri, astfel nct numrul total al deplasrilor s fie ct mai mic.
Exemplul de mai jos ne arat c problema astfel formulat nu este banal, adic
nu este indiferent n ce ordine se fac interclasrile.
Fie irurile S
1
, S
2
, S
3
de lungimi q
1
= 30, q
2
= 20, q
3
= 10. Dac interclasm pe S
1

cu S
2
, iar rezultatul l interclasm cu S
3
, numrul total al deplasrilor este
(30+20)+(50+10) = 110. Dac l interclasm pe S
3
cu S
2
, iar rezultatul l
interclasm cu S
1
, numrul total al deplasrilor este (10+20)+(30+30) = 90.
Atam fiecrei strategii de interclasare cte un arbore binar n care valoarea
fiecrui vrf este dat de lungimea irului pe care l reprezint. Dac irurile
Seciunea 6.3 Interclasarea optim a irurilor ordonate 117

S
1
, S
2
, , S
6
au lungimile q
1
= 30, q
2
= 10, q
3
= 20, q
4
= 30, q
5
= 50, q
6
= 10,
dou astfel de strategii de interclasare sunt reprezentate prin arborii din Figura
6.1.
Observm c fiecare arbore are 6 vrfuri terminale, corespunznd celor 6 iruri
iniiale i 5 vrfuri neterminale, corespunznd celor 5 interclasri care definesc
strategia respectiv. Numerotm vrfurile n felul urmtor: vrful terminal i,
1 i 6, va corespunde irului S
i
, iar vrfurile neterminale se numeroteaz de la
7 la 11 n ordinea obinerii interclasrilor respective (Figura 6.2).
Strategia greedy apare n Figura 6.1b i const n a interclasa mereu cele mai
scurte dou iruri disponibile la momentul respectiv.
Interclasnd irurile S
1
, S
2
, , S
n
, de lungimi q
1
, q
2
, , q
n
, obinem pentru
150
140
130
110
80
30
10
10
20
30
50
150
90
40
20
10
60
50
20
10
30 30
(a) (b)

Figura 6.1 Reprezentarea strategiilor de interclasare.
11
10
9
8
7
1
6
2
3
4
5
11
10
8
7
2
9
5
3
6
1 4
(a) (b)

Figura 6.2 Numerotarea vrfurilor arborilor din Figura 6.1.
118 Algoritmi greedy Capitolul 6

fiecare strategie cte un arbore binar cu n vrfuri terminale, numerotate de la 1 la
n, i n1 vrfuri neterminale, numerotate de la n+1 la 2n1. Definim, pentru un
arbore oarecare A de acest tip, lungimea extern ponderat:
L A a q
i i
i
n
( ) =
=

1

unde a
i
este adncimea vrfului i. Se observ c numrul total de deplasri de
elemente pentru strategia corespunztoare lui A este chiar L(A). Soluia optim a
problemei noastre este atunci arborele (strategia) pentru care lungimea extern
ponderat este minim.

Proprietatea 6.1 Prin metoda greedy se obine ntotdeauna interclasarea optim a
n iruri ordonate, deci strategia cu arborele de lungime extern ponderat minim.
Demonstraie: Demonstrm prin inducie. Pentru n = 1, proprietatea este
verificat. Presupunem c proprietatea este adevrat pentru n1 iruri. Fie A
arborele strategiei greedy de interclasare a n iruri de lungime q
1
q
2
q
n
. Fie
B un arbore cu lungimea extern ponderat minim, corespunztor unei strategii
optime de interclasare a celor n iruri. n arborele A apare subarborele
reprezentnd prima interclasare fcut conform strategiei greedy. n arborele B,
fie un vrf neterminal de adncime maxim. Cei doi fii ai acestui vrf sunt atunci
dou vrfuri terminale q
j
i q
k
. Fie B' arborele obinut din B schimbnd ntre ele
vrfurile q
1
i q
j
, respectiv q
2
i q
k
. Evident, L(B') L(B). Deoarece B are
lungimea extern ponderat minim, rezult c L(B') = L(B). Eliminnd din B'
vrfurile q
1
i q
2
, obinem un arbore B" cu n1 vrfuri terminale q
1
+q
2
, q
3
, , q
n
.
Arborele B' are lungimea extern ponderat minim i L(B') = L(B") + (q
1
+q
2
).
Rezult c i B" are lungimea extern ponderat minim. Atunci, conform ipotezei
induciei, avem L(B") = L(A'), unde A' este arborele strategiei greedy de
interclasare a irurilor de lungime q
1
+q
2
, q
3
, , q
n
. Cum A se obine din A'
atand la vrful q
1
+q
2
fiii q
1
i q
2
, iar B' se obine n acelai mod din B", rezult
c L(A) = L(B') = L(B). Proprietatea este deci adevrat pentru orice n.

La scrierea algoritmului care genereaz arborele strategiei greedy de interclasare
q + q
q q
1 2
1 2

Seciunea 6.3 Interclasarea optim a irurilor ordonate 119

vom folosi un min-heap. Fiecare element al min-heap-ului este o pereche (q, i)
unde i este numrul unui vrf din arborele strategiei de interclasare, iar q este
lungimea irului pe care l reprezint. Proprietatea de min-heap se refer la
valoarea lui q.
Algoritmul interopt va construi arborele strategiei greedy. Un vrf i al arborelui
va fi memorat n trei locaii diferite coninnd:
LU[i] = lungimea irului reprezentat de vrf
ST[i] = numrul fiului stng
DR[i] = numrul fiului drept
procedure interopt(Q[1 .. n])
{construiete arborele strategiei greedy de interclasare
a irurilor de lungimi Q[i] = q
i
, 1 i n}
H min-heap vid
for i 1 to n do
(Q[i], i) H {insereaz n min-heap}
LU[i] Q[i]; ST[i] 0; DR[i] 0
for i n+1 to 2n1 do
(s, j) H {extrage rdcina lui H}
(r, k) H {extrage rdcina lui H}
ST[i] j; DR[i] k; LU[i] s+r
(LU[i], i) H {insereaz n min-heap}
n cazul cel mai nefavorabil, operaiile de inserare n min-heap i de extragere din
min-heap necesit un timp n ordinul lui log n (revedei Exerciiul 5.27). Restul
operaiilor necesit un timp constant. Timpul total pentru interopt este deci n
O(n log n).
6.4 Implementarea arborilor de interclasare
Transpunerea procedurii interopt ntr-un limbaj de programare prezint o singur
dificultate generat de utilizarea unui min-heap de perechi vrf-lungime. n
limbajul C++, implementarea arborilor de interclasare este aproape o operaie de
rutin, deoarece clasa parametric heap (Seciunea 4.2.2) permite manipularea
unor heap-uri cu elemente de orice tip n care este definit operatorul de comparare
>. Altfel spus, nu avem dect s construim o clas format din perechi
vrf-lungime (pondere) i s o completm cu operatorul > corespunztor. Vom
numi aceast clas vp, adic vrf-pondere.

120 Algoritmi greedy 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

Scopul clasei vp (definit n fiierul vp.h) nu este de a introduce un nou tip de
date, ci mai curnd de a facilita manipularea structurii vrf-pondere, structur
util i la reprezentarea grafurilor. Din acest motiv, nu exist nici un fel de
ncapsulare, toi membrii fiind publici. Pentru o mai mare comoditate n utilizare,
am inclus n definiie cei doi operatori de conversie, la int, respectiv la float,
precum i operatorii de intrare/ieire.
Nu ne mai rmne dect s precizm structura arborelui de interclasare. Cel mai
simplu este s prelum structura folosit n procedura interopt din Seciunea 6.3:
arborele este format din trei tablouri paralele, care conin lungimea irului
reprezentat de vrful respectiv i indicii celor doi fii. Pentru o scriere mai
compact, vom folosi totui o structur puin diferit: un tablou de elemente de tip
nod, fiecare nod coninnd trei cmpuri corespunztoare informaiilor de mai sus.
Clasa nod este similar clasei vp, att ca structur, ct i prin motivaia
introducerii ei.

Seciunea 6.4 Implementarea arborilor de interclasare 121

class nod {
public:
int lu; // lungimea
int st; // fiul stang
int dr; // fiul drept
};

inline ostream& operator <<( ostream& os, nod& nd ) {
os << " <" << nd.st << "< "
<< nd.lu
<< " >" << nd.dr << "> ";
return os;
}

n limbajul C++, funcia de construire a arborelui strategiei greedy se obine
direct, prin transcrierea procedurii interopt.

tablou<nod> interopt( const tablou<int>& Q ) {
int n = Q.size( );

tablou<nod> A( 2 * n - 1 ); // arborele de interclasare
heap <vp> H( 2 * n - 1 );

for ( int i = 0; i < n; i++ ) {
H.insert( vp(i, Q[i]) );
A[i].lu = Q[i]; A[i].st = A[i].dr = -1;
}
for ( i = n; i < 2 * n - 1; i++ ) {
vp s; H.delete_max( s );
vp r; H.delete_max( r );
A[i].st = s; A[i].dr = r;
A[i].lu = (float)s + (float)r;
H.insert( vp(i, A[i].lu) );
}
return A;
}

Funcia de mai sus conine dou aspecte interesante:
Constructorul vp(int, float) este invocat explicit n funcia de inserare n
heap-ul H. Efectul acestei invocri const n crearea unui obiect temporar de
tip vp, obiect distrus dup inserare. O notaie foarte simpl ascunde deci i o
anumit ineficien, datorat crerii i distrugerii obiectului temporar.
Operatorul de conversie la int este invocat implicit n expresiile A[i].st = s
i A[i].dr = r, iar n expresia A[i].lu = (float)s + (float)r,
operatorul de conversie la float trebuie s fie specificat explicit. Semantica
limbajului C++ este foarte clar relativ la conversii: cele utilizator au
prioritate fa de cele standard, iar ambiguitatea n selectarea conversiilor
posibile este semnalat ca eroare. Dac n primele dou atribuiri conversia lui
s i r la int este singura posibilitate, scrierea celei de-a treia sub forma
122 Algoritmi greedy Capitolul 6

A[i].lu = s + r este ambigu, expresia s + r putnd fi evaluat att ca int
ct i ca float.
n final, nu ne mai rmne dect s testm funcia interopt(). Vom folosi un
tablou l cu lungimi de iruri, lungimi extrase din stream-ul standard de intrare.

main( ) {
tablou<int> l;
cout << "Siruri: "; cin >> l;
cout << "Arborele de interclasare: ";
cout << interopt( l ) << '\n';
return 1;
}

Strategia de interclasare optim pentru cele ase lungimi folosite ca exemplu n
Seciunea 6.3:

[ 6 ] 30 10 20 30 50 10

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
O alt aplicaie a strategiei greedy i a arborilor binari cu lungime extern
ponderat minim este obinerea unei codificri ct mai compacte a unui text.
Un principiu general de codificare a unui ir de caractere este urmtorul: se
msoar frecvena de apariie a diferitelor caractere dintr-un eantion de text i se
atribuie cele mai scurte coduri, celor mai frecvente caractere, i cele mai lungi
coduri, celor mai puin frecvente caractere. Acest principiu st, de exemplu, la
baza codului Morse. Pentru situaia n care codificarea este binar, exist o
metod elegant pentru a obine codul respectiv. Aceast metod, descoperit de
Huffman (1952) folosete o strategie greedy i se numete codificarea Huffman. O
vom descrie pe baza unui exemplu.
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.
Etichetm muchia stng cu 1 i muchia dreapt cu 0. Rearanjm tabelul de
frecvene:
S (10), I (29), O (9), {P, T} (4+5 = 9)
Mulimea {P, T} semnific evenimentul reuniune a celor dou evenimente
independente corespunztoare apariiei literelor P i T. Continum procesul,
obinnd arborele
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
4+5
4 5
1 0

18
9 9
1 0
4 5
1 0

124 Algoritmi greedy Capitolul 6

dup diverse criterii. Lungimea extern ponderat a unui arbore de codificare
este:
a f
i i
i
n
=

1

unde a
i
este adncimea vrfului terminal corespunztor literei i. Se observ c
lungimea extern ponderat este egal cu numrul total de caractere din
codificarea textului considerat. Codificarea cea mai compact a unui text
corespunde deci arborelui de codificare de lungime extern ponderat minim. Se
poate demonstra c arborele de codificare Huffman minimizeaz lungimea extern
ponderat pentru toi arborii de codificare cu vrfurile terminale avnd valorile
f
1
, f
2
, , f
n
. Prin strategia greedy se obine deci ntotdeauna codificarea binar
cea mai compact a unui text.
Arborii de codificare pe care i-am considerat n acest seciune corespund unei
codificri de tip special: codificarea unei litere nu este prefixul codificrii nici
unei alte litere. O astfel de codificare este de tip prefix. Codul Morse nu face
parte din aceast categorie. Codificarea cea mai compact a unui ir de caractere
poate fi ntotdeauna obinut printr-un cod de tip prefix. Deci, concentrndu-ne
atenia asupra acestei categorii de coduri, nu am pierdut nimic din generalitate.
6.6 Arbori pariali de cost minim
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,
9 0
1 0
4 5
57
28 29
1 0
10 18
1 0
S
I
P T
O
1 0

Figura 6.3 Arborele de codificare Huffman.
Seciunea 6.6 Arbori pariali de cost minim 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). Eliminndu-
l 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 Algoritmul lui Kruskal
Arborele parial de cost minim poate fi construit muchie, cu muchie, dup
urmtoarea metod a lui Kruskal (1956): se alege nti muchia de cost minim, iar
126 Algoritmi greedy Capitolul 6

apoi se adaug repetat muchia de cost minim nealeas anterior i care nu formeaz
cu precedentele un ciclu. Alegem astfel #V1 muchii. Este uor de dedus c
obinem n final un arbore (revedei Exerciiul 3.2). Este ns acesta chiar arborele
parial de cost minim cutat?
nainte de a rspunde la ntrebare, s considerm, de exemplu, graful din Figura
6.4a. Ordonm cresctor (n funcie de cost) muchiile grafului: {1, 2}, {2, 3},
{4, 5}, {6, 7}, {1, 4}, {2, 5}, {4, 7}, {3, 5}, {2, 4}, {3, 6}, {5, 7}, {5, 6} i apoi
aplicm algoritmul. Structura componentelor conexe este ilustrat, pentru fiecare
pas, n Tabelul 6.1.
Mulimea A este iniial vid i se completeaz pe parcurs cu muchii acceptate
1 2 3
6 5 4
7
1 2
4
6
4
5
6
3 8
7
4
3
1 2 3
6 5 4
7
1 2
4
3
4 3
(a) (b)

Figura 6.4 Un graf i arborele su parial de cost minim.
Pasul Muchia considerat Componentele conexe ale
subgrafului <V, A>
iniializare {1}, {2}, {3}, {4}, {5}, {6}, {7}
1 {1, 2} {1, 2}, {3}, {4}, {5}, {6}, {7}
2 {2, 3} {1, 2, 3}, {4}, {5}, {6}, {7}
3 {4, 5} {1, 2, 3}, {4, 5}, {6}, {7}
4 {6, 7} {1, 2, 3}, {4, 5}, {6, 7}
5 {1, 4} {1, 2, 3, 4, 5}, {6, 7}
6 {2, 5} respins (formeaz ciclu)
7 {4, 7} {1, 2, 3, 4, 5, 6, 7}
Tabelul 6.1 Algoritmul lui Kruskal aplicat grafului din Figura 6.4a.
Seciunea 6.6 Arbori pariali de cost minim 127

(care nu formeaz un ciclu cu muchiile deja existente n A). n final, mulimea A
va conine muchiile {1, 2}, {2, 3}, {4, 5}, {6, 7}, {1, 4}, {4, 7}. La fiecare pas,
graful parial <V, A> formeaz o pdure de componente conexe, obinut din
pdurea precedent unind dou componente. Fiecare component conex este la
rndul ei un arbore parial de cost minim pentru vrfurile pe care le conecteaz.
Iniial, fiecare vrf formeaz o component conex. La sfrit, vom avea o singur
component conex, care este arborele parial de cost minim cutat (Figura 6.4b).
Ceea ce am observat n acest caz particular este valabil i pentru cazul general,
din Proprietatea 6.2 rezultnd:

Proprietatea 6.3 n algoritmul lui Kruskal, la fiecare pas, graful parial <V, A>
formeaz o pdure de componente conexe, n care fiecare component conex este
la rndul ei un arbore parial de cost minim pentru vrfurile pe care le conecteaz.
n final, se obine arborele parial de cost minim al grafului G.

Pentru a implementa algoritmul, trebuie s putem manipula submulimile formate
din vrfurile componentelor conexe. Folosim pentru aceasta o structur de
mulimi disjuncte i procedurile de tip find i merge (Seciunea 3.5). n acest caz,
este preferabil s reprezentm graful ca o list de muchii cu costul asociat lor,
astfel nct s putem ordona aceast list n funcie de cost. Iat algoritmul:
function Kruskal(G = <V, M>)
{iniializare}
sorteaz M cresctor n funcie de cost
n #V
A {va conine muchiile arborelui parial de cost minim}
iniializeaz n mulimi disjuncte coninnd
fiecare cte un element din V
{bucl greedy}
repeat
{u, v} muchia de cost minim care
nc nu a fost considerat
ucomp find(u)
vcomp find(v)
if ucomp vcomp then merge(ucomp, vcomp)
A A {{u, v}}
until #A = n1
return A
Pentru un graf cu n vrfuri i m muchii, presupunnd c se folosesc procedurile
find3 i merge3, numrul de operaii pentru cazul cel mai nefavorabil este n:
128 Algoritmi greedy Capitolul 6

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 n1 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 Algoritmul lui Prim
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
Pasul Muchia considerat U
iniializare {1}
1 {2, 1} {1, 2}
2 {3, 2} {1, 2, 3}
3 {4, 1} {1, 2, 3, 4}
4 {5, 4} {1, 2, 3, 4, 5}
5 {7, 4} {1, 2, 3, 4, 5, 6}
6 {6, 7} {1, 2, 3, 4, 5, 6, 7}
Tabelul 6.2 Algoritmul lui Prim aplicat grafului din Figura 6.4a.
Seciunea 6.6 Arbori pariali de cost minim 129

dintre extremitile acestei muchii este un vrf n arborele precedent). Arborele
parial de cost minim crete natural, cu cte o ramur, pn cnd va atinge toate
vrfurile din V, adic pn cnd U = V. Funcionarea algoritmului, pentru
exemplul din Figura 6.4a, este ilustrat n Tabelul 6.2. La sfrit, A va conine
aceleai muchii ca i n cazul algoritmului lui Kruskal. Faptul c algoritmul
funcioneaz ntotdeauna corect este exprimat de urmtoarea proprietate, pe care o
putei demonstra folosind Proprietatea 6.2.

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.

Descrierea formal a algoritmului este dat n continuare.
function Prim-formal(G = <V, M>)
{iniializare}
A {va conine muchiile arborelui parial de cost minim}
U {un vrf oarecare din V}
{bucl greedy}
while U V do
gsete {u, v} de cost minim astfel ca u V \ U i v U
A A {{u, v}}
U U {u}
return A
Pentru a obine o implementare simpl, presupunem c: vrfurile din V sunt
numerotate de la 1 la n, V = {1, 2, , n}; matricea simetric C d costul fiecrei
muchii, cu C[i, j] = +, dac muchia {i, j} nu exist. Folosim dou tablouri
paralele. Pentru fiecare i V \ U, vecin[i] conine vrful din U, care este conectat
de i printr-o muchie de cost minim; mincost[i] d acest cost. Pentru i U, punem
mincost[i] = 1. Mulimea U, n mod arbitrar iniializat cu {1}, nu este
reprezentat explicit. Elementele vecin[1] i mincost[1] nu se folosesc.
130 Algoritmi greedy Capitolul 6

function Prim(C[1 .. n, 1 .. n])
{iniializare; numai vrful 1 este n U}
A
for i 2 to n do vecin[i] 1
mincost[i] C[i, 1]
{bucl greedy}
repeat n1 times
min +
for j 2 to n do
if 0 < mincost[ j] < min then min mincost[ j]
k j
A A {{k, vecin[k]}}
mincost[k] 1 {adaug vrful k la U}
for j 2 to n do
if C[k, j] < mincost[ j] then mincost[ j] C[k, j]
vecin[ j] k
return A
Bucla principal se execut de n1 ori i, la fiecare iteraie, buclele for din
interior necesit un timp n O(n). Algoritmul Prim necesit, deci, un timp n
O(n
2
). Am vzut c timpul pentru algoritmul lui Kruskal este n O(m log n), unde
m = #M. Pentru un graf dens (adic, cu foarte multe muchii), se deduce c m se
apropie de n(n1)/2. n acest caz, algoritmul Kruskal necesit un timp n
O(n
2
log n) i algoritmul Prim este probabil mai bun. Pentru un graf rar (adic, cu
un numr foarte mic de muchii), m se apropie de n i algoritmul Kruskal necesit
un timp n O(n log n), fiind probabil mai eficient dect algoritmul Prim.
6.7 Implementarea algoritmului lui Kruskal
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 Implementarea algoritmului lui Kruskal 131

int ucomp = s.find3( m.u ),
vcomp = s.find3( m.v );

if ( ucomp != vcomp ) {
s.merge3( ucomp, vcomp );
A[ nA++ ] = m;
}
} while ( nA != n - 1 );

return A;
}

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;
}

pentru graful din Figura 6.4a, obinem urmtoarele rezultate:

Arborele de cost minim Kruskal:
[6]: { 1, 2; 1 } { 2, 3; 2 } { 4, 5; 3 } { 6, 7; 3 }
{ 1, 4; 4 } { 4, 7; 4 }

Clasa muchie, folosit n implementarea algoritmului lui Kruskal, trebuie s
permit:
132 Algoritmi greedy Capitolul 6

Iniializarea obiectelor, inclusiv cu valori implicite (iniializare util la
construirea tablourilor de muchii).
Compararea obiectelor n funcie de cost (operaie folosit de min-heap).
Operaii de citire i scriere (invocate indirect de operatorii respectivi din clasa
tablou<T>).
Pornind de la aceste cerine, se obine urmtoarea implementare, coninut n
fiierul muchie.h.

#ifndef __MUCHIE_H
#define __MUCHIE_H

class muchie {
public:
muchie( int iu = 0, int iv = 0, float ic = 0. )
{ u = iu; v = iv; cost = ic; }

int u, v;
float cost;
};

inline operator >( const muchie& a, const muchie& b ) {
return a.cost < b.cost;
}

inline istream& operator >>( istream& is, muchie& m ) {
is >> m.u >> m.v >> m.cost; m.u--; m.v--;
return is;
}

inline ostream& operator<< ( ostream& os, muchie& m ) {
return os << "{ " << (m.u+1) << ", " << (m.v+1)
<< "; " << m.cost << " }";
}

#endif

n ceea ce privete clasa set, folosit i ea n implementarea algoritmului
Kruskal, vom urma precizrile din Seciunea 3.5 relative la manipularea
mulimilor disjuncte. ncapsularea, ntr-o clas, a structurii de mulimi disjuncte i
a procedurilor find3 i merge3 nu prezint nici un fel de dificulti. Vom prezenta,
totui, implementarea clasei set, deoarece spaiul de memorie folosit este redus la
jumtate.
La o analiz mai atent a procedurii merge3, observm c tabloul nlimii
arborilor este folosit doar pentru elementele care sunt i etichete de mulimi (vezi
Exerciiul 3.13). Aceste elemente, numite elemente canonice, sunt rdcini ale
arborilor respectivi. Altfel spus, un element canonic nu are tat i valoarea lui
este folosit doar pentru a-l diferenia de elementele care nu sunt canonice. n
Seciunea 6.7 Implementarea algoritmului lui Kruskal 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 set::merge3( int a, int b ) {
// sunt a si b etichete de multimi?
if ( set[ a ] >= 0 ) a = find3( a );
if ( set[ b ] >= 0 ) b = find3( b );

// sunt multimile a si b diferite?
if ( a == b ) return;

// reuniunea propriu-zisa
if ( set[ a ] == set[ b ] ) set[ set[ b ] = a ]--;
else if ( set[ a ] < set[ b ] ) set[ b ] = a;
else set[ a ] = b;

return;
}

134 Algoritmi greedy Capitolul 6

int set::find3( int x ) {
int r = x;
while ( set[ r ] >= 0 )
r = set[ r ];

int i = x;
while ( i != r )
{ int j = set[ i ]; set[ i ] = r; i = j; }

return r;
}

Fiierul header set.h este:

#ifndef __SET_H
#define __SET_H

#include "heap.h"

class set {
public:
set( int );
void merge3( int, int );
int find3 ( int );

private:
tablou<int> set;
};

#endif

6.8 Cele mai scurte drumuri care pleac din acelai
punct
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 Cele mai scurte drumuri care pleac din acelai punct 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:
1 2
4 3
5
50
50
5 100
30
20
10
10

Figura 6.5 Un graf orientat.
136 Algoritmi greedy Capitolul 6


Proprietatea 6.5. n algoritmul lui Dijkstra, dac un vrf i
i) este n S, atunci D[i] d lungimea celui mai scurt drum de la surs ctre i;
ii) nu este n S, atunci D[i] d lungimea celui mai scurt drum special de la surs
ctre i.

La terminarea algoritmului, toate vrfurile grafului, cu excepia unuia, sunt n S.
Din proprietatea precedent, rezult c algoritmul lui Dijkstra funcioneaz
corect.
Dac dorim s aflm nu numai lungimea celor mai scurte drumuri, dar i pe unde
trec ele, este suficient s adugm un tablou P[2 .. n], unde P[v] conine numrul
nodului care l precede pe v n cel mai scurt drum. Pentru a gsi drumul complet,
nu avem dect s urmrim, n tabloul P, vrfurile prin care trece acest drum, de la
destinaie la surs. Modificrile n algoritm sunt simple:
iniializeaz P[i] cu 1, pentru 2 i n
coninutul buclei for cea mai interioar se nlocuiete cu
if D[w] > D[v]+L[v, w] then D[w] D[v]+L[v, w]
P[w] v
bucla repeat se execut de n1 ori
S presupunem c aplicm algoritmul Dijkstra asupra unui graf cu n vrfuri i m
muchii. Iniializarea necesit un timp n O(n). Alegerea lui v din bucla repeat
presupune parcurgerea tuturor vrfurilor coninute n C la iteraia respectiv, deci
a n1, n2, , 2 vrfuri, ceea ce necesit n total un timp n O(n
2
). Bucla for
interioar efectueaz n2, n3, , 1 iteraii, totalul fiind tot n O(n
2
). Rezult c
algoritmul Dijkstra necesit un timp n O(n
2
).
ncercm s mbuntim acest algoritm. Vom reprezenta graful nu sub forma
matricii de adiacen L, ci sub forma a n liste de adiacen, coninnd pentru
fiecare vrf lungimea muchiilor care pleac din el. Bucla for interioar devine
astfel mai rapid, deoarece putem s considerm doar vrfurile w adiacente lui v.
Aceasta nu poate duce la modificarea ordinului timpului total al algoritmului,
Pasul v C D
iniializare {2, 3, 4, 5} [50, 30, 100, 10]
1 5 {2, 3, 4} [50, 30, 20, 10]
2 4 {2, 3} [40, 30, 20, 10]
3 3 {2} [35, 30, 20, 10]
Tabelul 6.3 Algoritmul lui Dijkstra aplicat grafului din Figura 6.5.
Seciunea 6.8 Cele mai scurte drumuri care pleac din acelai punct 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 Implementarea algoritmului lui Dijkstra
Aceast seciune este dedicat implementrii algoritmului Dijkstra-modificat
pentru determinarea celor mai scurte drumuri care pleac din acelai vrf. Dup
cum am vzut, acest algoritm este de preferat n cazul grafurilor rare, timpul lui
fiind n ordinul lui O(m log n), unde m este numrul de muchii, iar n numrul de
vrfuri ale unui graf conex.
n implementarea noastr, tipul de date fundamental este clasa vp
(vrf-pondere), definit cu ocazia implementrii arborilor de interclasare. Vom
folosi aceast clas pentru:
Min-heap-ul C format din perechi (v, d), ponderea d fiind lungimea celui mai
scurt drum special de la vrful surs la vrful v.
138 Algoritmi greedy Capitolul 6

Reprezentarea grafului G prin liste de adiacen. Pentru fiecare vrf v,
perechea (w, l) este muchia de lungime l cu extremitile n v i w.
Tabloul P, al rezultatelor. Elementul P[i], de valoare (v, d), reprezint vrful
v care precede vrful i n cel mai scurt drum de la vrful surs, d fiind
lungimea acestui drum.
Graful G este implementat ca un tablou de liste de elemente de tip vrf-pondere.
Tipul graf, introdus prin

typedef tablou< lista<vp> > graf;

este un sinonim pentru aceast structur.
Definiia de mai sus merit o clip de atenie, deoarece exemplific una din
puinele excepii lexicale din C++. n limbajul C++, ca i n limbajul C, noiunea
de separator este inexistent. Separarea atomilor lexicali ai limbajului
(identificatori, operatori, cuvinte cheie, constante) prin caracterele albe spaiu
sau tab este opional. Totui, n typedef-ul anterior, cele dou semne > trebuie
separate, pentru a nu fi interpretate ca operatorul de decalare >>.
Manipularea grafului G, definit ca graf G, implic fixarea unui vrf i apoi
operarea asupra listei asociate vrfului respectiv. Pentru o simpl parcurgere, nu
avem dect s definim iteratorul iterator<vp> g i s-l iniializm cu una din
listele de adiacen, de exemplu cu cea corespunztoare vrfului 2: g = G[ 2 ];.
Dac w este un obiect de tip vp, atunci, prin instruciunea

while( g( w ) ) {
// ...
}

obiectul w va conine, rnd pe rnd, toate extremitile i lungimile muchiilor care
pleac din vrful 2.
Structura obiectului graf G asociat grafului din Figura 6.5, structur tiprit prin
cout << G, este:

[5]: { { 5; 10 } { 4; 100 } { 3; 30 } { 2; 50 } }
{ }
{ { 4; 50 } { 2; 5 } }
{ { 2; 20 } }
{ { 4; 10 } }

Executarea acestei instruciuni implic invocarea operatorilor de inserare << ai
tuturor celor 3 clase implicate, adic vp, tablou<T> i lista<E>.
Seciunea 6.9 Implementarea algoritmului lui Dijkstra 139

Citirea grafului G se realizeaz prin citirea muchiilor i inserarea lor n listele de
adiacen. n acest scop, vom folosi aceeai clas muchie, utilizat i n
implementarea algoritmului lui Kruskal:

int n, m = 0; // #varfuri si #muchii
muchie M;

cout << "Numarul de varfuri... "; cin >> n;
graf G( n );

cout << "Muchiile... ";
while( cin >> M ) {
// aici se poate verifica corectitudinea muchiei M
G[ M.u ].insert( vp( M.v, M.cost ) );
m++;
}

Algoritmul Dijkstra-modificat este implementat prin funcia

tablou<vp> Dijkstra( const graf& G, int m, int s );

funcie care returneaz tabloul tablou<vp> P(n). n lista de argumente a acestei
funcii, m este numrul de muchii, iar s este vrful surs. Dup cum am menionat,
P[i].v (sau (int)P[i]) este vrful care precede vrful i pe cel mai scurt drum
de la surs ctre i, iar P[i].p (sau (float)P[i]) este lungimea acestui drum. De
exemplu, pentru acelai graf din Figura 6.5, secvena:

for ( int s = 0; s < n; s++ ) {
cout << "\nCele mai scurte drumuri de la varful "
<< (s + 1) << " sunt:\n";
cout << 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

Cele mai scurte drumuri de la varful 4 sunt:
[5]: { 4; 3.37e+38 } { 4; 20 } { 4; 3.37e+38 }
{ 1; 0 } { 4; 3.37e+38 }

Cele mai scurte drumuri de la varful 5 sunt:
[5]: { 5; 3.37e+38 } { 4; 30 } { 5; 3.37e+38 }
{ 5; 10 } { 1; 0 }

unde 3.37e+38 este constanta MAXFLOAT din fiierul header <values.h>.
MAXFLOAT este o aproximare rezonabil pentru +, fiind cel mai mare numr real
admis de calculatorul pentru care se compileaz programul.
Datele locale funciei Dijkstra() sunt heap-ul heap<vp> C(n + m) i tabloul
tablou<vp> P(n) al celor mai scurte drumuri (incluznd i distanele respective)
de la fiecare din cele n vrfuri la vrful surs. Iniial, distanele din P[s] sunt +
(constanta MAXFLOAT din <values.h>), exceptnd vrful s i celelalte vrfuri
adiacente lui s, vrfuri incluse i n heap-ul C. Iniializarea variabilelor P i C
este realizat prin secvena:

vp w;

// initializare
for ( int i = 0; i < n; i++ )
P[ i ] = vp( s, MAXFLOAT );
for ( iterator<vp> g = G[ s ]; g( w ); )
{ C.insert( w ); P[ w ] = vp( s, w ); }
P[ s ] = vp( 0, 0 );

Se observ aici invocarea explicit a constructorului clasei vp pentru iniializarea
elementelor tabloului P. Din pcate, iniializarea nu este direct, ci prin
intermediul unui obiect temporar de tip vp, obiect distrus dup atribuire.
Iniializarea direct este posibil, dac vom completa clasa vp cu o funcie de
genul

vp& vp::set( int varf, float pondere )
{ v = varf; p = pondere; return *this; }

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 Implementarea algoritmului lui Dijkstra 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 );

Bucla greedy a funciei Dijkstra()

vp v;
float dw;

// 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 ) ) );
}

se obine prin traducerea direct a descrierii algoritmului Dijkstra-modificat.
Fiind dificil s cutm n heap-ul C elemente (w, D[w]) dup valoarea lui w, am
inlocuit urmtoarele operaii:
i) cutarea elementului (w, D[w]) pentru un w fixat
ii) modificarea valorii D[w]
iii) refacerea proprietii de heap
cu o simpl inserare n heap a unui nou element (w, D[w]), D[w] fiind modificat
corespunztor. Din pcate, aceast simplificare poate mri heap-ul, deoarece
exist posibilitatea ca pentru fiecare muchie s fie inserat cte un nou element.
Numrul de elemente din heap va fi ns totdeauna mai mic dect n + m. Timpul
algoritmului rmne n O(m log n).
Crearea unui obiect temporar la inserarea n heap este justificat aici chiar prin
algoritm. Conform precizrilor de mai sus, actualizarea distanelor se realizeaz
indirect, prin inserarea unui nou obiect. S remarcm i nlocuirea tabloului
redundant D cu membrul float din tabloul P.
n final, dup executarea de n-2 ori a buclei greedy, funcia Dijkstra() trebuie
s returneze tabloul P:

return P;

Dac secvenele prezentate pn acum nu v sunt suficiente pentru a scrie funcia
Dijkstra() i programul de test, iat forma lor complet:
142 Algoritmi greedy Capitolul 6


#include <iostream.h>
#include <values.h>

#include "tablou.h"
#include "heap.h"
#include "muchie.h"
#include "lista.h"
#include "vp.h"

typedef tablou< lista<vp> > graf;

tablou<vp> Dijkstra( const graf& G, int m, int s ) {
int n = G.size( ); // numarul de varfuri ale grafului G

heap<vp> C( m );
tablou<vp> P( n );

vp v, w; // muchii
float dw; // 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; // #varfuri si #muchii
muchie M;

cout << "Numarul de varfuri... "; cin >> n;
graf G( n );

Seciunea 6.9 Implementarea algoritmului lui Dijkstra 143

cout << "Muchiile... ";
while( cin >> M ) {
// aici se poate verifica corectitudinea muchiei M
G[ M.u ].insert( vp( M.v, M.cost ) );
m++;
}

cout << "\nListele de adiacenta:\n"; cout << G << '\n';

for ( int s = 0; s < n; s++ ) {
cout << "\nCele mai scurte drumuri de la varful "
<< (s + 1) << " sunt:\n";
cout << Dijkstra( G, m, s ) << '\n';
}

return 0;
}
6.10 Euristica greedy
Pentru anumite probleme, se poate accepta utilizarea unor algoritmi despre care
nu se tie dac furnizeaz soluia optim, dar care furnizeaz rezultate
acceptabile, sunt mai uor de implementat i mai eficieni dect algoritmii care
dau soluia optim. Un astfel de algoritm se numete euristic.
Una din ideile frecvent utilizate n elaborarea algoritmilor euristici const n
descompunerea procesului de cutare a soluiei optime n mai multe subprocese
succesive, fiecare din aceste subprocese constnd dintr-o optimizare. O astfel de
strategie nu poate conduce ntotdeauna la o soluie optim, deoarece alegerea unei
soluii optime la o anumit etap poate mpiedica atingerea n final a unei soluii
optime a ntregii probleme; cu alte cuvinte, optimizarea local nu implic, n
general, optimizarea global. Regsim, de fapt, principiul care st la baza metodei
greedy. Un algoritm greedy, despre care nu se poate demonstra c furnizeaz
soluia optim, este un algoritm euristic.
Vom da dou exemple de utilizare a algoritmilor greedy euristici.
6.10.1 Colorarea unui graf
Fie G = <V, M> un graf neorientat, ale crui vrfuri trebuie colorate astfel nct
oricare dou vrfuri adiacente s fie colorate diferit. Problema este de a obine o
colorare cu un numr minim de culori.
Folosim urmtorul algoritm greedy: alegem o culoare i un vrf arbitrar de
pornire, apoi considerm vrfurile rmase, ncercnd s le colorm, fr a
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.

*
K. Appel i W. Haken, n 1976.
1 2
3
4
5

Figura 6.6 Un graf care va fi colorat.
Seciunea 6.10 Euristica greedy 145

6.10.2 Problema comis-voiajorului
Se cunosc distanele dintre mai multe orae. Un comis-voiajor pleac dintr-un ora
i dorete s se ntoarc n acelai ora, dup ce a vizitat fiecare din celelalte
orae exact o dat. Problema este de a minimiza lungimea drumului parcurs. i
pentru aceast problem, toi algoritmii care gsesc soluia optim sunt
exponeniali.
Problema poate fi reprezentat printr-un graf neorientat, n care oricare dou
vrfuri diferite ale grafului sunt unite ntre ele printr-o muchie, de lungime
nenegativ. Cutm un ciclu de lungime minim, care s se nchid n vrful
iniial i care s treac prin toate vrfurile grafului.
Conform strategiei greedy, vom construi ciclul pas cu pas, adugnd la fiecare
iteraie cea mai scurt muchie disponibil cu urmtoarele proprieti:
nu formeaz un ciclu cu muchiile deja selectate (exceptnd pentru ultima
muchie aleas, care completeaz ciclul)
nu exist nc dou muchii deja selectate, astfel nct cele trei muchii s fie
incidente n acelai vrf
De exemplu, pentru ase orae a cror matrice a distanelor este dat n Tabelul
6.4, muchiile se aleg n ordinea: {1, 2}, {3, 5}, {4, 5}, {2, 3}, {4, 6}, {1, 6} i se
obine ciclul (1, 2, 3, 5, 4, 6, 1) de lungime 58. Algoritmul greedy nu a gsit
ciclul optim, deoarece ciclul (1, 2, 3, 6, 4, 5, 1) are lungimea 56.
6.11 Exerciii
6.1 Presupunnd c exist monezi de:
La:
De la:
2 3 4 5 6
1 3 10 11 7 25
2 6 12 8 26
3 9 4 20
4 5 15
5 18
Tabelul 6.4 Matricea distanelor pentru problema comis-voiajorului.
146 Algoritmi greedy Capitolul 6

i) 1, 5, 12 i 25 de uniti, gsii un contraexemplu pentru care algoritmul
greedy nu gsete soluia optim;
ii) 10 i 25 de uniti, gsii un contraexemplu pentru care algoritmul greedy nu
gsete nici o soluie cu toate c exist soluie.

6.2 Presupunnd c exist monezi de:
k
0
, k
1
, , k
n1

uniti, pentru k N, k > 1 oarecare, artai c metoda greedy d mereu soluia
optim. Considerai c n este un numr finit i c din fiecare tip de moned exist
o cantitate nelimitat.

6.3 Pe o band magnetic sunt n programe, un program i de lungime l
i
fiind
apelat cu probabilitatea p
i
, 1 i n, p
1
+p
2
++p
n
= 1. Pentru a citi un program,
trebuie s citim banda de la nceput. n ce ordine s memorm programele pentru
a minimiza timpul mediu de citire a unui program oarecare?
Indicaie: Se pun n ordinea descresctoare a rapoartelor p
i
/ l
i
.

6.4 Analizai eficiena algoritmului greedy care planific ordinea clienilor
ntr-o staie de servire, minimiznd timpul mediu de ateptare.

6.5 Pentru un text format din n litere care apar cu frecvenele f
1
, f
2
, , f
n
,
demonstrai c arborele de codificare Huffman minimizeaz lungimea extern
ponderat pentru toi arborii de codificare cu vrfurile terminale avnd valorile
f
1
, f
2
, , f
n
.

6.6 Ci bii ocup textul ABRACADABRA dup codificarea Huffman?

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 Elaborai algoritmul de compactare Huffman a unui ir de caractere.

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.
Un cod Gray este o secven de 2
n
elemente astfel nct:
i) fiecare element este un ir de n bii
ii) oricare dou elemente sunt diferite
iii) oricare dou elemente consecutive difer exact printr-un bit (primul element
este considerat succesorul ultimului element)
Se observ c un cod Gray nu este de tip prefix. Elaborai un algoritm recursiv
pentru a construi codul Gray pentru orice n dat. Gndii-v cum ai putea utiliza
un astfel de cod.
Indicaie: Pentru n = 1 putem folosi secvena (0, 1). Presupunem c avem un cod
Gray pentru n1, unde n > 1. Un cod Gray pentru n poate fi construit prin
concatenarea a dou subsecvene. Prima se obine prefixnd cu 0 fiecare element
al codului Gray pentru n1. A doua se obine citind n ordine invers codul Gray
pentru n1 i prefixnd cu 1 fiecare element rezultat.

6.11 Demonstrai c graful parial definit ca arbore parial de cost minim este
un arbore.
Indicaie: Artai c orice graf conex cu n vrfuri are cel puin n1 muchii i
revedei Exerciiul 3.2.

6.12 Dac n algoritmul lui Kruskal reprezentm graful nu printr-o list de
muchii, ci printr-o matrice de adiacen, care conine costurile muchiilor, ce se
poate spune despre timp?

6.13 Ce se ntmpl dac rulm algoritmul i) Kruskal, ii) Prim pe un graf
neconex?

6.14 Ce se ntmpl n cazul algoritmului: i) Kruskal, ii) Prim dac permitem
muchiilor s aib cost negativ?

6.15 S presupunem c am gsit arborele parial de cost minim al unui graf G.
Elaborai un algoritm de actualizare a arborelui parial de cost minim, dup ce am
adugat n G un nou vrf, mpreun cu muchiile incidente lui. Analizai algoritmul
obinut.
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 Scriei algoritmul greedy pentru colorarea unui graf i analizai eficiena
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 f f
k k k
r
= + +
1 2
...
unde
k
1
>> k
2
>> >> k
r
>> 0
n acest reprezentare Fibonacci a numerelor, singura valoare posibil pentru f
k
1

este cel mai mare termen din irul lui Fibonacci pentru care f i
k
1
; singura
valoare posibil pentru f
k
2
este cel mai mare termen pentru care f i f
k k
2 1
etc.
Reprezentarea Fibonacci a unui numr nu conine niciodat doi termeni
consecutivi ai irului lui Fibonacci.
Pentru 0 i f
n
1, n 3, numim codificarea Fibonacci de ordinul n al lui i
secvena de bii b
n1
, b
n2
, , b
2
, unde
i b f
j j
j
n
=
=

2
1

Seciunea 6.11 Exerciii 149

este reprezentarea Fibonacci a lui i. De exemplu, pentru i = 6, codificarea de
ordinul 6 este 1001, iar codificarea de ordinul 7 este 01001. Se observ c n
codificarea Fibonacci nu apar doi de 1 consecutiv.
Dai un algoritm pentru determinarea codificrii Fibonacci de ordinul n al lui i,
unde n i i sunt oarecare.

6.22 Codul Fibonacci de ordinul n, n 2, este secvena C
n
a celor f
n
codificri
Fibonacci de ordinul n ale lui i, atunci cnd i ia toate valorile 0 i f
n
1. De
exemplu, dac notm cu irul nul, obinem: C
2
= (), C
3
= (0, 1),
C
4
= (00, 01, 10), C
5
= (000, 001, 010, 100, 101) etc. Elaborai un algoritm
recursiv care construiete codul Fibonacci pentru orice n dat. Gndii-v cum ai
putea utiliza un astfel de cod.
Indicaie: Artai c putem construi codul Fibonacci de ordinul n, n 4, prin
concatenarea a dou subsecvene. Prima subsecven se obine prefixnd cu 0
fiecare codificare din C
n1
. A doua subsecven se obine prefixnd cu 10 fiecare
codificare din C
n2
.
149
7. Algoritmi divide et
impera
7.1 Tehnica divide et impera
Divide et impera este o tehnic de elaborare a algoritmilor care const n:
Descompunerea cazului ce trebuie rezolvat ntr-un numr de subcazuri mai
mici ale aceleiai probleme.
Rezolvarea succesiv i independent a fiecruia din aceste subcazuri.
Recompunerea subsoluiilor astfel obinute pentru a gsi soluia cazului iniial.
S presupunem c avem un algoritm A cu timp ptratic. Fie c o constant, astfel
nct timpul pentru a rezolva un caz de mrime n este t
A
(n) cn
2
. S presupunem
c este posibil s rezolvm un astfel de caz prin descompunerea n trei subcazuri,
fiecare de mrime n/2(. Fie d o constant, astfel nct timpul necesar pentru
descompunere i recompunere este t(n) dn. Folosind vechiul algoritm i ideea de
descompunere-recompunere a subcazurilor, obinem un nou algoritm B, pentru
care:
t
B
(n) = 3t
A
(n/2()+t(n) 3c((n+1)/2)
2
+dn = 3/4cn
2
+(3/2+d)n+3/4c
Termenul 3/4cn
2
domin pe ceilali cnd n este suficient de mare, ceea ce
nseamn c algoritmul B este n esen cu 25% mai rapid dect algoritmul A. Nu
am reuit ns s schimbm ordinul timpului, care rmne ptratic.
Putem s continum n mod recursiv acest procedeu, mprind subcazurile n
subsubcazuri etc. Pentru subcazurile care nu sunt mai mari dect un anumit prag
n
0
, vom folosi tot algoritmul A. Obinem astfel algoritmul C, cu timpul
t n
t n n n
t n t n n n
C
A
C
( )
( / ) ( )
=

( + >

( ) pentru
pentru
0
0
3 2

Conform rezultatelor din Seciunea 5.3.5, t
C
(n) este n ordinul lui n
lg 3
. 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:
150 Algoritmi divide et impera 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
timpului, este influenat ns constanta multiplicativ a lui n
lg 3
, ceea ce poate
avea un rol considerabil n eficiena algoritmului. Pentru un algoritm divide et
impera oarecare, chiar dac ordinul timpului nu poate fi mbuntit, se dorete
optimizarea acestui prag n sensul obinerii unui algoritm ct mai eficient. Nu
exist o metod teoretic general pentru aceasta, pragul optim depinznd nu
numai de algoritmul n cauz, dar i de particularitatea implementrii.
Considernd o implementare dat, pragul optim poate fi determinat empiric, prin
msurarea timpului de execuie pentru diferite valori ale lui n
0
i cazuri de mrimi
diferite.
n general, se recomand o metod hibrid care const n: i) determinarea
teoretic a formei ecuaiilor recurente; ii) gsirea empiric a valorilor
constantelor folosite de aceste ecuaii, n funcie de implementare.
Revenind la exemplul nostru, pragul optim poate fi gsit rezolvnd ecuaia
t
A
(n) = 3t
A
(n/2() + t(n)
Empiric, gsim n
0
67, adic valoarea pentru care nu mai are importan dac
aplicm algoritmul A n mod direct, sau dac continum descompunerea. Cu alte
cuvinte, atta timp ct subcazurile sunt mai mari dect n
0
, este bine s continum
descompunerea. Dac continum ns descompunerea pentru subcazurile mai mici
dect n
0
, eficiena algoritmului scade.
Observm c metoda divide et impera este prin definiie recursiv. Uneori este
posibil s eliminm recursivitatea printr-un ciclu iterativ. Implementat pe o
main convenional, versiunea iterativ poate fi ceva mai rapid (n limitele
unei constante multiplicative). Un alt avantaj al versiunii iterative ar fi faptul c
economisete spaiul de memorie. Versiunea recursiv folosete o stiv necesar
Seciunea 7.1 Tehnica divide et impera 151

memorrii apelurilor recursive. Pentru un caz de mrime n, numrul apelurilor
recursive este de multe ori n (log n), uneori chiar n (n).
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
bucla for se execut n medie de (n
2
+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 Algoritmi divide et impera Capitolul 7

function binrec(T[i .. j], x)
{caut binar pe x n subtabloul T[i .. j]; aceast procedur
este apelat doar cnd T[i] x < T[ j+1] i i j}
if i = j then return i
k (i+j+1) div 2
if x < T[k] then return binrec(T[i .. k1], x)
else return binrec(T[k .. j], x)
Algoritmul binsearch necesit un timp n (log n), indiferent de poziia lui x n T
(demonstrai acest lucru, revznd Seciunea 5.3.5). Procedura binrec execut
doar un singur apel recursiv, n funcie de rezultatul testului x < T[k]. Din
aceast cauz, cutarea binar este, mai curnd, un exemplu de simplificare, dect
de aplicare a tehnicii divide et impera.
Iat i versiunea iterativ a acestui algoritm:
function iterbin1(T[1 .. n], x)
{cutare binar iterativ}
if n = 0 or x < T[1] then return 0
i 1; j n
while i < j do
{T[i] x < T[ j+1]}
k (i+j+1) div 2
if x < T[k] then j k1
else i k
return i
Acest algoritm de cutare binar pare ineficient n urmtoarea situaie: dac la un
anumit pas avem x = T[k], se continu totui cutarea. Urmtorul algoritm evit
acest inconvenient, oprindu-se imediat ce gsete elementul cutat.
function iterbin2(T[1 .. n], x)
{variant a cutrii binare iterative}
if n = 0 or x < T[1] then return 0
i 1; j n
while i < j do
{T[i] x < T[ j+1]}
k (i+j) div 2
case x < T[k]: j k1
x T[k+1]: i k+1
otherwise: i, j k
return i
Timpul pentru iterbin1 este n (log n). Algoritmul iterbin2 necesit un timp care
depinde de poziia lui x n T, fiind n (1), (log n), (log n) pentru cazurile cel
mai favorabil, mediu i respectiv, cel mai nefavorabil.
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 Mergesort (sortarea prin interclasare)
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
precis, pentru a sorta un tablou de n = 2
k
elemente, presupunnd c
descompunerea este total, acest spaiu este de
2 2 2 2 1 2 2 2
1 2
( ... )
k k k
n

+ + + + = =
154 Algoritmi divide et impera 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
timp n (n
2
), 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
timp de execuie, care este n (n
2
). 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 Mergesort n clasele tablou<T> i lista<E>
7.4.1 O soluie neinspirat
Dei eficient n privina timpului, algoritmul de sortare prin interclasare are un
handicap important n ceea ce privete memoria necesar. ntr-adevr, orice
tablou de n elemente este sortat ntr-un timp n (n log n), dar utiliznd un spaiu
suplimentar de memorie
*
de 2n elemente. Pentru a reduce consumul de memorie,
n implementarea acestui algoritm nu vom utiliza variabilele intermediare U i V
de tip tablou<T>, ci o unic zon de auxiliar de n elemente.
Convenim s implementm procedura mergesort din Seciunea 7.3 ca membru
private al clasei parametrice tablou<T>. Invocarea acestei proceduri se va
realiza prin funcia membr

*
Spaiul suplimentar utilizat de algoritmul mergesort poate fi independent de numrul elementelor
tabloului de sortat. Detaliile de implementare a unei astfel de strategii se gsesc n D. E. Knuth,
Tratat de programarea calculatoarelor. Sortare i cutare, Seciunea 5.2.4.
Seciunea 7.4 Mergesort n clasele tablou<T> i lista<E> 155


template <class T>
tablou<T>& tablou<T>::sort( ) {
T *aux = new T[ d ]; // alocarea zonei de interclasare
mergesort( 0, d, aux ); // si sortarea propriu-zisa
delete [ ] aux; // eliberarea zonei alocate

return *this;
}

156 Algoritmi divide et impera Capitolul 7

Am preferat aceast manier de ncapsulare din urmtoarele dou motive:
Alocarea i eliberarea spaiului suplimentar necesar interclasrii se face o
singur dat, nainte i dup terminarea sortrii. Funcia mergesort(), ca
funcie recursiv, nu poate avea controlul asupra alocrii i eliberrii acestei
zone.
Algoritmul mergesort are trei parametri care pot fi ignorai la apelarea funciei
de sortare. Acetia sunt: adresa zonei suplimentare de memorie i cei doi indici
prin care se ncadreaz elementele de sortat din tablou.
Dup cum se poate vedea n Exerciiul 7.7, implementarea interclasrii se
simplific mult prin utilizarea unor valori santinel n tablourile de interclasat.
Funcia mergesort():

template <class T>
void tablou<T>::mergesort( int st, int dr, T *x ) {
if ( dr - st > 1 ) {
// mijlocul intervalului
int m = ( st + dr ) / 2;

// sortarea celor doua parti
mergesort( st, m );
mergesort( m, dr );

// pregatirea zonei x pentru interclasare
int k = st;
for ( int i = st; i < m; ) x[ i++ ] = a[ k++ ];
for ( int j = dr; j > m; ) x[ --j ] = a[ k++ ];

// interclasarea celor doua parti din x in zona a
i = st; j = dr - 1;
for ( k = st; k < dr; k++ )
a[ k ] = x[ j ] > x[ i ]? x[ i++ ]: x[ j-- ];
}
}

se adapteaz surprinztor de simplu la utilizarea santinelelor. Nu avem dect s
transferm n zona auxiliar cele dou jumti deja sortate, astfel nct valorile
maxime s fie la mijlocul acestei zone. Altfel spus, prima jumtate va fi
transferat cresctor, iar cea de-a doua descresctor, n continuarea primei
jumti. ncepnd interclasarea cu valorile minime, valoarea maxim din fiecare
jumtate este santinel pentru cealalt jumtate.
Sortarea prin interclasare prezint un avantaj foarte important fa de alte metode
de sortare deoarece elementele de sortat sunt parcurse secvenial, element dup
element. Din acest motiv, metoda este potrivit pentru sortarea fiierelor sau
listelor. De exemplu, procedura de sortare prin interclasare a obiectelor de tip
lista<E>

Seciunea 7.4 Mergesort n clasele tablou<T> i lista<E> 157

template <class E>
lista<E>& lista<E>::sort() {
if ( head )
head = mergesort( head );

return *this;
}

rearanjeaz nodurile n ordinea cresctoare a cheilor, fr a folosi noduri sau liste
temporare. Preul n spaiu suplimentar de memorie este totui pltit, deoarece
orice list nlnuit necesit memorie n ordinul numrului de elemente pentru
realizarea nlnuirii.
Conform algoritmului mergesort, lista se mparte n dou pari egale, iar dup
sortarea fiecreia se realizeaz interclasarea. mprirea listei n cele dou pri
egale nu se poate realiza direct, ca n cazul tablourilor, ci n mai muli pai.
Astfel, vom parcurge lista pn la sfrit, pentru a putea determina elementul din
mijloc. Apoi stabilim care este elementul din mijloc i, n final, izolm cele dou
pri, fiecare n cte o list. n funcia mergesort():

template <class E>
nod<E>* mergesort ( nod<E> *c ) {
if ( c && c->next ) {
// sunt cel putin doua noduri in lista
nod<E> *a = c, *b;

for ( b = c->next; b; a = a->next )
if ( b->next ) b = b->next->next;
else break;
b = a->next; a->next = 0;
return merge( mergesort( c ), mergesort( b ) );
}
else
// lista contine cel mult un nod
return c;
}

mprirea listei se realizeaz printr-o singur parcurgere, dar cu dou adrese de
noduri, a i b. Principiul folosit este urmtorul: dac b nainteaz n parcurgerea
listei de dou ori mai repede dect a, atunci cnd b a ajuns la ultimul nod, a este
la nodul de mijloc al listei.
Spre deosebire de algoritmul mergesort, sortarea listelor prin interclasare nu
deplaseaz valorile de sortat. Funcia merge() interclaseaz listele de la adresele
a i b prin simpla modificare a legturilor nodurilor.

158 Algoritmi divide et impera Capitolul 7

template <class E>
nod<E>* merge( nod<E> *a, nod<E> *b ) {
nod<E> *head; // primul nod al listei interclasate

if ( a && b )
// ambele liste sunt nevide;
// stabilim primul nod din lista interclasata
if ( a->val > b->val ) { head = b; b = b->next; }
else { head = a; a = a->next; }
else
// cel putin una din liste este vida;
// nu avem ce interclasa
return a? a: b;

// interclasarea propriu-zisa
nod<E> *c = head; // ultimul nod din lista interclasata
while ( a && b )
if ( a->val > b->val ) { c->next = b; c = b; b = b->next; }
else { c->next = a; c = a; a = a->next; }

// cel putin una din liste s-a epuizat
c->next = a? a: b;

// se returneaza primul nod al listei interclasate
return head;
}

Funcia de sortare mergesort(), mpreun cu cea de interclasare merge(),
lucreaz exclusiv asupra nodurilor. Deoarece aceste funcii sunt invocate doar la
nivel de list, ele nu sunt membre n clasa nod<E>, ci doar friend fa de aceast
clas. ncapsularea lor este realizat prin mecanismul standard al limbajului C++.
Dei aceste funcii aparin domeniului global, ele nu pot fi invocate de aici
datorit obiectelor de tip nod<E>, obiecte accesibile doar din domeniul clasei
lista<E>. Aceast manier de ncapsulare nu este complet sigur, deoarece, chiar
dac nu putem manipula obiecte de tip nod<E>, totui putem lucra cu adrese de
nod<E>. De exemplu, funcia

void f( ) {
mergesort( (nod<int> *)0 );
}

trece de compilare, dar efectele ei la rularea programului sunt imprevizibile.
Prezena funciilor de sortare n tablou<T> i lista<E> (de fapt i n nod<E>)
impune completarea claselor T i E cu operatorul de comparare >. Orice tentativ
de a defini (atenie, de a defini i nu de a sorta) obiecte de tip tablou<T> sau
lista<E> este semnalat ca eroare de compilare, dac tipurile T sau E nu au
definit acest operator. Situaia apare, deoarece generarea unei clase parametrice
implic generarea tuturor funciilor membre. Deci, chiar dac nu invocm funcia
Seciunea 7.4 Mergesort n clasele tablou<T> i lista<E> 159

de sortare pentru tipul tablou<T>, ea este totui generat, iar generarea ei
necesit operatorul de comparare al tipului T.
De exemplu, pentru a putea lucra cu liste de muchii, lista<muchie>, sau tablouri
de tablouri, tablou< tablou<T> >, vom implementa operatorii de comparare
pentru clasa muchie i clasa tablou<T>. Muchiile sunt comparate n funcie de
costul lor, dar cum vom proceda cu tablourile? O soluie este de a lucra conform
ordinii lexicografice, adic de a aplica aceeai metod care se aplic la ordonarea
numelor n cartea de telefoane, sau n catalogul colar:

template <class T>
operator > ( const tablou<T>& a, const tablou<T>& b ) {
// minumul elementelor
int as = a.size( ), bs = b.size( );
int n = as < bs? as: bs;

// comparam pana la prima diferenta
for ( int i = 0; i < n; i++ )
if ( a[ i ] != b[ i ] ) return a[ i ] > b[ i ];

// primele n elemente sunt identice
return as > bs;
}

Atunci cnd operatorii de comparare nu prezint interes, sau nu pot fi definii, i
putem implementa ca funcii inefective. Astfel, dac avem nevoie de un tablou de
liste sau de o list de liste asupra crora nu vom aplica operaii de sortare, va
trebui s definim operatorii inefectivi:

template <class E>
operator >( const lista<E>&, const lista<E>& ) {
return 1;
}

n concluzie, extinderea claselor tablou<T> i lista<E> cu funciile de sortare
nu menine compatibilitatea acestor clase fa de aplicaiile dezvoltate pn acum.
Oricnd este posibil ca recompilarea unei aplicaii n care se utilizeaz, de
exemplu, tablouri sau liste cu elemente de tip XA, XB etc, s devin un comar,
deoarece, chiar dac nu are nici un sens, trebuie s completm fiecare clas XA, XB
etc, cu operatorul de comparare >.
Programarea orientat pe obiect se folosete tocmai pentru a evita astfel de
situaii, nu pentru a le genera.
160 Algoritmi divide et impera Capitolul 7

7.4.2 Tablouri sortabile i liste sortabile
Sortarea este o operaie care completeaz facilitile clasei tablou<T>, fr a
exclude utilizarea acestei clase pentru tablouri nesortabile. Din acest motiv,
funciile de sortare nu pot fi funcii membre n clasa tablou<T>.
O soluie posibil de ncapsulare a sortrii este de a construi, prin derivare
public din tablou<T>, subtipul tablouSortabil<T>, care s conin tot ceea ce
este necesar pentru sortare. Mecanismului standard de conversie, de la tipul
derivat public la tipul de baz, permite ca un tablouSortabil<T> s poat fi
folosit oricnd n locul unui tablou<T>.
n continuare, vom prezenta o alt variant de ncapsulare, mai puin clasic, prin
care atributul sortabil este considerat doar n momentul invocrii funciei de
sortatre, nu apriori, prin definirea obiectului ca sortabil.
Sortarea se invoc prin funcia

template <class T>
tablou<T>& mergesort( tablou<T>& t ) {
( tmsort<T> )t;
return t;
}

care const n conversia tabloului t la tipul tmsort<T>. Clasa tmsort<T>
ncapsuleaz absolut toate detaliile sortrii. Fiind vorba de sortarea prin
interclasare, detaliile de implementare sunt cele stabilite n Seciunea 7.4.1.

template <class T>
class tmsort {
public:
tmsort( tablou<T>& );

private:
T *a; // adresa zonei de sortat
T *x; // zona auxiliara de interclasare

void mergesort( int, int );
};

Sortarea, de fapt transformarea tabloului t ntr-un tablou sortat, este realizat prin
constructorul

Seciunea 7.4 Mergesort n clasele tablou<T> i lista<E> 161

template <class T>
tmsort<T>::tmsort( tablou<T>& t ): a( t.a ) {
x = new T[ t.size( ) ]; // alocarea zonei de interclasare
mergesort( 0, t.size( ) ); // sortarea
delete [ ] x; // eliberarea zonei alocate
}

Dup cum se observ, n acest constructor se folosete membrul privat T *a
(adresa zonei alocate elementelor tabloului) din clasa tablou<T>. Iat de ce, n
clasa tablou<T> trebuie fcut o modificare (singura dealtfel): clasa tmsort<T>
trebuie declarat friend.
Funcia mergesort() este practic neschimbat:

template <class T>
void tmsort<T>::mergesort( int st, int dr ) {
// ...
// corpul functiei void mergesort( int, int, T* )
// din Sectiunea 7.4.1.
// ...
}

Pentru sortarea listelor se procedeaz analog, transformnd implementarea din
Seciunea 7.4.1 n cea de mai jos.

template <class E>
lista<E>& mergesort( lista<E>& l ) {
( lmsort<E> )l;
return l;
}

template <class E>
class lmsort {
public:
lmsort( lista<E>& );

private:
nod<E>* mergesort( nod<E>* );
nod<E>* merge( nod<E>*, nod<E>* );
};

template <class E>
lmsort<E>::lmsort( lista<E>& l ) {
if ( l.head )
l.head = mergesort( l.head );
}

162 Algoritmi divide et impera Capitolul 7

template <class E>
nod<E>* lmsort<E>::mergesort ( nod<E> *c ) {
// ...
// corpul functiei nod<E>* mergesort( nod<E>* )
// din Sectiunea 7.4.1.
// ...
}

template <class E>
nod<E>* lmsort<E>::merge( nod<E> *a, nod<E> *b ) {
// ...
// corpul functiei nod<E>* merge( nod<E>*, nod<E>* )
// din Sectiunea 7.4.1.
// ...
}

Nu uitai de declaraia friend! Clasa lmsort<E> folosete membrii privai att
din clasa lista<E>, ct i din clasa nod<E>, deci trebuie declarat friend n
ambele.
7.5 Quicksort (sortarea rapid)
Algoritmul de sortare quicksort, inventat de Hoare n 1962, se bazeaz de
asemenea pe principiul divide et impera. Spre deosebire de mergesort, partea
nerecursiv a algoritmului este dedicat construirii subcazurilor i nu combinrii
soluiilor lor.
Ca prim pas, algoritmul alege un element pivot din tabloul care trebuie sortat.
Tabloul este apoi partiionat n dou subtablouri, alctuite de-o parte i de alta a
acestui pivot n urmtorul mod: elementele mai mari dect pivotul sunt mutate n
dreapta pivotului, iar celelalte elemente sunt mutate n stnga pivotului. Acest
mod de partiionare este numit pivotare. n continuare, cele dou subtablouri sunt
sortate n mod independent prin apeluri recursive ale algoritmului. Rezultatul este
tabloul complet sortat; nu mai este necesar nici o interclasare. Pentru a echilibra
mrimea celor dou subtablouri care se obin la fiecare partiionare, ar fi ideal s
alegem ca pivot elementul median. Intuitiv, mediana unui tablou T este elementul
m din T, astfel nct numrul elementelor din T mai mici dect m este egal cu
numrul celor mai mari dect m (o definiie riguroas a medianei unui tablou este
dat n Seciunea 7.6). Din pcate, gsirea medianei necesit mai mult timp dect
merit. De aceea, putem pur i simplu s folosim ca pivot primul element al
tabloului. Iat cum arat acest algoritm:
Seciunea 7.5 Quicksort (sortarea rapid) 163

procedure quicksort(T[i .. j])
{sorteaz n ordine cresctoare tabloul T[i .. j]}
if ji este mic
then insert(T[i .. j])
else pivot(T[i .. j], l)
{dup pivotare, avem:
i k < l T[k] T[l]
l < k j T[k] > T[l]}
quicksort(T[i .. l1])
quicksort(T[l+1 .. j])
Mai rmne s concepem un algoritm de pivotare cu timp liniar, care s parcurg
tabloul T o singur dat. Putem folosi urmtoarea tehnic de pivotare: parcurgem
tabloul T o singur dat, pornind ns din ambele capete. ncercai s nelegei
cum funcioneaz acest algoritm de pivotare, n care p = T[i] este elementul pivot:
procedure pivot(T[i .. j], l)
{permut elementele din T[i .. j] astfel nct, n final,
elementele lui T[i .. l1] sunt p,
T[l] = p,
iar elementele lui T[l+1 .. j] sunt > p}
p T[i]
k i; l j+1
repeat k k+1 until T[k] > p or k j
repeat l l1 until T[l] p
while k < l do
interschimb T[k] i T[l]
repeat k k+1 until T[k] > p
repeat l l1 until T[l] p
{pivotul este mutat n poziia lui final}
interschimb T[i] i T[l]
Intuitiv, ne dm seama c algoritmul quicksort este ineficient, dac se ntmpl n
mod sistematic ca subcazurile T[i .. l1] i T[l+1 .. j] s fie puternic neechilibrate.
Ne propunem n continuare s analizm aceast situaie n mod riguros.
Operaia de pivotare necesit un timp n (n). Fie constanta n
0
, astfel nct, n
cazul cel mai nefavorabil, timpul pentru a sorta n > n
0
elemente prin quicksort s
fie
t(n) (n) + max{t(i)+t(ni1) | 0 i n1}
Folosim metoda induciei constructive pentru a demonstra independent c
t O(n
2
) i t (n
2
).
164 Algoritmi divide et impera Capitolul 7

Putem considera c exist o constant real pozitiv c, astfel nct t(i) ci
2
+c/2
pentru 0 i n
0
. Prin ipoteza induciei specificate parial, presupunem c
t(i) ci
2
+c/2 pentru orice 0 i < n. Demonstrm c proprietatea este adevrat i
pentru n. Avem
t(n) dn + c + c max{i
2
+(ni1)
2
| 0 i n1}
d fiind o alt constant. Expresia i
2
+(ni1)
2
i atinge maximul atunci cnd i este
0 sau n1. Deci,
t(n) dn + c + c(n1)
2
= cn
2
+ c/2 + n(d2c) + 3c/2
Dac lum c 2d, obinem t(n) cn
2
+c/2. Am artat c, dac c este suficient de
mare, atunci t(n) cn
2
+c/2 pentru orice n 0, adic, t O(n
2
). Analog se arat c
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
timp n (n
2
).
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
t(n) (n) + 1/n ( ( ) ( )) t l t n l
l
n
+
=

1
1

Mai precis, fie n
0
i d dou constante astfel nct pentru orice n > n
0
, avem
t(n) dn + 1/n ( ( ) ( )) t l t n l
l
n
+
=

1
1
= dn + 2/n t i
i
n
( )
=

0
1

Prin analogie cu mergesort, este rezonabil s presupunem c t O(n log n) i s
aplicm tehnica induciei constructive, cutnd o constant c, astfel nct
t(n) cn lg n.
Deoarece i lg i este o funcie nedescresctoare, avem
Seciunea 7.5 Quicksort (sortarea rapid) 165

i i x x x
x
x
e
x
n
n
e
n
i n
n
x n
n
x n
n
lg lg lg
lg
lg
lg
= +

= +
= +

(
(

0
0
0
1
1 2
2
1
1
2
2
2 4 2 4
d
pentru n
0
1.
innd cont de aceast margine superioar pentru
i i
i n
n
lg
= +

0
1
1

putei demonstra prin inducie matematic c t(n) cn lg n pentru orice
n > n
0
1, unde
c
d
e
n e
t i
i
n
= +
+
=

2 4
1
0
2
0
0
lg
( ) lg
( )
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
2
), 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.
7.6 Selecia unui element dintr-un tablou
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 Algoritmi divide et impera Capitolul 7

#{i {1, , n} | T[i] < m} = n #{i {1, , n} | T[i] m}
Condiia
#{i {1, , n} | T[i] < m} < n/2(
este deci echivalent cu condiia
#{i {1, , n} | T[i] m} > n n/2( = n/2
Algoritmul naiv pentru determinarea medianei lui T const n a sorta cresctor
tabloul i a extrage apoi elementul din poziia n/2(. Folosind mergesort, de
exemplu, acest algoritm necesit un timp n (n log n). Putem gsi o metod mai
eficient? Pentru a rspunde la aceast ntrebare, vom considera o problem mai
general.
Fie T un tablou de n elemente i fie k un ntreg, 1 k n. Problema seleciei
const n gsirea celui de-al k-lea cel mai mic element al lui T, adic a elementul
m pentru care avem:
#{i {1, , n} | T[i] < m} < k
#{i {1, , n} | T[i] m} k
Cu alte cuvinte, este al k-lea element n T, dac tabloul este sortat n ordine
cresctoare. De exemplu, mediana lui T este al n/2(-lea cel mai mic element al
lui T. Deoarece n/2( = (n+1)/2 = (n+1) div 2, mediana lui T este totodat al
((n+1) div 2)-lea cel mai mic element al lui T.
Urmtorul algoritm, nc nu pe deplin specificat, rezolv problema seleciei
ntr-un mod similar cu quicksort dar i cu binsearch.
function selection(T[1 .. n], k)
{gsete al k-lea cel mai mic element al lui T;
se presupune c 1 k n}
if n este mic then sorteaz T
return T[k]
p un element pivot din T[1 .. n]
u #{i {1, , n} | T[i] < p}
v #{i {1, , n} | T[i] p}
if u k then
array U[1 .. u]
U elementele din T mai mici dect p
{cel de-al k-lea cel mai mic element al lui T este
i cel de-al k-lea cel mai mic element al lui U}
return selection (U, k)
if v k then {am gsit!} return p

Seciunea 7.6 Selecia unui element dintr-un tablou 167

{situaia cnd u < k i v < k}
array V[1 .. nv]
V elementele din T mai mari dect p
{cel de-al k-lea cel mai mic element al lui T este
i cel de-al (kv)-lea cel mai mic element al lui V}
return selection(V, kv)
Care element din T s fie ales ca pivot? O alegere natural este mediana lui T,
astfel nct U i V s fie de mrimi ct mai apropiate (chiar dac cel mult unul din
aceste subtablouri va fi folosit ntr-un apel recursiv). Dac n algoritmul selection
alegerea pivotului se face prin atribuirea
p selection(T, (n+1) div 2)
ajungem ns la un cerc vicios.
S analizm algoritmul de mai sus, presupunnd, pentru nceput, c gsirea
medianei este o operaie elementar. Din definiia medianei, rezult c u < n/2(
i v n/2(. Obinem atunci relaia nv n/2. Dac exist un apel recursiv,
atunci tablourile U i V conin fiecare cel mult n/2 elemente. Restul operaiilor
necesit un timp n ordinul lui n. Fie t
m
(n) timpul necesar acestei metode, n cazul
cel mai nefavorabil, pentru a gsi al k-lea cel mai mic element al unui tablou de n
elemente. Avem
t
m
(n) O(n) + max{t
m
(i) | i n/2}
De aici se deduce (Exerciiul 7.17) c t
m
O(n).
Ce facem ns dac trebuie s inem cont i de timpul pentru gsirea pivotului?
Putem proceda ca n cazul quicksort-ului i s renunm la median, alegnd ca
pivot primul element al tabloului. Algoritmul selection astfel precizat are timpul
pentru cazul mediu n ordinul exact al lui n. Pentru cazul cel mai nefavorabil, se
obine ns un timp n ordinul lui n
2
.
Putem evita acest caz cel mai nefavorabil cu timp ptratic, fr s sacrificm
comportarea liniar pentru cazul mediu. Ideea este s gsim rapid o aproximare
bun pentru median. Presupunnd n 5, vom determina pivotul prin atribuirea
p pseudomed(T)
unde algoritmul pseudomed este:
function pseudomed(T[1 .. n])
{gsete o aproximare a medianei lui T}
s n div 5
array S[1 .. s]
for i 1 to s do S[i] adhocmed5(T[5i4 .. 5i])
return selection(S, (s+1) div 2)
168 Algoritmi divide et impera 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 Selecia unui element dintr-un tablou 169

Presupunnd c folosim p pseudomed(T), adic pivotul este pseudomediana,
fie t(n) timpul necesar algoritmului selection, n cazul cel mai nefavorabil, pentru
a gsi al k-lea cel mai mic element al unui tablou de n elemente. Din inegalitile
#{i {1, , n} | T[i] m} (3n12)/10
#{i {1, , n} | T[i] < m} < (7n+27)/10
rezult c, pentru n suficient de mare, tablourile U i V au cel mult 3n/4 elemente
fiecare. Deducem relaia
t(n) O(n) + t(n/5) + max{t(i) | i 3n/4} (*)
Vom arta c t (n). S considerm funcia f : N R

, definit prin recurena


f(n) = f(n/5) + f(3n/4) + n
pentru n N. Prin inducie constructiv, putem demonstra c exist constanta
real pozitiv a astfel nct f(n) an pentru orice n N. Deci, f O(n). Pe de
alt parte, exist constanta real pozitiv c, astfel nct t(n) cf(n) pentru orice
n N
+
. Este adevrat atunci i relaia t O(n). Deoarece orice algoritm care
rezolv problema seleciei are timpul de execuie n (n), rezult t (n), deci,
t (n).
Generaliznd, vom ncerca s aproximm mediana nu numai prin mprire la
cinci, ci prin mprire la un ntreg q oarecare, 1 < q n. Din nou, pentru n
suficient de mare, tablourile U i V au cel mult 3n/4 elemente fiecare. Relaia (*)
devine
t(n) O(n) + t(n/q) + max{t(i) | i 3n/4} (**)
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.





Figura 7.1 Vizualizarea pseudomedianei.
170 Algoritmi divide et impera 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
Alice i Bob doresc s comunice anumite secrete prin telefon. Convorbirea
telefonic poate fi ns ascultat i de Eva. n prealabil, Alice i Bob nu au stabilit
nici un protocol de codificare i pot face acum acest lucru doar prin telefon. Eva
va asculta ns i ea modul de codificare. Problema este cum s comunice Alice i
Bob, astfel nct Eva s nu poat descifra codul, cu toate c va cunoate i ea
protocolul de codificare
*
.
Pentru nceput, Alice i Bob convin n mod deschis asupra unui ntreg p cu cteva
sute de cifre i asupra unui alt ntreg g ntre 2 i p1. Securitatea secretului nu
este compromis prin faptul c Eva afl aceste numere.
La pasul doi, Alice i Bob aleg la ntmplare cte un ntreg A, respectiv B, mai
mici dect p, fr s-i comunice aceste numere. Apoi, Alice calculeaz
a = g
A
mod p i transmite rezultatul lui Bob; similar, Bob transmite lui Alice
valoarea b = g
B
mod p. n final, Alice calculeaz x = b
A
mod p, iar Bob calculeaz
y = a
B
mod p. Vor ajunge la acelai rezultat, deoarece x = y = g
AB
mod p. Aceast
valoare este deci cunoscut de Alice i Bob, dar rmne necunoscut lui Eva.
Evident, nici Alice i nici Bob nu pot controla direct care va fi aceast valoare.
Deci ei nu pot folosi acest protocol pentru a schimba n mod direct un anumit
mesaj. Valoarea rezultat poate fi ns cheia unui sistem criptografic
convenional.
Interceptnd convorbirea telefonic, Eva va putea cunoate n final urmtoarele
numere: p, q, a i b. Pentru a-l deduce pe x, ea trebuie s gseasc un ntreg A',
astfel nct a = g
A'
mod p i s procedeze apoi ca Alice pentru a-l calcula pe
x' = b
A'
mod p. Se poate arta (Exerciiul 7.21) c x' = x, deci c Eva poate calcula
astfel corect secretul lui Alice i Bob.
Calcularea lui A' din p, g i a este cunoscut ca problema logaritmului discret i
poate fi realizat de urmtorul algoritm.

*
O prim soluie a acestei probleme a fost dat n 1976 de W. Diffie i M. E. Hellman. ntre timp s-
au 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
exist un ntreg A, astfel nct 3 = 2
A
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 = g
A
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)
a 1
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)
a 1
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
= (((x
2
x)
2
)
2
)
2
x
172 Algoritmi divide et impera Capitolul 7

L-am obinut deci pe x
25
prin doar dou nmuliri i patru ridicri la ptrat. Dac
n expresia
x
25
= (((x
2
x)
2
1)
2
1)
2
x
nlocuim fiecare x cu un 1 i fiecare 1 cu un 0, obinem secvena 11001, adic
reprezentarea binar a lui 25. Formula precedent pentru x
25
are aceast form,
deoarece x
25
= x
24
x, x
24
= (x
12
)
2
etc. Rezult un algoritm divide et impera n care
se testeaz n mod recursiv dac exponentul curent este par sau impar.
function dexpo(g, A, p)
if A = 0 then return 1
if A este impar then a dexpo(g, A1, p)
return (ag mod p)
else a dexpo(g, A/2, p)
return (aa mod p)
Fie h(A) numrul de nmuliri modulo p efectuate atunci cnd se calculeaz
dexpo(g, A, p), inclusiv ridicarea la ptrat. Atunci,
h A
A
h A A
h A
( ) ( )
( / )
= +
+

0
1 1
1 2
pentru = 0
pentru 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 A A A
k k1 0
... 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 173

g
2c
mod p = (g
c
)
2
mod p
g
2c+1
mod p = g(g
c
)
2
mod p
n funcie de valoarea lui A
i
(dac este 0, respectiv 1). La sfritul pasului i,
valoarea lui c, n reprezentare binar, este A A A
k k i 1
... . Reprezentrea binar a lui
A este parcurs de la stnga spre dreapta, invers ca la algoritmul dexpo. Variabila
c a fost introdus doar pentru a nelege mai bine cum funcioneaz algoritmul i
putem, desigur, s o eliminm.
Dac parcurgem reprezentarea binar a lui A de la dreapta spre stnga, obinem un
alt algoritm iterativ la fel de interesant.
function dexpoiter2(g, A, p)
n A; y g; a 1
while n > 0 do
if n este impar then a ay mod p
y yy mod p
n n div 2
return a
Pentru a compara aceti trei algoritmi, vom considera urmtorul exemplu.
Algoritmul dexpo l calculeaz pe x
15
sub forma (((1 x)
2
x)
2
x)
2
x, cu apte nmuliri;
algoritmul dexpoiter1 sub forma (((1
2
x)
2
x)
2
x)
2
x, cu opt nmuliri; iar dexpoiter2
sub forma 1 x x
2
x
4
x
8
, tot cu opt nmuliri (ultima din acestea fiind pentru
calcularea inutil a lui x
16
).
Se poate observa c nici unul din aceti algoritmi nu minimizeaz numrul de
nmuliri efectuate. De exemplu, x
15
poate fi obinut prin ase nmuliri, sub forma
((x
2
x)
2
x)
2
x. Mai mult, x
15
poate fi obinut prin doar cinci nmuliri (Exerciiul
7.22).
7.8 nmulirea matricilor
Pentru matricile A i B de n n elemente, dorim s obinem matricea produs
C = AB. Algoritmul clasic provine direct din definiia nmulirii a dou matrici i
necesit n
3
nmuliri i (n1)n
2
adunri scalare. Timpul necesar pentru calcularea
matricii C este deci n (n
3
). Problema pe care ne-o punem este s gsim un
algoritm de nmulire matricial al crui timp s fie ntr-un ordin mai mic dect
n
3
. Pe de alt parte, este clar c (n
2
) este o limit inferioar pentru orice
algoritm de nmulire matricial, deoarece trebuie n mod necesar s parcurgem
cele n
2
elemente ale lui C.
174 Algoritmi divide et impera Capitolul 7

Strategia divide et impera sugereaz un alt mod de calcul a matricii C. Vom
presupune n continuare c n este o putere a lui doi. Partiionm pe A i B n cte
patru submatrici de n/2 n/2 elemente fiecare. Matricea produs C se poate calcula
conform formulei pentru produsul matricilor de 2 2 elemente:
A A
A A
B B
B B
C C
C C
11 12
21 22
11 12
21 22
11 12
21 22
|
\

|
|
\

|
=
|
\

|

Seciunea 7.8 nmulirea matricilor 175

unde
C A B A B C A B A B
C A B A B C A B A B
11 11 11 12 21 12 11 12 12 22
21 21 11 22 21 22 21 12 22 22
= + = +
= + = +

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
timp n (n
2
). Timpul total pentru algoritmul divide et impera rezultat este
t(n) 8t(n/2) + (n
2
)
Definim funcia
f n
n
f n n n
( )
( / )
=
=
+

1 1
8 2 1
2
pentru
pentru

Din Proprietatea 5.2 rezult c f (n
3
). Procednd ca n Seciunea 5.1.2,
deducem c t ( f ) = (n
3
), ceea ce nseamn c nu am ctigat nc nimic fa
de metoda clasic.
n timp ce nmulirea matricilor necesit un timp cubic, adunarea matricilor
necesit doar un timp ptratic. Este, deci, de dorit ca n formulele pentru
calcularea submatricilor C s folosim mai puine nmuliri, chiar dac prin aceasta
mrim numrul de adunri. Este ns acest lucru i posibil? Rspunsul este
afirmativ. n 1969, Strassen a descoperit o metod de calculare a submatricilor
C
ij
, care utilizeaz 7 nmuliri i 18 adunri i scderi. Pentru nceput, se
calculeaz apte matrici de n/2 n/2 elemente:
P A A B B
Q A A B
R A B B
S A B B
T A A B
U A A B B
V A A B B
= + +
= +
=
=
= +
= +
= +
( ) ( )
( )
( )
( )
( )
( ) ( )
( ) ( )
11 22 11 22
21 22 11
11 12 22
22 21 11
11 12 22
21 11 11 22
12 22 21 22

176 Algoritmi divide et impera Capitolul 7

Este uor de verificat c matricea produs C se obine astfel:
C P S T V C R T
C Q S C P R Q U
11 12
21 22
= + + = +
= + = + +

Timpul total pentru noul algoritm divide et impera este
t(n) 7t(n/2) + (n
2
)
i n mod similar deducem c t (n
lg 7
). Deoarece lg 7 < 2,81, rezult c
t O(n
2, 81
). Algoritmul lui Strassen este deci mai eficient dect algoritmul clasic
de nmulire matricial.
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.
Limita O(n
2, 81
) 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).

*
S-au propus i metode complet diferite. Astfel, D. Coppersmith i S. Winograd au gsit n 1987 un
algoritm cu timpul n O(n
2,376
).
7.9 nmulirea numerelor ntregi mari
Pentru anumite aplicaii, trebuie s considerm numere ntregi foarte mari. Dac
ai implementat algoritmii pentru generarea numerelor lui Fibonacci, probabil c
Seciunea 7.9 nmulirea numerelor ntregi mari 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
(convenim s spunem c un ntreg k are j cifre dac k < 10
j
, chiar dac k < 10
j1
).
Dac s = n/2, reprezentm pe u i v astfel:
u = 10
s
w + x, v = 10
s
y + z, unde 0 x < 10
s
, 0 z < 10
s

ntregii w i y au cte n/2( cifre, iar ntregii x i z au cte n/2 cifre. Din relaia
uv = 10
2s
wy + 10
s
(wz+xy) + xz
obinem urmtorul algoritm divide et impera pentru nmulirea a dou numere
ntregi mari.
function nmulire(u, v)
n cel mai mic ntreg astfel nct u i v s aib fiecare n cifre
if n este mic then calculeaz n mod clasic produsul uv
return produsul uv astfel calculat
s n div 2
w u div 10
s
; x u mod 10
s

y v div 10
s
; z v mod 10
s

return nmulire(w, y) 10
2s

+ (nmulire(w, z)+nmulire(x, y)) 10
s

+ nmulire(x, z)
w x
y z
u
v
n
n / 2( n / 2

178 Algoritmi divide et impera Capitolul 7

Presupunnd c folosim reprezentarea din Exerciiul 7.28, nmulirile sau
mpririle cu 10
2s
i 10
s
, ca i adunrile, sunt executate ntr-un timp liniar.
Acelai lucru este atunci adevrat i pentru restul mpririi ntregi, deoarece
u mod 10
s
= u 10
s
w, v mod 10
s
= v 10
s
y
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
2
). (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!
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)
return 10
2s
p + 10
s
(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
| n este o putere a lui 2)
Seciunea 7.9 nmulirea numerelor ntregi mari 179

Sau, mai elegant, putei ajunge la acelai rezultat aplicnd o schimbare de
variabil (o recuren asemntoare a fost discutat n Seciunea 5.3.5). Deci,
t O(n
lg 3
| n este o putere a lui 2)
innd din nou cont c t este nedescresctoare, aplicm Proprietatea 5.1 i
obinem t O(n
lg 3
).
n concluzie, este posibil s nmulim doi ntregi de n cifre ntr-un timp n
O(n
lg 3
), deci i n O(n
1, 59
). Ca i la metoda lui Strassen, datorit constantelor
multiplicative implicate, acest algoritm este interesant n practic doar pentru
valori mari ale lui n. O implementare bun nu va folosi probabil baza 10, ci baza
cea mai mare pentru care hardware-ul permite ca dou cifre s fie nmulite
direct.
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 Se poate nlocui n algoritmul iterbin1:
i) k (i+j+1) div 2 cu k (i+j) div 2?
ii) i k cu i k+1?
iii) j k1 cu j k?

7.3 Observai c bucla while din algoritmul insert (Seciunea 1.3) folosete o
cutare secvenial (de la coad la cap). S nlocuim aceast cutare secvenial
cu o cutare binar. Pentru cazul cel mai nefavorabil, ajungem oare acum ca
timpul pentru sortarea prin inserie s fie n ordinul lui n log n?

7.4 Artai c timpul pentru iterbin2 este n (1), (log n), (log n) pentru
cazurile cel mai favorabil, mediu i respectiv, cel mai nefavorabil.

7.5 Fie T[1 .. n] un tablou ordonat cresctor de ntregi diferii, unii putnd fi
negativi. Dai un algoritm cu timpul n O(log n) pentru cazul cel mai nefavorabil,
care gsete un index i, 1 i n, cu T[i] = i, presupunnd c acest index exist.
180 Algoritmi divide et impera 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 ptrat(0, n+1, n), ptrat fiind funcia
function ptrat(a, b, n)
if a = b1 then return a
m (a+b) div 2
if m
2
n then ptrat(m, b, n)
else ptrat(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
Se poate obine un algoritm i mai simplu, dac se presupune c avem acces la
locaiile U[N+1] i V[M+1], pe care le vom iniializa cu o valoare maximal i le
vom folosi ca santinele:
i, j 1
U[N+1], V[M+1] +
for k 1 to N+M do
if U[i] < V[j] then T[k] U[i]
i i+1
else T[k] V[j]
j j+1
Seciunea 7.10 Exerciii 181

Mai rmne s analizai eficiena celor doi algoritmi.

7.8 Modificai algoritmul mergesort astfel nct T s fie separat nu n dou, ci
n trei pri de mrimi ct mai apropiate. Analizai algoritmul obinut.

7.9 Artai c, dac n algoritmul mergesort separm pe T n tabloul U, avnd
n1 elemente, i tabloul V, avnd un singur element, obinem un algoritm de
sortare cu timpul de execuie n (n
2
). Acest nou algoritm seamn cu unul dintre
algoritmii deja cunoscui. Cu care anume?

7.10 Iat i o alt procedur de pivotare:
procedure pivot1(T[i .. j], l)
p T[i]
l i
for k i+1 to j do
if T[k] p then l l+1
interschimb T[k] i T[l]
interschimb T[i] i T[l]
Argumentai de ce procedura este corect i analizai eficiena ei. Comparai
numrul maxim de interschimbri din procedurile pivot i pivot1. Este oare
rentabil ca n algoritmul quicksort s nlocuim procedura pivot cu procedura
pivot1?

7.11 Argumentai de ce un apel funny-sort(T[1 ..n ]) al urmtorului algoritm
sorteaz corect elementele tabloului T[1 .. n].
procedure funny-sort(T[i .. j])
if T[i] > T[ j] then interschimb T[i] i T[ j]
if i < j1 then k ( ji+1) div 3
funny-sort(T[i .. jk])
funny-sort(T[i+k .. j])
funny-sort(T[i .. jk])
Este oare acest simpatic algoritm i eficient?

7.12 Este un lucru elementar s gsim un algoritm care determin minimul
dintre elementele unui tablou T[1 .. n] i utilizeaz pentru aceasta n1 comparaii
ntre elemente ale tabloului. Mai mult, orice algoritm care determin prin
comparaii minimul elementelor din T efectueaz n mod necesar cel puin n1
comparaii. n anumite aplicaii, este nevoie s gsim att minimul ct i maximul
182 Algoritmi divide et impera Capitolul 7

dintr-o mulime de n elemente. Iat un algoritm care determin minimul i
maximul dintre elementele tabloului T[1 .. n]:
procedure fmaxmin1(T[1 .. n], max, min)
max, min T[1]
for i 2 to n do
if max < T[i] then max T[i]
if min > T[i] then min T[i]
Acest algoritm efectueaz 2(n1) comparaii ntre elemente ale lui T. Folosind
tehnica divide et impera, elaborai un algoritm care s determine minimul i
maximul dintre elementele lui T prin mai puin de 2(n1) comparaii. Putei
presupune c n este o putere a lui 2.
Soluie: Un apel fmaxmin2(T[1 .. n], max, min) al urmtorului algoritm gsete
minimul i maximul cerute
procedure fmaxmin2(T[i .. j], max, min)
case i = j : max, min T[i]
i = j1 : if T[i] < T[ j] then max T[ j]
min T[i]
else max T[i]
min T[ j]
otherwise : m (i+j) div 2
fmaxmin2(T[i .. m], smax, smin)
fmaxmin2(T[m+1 .. j], dmax, dmin)
max maxim(smax, dmax)
min minim(smin, dmin)
Funciile maxim i minim determin, prin cte o singur comparaie, maximul,
respectiv minimul, a dou elemente.
Putem deduce c att fmaxmin1, ct i fmaxmin2 necesit un timp n (n) pentru a
gsi minimul i maximul ntr-un tablou de n elemente. Constanta multiplicativ
asociat timpului n cele dou cazuri difer ns. Notnd cu C(n) numrul de
comparaii ntre elemente ale tabloului T efectuate de procedura fmaxmin2,
obinem recurena
C n
C n C n
n
n
n
( ) =
(

=
=
0
1
2 2 2 ( / ) ( / ) + +
pentru 1
pentru 2
pentru > 2

Considerm n = 2
k
i folosim metoda iteraiei:
C n C n C n
k i
i
k
k k
( ) ( / ) ( ) / = + = = + = + =

2 2 2 2 2 2 2 2 2 3 2 2
1
1
1
1
K
Seciunea 7.10 Exerciii 183

Algoritmul fmaxmin2 necesit cu 25% mai puine comparaii dect fmaxmin1. Se
poate arta c nici un algoritm bazat pe comparaii nu poate folosi mai puin de
3n/22 comparaii. n acest sens, fmaxmin2 este, deci, optim.
Este procedura fmaxmin2 mai eficient i n practic? Nu n mod necesar. Analiza
ar trebui s considere i numrul de comparaii asupra indicilor de tablou, precum
i timpul necesar pentru rezolvarea apelurilor recursive n fmaxmin2. De
asemenea, ar trebui s cunoatem i cu ct este mai costisitoare o comparaie de
elemente ale lui T, dect o comparaie de indici (adic, de ntregi).

7.13 n ce const similaritatea algoritmului selection cu algoritmul i) quicksort
i ii) binsearch?

7.14 Generalizai procedura pivot, partiionnd tabloul T n trei seciuni
T[1 .. i1], T[i .. j], T[ j+1 .. n], coninnd elementele lui T mai mici dect p,
egale cu p i respectiv, mai mari dect p. Valorile i i j vor fi calculate n
procedura de pivotare i vor fi returnate prin aceast procedur.

7.15 Folosind ca model versiunea iterativ a cutrii binare i rezultatul
Exerciiului 7.14, elaborai un algoritm nerecursiv pentru problema seleciei.

7.16 Analizai urmtoarea variant a algoritmului quicksort.
procedure quicksort-modificat(T[1 .. n])
if n = 2 and T[2] < T[1]
then interschimb T[1] i T[2]
else if n > 2 then
p selection(T, (n+1) div 2)
arrays U[1 .. (n+1) div 2 ], V[1 .. n div 2]
U elementele din T mai mici dect p
i, n completare, elemente egale cu p
V elementele din T mai mari dect p
i, n completare, elemente egale cu p
quicksort-modificat(U)
quicksort-modificat(V)

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 Algoritmi divide et impera Capitolul 7

Soluie: Fie n
0
i d dou constante astfel nct pentru n > n
0
avem
t
m
(n) dn + max{t
m
(i) | i n/2}
Putem considera c exist constanta real pozitiv c astfel nct t
m
(i) ci+c,
pentru 0 i n
0
. Prin ipoteza induciei specificate parial presupunem c
t(i) ci+c, pentru orice 0 i < n. Atunci
t
m
(n) dn+c+cn/2 = cn+c+dncn/2( cn+c
deoarece putem s alegem constanta c suficient de mare, astfel nct cn/2( dn.
Am artat deci prin inducie c, dac c este suficient de mare, atunci t
m
(n) cn+c,
pentru orice n 0. Adic, t
m
O(n).

7.18 Artai c lund p T[1] n algoritmul selection i considernd cazul
cel mai nefavorabil, determinarea celui de-al k-lea cel mai mic element al lui
T[1 .. n] necesit un timp de execuie n O(n
2
).

7.19 Fie U[1 .. n] i V[1 .. n] dou tablouri de elemente ordonate
nedescresctor. Elaborai un algoritm care s gseasc mediana celor 2n elemente
ntr-un timp de execuie n O(log n).

7.20 Un element x este majoritar n tabloul T[1 .. n], dac
#{i | T[i] = x} > n/2. Elaborai un algoritm liniar care s determine elementul
majoritar n T (dac un astfel de element exist).

7.21 S presupunem c Eva a gsit un A' pentru care
a = g
A'
mod p = g
A
mod p
i c exist un B, astfel nct b = g
B
mod p. Artai c
x' = b
A'
mod p = b
A
mod p = x
chiar dac A' A.

7.22 Artai cum poate fi calculat x
15
prin doar cinci nmuliri (inclusiv ridicri
la ptrat).
Soluie: x
15
= (((x
2
)
2
)
2
)
2
x
1


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?
Indicaie: Din Exerciiul 1.7, deducem c f
n
= m
n
22
1 ( )
, unde m
n
22
1 ( )
este elementul
de pe ultima linie i ultima coloan ale matricii M
n1
. Rmne s elaborai un
algoritm similar cu dexpo pentru a afla matricea putere M
n1
. Dac, n loc de
dexpo, folosii ca model algoritmul dexpoiter2, obinei algoritmul fib3.

7.24 Demonstrai c algoritmul lui Strassen necesit un timp n O(n
lg 7
),
folosind de aceast dat metoda iteraiei.
Soluie: Fie dou constante pozitive a i c, astfel nct timpul pentru algoritmul
lui Strassen este
t(n) 7t(n/2) + cn
2

pentru n > 2, iar t(n) a pentru n 2. Obinem
t(n) cn
2
(1+7/4+(7/4)
2
++(7/4)
k2
) + a7
k1
cn
2
(7/4)
lg n
+ a7
lg n
= cn
lg 4+lg 7lg 4
+ 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
este tot n (n
lg 7
).
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, r div 2)
star(x+r, y+r, r div 2)
star(xr, yr, r div 2)
star(x+r, yr, r div 2)
box(x, y, r)
Care este rezultatul, dac box(x, y, r) apare naintea celor patru apeluri recursive?
Artai c timpul de execuie pentru un apel star(a, b, c) este n (c
2
).
186 Algoritmi divide et impera 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).
185
8. Algoritmi de
programare dinamic
8.1 Trei principii fundamentale ale programrii
dinamice
Programarea dinamic, ca i metoda divide et impera, rezolv problemele
combinnd soluiile subproblemelor. Dup cum am vzut, algoritmii divide et
impera partiioneaz problemele n subprobleme independente, rezolv
subproblemele n mod recursiv, iar apoi combin soluiile lor pentru a rezolva
problema iniial. Dac subproblemele conin subsubprobleme comune, n locul
metodei divide et impera este mai avantajos de aplicat tehnica programrii
dinamice.
S analizm ns pentru nceput ce se ntmpl cu un algoritm divide et impera n
aceast din urm situaie. Descompunerea recursiv a cazurilor n subcazuri ale
aceleiai probleme, care sunt apoi rezolvate n mod independent, poate duce
uneori la calcularea de mai multe ori a aceluiai subcaz, i deci, la o eficien
sczut a algoritmului. S ne amintim, de exemplu, de algoritmul fib1 din
Capitolul 1. Sau, s calculm coeficientul binomial
n
k
n
k
n
k |
\

| =

|
\

| +
|
\

1
1
1
1
pentru 0 < k < n
altfel

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
Exerciiul 2.5). Deoarece rezultatul final este obinut prin adunarea a
n
k
|
\

| de 1,
rezult c timpul de execuie pentru un apel C(n, k) este n (
n
k
|
\

| ).
186 Algoritmi de programare dinamic Capitolul 8

Dac memorm rezultatele intermediare ntr-un tablou de forma
(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.
0 1 2 ... k1 k
0 1
1 1 1
2 1 2 1
M

n1

n
k

|
\

|
1
1

n
k
|
\

|
1


n

n
k
|
\

|
Seciunea 8.1 Trei principii fundamentale ale programrii dinamice 187

Ca i n cazul algoritmilor greedy, soluia optim nu este n mod necesar unic.
Dezvoltarea unui algoritm de programare dinamic poate fi descris de
urmtoarea succesiune de pai:
se caracterizeaz structura unei soluii optime
se definete recursiv valoarea unei soluii optime
se calculeaz de jos n sus valoarea unei soluii optime
Dac pe lng valoarea unei soluii optime se dorete i soluia propriu-zis,
atunci se mai efectueaz urmtorul pas:
din informaiile calculate se construiete de sus n jos o soluie optim
Acest pas se rezolv n mod natural printr-un algoritm recursiv, care efectueaz o
parcurgere n sens invers a secvenei optime de decizii calculate anterior prin
algoritmul de programare dinamic.
8.2 O competiie
n acest prim exemplu de programare dinamic nu ne vom concentra pe principiul
optimalitii, ci pe structura de control i pe ordinea rezolvrii subcazurilor. Din
aceast cauz, problema considerat n aceast seciune nu va fi o problem de
optimizare.
S ne imaginm o competiie n care doi juctori A i B joac o serie de cel mult
2n1 partide, ctigtor fiind juctorul care acumuleaz primul n victorii.
Presupunem c nu exist partide egale, c rezultatele partidelor sunt independente
ntre ele i c pentru orice partid exist o probabilitate p constant ca s ctige
juctorul A i o probabilitate q = 1p ca s ctige juctorul B.
Ne propunem s calculm P(i, j), probabilitatea ca juctorul A s ctige
competiia, dat fiind c mai are nevoie de i victorii i c juctorul B mai are
nevoie de j victorii pentru a ctiga. n particular, la nceputul competiiei aceast
probabilitate este P(n, n), deoarece fiecare juctor are nevoie de n victorii. Pentru
1 i n, avem P(0, i) = 1 i P(i, 0) = 0. Probabilitatea P(0, 0) este nedefinit.
Pentru i, j 1, putem calcula P(i, j) dup formula:
P(i, j) = pP(i1, j) + qP(i, j1)
algoritmul corespunztor fiind:
function P(i, j)
if i = 0 then return 1
if j = 0 then return 0
return pP(i1, j) + qP(i, j1)
188 Algoritmi de programare dinamic Capitolul 8

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
k
), iar dac
i = j = n, atunci t O(4
n
). Dac urmrim modul n care sunt generate apelurile
recursive (Figura 8.1), observm c este identic cu cel pentru calculul ineficient al
coeficienilor binomiali:
C(i+j, j) = C((i1)+j, j) + C(i+( j1), j1)
Din Exerciiul 8.1 rezult c numrul total de apeluri recursive este
2 2
i j
j
+ |
\

|
Timpul de execuie pentru un apel P(n, n) este deci n (
2n
n
|
\

|
). innd cont i de
Exerciiul 8.3, obinem c timpul pentru calculul lui P(n, n) este n
O(4
n
) (4
n
/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
P (i1, j)
P (i1, j1) P (i2, j)
P (i, j1)
P (i1, j2)
P (i, j) nc k partide de jucat
nc k2 partide de jucat
nc k1 partide de jucat

Figura 8.1 Apelurile recursive efectuate dup un apel al funciei P(i, j).
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
execuie pentru un apel serie(n, p) este n (n
2
). 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 nmulirea nlnuit a matricilor
Ne propunem s calculm produsul matricial
M = M
1
M
2
M
n

Deoarece nmulirea matricilor este asociativ, putem opera aceste nmuliri n
mai multe moduri. nainte de a considera un exemplu, s observm c nmulirea
clasic a unei matrici de p q elemente cu o matrice de q r elemente necesit
pqr nmuliri scalare.

Dac dorim s obinem produsul ABCD al matricilor A de 13 5, B de 5 89, C
de 89 3 i D de 3 34 elemente, n funcie de ordinea efecturii nmulirilor
matriciale (dat prin paranteze), numrul total de nmuliri scalare poate s fie
foarte diferit:
(((AB)C)D) 10582 nmuliri
((AB)(CD)) 54201 nmuliri
((A(BC))D) 2856 nmuliri
(A((BC)D)) 4055 nmuliri
(A(B(CD))) 26418 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 Algoritmi de programare dinamic 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
T n T i T n i
i
n
( ) ( ) ( ) =
=

1
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
n
n
n
( ) =

|
\

|
1 2 2
1

Din Exerciiul 8.3 rezult T (4
n
/n
2
). Deoarece, pentru fiecare mod de
parantezare, operaia de numrare a nmulirilor scalare necesit un timp n (n),
determinarea modului optim de a-l calcula pe M este n (4
n
/n). Aceast metod
direct este deci foarte neperformant i o vom mbunti n cele ce urmeaz.
Din fericire, principiul optimalitii se poate aplica la aceast problem. De
exemplu, dac cel mai bun mod de a nmuli toate matricile presupune prima
tietur ntre a i-a i a i+1-a matrice a produsului, atunci subprodusele
M
1
M
2
M
i
i M
i+1
M
i+2
M
n
trebuie i ele calculate ntr-un mod optim.
Aceasta ne sugereaz s aplicm programarea dinamic.
Vom construi tabloul m[1 .. n, 1 .. n], unde m[i, j] este numrul minim de
nmuliri scalare necesare pentru a calcula partea M
i
M
i+1
M
j
a produsului
iniial. Soluia problemei iniiale va fi dat de m[1, n]. Presupunem c tabloul
d[0 .. n] conine dimensiunile matricilor M
i
, astfel nct matricea M
i
este de
dimensiune d[i1] d[i], 1 i n. Construim tabloul m diagonal cu diagonal:
diagonala s conine elementele m[i, j] pentru care ji = s. Obinem astfel
succesiunea
Seciunea 8.3 nmulirea nlnuit a matricilor 191

s = 0 : m[i, i] = 0, i=1, 2, , n
s = 1 : m[i, i+1] = d[i1] d[i] d[i+1], i=1, 2, , n1
1 < s < n : m[i, i+s] = min
i k i s < +
(m[i, k] + m[k+1, i+s] + d[i1] d[k] d[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 s s n s s n n n n n n n
s
n
s
n
s
n
= = =
=


1
1
1
1
2
1
1
2 3
1 2 1 2 1 6 6
192 Algoritmi de programare dinamic Capitolul 8

Timpul de execuie este deci n (n
3
), ceea ce reprezint un progres remarcabil
fa de metoda exponenial care verific toate parantezrile posibile
*
.
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 n1 do
for i 1 to ns do
m[i, i+s] +
for k i to i+s1 do
q m[i, k] + m[k+1, i+s] + d[i1] 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).
j = 1 2 3 4
i = 1
2
3
4
0 5785 1530 2856
0
1335 1845 0
9078
0
s = 3
s = 2
s = 1
s = 0

Figura 8.2 Exemplu de nmulire nlnuit a unor matrici.
Seciunea 8.3 nmulirea nlnuit a matricilor 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
Implementarea operaiilor cu matrici i, n particular, a algoritmilor de nmulire
prezentai n Seciunile 7.8 i 8.3 necesit, n primul rnd, clarificarea unor
aspecte legate de utilizarea tablourilor n limbajele C i C++.
n privina tablourilor, limbajul C++ nu aduce nimic nou fa de urmtoarele dou
reguli preluate din limbajul C:
Din punct de vedere sintactic, noiunea de tablou multidimensional nu exist.
Regula este surprinztoare deoarece, n mod cert, putem utiliza tablouri
multidimensionale. De exemplu, int a[2][5] este un tablou multidimensional
(bidimensional) corect definit, avnd dou linii i cinci coloane, iar a[1][2]
este unul din elementele sale, i anume al treilea de pe a doua linie. Aceast
contradicie aparent este generat de o ambiguitate de limbaj: prin int
a[2][5] am definit, de fapt, dou tablouri de cte cinci elemente. Altfel spus,
a este un tablou de tablouri i, ca o prim consecin, rezult c numrul
dimensiunilor unui tablou multidimensional este nelimitat. O alt consecin
este chiar modalitatea de memorare a elementelor. Aa cum este normal, cele
dou tablouri (de cte cinci elemente) din a sunt memorate ntr-o zon
continu de memorie, unul dup altul. Deci, elementele tablourilor
bidimensionale sunt memorate pe linii. n general, elementele tablourilor
multidimensionale sunt memorate astfel nct ultimul indice variaz cel mai
rapid.
Un identificator de tablou este, n acelai timp, un pointer a crui valoare este
adresa primului element al tabloului. Prin aceast regul, tablourile sunt
identificate cu adresele primelor lor elemente. De exemplu, identificatorul a de
194 Algoritmi de programare dinamic Capitolul 8

mai sus (definit ca int a[2][5]) este de tip pointer la un tablou cu cinci
elemente ntregi, adic int (*)[5], iar a[0] i a[1] sunt adrese de ntregi,
adic int*. Mai exact, expresia a[0] este adresa primei linii din matrice (a
primului tablou de cinci elemente) i este echivalent cu *(a+0), iar expresia
a[1] este adresa celei de-a doua linii din matrice (a celui de-al doilea tablou
de cinci elemente), adic *(a+1). n final, deducem c a[1][2] este
echivalent cu *(*(a+1)+2), ceea ce ilustreaz echivalena operatorului de
indexare i a celui de indirectare.
n privina echivalenei identificatorilor de tablouri i a pointerilor, nu mai putem
fi att de categorici. S pornim de la urmtoarele dou definiii:

int a[ 2 ][ 5 ];
int *b[ 2 ] = {
a[ 0 ] // adica b[ 0 ] = &a[ 0 ][ 0 ]
a[ 1 ] // adica b[ 1 ] = &a[ 1 ][ 0 ]
};

unde a este un tablou de 2 5 elemente ntregi, iar b este un tablou de dou
adrese de ntregi. Structura zonelor de memorie de la adresele a i b este
prezentat n Figura 8.3.
Evalund expresia b[1][2], obinem *(*(b+1)+2), adic elementul a[1][2],
element adresat i prin expresia echivalent *(*(a+1)+2). Se observ c valoarea
pointerului *(b+1) este memorat n al doilea element din b (de adres b+1), n
timp ce valoarea *(a+1), tot de tip pointer la int, nu este memorat, fiind
substituit direct cu adresa celei de-a doua linii din a. Pentru sceptici, programul
urmtor ilustreaz aceste afirmaii.

12
b
a
1 0
00 01 03 02 10 11 13

Figura 8.3 Structura zonelor de memorie de la adresele a i b.
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;
}

Tratarea diferit a expresiilor echivalente *(b+1) i *(a+1) se datoreaz faptului
c identificatorii de tablouri nu sunt de tip pointer, ci de tip pointer constant.
Valoarea lor nu poate fi modificat, deoarece este o constant rezultat n urma
compilrii programului. Astfel, dac definim

char x[ ] = "algoritm";
char *y = "eficient";

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 );

putem scrie oricnd

for ( int i = 0; i < D; i++ ) x[ i ] = i;

dar nu i

for ( i = 0; i < D; i++ ) *( x + i ) = i;

deoarece expresia x+i nu poate fi calculat. Cu alte cuvinte, identificatorii de tip
tablou<T> nu mai sunt asimilai tipului pointer. ntr-adevr, identificatorul x,
definit ca tablou<float> x( D ), nu este identificatorul unui tablou predefinit,
196 Algoritmi de programare dinamic Capitolul 8

ci al unui tip definit utilizator, tip care, ntmpltor, are un comportament de
tablou. Dac totui dorim ca expresia *(x+i) s fie echivalent cu x[i], nu avem
dect s definim n clasa tablou<T> operatorul

template <class T>
T* operator +( tablou<T>& t, int i ) {
return &t[ i ];
}

n continuare, ne ntrebm dac avem posibilitatea de a defini tablouri
multidimensionale prin clasa tablou<T>, fr a introduce un tip nou. Rspunsul
este afirmativ i avem dou variante de implementare:
Orice clas permite definirea unor tablouri de obiecte. n particular, pentru
clasa tablou<T>, putem scrie

tablou<int> c[ 3 ];

ceea ce nseamn c c este un tablou de trei elemente de tip tablou<int>.
Iniializarea acestor elemente se realizeaz prin specificarea explicit a
argumentelor constructorilor.

tablou<int> x( 5 ); // un tablou de 5 de elmente
tablou<int> c[ 3 ] = { tablou<int>( x ),
tablou<int>( 9 )
};

n acest exemplu, primul element se iniializeaz prin constructorul de copiere,
al doilea prin constructorul cu un singur argument int (numrul elementelor),
iar al treilea prin constructorul implicit. n expresia c[1][4], care se refer la
al cincilea element din cea de-a doua linie, primul operator de indexare folosit
este cel predefinit, iar al doilea este cel suprancrcat n clasa tablou<T>. Din
pcate, c este n cele din urm tot un tablou predefinit, avnd deci toate
deficienele menionate n Seciunea 4.1. n particular, este imposibil de
verificat corectitudinea primului indice, n timp ce verificarea celui de-al
doilea poate fi activat selectiv, pentru fiecare linie.
O a doua modalitate de implementare a tablourilor multidimensionale
utilizeaz din plin facilitile claselor parametrice. Prin instruciunea

tablou< tablou<int> > d( 3 );

obiectul d este definit ca un tablou cu trei elemente, fiecare element fiind un
tablou de int.
Problema care apare aici este cum s dimensionm cele trei tablouri membre,
tablouri iniializate prin constructorul implicit. Nu avem nici o modalitate de a
specifica argumentele constructorilor (ca i n cazul alocrii tablourilor prin
Seciunea 8.4 Tablouri multidimensionale 197

operatorul new), unica posibilitate rmnnd atribuirea explicit sau funcia de
modificare a dimensiunii (redimensionare).

tablou<int> x( 25 );
tablou< tablou<int> > d( 3 );
d[ 0 ] = x; // prima linie se initializeaza cu x
d[ 1 ].newsize( 16 ); // a doua linie se redimensioneaza
// a treia linie nu se modifica

Adresarea elementelor tabloului d const n evaluarea expresiilor de genul
d[1][4], unde operatorii de indexare [] sunt, de aceast dat, ambii din clasa
parametric tablou<T>. n consecin, activarea verificrilor de indici poate fi
invocat fie prin d.vOn(), pentru indicele de linie, fie separat n fiecare linie,
prin d[i].vOn(), pentru cel de coloan.
n anumite situaii, tablourile multidimensionale definite prin clasa parametric
tablou<T> au un avantaj important fa de cele predefinite, n ceea ce privete
consumul de memorie. Pentru fixarea ideilor, s considerm tablouri
bidimensionale, adic matrici. Dac liniile unei matrici nu au acelai numr de
elemente, atunci:
n tablourile predefinite, fiecare linie este de lungime maxim.
n tablourile bazate pe clasa tablou<T>, fiecare linie poate fi dimensionat
corespunztor numrului efectiv de elemente.
O matrice este triunghiular, atunci cnd doar elementele situate de-o parte a
diagonalei principale
*
sunt efectiv utilizate. n particular, o matrice triunghiular
este inferior triunghiular, dac folosete numai elementele de sub diagonala
principal i superior trunghiular, n caz contrar. Matricile trunghiulare au deci
nevoie numai de aproximativ jumtate din spaiul necesar unei matrici obinuite.
Tablourile bazate pe clasa tablou<T> permit implementarea matricilor
triunghiulare n spaiul strict necesar, prin dimensionarea corespunztoare a
fiecrei linii. Pentru tablourile predefinite, acest lucru este posibil doar prin
utilizarea unor artificii de calcul la adresarea elementelor.

*
Diagonala principal este diagonala care unete colul din stnga sus cu cel din dreapta jos.
8.5 Determinarea celor mai scurte drumuri ntr-un
graf
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.
198 Algoritmi de programare dinamic Capitolul 8

Vom presupune c vrfurile sunt numerotate de la 1 la n i c matricea L d
lungimea fiecrei muchii: L[i, i] = 0, L[i, j] 0 pentru i j, L[i, j] = + dac
muchia (i, j) nu exist.
Principiul optimalitii este valabil: dac cel mai scurt drum de la i la j trece prin
vrful k, atunci poriunea de drum de la i la k, ct i cea de la k la j, trebuie s fie,
de asemenea, optime.
Construim o matrice D care s conin lungimea celui mai scurt drum ntre fiecare
pereche de vrfuri. Algoritmul de programare dinamic iniializeaz pe D cu L.
Apoi, efectueaz n iteraii. Dup iteraia k, D va conine lungimile celor mai
scurte drumuri care folosesc ca vrfuri intermediare doar vrfurile din
{1, 2, , k}. Dup n iteraii, obinem rezultatul final. La iteraia k, algoritmul
trebuie s verifice, pentru fiecare pereche de vrfuri (i, j), dac exist sau nu un
drum, trecnd prin vrful k, care este mai bun dect actualul drum optim ce trece
doar prin vrfurile din {1, 2, , k1}. Fie D
k
matricea D dup iteraia k.
Verificarea necesar este atunci:
D
k
[i, j] = min(D
k1
[i, j], D
k1
[i, k] + D
k1
[k, j])
unde am fcut uz de principiul optimalitii pentru a calcula lungimea celui mai
scurt drum via k. Implicit, am considerat c un drum optim care trece prin k nu
poate trece de dou ori prin k.
Acest algoritm simplu este datorat lui Floyd (1962):
function Floyd(L[1 .. n, 1 .. n])
array D[1 .. n, 1 .. n]
D L
for k 1 to n do
for i 1 to n do
for j 1 to n do
D[i, j] min(D[i, j], D[i, k]+D[k, j])
return D
Seciunea 8.5 Determinarea celor mai scurte drumuri ntr-un graf 199

De exemplu, dac avem
D L
0
0 5
50 0 15 5
30 0 15
15 5 0
= =

|
\

|
|
|
|
|

obinem succesiv

D D
1 2
0 5
50 0 15 5
30 35 0 15
15 20 5 0
0 5 20 10
50 0 15 5
30 35 0 15
15 20 5 0
=
|
\

|
|
|
|
|
=
|
\

|
|
|
|
|

D D
3 4
0 5 20 10
45 0 15 5
30 35 0 15
15 20 5 0
0 5 15 10
20 0 10 5
30 35 0 15
15 20 5 0
=
|
\

|
|
|
|
|
=
|
\

|
|
|
|
|

Putei deduce c algoritmul lui Floyd necesit un timp n (n
3
). Un alt mod de a
rezolva aceast problem este s aplicm algoritmul Dijkstra (Capitolul 6) de n
ori, alegnd mereu un alt vrf surs. Se obine un timp n n (n
2
), adic tot n
(n
3
). Algoritmul lui Floyd, datorit simplitii lui, are ns constanta
multiplicativ mai mic, fiind probabil mai rapid n practic. Dac folosim
algoritmul Dijkstra-modificat n mod similar, obinem un timp total n
O(max(mn, n
2
) log n), unde m = #M. Dac graful este rar, atunci este preferabil s
aplicm algoritmul Dijkstra-modificat de n ori; dac graful este dens (m n
2
),
este mai bine s folosim algoritmul lui Floyd.
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 D[i, j] D[i, k]+D[k, j]
P[i, j] k
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 Algoritmi de programare dinamic Capitolul 8

j trece prin k i urmeaz s consultm recursiv elementele P[i, k] i P[k, j] pentru
a gsi i celelalte vrfuri intermediare.
Pentru exemplul precedent se obine
P =
|
\

|
|
|
|
|
0 0 4 2
4 0 4 0
0 1 0 0
0 1 0 0

Deoarece P[1, 3] = 4, cel mai scurt drum de la 1 la 3 trece prin 4. Deoarece
P[1, 4] = 2, cel mai scurt drum de la 1 la 4 trece prin 2. Rezult c cel mai scurt
drum de la 1 la 3 este: 1, 2, 4, 3.
8.6 Arbori binari optimi de cutare
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.

*
n aceast seciune vom subnelege c toi arborii de cutare sunt binari.
C
A E
B E D G
F H

Figura 8.4 Un arbore binar de cutare.
Seciunea 8.6 Arbori binari optimi de cutare 201

Aceast structur de date este util, deoarece permite o cutare eficient a
valorilor n arbore (Exerciiul 8.10). De asemenea, este posibil s actualizm un
arbore de cutare (s tergem un vrf, s modificm valoarea unui vrf, sau s
adugm un vrf) ntr-un mod eficient, fr s distrugem proprietatea de arbore de
cutare.
Cu o mulime dat de chei, se pot construi mai muli arbori de cutare (Figura
8.5).
Pentru a cuta o cheie X n arborele de cutare, X va fi comparat la nceput cu
cheia rdcinii arborelui. Dac X este mai mic dect cheia rdcinii, atunci se
continu cutarea n subarborele stng; dac X este egal cu cheia rdcinii,
atunci cutarea se ncheie cu succes; dac X este mai mare dect cheia rdcinii,
atunci se continu cutarea n subarborele drept. Se continu apoi recursiv acest
proces.
De exemplu, n arborele din Figura 8.4 putem gsi cheia E prin dou comparaii,
n timp ce aceeai cheie poate fi gsit n arborele din Figura 8.5 printr-o singur
comparaie. Dac cheile A, B, C, , H au aceeai probabilitate, atunci pentru a
gsi o cheie oarecare sunt necesare n medie:
(2+3+1+3+2+4+3+4)/8 = 22/8 comparaii, pentru arborele din Figura 8.4
(4+3+2+3+1+3+2+3)/8 = 21/8 comparaii, pentru arborele din Figura 8.5
Cnd cheile sunt echiprobabile, arborele de cutare care minimizeaz numrul
mediu de comparaii necesare este arborele de cutare de nlime minim
(demonstrai acest lucru i gsii o metod pentru a construi arborele respectiv!).
Vom rezolva n continuare o problem mai general. S presupunem c avem
cheile c
1
< c
2
< < c
n
i c, n tabloul p, p[i] este probabilitatea cu care este
cutat cheia c
i
, 1 i n. Pentru simplificare, vom considera c sunt cutate doar
cheile prezente n arbore, deci c p[1]+p[2]++p[n] = 1. Ne propunem s gsim
arborele optim de cutare pentru cheile c
1
, c
2
, , c
n
, adic arborele care
minimizeaz numrul mediu de comparaii necesare pentru a gsi o cheie.
E
C G
D EF H
A
B

Figura 8.5 Un alt arbore binar de cutare.
202 Algoritmi de programare dinamic Capitolul 8

Problema este similar cu cea a gsirii arborelui cu lungimea extern ponderat
minim (Seciunea 6.3), cu deosebirea c, de aceast dat, trebuie s meninem
ordinea cheilor. Aceast restricie face ca problema gsirii arborelui optim de
cutare s fie foarte asemntoare cu problema nmulirii nlnuite a matricilor.
n esen, se poate aplica acelai algoritm.
Dac o cheie c
i
se afl ntr-un vrf de adncime d
i
, atunci sunt necesare d
i
+1
comparaii pentru a o gsi. Pentru un arbore dat, numrul mediu de comparaii
necesare este
p i d
i
i
n
[ ]( ) +
=

1
1

Dorim s gsim arborele pentru care acest numr este minim.
Vom rezolva aceast problem prin metoda programrii dinamice. Prima decizie
const n a determina cheia c
k
a rdcinii. S observm c este satisfcut
principiul optimalitii: dac avem un arbore optim pentru c
1
, c
2
, , c
n
i cu cheia
c
k
n rdcin, atunci subarborii si stng i drept sunt arbori optimi pentru cheile
c
1
, c
2
, , c
k1
, respectiv c
k+1
, c
k+2
, , c
n
. Mai general, ntr-un arbore optim
coninnd cele n chei, un subarbore oarecare este la rndul su optim pentru o
secven de chei succesive c
i
, c
i+1
, , c
j
, i j.
n tabloul C, s notm cu C[i, j] numrul mediu de comparaii efectuate ntr-un
subarbore care este optim pentru cheile c
i
, c
i+1
,, c
j
, atunci cnd se caut o cheie
X n arborele optim principal. Valoarea
m[i, j] = p[i] + p[i+1] + + p[ j]
este probabilitatea ca X s se afle n secvena c
i
, c
i+1
, , c
j
. Fie c
k
cheia rdcinii
subarborelui considerat. Atunci, probabilitatea comparrii lui X cu c
k
este m[i, j],
i avem:
C[i, j] = m[i, j] + C[i, k1] + C[k+1, j]
Pentru a obine schema de programare dinamic, rmne s observm c c
k
(cheia
rdcinii subarborelui) este aleas astfel nct
C[i, j] = m[i, j] + min
i k j
(C[i, k1]+C[k+1, j]) (*)
n particular, C[i, i] = p[i] i C[i, i1] = 0.

Dac dorim s gsim arborele optim pentru cheile c
1
< c
2
< < c
5
, cu
probabilitile
Seciunea 8.6 Arbori binari optimi de cutare 203

p[1] = 0,30 p[2] = 0,05 p[3] = 0,08
p[4] = 0,45 p[5] = 0,12
204 Algoritmi de programare dinamic Capitolul 8

calculm pentru nceput matricea m:
m =
|
\

|
|
|
|
|
|
|
0,30 0,35 0,43 0,88 1
0,05 0,13 0,58 0,70
0,08 0,53 0,65
0,45 0,57
0,12
,00

S notm c C[i, i] = p[i], 1 i 5. Din relaia (*), calculm celelalte valori
pentru C[i, j]:
C[1, 2] = m[1, 2] + min(C[1, 0]+C[2, 2], C[1, 1]+C[3, 2])
= 0,35 + min(0,05, 0,30) = 0,40
Similar,
C[2, 3] = 0,18 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 ji = 1, apoi
pentru ji = 2 etc. Cnd ji = q, avem de calculat nq valori ale lui C[i, j], fiecare
implicnd o alegere ntre q+1 posibiliti. Timpul necesar
*
este deci n
( ( )( ) ) ( ) n q q n
q
n
+ =
=

1
3
1
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

*
Dac inem cont de mbuntirile propuse de D. E. Knuth (Tratat de programarea
calculatoarelor. Sortare i cutare, Seciunea 6.2.2), acest algoritm de construire a arborilor
optimi de cutare poate fi fcut ptratic.
Seciunea 8.6 Arbori binari optimi de cutare 205

paralel cu tabloul C, vom construi tabloul r, astfel nct r[i, j] s conin valoarea
lui k pentru care este obinut n relaia (*) valoarea minim a lui C[i, j], unde
i < j. Generm un arbore binar, conform urmtoarei metode recursive:
rdcina este etichetat cu (1, n)
dac un vrf este etichetat cu (i, j), i < j, atunci fiul su stng va fi etichetat cu
(i, r[i, j]1) i fiul su drept cu (r[i, j]+1, j)
vrfurile terminale sunt etichetate cu (i, i)
Plecnd de la acest arbore, arborele de cutare optim se obine schimbnd
etichetele (i, j), i < j, n c
r[i, j]
, iar etichetele (i, i) n c
i
.
Pentru exemplul precedent, obinem astfel arborele optim din Figura 8.6.
Problema se poate generaliza, acceptnd s cutm i chei care nu se afl n
arbore. Arborele optim de cutare se obine n mod similar.
8.7 Arborii binari de cutare ca tip de dat
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:

c
4
c
5
c
1
c
3
c
2

Figura 8.6 Un arbore optim de cutare.
206 Algoritmi de programare dinamic Capitolul 8

template <class E>
class arbore {
// ... declaratii friend
public:
arbore( ) { root = 0; n = 0; }

// ... functii membre

private:
varf<E> *root; // adresa varfului radacina
int n; // numarul varfurilor din arbore
};

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 Arborii binari de cutare ca tip de dat 207

8.7.1 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) iniializarea unui tablou cu adresele vrfurilor n ordinea cresctoare a
probabilitilor cheilor
ii) stabilirea de noi legturi ntre vrfuri astfel nct arborele s fie optim.
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( );

unde funciile membre re_greedy() i re_prodin() sunt definte astfel:

template <class E>
arbore<E>& arbore<E>::re_greedy( ) {
// reorganizare prin metoda greedy
s8a<E> opt( root, n );
root = opt.greedy( );
return *this;
}
208 Algoritmi de programare dinamic Capitolul 8


template <class E>
arbore<E>& arbore<E>::re_prodin( ) {
// reorganziare prin programare dinamica
s8a<E> opt( root, n );
root = opt.prodin( );
return *this;
}

Dup adugarea tuturor funciilor i datelor membre necesare implementrii
funciilor greedy() i prodin(), clasa s8a are urmtoarea structur:

template <class E>
class s8a { // clasa pentru construirea arborelui optim
friend class arbore<E>;
private:
s8a( varf<E> *root, int nn ): pvarf( n = nn ) {
int i = 0; // indice in pvarf
setvarf( i, root ); // setarea elementelor din pvarf
}

// initializarea tabloului pvarf cu un arbore deja format
void setvarf( int&, varf<E>* );

varf<E>* greedy( ) { // "optim" prin algoritmul greedy
return _greedy( 0, n );
}

varf<E>* prodin( ) { // optim prin programare dinamica
_progDinInit( ); return _progDin( 0, n - 1 );
}

// functiile prin care se formeaza efectiv arborele
varf<E>* _greedy ( int, int );
varf<E>* _progDin ( int, int );
void _progDinInit( ); // initializeaza tabloul r

// date membre
tablou<varf<E>*> pvarf; // tabloul adreselor varfurilor
int n; // numarul varfurilor din arbore

// tabloul indicilor necesar alg. de programare dinamica
tablou< tablou<int> > r;
};

Seciunea 8.7 Arborii binari de cutare ca tip de dat 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 Algoritmi de programare dinamic Capitolul 8

template <class E>
class varf {
friend class arbore<E>;
friend class s8a<E>;

private:
varf( const E& v, float f = 0 ): key( v )
{ st = dr = tata = 0; p = f; }

varf<E> *st; // adresa fiului stang
varf<E> *dr; // adresa fiului drept
varf<E> *tata; // adresa varfului tata

E key; // cheia
float p; // frecventa utilizarii cheii curente
};

Implementarea celor dou metode de optimizare a arborelui urmeaz pas cu pas
algoritmul greedy i, respectiv, algoritmul de programare dinamic. Ambele
(re)stabilesc legturile dintre vrfuri printr-un proces recursiv, pornind fie direct
de la probabilitile elementelor, fie de la o matrice (matricea r) construit pe
baza acestor probabiliti. Funciile care stabilesc legturile, adic _progDin() i
_greedy(), sunt urmtoarele:

template <class E>
varf<E>* s8a<E>::_greedy( int m, int M ) {
// m si M sunt limitele subsecventei curente
if ( m == M ) return 0;

// se determina pozitia k a celei mai frecvente chei
int k; float pmax = pvarf[ k = m ]->p;
for ( int i = m; ++i < M; )
if ( pvarf[ i ]->p > pmax ) pmax = pvarf[ k = i ]->p;

// se selecteaza adresa varfului de pe pozitia k
varf<E> *actual = pvarf[ k ];

// se construiesc subarborii din stanga si din deapta
// se initializeaza legatura spre varful tata
if ( (actual->st = _greedy( m, k )) != 0 )
actual->st->tata = actual;
if ( (actual->dr = _greedy( k + 1, M )) != 0 )
actual->dr->tata = actual;

// subarborele curent este gata; se returneaza adresa lui
return actual;
}

Seciunea 8.7 Arborii binari de cutare ca tip de dat 211

template <class E>
varf<E>* s8a<E>::_progDin( int i, int j ) {
// i si j, i <=j, sunt coordonatele radacinii
// subarborelui curent in tabloul r
if ( i > j ) return 0;

// se selecteaza adresa varfului radacina
varf<E> *actual = pvarf[ r[ j ][ i ] ];

if ( i != j ) { // daca nu este un varf frunza ...
// se construiesc subarborii din stanga si din deapta
// se initializeaza legatura spre varful tata
if ( (actual->st = _progDin( i, r[j][i] - 1 )) != 0 )
actual->st->tata = actual;
if ( (actual->dr = _progDin( r[j][i] + 1, j )) != 0 )
actual->dr->tata = actual;
}

// subarborele curent este gata; se returneaza adresa lui
return actual;
}

Folosind notaiile introduse n descrierea algoritmului de optimizare prin
programare dinamic, funcia _progDinInit() construiete matricea r, unde
r[i][j], i < j, este indicele n tabloul pvarf al adresei vrfului etichetat cu
(i, j). n acest scop, se folosete o alt matrice C, unde C[i][j], i < j, este
numrul de comparaii efectuate n subarborele optim al cheilor cu indicii i, , j.
Iniial, C este completat cu probabilitile cumulate ale cheilor de indici i, , j.
Se observ c matricile r i C sunt superior triunghiulare. Totui, pentru
implementare, am preferat s lucrm cu matrici inferior triunghiulare, adic cu
transpusele matricilor r i C, deoarece adresarea elementelor ar fi fost altfel mai
complicat.

template <class E>
void s8a<E>::_progDinInit( ) {
int i, j, d;
tablou< tablou<float> > C; // tabloul C este local

// redimensionarea si initializarea tablourilor C si r
// ATENTIE! tablourile C si r sunt TRANSPUSE.
r.newsize( n );
C.newsize( n );
for ( i = 0; i < n; i++ ) {
r[ i ].newsize( i + 1 ); r[ i ][ i ] = i;
C[ i ].newsize( i + 1 ); C[ i ][ i ] = pvarf[ i ]->p;
}

212 Algoritmi de programare dinamic Capitolul 8

// pentru inceput C este identic cu m
for ( d = 1; d < n; d++ )
for ( i = 0; (j = i + d) < n; i++ )
C[ j ][ i ] = C[ j - 1 ][ i ] + C[ j ][ j ];

// elementele din C se calculeaza pe diagonale
for ( d = 1; d < n; d++ )
for ( i = 0; (j = i + d) < n; i++ ) {
// in calculul minimului dintre C[i][k-1]+C[k+1][j]
// consideram mai intai cazurile k=i si k=j in care
// avem C[i][i-1] = 0 si C[j+1][j] = 0
int k; float Cmin;
if ( C[ j ][ i + 1 ] < C[ j - 1 ][ i ] )
Cmin = C[ j ][ (k = i) + 1 ];
else
Cmin = C[ (k = j) - 1 ][ i ];

// au mai ramas de testat elementele i+1, ..., j-1
for ( int l = i + 1; l < j; l++ )
if ( C[ l - 1 ][ i ] + C[ j ][ l + 1 ] < Cmin )
Cmin = C[ (k = l) - 1 ][ i ] + C[ j ][ l + 1 ];

// minimul si pozitia lui sunt stabilite ...
C[ j ][ i ] += Cmin;
r[ j ][ i ] = k;
}
}

8.7.2 Cutarea n arbore
Principala operaie efectuat prin intermediul arborilor binari de cutare este
regsirea informaiei asociate unei anumite chei. Funcia de cutare search() are
ca argument cheia pe baza creia se va face cutarea i returneaz false sau true,
dup cum cheia fost regsit, sau nu a fost regsit n arbore. Cnd cutarea s-a
terminat cu succes, valoarea din arbore a cheii regsite este returnat prin
intermediul argumentului de tip referin, pentru a permite consultarea
informaiilor asociate.

template <class E>
int arbore<E>::search( E& k ) {
varf<E> *x = _search( root, k );
if ( !x ) return 0; // element absent
x->p++; // actualizarea frecventei
k = x->key; return 1;
}

Actualizarea probabilitilor cheilor din arbore, dup fiecare operaie de cutare,
este ceva mai delicat, deoarece impune stabilirea importanei evalurilor
existente n raport cu rezultatele cutrilor. De fapt, este vorba de un proces de
Seciunea 8.7 Arborii binari de cutare ca tip de dat 213

nvare care pornete de la anumite cunotine deja acumulate. Problema este de
a stabili gradul de importan al cunotinelor existente n raport cu cele nou
dobndite. nainte de a prezenta o soluie elementar a acestei probleme, s
observm c algoritmii de optimizare lucreaz cu probabiliti, dar numai ca
ponderi. n consecin, rezultatul optimizrii nu se schimb, dac n loc de
probabiliti se folosesc frecvene absolute.

Fie trei chei ale cror probabiliti de cutare au fost estimate iniial la 0,18, 0,65,
0,17. S presupunem c se dorete optimizarea arborelui de cutare asociat
acestor chei, att pe baza acestor estimri, ct i folosind rezultatele a 1000 de
cutri de instruire terminate cu succes
*
. Dac fixm ponderea estimrilor iniiale
n raport cu rezultatele instruirii la 5 / 2, atunci vom iniializa membrul p
(estimarea probabilitii cheii curente) din clasa varf<E> cu valorile
0,18 1000 (5 / 2) = 450
0,65 1000 (5 / 2) = 1625
0,17 1000 (5 / 2) = 425
Apoi, la fiecare cutare terminat cu success, membrul p corespunztor cheii
gsite se incrementeaz cu 1. De exemplu, dac prima cheie a fost gsit n 247
cazuri, a doua n 412 cazuri i a treia n 341 cazuri, atunci valorile lui p folosite
la optimizarea arborelui vor fi 697, 2037 i 766. Suma acestor valori este 3500,
valoare care corespunde celor 1000 de ncercri plus ponderea de 1000
(5 / 2) = 2500 asociat estimrii iniiale. Noile probabiliti, nvate prin
instruire, sunt:
697 / 3500 0,20
2037 / 3500 0,58
766 / 3500 0,22
Pentru verificarea rezultatelor de mai sus, s refacem calculele, lucrnd numai cu
probabiliti. Estimrile iniiale ale probabilitilor sunt 0,18, 0,65 i 0,17. n
urma instruirii, cele trei chei au fost cutate cu probabilitile:
247 / 1000 = 0,247
412 / 1000 = 0,412
697 / 1000 = 0,697

*
n procesul de optimizare pot fi implicate nu numai cutrile terminate cu succes, ci i cele
nereuite. Cutarea cheilor care nu sunt n arbore este tot att de costisitoare ca i cutarea celor
care sunt n arbore. Pentru detalii asupra acestei probleme se poate consulta D. E. Knuth, Tratat
de programarea calculatoarelor. Sortare i cutare, Seciunea 6.2.2.
214 Algoritmi de programare dinamic Capitolul 8

Avnd n vedere raportul de 5 / 2 stabilit ntre estimarea iniial i rezultatele
instruirii, probabilitile finale
*
sunt:
(0,18 5 + 0,247 2) / 7 0,20
(0,65 5 + 0,412 2) / 7 0,58
(0,17 5 + 0,697 2) / 7 0,22

Cutarea este, de fapt, o parcurgere a vrfurilor, realizat prin funcia
_search(varf<E>*, const E&). Aceast funcie nu face parte din clasa
arbore<E>, deoarece opereaz exclusiv asupra vrfurilor. Iat varianta ei
recursiv, mpreun cu alte dou funcii asemntoare: _min(), pentru
determinarea vrfului minim din arbore i _succ(), pentru determinarea
succesorului

.

template <class E>
varf<E>* _search( varf<E>* x, const E& k ) {
while ( x != 0 && k != x->key )
x = k > x->key? x->dr: x->st;
return x;
}

template <class E>
varf<E>* _min( varf<E>* x ) {
while ( x->st != 0 )
x = x->st;
return x;
}

template <class E>
varf<E>* _succ( varf<E>* x ) {
if ( x->dr != 0 ) return _min( x->dr );

varf<E> *y = x->tata;
while ( y != 0 && x == y->dr )
{ x = y; y = y->tata; }
return y;
}

Existena acestor funcii impune completarea clasei varf<E> cu declaraiile
friend corespunztoare.

*
Acest procedeu de estimare a probabilitilor printr-un proces de instruire poate fi formalizat
ntr-un cadru matematic riguros (R. Andonie, A Converse H-Theorem for Inductive Processes,
Computers and Artificial Intelligence, Vol. 9, 1990, No. 2, pp. 159167).

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 Arborii binari de cutare ca tip de dat 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.
5 4
K
S
S
K
(a) Vrful succesor S este
minim n subarborele drept
al vrfului K.
(b) Vrful K este maxim n
subarborele stng al
vrfului succesor S.

Figura 8.7 Poziiile relative ale vrfului K n raport cu sucesorul su S.
216 Algoritmi de programare dinamic Capitolul 8

8.7.3 Modificarea arborelui
Modificarea structurii arborelui de cutare, prin inserarea sau tergerea unor
vrfuri trebuie realizat astfel nct proprietatea de arbore de cutare s nu se
altereze. Cele dou operaii sunt diferite n privina complexitii. Inserarea este
simpl, fiind similar cutrii. tergerea este mai dificil i mult diferit de
operaiile cu care deja ne-am obinuit.
Pentru inserarea unei noi chei, vom folosi funcia

template <class E>
int arbore<E>::ins( const E& k, float p ) {
varf<E> *y = 0, *x = root;

while ( x != 0 ) {
y = x;
if ( k == x->key ) { // cheia deja exista in arbore
x->p += p; // se actualizeaza frecventa
return 0; // se returneaza cod de eroare
}
x = k > x->key? x->dr: x->st;
}

// cheia nu exista in arbore
varf<E> *z = new varf<E>( k, p );
z->tata = y;

if ( y == 0 ) root = z;
else if ( z->key > y->key ) y->dr = z;
else y->st = z;

n++; // in arbore este cu un varf mai mult
return 1;
}

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 Arborii binari de cutare ca tip de dat 217

atunci cnd z este situat undeva n interiorul arborelui, avnd ambele legturi
complete. n acest caz, nu vom mai terge vrful z, ci vrful y, succesorul lui z,
dar nu nainte de a copia coninutul lui y n z. tergerea vrfului y se face
conform unuia din cele dou cazuri de mai sus, deoarece, n mod sigur, y nu are
fiul stng. ntr-adevr, ntr-un arbore de cutare, succesorul unui vrf cu doi fii nu
are fiul stng, iar predecesorul
*
unui vrf cu doi fii nu are fiul drept (demonstrai
acest lucru!). Pentru ilustrarea celor trei situaii, am ters din arborele din Figura
8.8a vrfurile E (vrf cu doi fii), A (vrf cu un fiu) i L (vrf terminal).
Procedura de tergere se implementeaz astfel:

template <class E>
int arbore<E>::del( E& k ) {
varf<E> *z = _search( root, k ); // se cauta cheia k
if ( !z ) return 0; // 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 este z daca z are cel mult un fiu si
// succesorul lui z daca z are doi fii
// - x este fiul lui y sau 0 daca y nu are fii
varf<E> *y, *x;

*
Predecesorul unui vrf X este vrful care are cea mai mare cheie mai mic dect cheia vrfului X.
E
A R
C E H
N
M
L
P
H
C R
P N
M P
(a) (b)

Figura 8.8 tergerea vrfurilor E, A i L dintr-un arbore binar de cutare.
218 Algoritmi de programare dinamic Capitolul 8


y = z->st == 0 || z->dr == 0? z: _succ( z );
x = y->st != 0? y->st: y->dr;

// se elimina varful y din arbore astfel:
// 1. se stabileste legatura in x spre varful tata
if ( x != 0 )
x->tata = y->tata;

// 2. in varful tata se stabileste legatura spre x
if ( y->tata == 0 ) root = x;
else if ( y == y->tata->st ) y->tata->st = x;
else y->tata->dr = x;

// 3. daca z are 2 fii, succesorul lui ii ia locul
if ( y != z ) { z->key = y->key; z->p = y->p; }

// 4. stergerea propriu-zisa
y->st = y->dr = 0;
delete y;

return 1;
}

Complexitatea funciei de tergere este tipic pentru structurile de cutare. Aceste
structuri tind s devin att de compacte n organizarea lor intern, nct tergerea
fiecrei chei necesit reparaii destul de complicate. De aceea, deseori se prefer
o tergere lene (lazy deletion), prin care vrful este doar marcat ca ters,
tergerea efectiv realizndu-se cu ocazia unor reorganizri periodice.

Dei clasa arbore<E> este incomplet specificat, lipsind constructorul de copiere,
operatorul de atribuire, destructorul etc, operaiile implementate n aceast
seciune pot fi testate prin urmtorul program.

#include <iostream.h>
#include "arbore.h"

main( ) {
int n;
cout << "Numarul de varfuri ... "; cin >> n;

arbore<char> g; char c; float f;

cout << "Cheile si Frecventele lor:\n";
for ( int i = 0; i < n; i++ ) {
cout << "... ";
cin >> c; cin >> f;
g.ins( c, f );
}
Seciunea 8.7 Arborii binari de cutare ca tip de dat 219


cout << "Arborele initial:\n"; g.inord( );

cout << "\n\nDelete din initial (cheie) <EOF>:\n ...";
while( cin >> c ) {
if ( g.del( c ) ) {
cout << "\nSe sterge varful cu cheia: " << c;
cout << "\nInordine:\n"; g.inord( );
}
else
cout << "\nelement absent";
cout << "\n... ";
}
cin.clear( );

g.re_greedy( );
cout << "\n\nArborele Greedy:\n"; g.inord( );

cout << "\n\nInsert in Greedy "
<< "(cheie+frecventa) <EOF>:\n... ";
while( (cin >> c) && (cin >> f) ) {
g.ins( c, f );
cout << "\nInordine:\n"; g.inord( );
cout << "\n... ";
}
cin.clear( );

cout << "\n\nCautari in Greedy (cheie) <EOF>:\n ...";
while( cin >> c ) {
if ( g.search( c ) ) {
cout << "\nNodul cu cheia: " << c;
cout << "\nInordine:\n"; g.inord( );
}
else
cout << "\nelement absent";
cout << "\n... ";
}
cin.clear( );

cout << "\n\nDelete din Greedy (cheie) <EOF>:\n ...";
while( cin >> c ) {
if ( g.del( c ) ) {
cout << "\nSe sterge varful cu cheia: " << c;
cout << "\nInordine:\n"; g.inord( );
}
else
cout << "\nelement absent";
cout << "\n... ";
}
cin.clear( );

220 Algoritmi de programare dinamic Capitolul 8

g.re_prodin( );
cout << "Arborele Greedy re-ProgDin:\n"; g.inord( );

return 1;
}

Funcia arbore<E>::inord(), definit n Seciunea 9.2, realizeaz afiarea
arborelui, astfel nct s poat fi uor de reconstituit pe hrtie. De exemplu,
arborele din Figura 8.8b este afiat astfel:

0x166c ( key C, f 0, st 0x0000, dr 0x0000, tata 0x163c )
0x163c ( key H, f 0, st 0x166c, dr 0x165c, tata 0x0000 )
0x169c ( key M, f 0, st 0x0000, dr 0x0000, tata 0x168c )
0x168c ( key N, f 0, st 0x169c, dr 0x16ac, tata 0x165c )
0x16ac ( key P, f 0, st 0x0000, dr 0x0000, tata 0x168c )
0x165c ( key R, f 0, st 0x168c, dr 0x0000, tata 0x163c )

8.8 Programarea dinamic comparat cu tehnica
greedy
Att programarea dinamic, ct i tehnica greedy, pot fi folosite atunci cnd
soluia unei probleme este privit ca rezultatul unei secvene de decizii. Deoarece
principiul optimalitii poate fi exploatat de ambele metode, s-ar putea s fim
tentai s elaborm o soluie prin programare dinamic, acolo unde este suficient
o soluie greedy, sau s aplicm n mod eronat o metod greedy, atunci cnd este
necesar de fapt aplicarea programrii dinamice. Vom considera ca exemplu o
problem clasic de optimizare.
Un ho ptrunde ntr-un magazin i gsete n obiecte, un obiect i avnd valoarea v
i

i greutatea g
i
. Cum s-i optimizeze houl profitul, dac poate transporta cu un
rucsac cel mult o greutate G? Deosebim dou cazuri. n primul dintre ele, pentru
orice obiect i, se poate lua orice fraciune 0 x
i
1 din el, iar n al doilea caz,
x
i
{0,1}, adic orice obiect poate fi ncrcat numai n ntregime n rucsac.
Corespunztor acestor dou cazuri, obinem problema continu a rucsacului,
respectiv, problema 0/1 a rucsacului. Evident, houl va selecta obiectele astfel
nct s maximizeze funcia obiectiv
f x v x
i i
i
n
( ) =
=

1

unde x = (x
1
, x
2
, , x
n
), verific condiia
Seciunea 8.8 Programarea dinamic comparat cu tehnica greedy 221

g x G
i i
i
n
=

1

Soluia problemei rucsacului poate fi privit ca rezultatul unei secvene de decizii.
De exemplu, houl va decide pentru nceput asupra valorii lui x
1
, apoi asupra
valorii lui x
2
etc. Printr-o secven optim de decizii, el va ncerca s maximizeze
funcia obiectiv. Se observ c este valabil principiul optimalitii. Ordinea
deciziilor poate fi desigur oricare alta.
Problema continu a rucsacului se poate rezolva prin metoda greedy, selectnd la
fiecare pas, pe ct posibil n ntregime, obiectul pentru care v
i
/g
i
este maxim. Fr
a restrnge generalitatea, vom presupune c
v
1
/g
1
v
2
/g
2
v
n
/g
n

Putei demonstra c prin acest algoritm obinem soluia optim i c aceasta este
de forma x

= (1, , 1, x
k

, 0, , 0), k fiind un indice, 1 k n, astfel nct


0 x
k
1. Algoritmul greedy gsete secvena optim de decizii, lund la fiecare
pas cte o decizie care este optim local. Algoritmul este corect, deoarece nici o
decizie din secven nu este eronat. Dac nu considerm timpul necesar sortrii
iniiale a obiectelor, timpul este n ordinul lui n.
S trecem la problema 0/1 a rucsacului. Se observ imediat c tehnica greedy nu
conduce n general la rezultatul dorit. De exemplu, pentru g = (1, 2, 3),
v = (6, 10, 12), G = 5, algoritmul greedy furnizeaz soluia (1, 1, 0), n timp ce
soluia optim este (0, 1, 1). Tehnica greedy nu poate fi aplicat, deoarece este
generat o decizie (x
1
= 1) optim local, nu ns i global. Cu alte cuvinte, la
primul pas, nu avem suficient informaie local pentru a decide asupra valorii lui
x
1
. Strategia greedy exploateaz insuficient principiul optimalitii, considernd
c ntr-o secven optim de decizii fiecare decizie (i nu fiecare subsecven de
decizii, cum procedeaz programarea dinamic) trebuie s fie optim. Problema se
poate rezolva printr-un algoritm de programare dinamic, n aceast situaie
exploatndu-se complet principiul optimalitii. Spre deosebire de problema
continu, nu se cunoate nici un algoritm polinomial pentru problema 0/1 a
rucsacului.
Diferena esenial dintre tehnica greedy i programarea dinamic const n faptul
c metoda greedy genereaz o singur secven de decizii, exploatnd incomplet
principiul optimalitii. n programarea dinamic, se genereaz mai multe
subsecvene de decizii; innd cont de principiul optimalitii, se consider ns
doar subsecvenele optime, combinndu-se acestea n soluia optim final. Cu
toate c numrul total de secvene de decizii este exponenial (dac pentru fiecare
din cele n decizii sunt d posibiliti, atunci sunt posibile d
n
secvene de decizii),
algoritmii de programare dinamic sunt de multe ori polinomiali, aceast reducere
222 Algoritmi de programare dinamic Capitolul 8

a complexitii datorndu-se utilizrii principiului optimalitii. O alt
caracteristic important a programrii dinamice este c se memoreaz
subsecvenele optime, evitndu-se astfel recalcularea lor.
8.9 Exerciii
8.1 Demonstrai c numrul total de apeluri recursive necesare pentru a-l
calcula pe C(n, k) este 2
n
k
|
\

|
|
2.
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 n1 i demonstrm
pentru n.
Presupunem, pentru nceput, c 0 < k < n. Atunci, avem recurena
r(n, k) = r(n1, k1) + r(n1, k) + 2
Din relaia precedent, obinem
r(n, k) = 2
n
k

|
\

|
|
1
1
2 + 2
n
k
|
\

|
|
1
2 + 2 = 2
n
k
|
\

|
|
2
Dac k este 0 sau n, atunci r(n, k) = 0 i, deoarece n acest caz avem
n
k
|
\

|
|
= 1,
rezult c proprietatea este adevrat. Acest rezultat poate fi verificat practic,
rulnd programul din Exerciiul 2.5.

8.2 Artai c principiul optimalitii
i) este valabil n problema gsirii celui mai scurt drum dintre dou vrfuri ale
unui graf
ii) nu este valabil n problema determinrii celui mai lung drum simplu dintre
dou vrfuri ale unui graf

8.3 Demonstrai c
2n
k
|
\

|
|
4
n
/(2n+1).

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 j1 do
q min(q, rminscal(i, k)+rminscal(k+1, j)+d[i1]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( ji+1) numrul de apeluri recursive necesare pentru a-l
calcula pe rminscal(i, j). Pentru n > 2 avem
r n r k r n k r k r n
k
n
k
n
( ) ( ) ( ) ( ) ( ) = + =
=


1
1
1
1
2 2 1
iar r(2) = 2. Prin metoda iteraiei, deducei c r(n) 2
n1
, pentru n > 2. Timpul
pentru un apel rminscal(1, n) este atunci n (2
n
).

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 n1 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 Algoritmi de programare dinamic 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] = cheia vrfului
ST[i] = adresa fiului stng
DR[i] = adresa fiului drept
TATA[i] = adresa tatlui
(Dac se folosete o implementare prin tablouri paralele, atunci adresele sunt
indici de tablou). Presupunem c variabila root conine adresa rdcinii arborelui
i c o adres este zero, dac i numai dac vrful ctre care se face trimiterea
lipsete. Elaborai algoritmi pentru urmtoarele operaii n arborele de cutare:
i) Determinarea vrfului care conine o cheie v dat. Dac un astfel de vrf nu
exist, se va returna adresa zero.
ii) Determinarea vrfului care conine cheia minim.
iii) Determinarea succesorului unui vrf i dat (succesorul vrfului i este vrful
care are cea mai mic cheie mai mare dect KEY[i]).
Care este eficiena acestor algoritmi?
Soluie:
i) Apelm tree-search(root, v), tree-search fiind funcia:
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) Se apeleaz tree-min(root), tree-min fiind funcia:
function tree-min(i)
while ST[i] 0 do i ST[i]
return i
iii) Urmtorul algoritm returneaz succesorul vrfului i:
function tree-succesor(i)
if DR[i] 0 then return tree-min(DR[i])
j TATA[i]
while j 0 and i = DR[ j] do i j
j TATA[ j]
return j

8.11 Gsii o formul explicit pentru T(n), unde T(n) este numrul de arbori
de cutare diferii care se pot construi cu n chei distincte.
Indicaie: Facei legtura cu problema nmulirii nlnuite a matricilor.

8.12 Exist un algoritm greedy evident pentru a construi arborele optim de
cutare avnd cheile c
1
< c
2
< < c
n
: se plaseaz cheia cea mai probabil, c
k
, la
rdcin i se construiesc subarborii si stng i drept pentru cheile
c
1
, c
2
, , c
k1
, respectiv, c
k+1
, c
k+2
, , c
n
, n mod recursiv, pe acelai principiu.
i) Ct timp necesit algoritmul pentru cazul cel mai nefavorabil?
ii) Artai pe baza unui contraexemplu c prin acest algoritm greedy nu se obine
ntotdeauna arborele optim de cutare.

8.13 Un subcaz oarecare al problemei 0/1 a rucsacului se poate formula astfel:
S se gseasc
226 Algoritmi de programare dinamic Capitolul 8

V l j X v x
i i
l i j
( , , ) max =


unde maximul se ia pentru toi vectorii (x
l
, , x
j
) pentru care
g x X
i
l i j
i


x
i
{0, 1}, l i j
n particular, V(1, n, G) este valoarea maxim care se poate ncrca n rucsac n
cazul problemei iniiale. O soluie a acestei probleme se poate obine dac
considerm c deciziile se iau retrospectiv, adic n ordinea x
n
, x
n1
, , x
1
.
Principiul optimalitii este valabil i avem
V(1, n, G) = max(V(1, n1, G), V(1, n1, Gg
n
) + v
n
)
i, n general,
V(1, j, X) = max(V(1, j1, X), V(1, j1, Xg
j
) + v
j
)
unde V(1, 0, X) = 0 pentru X 0, iar V(1, j, X) = pentru X < 0. De aici se poate
calcula, prin tehnica programrii dinamice, valoarea V(1, n, G) care ne
intereseaz.
Gsii o recuren similar pentru situaia cnd deciziile se iau prospectiv, adic
n ordinea x
1
, x
2
, , x
n
.

8.14 Am vzut (n Seciunea 6.1) c tehnica greedy poate fi aplicat n
problema determinrii restului cu un numr minim de monezi doar pentru anumite
cazuri particulare. Problema se poate rezolva, n cazul general, prin metoda
programrii dinamice.
S presupunem c avem un numr finit de n tipuri de monezi, fiecare n numr
nelimitat, iar tabloul M[1 .. n] conine valoarea acestor monezi. Fie S suma pe
care dorim s o obinem, folosind un numr minim de monezi.
i) n tabloul C[1 .. n, 1 .. S], fie C[i, j] numrul minim de monezi necesare
pentru a obine suma j, folosind doar monezi de tipul M[1], M[2], , M[i],
unde C[i, j] = +, dac suma j nu poate fi obinut astfel. Gsii o recuren
pentru C[i, j].
ii) Elaborai un algoritm care folosete tehnica programrii dinamice pentru a
calcula valorile C[n, j], 1 j S. Algoritmul trebuie s utilizeze un singur
vector de S elemente. Care este timpul necesar, n funcie de n i S?
iii) Gsii un algoritm greedy care determin cum se obine suma S cu un numr
minim de monezi, presupunnd cunoscute valorile C[n, j].
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 (terge b)
ababc (adaug b)
abcbc (schimb a cu c)
Artai c aceast transformare nu este optim. Elaborai un algoritm de
programare dinamic care gsete numrul minim de operaii necesare (i le
specific) pentru a-l transforma pe u n v.

8.16 S considerm alfabetul = {a, b, c}. Pentru elementele lui definim
urmtoarea tabl de nmulire:
Observai c nmulirea definit astfel nu este nici comutativ i nici asociativ.
Gsii un algoritm eficient care examineaz irul x = x
1
x
2
x
n
de caractere ale
lui i decide dac x poate fi parantezat astfel nct expresia rezultat s fie a.
De exemplu, dac x = bbbba, algoritmul trebuie s returneze da deoarece
(b(bb))(ba) = a.

8.17 Artai c numrul de moduri n care un poligon convex cu n laturi poate
fi partiionat n n2 triunghiuri, folosind linii diagonale care nu se ntretaie, este
T(n1), unde T(n1) este al (n1)-lea numr catalan.

simbolul drept
a b c
simbolul a b b a
stng b c b a
c a c c

227
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
dou configuraii. Acest graf are aproximativ 10
120
vrfuri. Presupunnd c un
calculator ar fi capabil s genereze 10
11
vrfuri pe secund, generarea complet a
grafului asociat jocului de ah s-ar face n mai mult de 10
80
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
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
ni1 vrfuri n subarborele drept, este
t(n) c + max {t(i)+t(ni1) | 0 i n1}
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(n1) = dn+c+2cd
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).

Pentru fiecare din aceste tehnici de parcurgere, implementarea recursiv necesit,
n cazul cel mai nefavorabil, un spaiu de memorie n (n) (demonstrai acest
lucru!). Cu puin efort
*
, tehnicile menionate pot fi implementate astfel nct s
necesite un timp n (n) i un spaiu de memorie n (1), chiar dac vrfurile nu
conin adresa tatlui (caz n care problema devine trivial).
Conceptele de preordine i postordine se pot generaliza pentru arbori arbitrari
(nebinari). Timpul de parcurgere este tot n ordinul numrului de vrfuri.

*
O astfel de implementare poate fi gsit, de exemplu, n E. Horowitz i S. Sahni, Fundamentals of
Computer Algorithms, Seciunea 6.1.1.
Seciunea 9.2 Operaii de parcurgere n clasa arbore 229

9.2 Operaii de parcurgere n clasa arbore<E>
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 subarborii din stanga si din deapta; in
// fiecare se initializeaza legatura spre varful tata
if ( (z->st = _copy( x->st )) != 0 )
z->st->tata = z;
if ( (z->dr = _copy( x->dr )) != 0 )
z->dr->tata = z;
}
return z;
}

Invocarea acestei funcii este realizat att de ctre constructorul de copiere al
clasei arbore,
230 Explorri n grafuri Capitolul 9


template <class E>
arbore<E>::arbore( const arbore<E>& a ) {
root = _copy( a.root ); n = a.n;
}

ct i de ctre operatorul de atribuire:

template <class E>
arbore<E>& arbore<E>::operator =( const arbore<E>& a ) {
delete root;
root = _copy( a.root ); n = a.n;
return *this;
}

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; }

Efectul lui const n tergerea vrfurilor n postordine. Mai nti, se acioneaz
asupra sub-arborelui stng, apoi asupra celui drept, iar n final, dup execuia
corpului destructorului, operatorul delete elibereaz spaiul alocat vrfului
curent. Condiia de oprire a recursivitii este asigurat de operatorul delete, el
fiind inefectiv pentru adresele nule. n consecin, i destructorul clasei
arbore<E> const ntr-un simplu delete root:

~arbore( ) { delete root; }

Toate modalitile de parcurgere menionate n Seciunea 9.1 pot fi implementate
imediat, prin funciile corespunztoare. Noi ne-am rezumat la implementarea
parcurgerii n inordine deoarece, pe parcursul testrii clasei arbore<E>, am avut
nevoie de afiarea structurii arborelui. Funcia

template <class E>
void _inord( varf<E> *x ) {

if ( !x ) return;

_inord( x->st );
Seciunea 9.2 Operaii de parcurgere n clasa arbore 231

cout << x
<< " ( key " << x->key
<< ", f " << x->p
<< ", st " << x->st
<< ", dr " << x->dr
<< ", tata " << x->tata
<< " )";

_inord( x->dr );
}

apelabil din clasa arbore<E> prin

template <class E>
void arbore<E>::inord( ) { _inord( root ); }

este exact ceea ce ne trebuie pentru a afia ntreaga structur intern a arborelui.
9.3 Parcurgerea grafurilor n adncime
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

Acest mod de parcurgere se numete n adncime, deoarece ncearc s iniieze
ct mai multe apeluri recursive nainte de a se ntoarce dintr-un apel.
Parcurgerea n adncime a fost formulat cu mult timp n urm ca o tehnic de
explorare a unui labirint. O persoan care caut ceva ntr-un labirint i aplic
aceast tehnic are avantajul c urmtorul loc n care caut este mereu foarte
aproape.
Pentru graful din Figura 9.1a, presupunnd c pornim din vrful 1 i c vizitm
vecinii unui vrf n ordine numeric, parcurgerea vrfurilor n adncime se face n
ordinea: 1, 2, 3, 6, 5, 4, 7, 8.
Desigur, parcurgerea n adncime a unui graf nu este unic; ea depinde att de
alegerea vrfului iniial, ct i de ordinea de vizitare a vrfurilor adiacente.
Ct timp este necesar pentru a parcurge un graf cu n vrfuri i m muchii?
Deoarece fiecare vrf este vizitat exact o dat, avem n apeluri ale procedurii ad.
n procedura ad, cnd vizitm un vrf, testm marcajul fiecrui vecin al su. Dac
reprezentm graful prin liste de adiacen, adic prin ataarea la fiecare vrf a
listei de vrfuri adiacente lui, atunci numrul total al acestor testri este: m, dac
graful este orientat, i 2m, dac graful este neorientat. Algoritmul necesit un timp
n (n) pentru apelurile procedurii ad i un timp n (m) pentru inspectarea
mrcilor. Timpul de execuie este deci n (max(m, n)) = (m+n).
Dac reprezentm graful printr-o matrice de adiacen, se obine un timp de
execuie n (n
2
).
Parcurgerea n adncime a unui graf G, neorientat i conex, asociaz lui G un
arbore parial. Muchiile arborelui corespund muchiilor parcurse n G, iar vrful
ales ca punct de plecare devine rdcina arborelui. Pentru graful din Figura 9.1a,
un astfel de arbore este reprezentat n Figura 9.1b prin muchiile continue;
muchiile din G care nu corespund unor muchii ale arborelui sunt punctate. Dac
1
2 4 3
7 6 5 8
1
2 4 3
7 6 5 8
(a) (b)

Figura 9.1 Un graf neorientat i unul din arborii si pariali.
Seciunea 9.3 Parcurgerea grafurilor n adncime 233

graful G nu este conex, atunci parcurgerea n adncime asociaz lui G o pdure de
arbori, cte unul pentru fiecare component conex a lui G.
Dac dorim s i marcm numeric vrfurile n ordinea parcurgerii lor, adugm n
procedura ad, la nceput:
num num + 1
preord[v] num
unde num este o variabil global iniializat cu zero, iar preord[1 .. n] este un
tablou care va conine n final ordinea de parcurgere a vrfurilor. Pentru
parcurgerea din exemplul precedent, acest tablou devine:
1 2 3 6 5 4 7 8
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
Parcurgerea n adncime se dovedete util n numeroase probleme din teoria
grafurilor, cum ar fi: detectarea componentelor conexe (respectiv, tare conexe) ale
unui graf, sau verificarea faptului c un graf este aciclic. Ca exemplu, vom
rezolva n aceast seciune problema gsirii punctelor de articulare ale unui graf
conex.
Un vrf v al unui graf neorientat conex este un punct de articulare, dac subgraful
obinut prin eliminarea lui v i a muchiilor care plec din v nu mai este conex. De
exemplu, vrful 1 este un punct de articulare pentru graful din Figura 9.1. Un graf
neorientat este biconex (sau nearticulat) dac este conex i nu are puncte de
articulare. Grafurile biconexe au importante aplicaii practice: dac o reea de
telecomunicaii poate fi reprezentat printr-un graf biconex, aceasta ne garanteaz
c reeaua continu s funcioneze chiar i dup ce echipamentul dintr-un vrf s-a
defectat.
Este foarte util s putem verifica eficient dac un graf are puncte de articulare.
Urmtorul algoritm gsete punctele de articulare ale unui graf conex G.

1. Efectueaz o parcurgere n adncime a lui G pornind dintr-un vrf oarecare.
Fie A arborele parial generat de aceast parcurgere i preord tabloul care
conine ordinea de parcurgere a vrfurilor.
2. Parcurge arborele A n postordine. Pentru fiecare vrf v vizitat, calculeaz
minim[v] ca minimul dintre
234 Explorri n grafuri 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;
b. un vrf v diferit de rdcina lui A este un punct de articulare al lui G,
dac i numai dac v are un fiu x, astfel nct minim[x] preord[v].

Pentru exemplul din Figura 9.1b, rezult c tabloul minim este
1 1 1 6 2 2 6 6
iar vrfurile 1 i 4 sunt puncte de articulare.
Pentru a demonstra c algoritmul este corect, enunm pentru nceput o
proprietate care rezult din Exerciiul 9.8: orice muchie din G, care nu are o
muchie corespunztoare n A, conectez n mod necesar un vrf v cu un ascendent
al su n A. innd cont de aceast proprietate, valoarea minim[v] se poate defini
i astfel:

minim[v] = min{preord[w] | se poate ajunge din v n w urmnd oricte
muchii continue, iar apoi urmnd n sus
cel mult o muchie punctat}

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 Parcurgerea grafurilor n adncime 235

S presupunem c reprezentm diferitele stagii ale unui proiect complex printr-un
graf orientat aciclic: vrfurile sunt strile posibile ale proiectului, iar muchiile
corespund activitilor care se cer efectuate pentru a trece de la o stare la alta.
Figura 9.2 d un exemplu al acestui mod de reprezentare. O sortare topologic a
vrfurilor unui graf orientat aciclic este o operaie de ordonare liniar a
vrfurilor, astfel nct, dac exist o muchie (i, j), atunci i apare naintea lui j n
aceast ordonare.
Pentru graful din Figura 9.2, o sortare topologic este A, B, C, E, D, F, iar o alta
este A, B, E, C, D, F. n schimb, secvena A, B, C, D, E, F nu este n ordine
topologic.
Dac adugm la sfritul procedurii ad linia
write v
atunci procedura de parcurgere n adncime va afia vrfurile n ordine topologic
invers. Pentru a nelege de ce se ntmpl acest lucru, s observm c vrful v
este afiat dup ce toate vrfurile ctre care exist o muchie din v au fost deja
afiate.
9.4 Parcurgerea grafurilor n lime
Procedura de parcurgere n adncime, atunci cnd se ajunge la un vrf v oarecare,
exploreaz prima dat un vrf w adiacent lui v, apoi un vrf adiacent lui w etc.
Pentru a efectua o parcurgere n lime a unui graf (orientat sau neorientat),
aplicm urmtorul principiu: atunci cnd ajungem ntr-un vrf oarecare v
nevizitat, l marcm i vizitm apoi toate vrfurile nevizitate adiacente lui v, apoi
toate vrfurile nevizitate adiacente vrfurilor adiacente lui v etc. Spre deosebire
de parcurgerea n adncime, parcurgerea n lime nu este n mod natural
recursiv.
C
B D E A F
trezire duul mbrcare plecare
preparat
cafea
but
cafea

Figura 9.2 Un graf orientat aciclic.
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 Parcurgerea grafurilor n lime 237

timpul este n: i) (n+m), dac reprezentm graful prin liste de adiacen; ii)
(n
2
), 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 Salvarea i restaurarea arborilor binari de cutare
Importana operaiilor de salvare (backup) i restaurare (restore) este bine
cunoscut de ctre toi utilizatorii de calculatoare. ntr-un fel sau altul, este bine
ca informaiile s fie arhivate periodic pe un suport extern, astfel ca, n caz de
necesitate, s le putem reconstitui ct mai uor. Pentru clasa arbore<E> am decis
s implementm operaiile de salvare i restaurare, n scopul de a facilita
transferurile de arbori ntre programe. Vom exemplifica cu aceast ocazie, nu
numai parcurgerea n lime, ci i lucrul cu fiiere binare, prin intermediul
obiectelor de tip fstream din biblioteca standard de intrare/ieire a limbajului
C++, obiecte declarate n fiierul header <fstream.h>.
Convenim s memorm pe suportul extern att cheia, ct i probabilitatea
(frecvena) de acces a fiecrui vrf. Scrierea se va face cheie dup cheie (vrf
dup vrf), n ordinea obinut printr-un proces de vizitare a arborelui.
Restaurarea arborelui este realizat prin inserarea fiecrei chei ntr-un arbore
iniial vid. Citirea cheilor este secvenial, adic n ordinea n care au fost scrise
n fiier.
Parcurgerile n adncime (n preordine) i n lime au proprietatea c vrful
rdcin al arborelui i al fiecrui subarbore este vizitat (i deci inserat) naintea
vrfurilor fii. Avem astfel garantat reconstituirea corect a arborelui de cutare,
deoarece n momentul n care se insereaz o cheie oarecare, toate vrfurile
ascendente sunt deja inserate. n cele ce urmeaz, vom utiliza parcurgerea n
lime.
Parcurgerea n lime a arborilor binari se face conform algoritmului din
Seciunea 9.4, cu specificarea c, deoarece arborii sunt grafuri conexe i aciclice,
nu mai este necesar marcarea vrfurilor. n procedura de salvare,
238 Explorri n grafuri Capitolul 9


template <class E>
int arbore<E>::save( char *file ) {
ofstream f( file, ios::binary ); // deschide fisierul
if ( !f ) return 0; // eroare la deschidere

coada<varf<E>*> c( n + 1 ); // ptr. parcurgerea in latime
varf<E> *x; // varful curent

c.ins_q( root ); // primul element din coada
while ( c.del_q( x ) ) {
if ( !f.write( (char *) &(x->key), sizeof( x->key ) ) )
return 0; // eroare la scriere
if ( !f.write( (char *) &(x->p ), sizeof( x->p ) ) )
return 0; // eroare la scriere

if ( x->st ) c.ins_q( x->st );
if ( x->dr ) c.ins_q( x->dr );
}
f.close( );
return 1;
}

vizitarea unui vrf const n scrierea informaiilor asociate n fiierul de ieire. De
aceast dat, nu vom mai folosi operatorii de ieire >> ai claselor E i float, ci
vom copia, octet cu octet, imaginea binar a cheii i a probabilitii asociate.
Cheia este situat la adresa &(x->key) i are lungimea sizeof(x->key), sau
sizeof(E). Probabilitatea este situat la adresa &(x->p) i are lungimea
sizeof(x->p), sau sizeof(float). Operaia de scriere necesit un obiect de tip
ofstream, output file stream, creat pe baza numelui fiierului char *file. Prin
valoarea ios::binary din lista de argumente a constructorului clasei ofstream,
fiierul va fi deschis n modul binar de lucru i nu n modul implicit text.
Funcia de restaurare
Seciunea 9.5 Salvarea i restaurarea arborilor binari de cutare 239


template <class E>
int arbore<E>::rest( char *file ) {
ifstream f( file, ios::binary ); // deschide fisierul
if ( !f ) return 0; // eroare la deschidere

delete root;
root = 0; n = 0; // se va crea un nou arbore

E key; float p; // informatia din varful curent
while ( f.read( (char *) &key, sizeof( key ) ) &&
f.read( (char *) &p, sizeof( p ) ) )
ins( key, p );

f.close( );
return 1;
}

const n deschiderea fiierului binar cu numele dat de parametrul char *file
prin intermediul unui obiect de tip ifstream, input file stream, citirea celor dou
componente ale fiecrui vrf (cheia key i frecvena p) i inserarea vrfului
corespunztor n arbore. Neavnd certitudinea c iniial arborele este vid, funcia
de restaurare terge toate vrfurile arborelui nainte de a ncepe inserarea cheilor
citite din fiier.
Testarea corectitudinii operaiilor din clasele ifstream i ofstream se realizeaz
prin invocarea implicit a operatorului de conversie la int. Acest operator
returneaz false, dac starea stream-lui corespunde unei erori, sau true, n caz
contrar. Invocarea lui este implicit, deoarece funciile membre ifstream::read
i ofstream::write returneaz obiectul invocator, iar sintaxa instruciunii while
solicit o expresie de tip ntreg. Acest operator de conversie la int este motenit
de la clasa ios, input-output stream, clas din care sunt derivate toate celelalte
clase utilizate pentru operaiile de intrare/ieire.
9.6 Backtracking
Backtracking (n traducere aproximativ, cutare cu revenire) este un principiu
fundamental de elaborare a algoritmilor pentru probleme de optimizare, sau de
gsire a unor soluii care ndeplinesc anumite condiii. Algoritmii de tip
backtracking se bazeaz pe o tehnic special de explorare a grafurilor orientate
implicite. Aceste grafuri sunt de obicei arbori, sau, cel puin, nu conin cicluri.
Pentru exemplificare, vom considera o problem clasic: cea a plasrii a opt
regine pe tabla de ah, astfel nct nici una s nu intre n zona controlat de o alta.
O metod simplist de rezolvare este de a ncerca sistematic toate combinaiile
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
8
4 426165368

= . . .
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
De aceast dat, numrul combinaiilor este redus la 8
8
= 16.777.216, algoritmul
oprindu-se de fapt dup ce inspecteaz 1.299.852 combinaii i gsete prima
soluie.
Vom proceda acum la o nou mbuntire. Dac introducem i restricia ca dou
regine s nu se afle pe aceeai coloan, o configuraie pe tabla de ah se poate
reprezenta ca o permutare a primilor opt ntregi. Algoritmul devine
procedure regine2
posibil permutarea iniial
while posibil permutarea final and not soluie(posibil) do
posibil urmtoarea permutare
if soluie(posibil) then write posibil
else write nu exist soluie
Sunt mai multe posibiliti de a genera sistematic toate permutrile primilor n
ntregi. De exemplu, putem pune fiecare din cele n elemente, pe rnd, n prima
poziie, genernd de fiecare dat recursiv toate permutrile celor n1 elemente
rmase:
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

Acest mod de abordare are dou avantaje fa de algoritmul regine2. n primul
rnd, numrul de vrfuri n arbore este mai mic dect 8!. Deoarece este dificil s
calculm teoretic acest numr, putem numra efectiv vrfurile cu ajutorul
calculatorului: #V = 2057. De fapt, este suficient s explorm 114 vrfuri pentru a
ajunge la prima soluie. n al doilea rnd, pentru a decide dac un vector este
(k+1)-promitor, cunoscnd c este extensia unui vector k-promitor, trebuie
doar s verificm ca ultima regin adugat s nu fie pus ntr-o poziie controlat
de reginele deja plasate. Ca s apreciem ct am ctigat prin acest mod de
verificare, s observm c n algoritmul regine2, pentru a decide dac o anumit
permutare este o soluie, trebuia s verificm fiecare din cele 28 de perechi de
regine de pe tabl.
Am ajuns, n fine, la un algoritm performant, care afieaz toate soluiile
problemei celor opt regine. Din programul principal, apelm regine(0),
presupunnd c posibil[1 .. 8] este un tablou global.
procedure regine(k)
{posibil[1 .. k] este k-promitor}
if k = 8 then write posibil {este o soluie}
else {exploreaz extensiile (k+1)-promitoare
ale lui posibil}
for j 1 to 8 do
if plasare(k, j) then posibil[k+1] j
regine(k+1)
function plasare(k, j)
{returneaz true, dac i numai dac se
poate plasa o regin n poziia (k+1, j)}
for i 1 to k do
if jposibil[i] {k+1i, 0, ik1} then return false
return true
Problema se poate generaliza, astfel nct s plasm n regine pe o tabl de n linii
i n coloane. Cu ajutorul unor contraexemple, putei arta c problema celor n
regine nu are n mod necesar o soluie. Mai exact, pentru n 3 nu exist soluie,
iar pentru n 4 exist cel puin o soluie.
Pentru valori mai mari ale lui n, avantajul metodei backtracking este, dup cum ne
i ateptm, mai evident. Astfel, n problema celor dousprezece regine,
algoritmul regine2 consider 479.001.600 permutri posibile i gsete prima
soluie la a 4.546.044 configuraie examinat. Arborele explorat prin algoritmul
regine conine doar 856.189 vrfuri, prima soluie obinndu-se deja la vizitarea
celui de-al 262-lea vrf.
Seciunea 9.6 Backtracking 243

Algoritmii backtracking pot fi folosii i atunci cnd soluiile nu au n mod
necesar aceeai lungime. Presupunnd c nici o soluie nu poate fi prefixul unei
alte soluii, iat schema general a unui algoritm backtracking:
procedure backtrack(v[1 .. k])
{v este un vector k-promitor}
if v este o soluie
then write v
else for fiecare vector w care este (k+1)-promitor,
astfel nct w[1 .. k] = v[1 .. k]
do backtrack(w[1 .. k+1])
Exist foarte multe aplicaii ale algoritmilor backtracking. Putei ncerca astfel
rezolvarea unor probleme ntlnite n capitolele anterioare: problema colorrii
unui graf, problema 0/1 a rucsacului, problema monezilor (cazul general). Tot
prin backtracking putei rezolva i o variant a problemei comis-voiajorului, n
care admitem c exist orae fr legtur direct ntre ele i nu se cere ca ciclul
s fie optim.
Parcurgerea n adncime, folosit n algoritmul regine, devine i mai avantajoas
atunci cnd ne mulumim cu o singur soluie a problemei. Sunt ns i probleme
pentru care acest mod de explorare nu este avantajos.
Anumite probleme pot fi formulate sub forma explorrii unui graf implicit care
este infinit. n aceste cazuri, putem ajunge n situaia de a explora fr sfrit o
anumit ramur infinit. De exemplu, n cazul cubului lui Rubik, explorarea
manipulrilor necesare pentru a trece dintr-o configuraie ntr-alta poate cicla la
infinit. Pentru a evita asemenea situaii, putem utiliza explorarea n lime a
grafului. n cazul cubului lui Rubik, mai avem astfel un avantaj: obinem n primul
rnd soluiile care necesit cel mai mic numr de manipulri. Aceast idee este
ilustrat de Exerciiul 9.15.
Am vzut c algoritmii backtracking pot folosi att explorarea n adncime ct i
n lime. Ceea ce este specific tehnicii de explorare backtracking este testul de
fezabilitate, conform cruia, explorarea anumitor vrfuri poate fi abandonat.
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 <ik, min(2k, ik)>, 1 k j. Vrful corespunztor poziiei iniiale
ntr-un joc cu n bee, n 2, este <n, n1>. 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 < i1, sunt inaccesibile. Vrful <0, 0> corespunde
unei poziii de pierdere.
Seciunea 9.7 Grafuri i jocuri 245

Figura 9.3 reprezint graful corespunztor jocului cu cinci bee iniiale: vrfurile
albe corespund poziiilor de ctig, vrfurile gri corespund poziiilor de pierdere,
muchiile continue corespund mutrilor prin care se ctig, iar muchiile
punctate corespund mutrilor prin care se pierde. Dintr-o poziie de pierdere nu
pleac nici o muchie continu, aceasta corespunznd faptului c din astfel de
poziii nu exist nici o mutare prin care se poate ctiga.
Se observ c juctorul care are prima mutare ntr-un joc cu dou, trei, sau cinci
bee nu are nici o strategie de ctig, dar are o astfel de strategie ntr-un joc cu
patru bee.

<2,2> <5,4> <4,2>
<3,3> <0,0> <4,3> <3,2>
<1,1> <2,1>

Figura 9.3 Graful unui joc.
246 Explorri n grafuri Capitolul 9

Urmtorul algoritm recursiv determin dac o poziie este de ctig.
function rec(i, j)
{returneaz true dac i numai dac vrful
<i, j> reprezint o poziie de ctig;
presupunem c 0 j i}
for k 1 to j do
if not rec(ik, min(2k, ik)) then return true
return false
Algoritmul are acelai defect ca i algoritmul fib1 (Capitolul 1): calculeaz n
mod repetat anumite valori. De exemplu, rec(5, 4) returneaz false dup ce a
apelat succesiv
rec(4, 2), rec(3, 3), rec(2, 2), rec(1, 1)
Dar rec(3, 3) apeleaz, de asemenea, rec(2, 2) i rec(1, 1).
Putem evita acest lucru, construind prin programarea dinamic o matrice boolean
global, astfel nct G[i, j] = true, dac i numai dac <i, j> este o poziie de
ctig. Fie n numrul maxim de bee folosite. Ca de obicei n programarea
dinamic, calculm matricea G de jos n sus:
procedure din(n)
{calculeaz de jos n sus matricea G[1..n, 1..n]}
G[0, 0] false
for i 1 to n do
for j 1 to i do
k 1
while k < j and G[ik, min(2k, ik)] do k k+1
G[i, j] not G[ik, min(2k, ik)]
Prin tehnica programrii dinamice, fiecare valoare a lui G este calculat o singur
dat. Pe de alt parte ns, n acest context multe din valorile lui G sunt calculate
n mod inutil. Astfel, este inutil s-l calculm pe G[i, j] atunci cnd j este impar i
j < i1. Iat i un alt exemplu de calcul inutil: tim c <15, 14> este o poziie de
ctig, imediat ce am aflat c al doilea succesor al su, <13, 4>, este o poziie de
pierdere; valoarea lui G(12, 6) nu mai este util n acest caz. Nu exist ns nici
un raionament de jos n sus pentru a nu-l calcula pe G[12, 6]. Pentru a-l calcula
pe G[15, 14], algoritmul din calculeaz 121 de valori G[i, j], ns utilizeaz
efectiv doar 27 de valori.
Algoritmul recursiv rec este ineficient, deoarece calculeaz anumite valori n mod
repetat. Pe de alt parte, datorit raionamentului de sus n jos, nu calculeaz
niciodat valori pe care s nu le i utilizeze.
Rezult c avem nevoie de o metod care s mbine avantajele formulrii
recursive cu cele ale programrii dinamice. Cel mai simplu este s adugm
Seciunea 9.7 Grafuri i jocuri 247

algoritmului recursiv o funcie de memorie care s memoreze dac un vrf a fost
deja vizitat sau nu. Pentru aceasta, definim matricea boolean global
init[0 .. n, 0 .. n], iniializat cu false.
function nim(i, j)
if init[i, j] then return G[i, j]
init[i, j] true
for k 1 to j do
if not nim(ik, min(2k, ik)) then G[i, j] true
return true
G[i, j] false
return false
Deoarece matricea init trebuie iniializat, aparent nu am ctigat nimic fa de
algoritmul care folosete programarea dinamic. Avantajul obinut este ns mare,
deoarece operaia de iniializare se poate face foarte eficient, dup cum vom
vedea n Seciunea 10.2.
Cnd trebuie s soluionm mai multe cazuri similare ale aceleiai probleme,
merit uneori s calculm cteva rezultate auxiliare care s poat fi apoi folosite
pentru accelerarea soluionrii fiecrui caz. Aceast tehnic se numete
precondiionare i este exemplificat n Exerciiul 9.7.
Jocul nim este suficient de simplu pentru a permite i o rezolvare mai eficient
dect prin algoritmul nim, fr a folosi graful asociat. Algoritmul de mai jos
determin strategia de ctig folosind precondiionarea. ntr-o poziie iniial cu n
bee, se apeleaz la nceput precond(n). Se poate arta c un apel precond(n)
necesit un timp n (n). Dup aceea, orice apel mutare(i, j), 1 j i, returneaz
ntr-un timp n (1) cte bee s fie ridicate din poziia <i, j>, pentru o mutare de
ctig. Dac poziia <i, j> este de pierdere, n mod convenional se indic
ridicarea unui b, ceea ce ntrzie pe ct posibil pierderea inevitabil a jocului.
Tabloul T [0 .. n] este global.
procedure precond(n)
T[0]
for i 1 to n do
k 1
while T[ik] 2k do k k+1
T[i] k
function mutare(i, j)
if j < T[i] then return 1 {prelungete agonia!}
return T[i]
Nu vom demonstra aici corectitudinea acestui algoritm.
248 Explorri n grafuri Capitolul 9

9.7.2 ahul i tehnica minimax
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)
v w
Seciunea 9.7 Grafuri i jocuri 249

Complexitatea jocului de ah este ns att de mare nct este imposibil s gsim o
astfel de funcie de evaluare perfect.
Presupunnd c funcia de evaluare nu este perfect, o strategie bun pentru Alb
este s prevad c Negrul va replica cu o mutare care minimizeaz funcia eval.
Albul gndete astfel cu o mutare n avans, iar funcia de evaluare este calculat
n mod dinamic.
val
for fiecare w succesor al lui u do
if w nu are succesor
then valw eval(w)
else valw min{eval(x) | x este succesor al lui w}
if valw val then val valw
v w
Pentru a aduga i mai mult dinamism funciei eval, este preferabil s investigm
mai multe mutri n avans. Din poziia u, analiznd n mutri n avans, Albul va
muta atunci n poziia v dat de
val
for fiecare w succesor al lui u do
if negru(w, n) val then val negru(w, n)
v w
Funciile negru i alb sunt urmtoarele:
function negru(w, n)
if n = 0 or w nu are succesori
then return eval(w)
return min{alb(x, n1) | x este succesor al lui w}

function alb(x, n)
if n = 0 or x nu are succesori
then return eval(x)
return max{negru(w, n1) | w este succesor al lui x}
Acum nelegem de ce aceast tehnic este numit minimax: Negrul ncearc s
minimizeze avantajul pe care l permite Albului, iar Albul ncearc s maximizeze
avantajul pe care l poate obine la fiecare mutare.
Tehnica minimax poate fi mbuntit n mai multe feluri. Astfel, explorarea
anumitor ramuri poate fi abandonat mai curnd, dac din informaia pe care o
deinem asupra lor, deducem c ele nu mai pot influena valoarea vrfurilor
situate la un nivel superior. Acest mbuntire se numete retezare alfa-beta
(alpha-beta pruning) i este exemplificat n Figura 9.4. Presupunnd c valorile
numerice ataate vrfurilor terminale sunt valorile funciei eval calculate n
250 Explorri n grafuri Capitolul 9

poziiile respective, celelalte valori se pot calcula prin tehnica minimax,
parcurgnd arborele n postordine. Obinem succesiv eval(b) = 5, eval( f ) = 6,
eval(g) = 3. n acest moment tim deja c eval(c) 3 i, fr s-l mai calculm pe
eval(h), obinem valoarea eval(a) = 5. Cu alte cuvinte, la o anumit faz a
explorrii am dedus c putem abandona explorarea subarborelui cu rdcina n h
(l putem reteza).
Tehnica minimax determin n final strategia reprezentat n Figura 9.4 prin
muchiile continue.
9.8 Grafuri AND/OR
Multe probleme se pot descompune ntr-o serie de subprobleme, astfel nct
rezolvarea tuturor acestor subprobleme, sau a unora din ele, s duc la rezolvarea
problemei iniiale. Descompunerea unei probleme complexe, n mod recursiv, n
subprobleme mai simple poate fi reprezentat printr-un graf orientat. Aceast
descompunere se numete reducerea problemei i este folosit n demonstrarea
automat, integrare simbolic i, n general, n inteligena artificial. ntr-un graf
orientat de acest tip vom permite unui vrf neterminal v oarecare dou alternative.
Vrful v este de tip AND dac reprezint o problem care este rezolvat doar dac
toate subproblemele reprezentate de vrfurile adiacente lui v sunt rezolvate.
Vrful v este de tip OR dac reprezint o problem care este rezolvat doar dac
cel puin o subproblem reprezentat de vrfurile adiacente lui v este rezolvat.
Un astfel de graf este de tip AND/OR.
De exemplu, arborele AND/OR din Figura 9.5 reprezint reducerea problemei A.
Vrfurile terminale reprezint probleme primitive, marcate ca rezolvabile
(vrfurile albe), sau nerezolvabile (vrfurile gri). Vrfurile neterminale reprezint
a
b c
f e d g
i j k l
h
5 7
6 3
...
cine mut: regula:
Albul
Negrul
Albul
Negrul
max
min
max
min
2 1

Figura 9.4 Retezare alfa-beta.
Seciunea 9.8 Grafuri AND/OR 251

probleme despre care nu se tie a priori dac sunt rezolvabile sau nerezolvabile.
Vrful A este un vrf AND (marcm aceasta prin unirea muchiilor care pleac din
A), vrfurile C i D sunt vrfuri OR. S presupunem acum c dorim s aflm dac
problema A este rezolvabil. Deducem succesiv c problemele C, D i A sunt
rezolvabile.
ntr-un arbore oarecare AND/OR, urmtorul algoritm determin dac problema
reprezentat de un vrf oarecare u este rezolvabil sau nu. Un apel sol(u) are ca
efect parcurgerea n postordine a subarborelui cu rdcina n u i returnarea
valorii true, dac i numai dac problema este rezolvabil.
function sol(v)
case
v este terminal: if v este rezolvabil
then return true
else return false
v este un vrf AND: for fiecare vrf w adiacent lui v do
if not sol(w) then return false
return true
v este un vrf OR: for fiecare vrf w adiacent lui v do
if sol(w) then return true
return false
Ca i n cazul retezrii alfa-beta, dac n timpul explorrii se poate deduce c un
vrf este rezolvabil sau nerezolvabil, se abandoneaz explorarea descendenilor
si. Printr-o modificare simpl, algoritmul sol poate afia strategia de rezolvare a
problemei reprezentate de u, adic subproblemele rezolvabile care conduc la
rezolvarea problemei din u.
Cu anumite modificri, algoritmul se poate aplica asupra grafurilor AND/OR
oarecare. Similar cu tehnica backtracking, explorarea se poate face att n
adncime (ca n algoritmul sol), ct i n lime.
B D C
G F E H
A

Figura 9.5 Un arbore AND/OR.
252 Explorri n grafuri Capitolul 9

9.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] = valoarea vrfului
ST[i] = adresa fiului stng
DR[i] = adresa fiului drept
(Dac se folosete o implementare prin tablouri paralele, atunci adresele sunt
indici de tablou). Presupunem c variabila root conine adresa rdcinii arborelui
i c o adres este zero, dac i numai dac vrful ctre care se face trimiterea
lipsete. Scriei algoritmii de parcurgere n inordine, preordine i postordine a
arborelui. La fiecare consultare afiai valoarea vrfului respectiv.
Soluie: Pentru parcurgerea n inordine apelm inordine(root), inordine fiind
procedura
procedure inordine(i)
if i 0 then
inordine(ST[i])
write VAL[i]
inordine(DR[i])

9.4 Dai un algoritm care folosete parcurgerea i) n adncime ii) n lime
pentru a afla numrul componentelor conexe ale unui graf neorientat. n
particular, putei determina astfel dac graful este conex. Facei o comparaie cu
algoritmul din Exerciiul 3.12.

9.5 ntr-un graf orientat, folosind principiul parcurgerii n lime, elaborai un
algoritm care gsete cel mai scurt ciclu care conine un anumit vrf dat. n locul
parcurgerii n lime, putei folosi parcurgerea n adncime?
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] w este un descendent al lui v,
sau v este la stnga lui w n arbore
postord[v] > postord[w] w este un descendent al lui v,
sau v este la dreapta lui w n arbore
Deci, w este un descendent al lui v, dac i numai dac:
preord[v] < preord[w] i postord[v] > postord[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, n1, , 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) Prin vectorul indicilor: (1, 2, 4). n aceast reprezentare, vectorii soluie au
lungimea variabil.
ii) Prin vectorul boolean x = (1, 1, 0, 1), unde x[i] = 1, dac i numai dac w
i
a
fost selectat n soluie. De aceast dat, vectorii soluie au lungimea
constant.

9.14 Un cal este plasat n poziia arbitrar (i, j), pe o tabl de ah de n n
ptrate. Concepei un algoritm backtracking care determin n
2
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) k = 0, dac poziia este de pierdere.
ii) 1 k j, dac a lua k bee este o mutare de ctig.

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.
255
10. Derivare public,
funcii virtuale
Derivarea public i funciile virtuale sunt mecanismele eseniale pentru
programarea orientat pe obiect n limbajul C++. Cele dou exemple prezentate n
acest capitol au fost alese pentru a ilustra nu numai elegana deosebit a
programrii orientate pe obiect, ci i problemele care pot apare atunci cnd se fac
greeli de proiectare.
10.1 Ciurul lui Eratostene
Acest exemplu este construit pornind de la un cunoscut algoritm pentru
determinarea numerelor prime. Geograful i astronomul grec Eratostene din
Cirena (sec. III a.Ch.) a avut ideea de a transforma proprietatea numerelor prime
de a nu fi multiplii nici unuia din numerele mai mici dect ele, ntr-un criteriu de
selecie (cernere): din irul primelor primelor n numere naturale se elimin pe
rnd multiplii lui 2, 3, 5 etc, elementele rmase fiind cele prime. Astfel, 2 fiind
prim, din ir se elimin multiplii lui 2 adic 4, 6, 8 etc. Urmtorul numr rmas
este 3, deci 3 este prim i se vor elimina numerele 9, 15, 21 etc, multiplii pari ai
lui 3 fiind deja eliminai. i aa mai departe, pn la determinarea tuturor
numerelor prime mai mici dect un numr dat.
Implementarea ciurului lui Eratostene prezentat n continuare
*
nu este foarte
eficient ca timp de execuie i memorie utilizat. n schimb, este att de
orientat pe obiect, nct merit s fie prezentat ca una din cele mai tipice
aplicaii C++. Ciurul este construit dintr-un ir de site i un generator de numere
numit contor. Fiecare sit corespunde unui numr prim. Ea solicit valori
(numere) de cernut de la sita urmtoare i las s treac, returnnd sitei
anterioare, doar acele valori care nu sunt multipli ai numrului prim corespunztor
sitei. Ultimul element n aceast structur de site este contorul, care nu face dect
s genereze numere, rnd pe rnd. Primul element este ciurul propriu-zis, din care
vor iei doar numere prime. n plus, ciurul mai are sarcina de a crea sita
corespunztoare numrului prim tocmai determinat.
La nceput, avem doar ciurul i contorul, acesta din urm iniializat cu valoarea 2
(Figura 10.1). Prima valoare extras din ciur este i prima valoare returnat de

*
Implementarea este preluat din R. Sethi, Programming Languages. Concepts and Constructs,
Seciunea 6.7.
256 Derivare public, funcii virtuale Capitolul 10

contor, i anume 2. Dup aceast prim iteraie, contorul va avea valoarea 3, iar
ntre ciur i contor se va insera prima sit, sit corespunztoare lui 2. Ciurul va
solicita o nou valoare sitei 2 care, la rndul ei, va solicita o nou valoare
contorului. Contorul emite 3, schimbndu-i valoarea la 4, 3 va trece prin sita 2 i
va ajunge la ciur. Imediat, sita 3 se insereaz n lista existent.
Contorul, la solicitarea ciurului, solicitare transmis sitei 3, apoi sitei 2, va
returna n continuare 4. Valoarea 4 nu trece de sita 2, dar sita 2 insist, cci
trebuie s rspund solicitrii primite, astfel nct va primi un 5. Aceast valoare
trece de toate sitele i lista are un nou element. Continund procesul, constatm c
6 se blocheaz la sita 2, 7 trece prin toate sitele (5, 3, 2), iar valorile 8 i 9 sunt
blocate de sitele 2, respectiv 3. La un moment dat, contorul va avea valoarea n,
iar n list vor fi sitele corespunztoare tuturor numerelor prime mai mici dect n.
Pentru implementarea acestui comportament, avem nevoie de o list nlnuit, n
care fiecare element este surs de valori pentru predecesor i i cunoate propria
surs (elementul succesor). Altfel spus, fiecare element are cel puin doi membri:
adresa sursei i funcia care cerne valorile.

class element {
public:
element( element *src ) { sursa = src; }
virtual int cerne( ) { return 0; }

protected:
element *sursa;
};

Acest element este un prototip, deoarece lista conine trei tipuri diferite de
elemente, difereniate prin funcia de cernere:
contor (2)
ciur
contor (3)
sita (2)
ciur
contor (4)
sita (2)
sita (3)
ciur

Figura 10.1 Ciurul lui Eratostene.
Seciunea 10.1 Ciurul lui Eratostene 257

Ciurul, care creeaz site.
Sitele, care cern valorile.
Contorul, care doar genereaz valori.
Cele trei tipuri fiind particularizri ale tipului element, le vom deriva public din
acesta, crend astfel trei subtipuri.

class contor: public element {
public:
contor( int v ): element( 0 ) { valoare = v; }
int cerne( ) { return valoare++; };

private:
int valoare;
};

class ciur: public element {
public:
ciur( element *src ): element( src ) { }
int cerne( );
};

class sita: public element {
public:
sita( element *src, int f ): element(src) { factor = f; }
int cerne( );

private:
int factor;
};

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;
}
}

Pentru ciur-ul propriu-zis, funcia de cernere nu mai are nimic de cernut.
Valoarea primit de la sita surs este n mod sigur un numr prim, motiv pentru
care ciur-ul va crea sita corespunztoare.

258 Derivare public, funcii virtuale 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 Ciurul lui Eratostene 259

n instruciunea int n = sursa->cerne( ) din clasele sita i ciur,
membrul sursa, motenit de la clasa de baz element, este de tip pointer la
element, deci funcia cerne() invocat este cea definit n clasa element. n
consecin, ceea ce se obine ar trebui s fie un ir infinit de 0-uri (desigur, n
limita memoriei disponibile).
Exist o nepotrivire ntre argumentele formale (de tip pointer la element) ale
constructorilor claselor ciur, sita i argumentele actuale cu care sunt
invocai aceti constructori. Astfel, constructorul clasei ciur este invocat cu
un pointer la contor, iar constructorul clasei sita este invocat prima dat cu
un pointer la contor i apoi cu pointeri la sita.
Elementele eseniale n elucidarea acestor aspecte sunt derivrile publice i
definiia din clasa element:

virtual int cerne( ) { return 0; }

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 Derivare public, funcii virtuale 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.
10.2 Tablouri iniializate virtual
Tabloul, una din cele mai simple structuri de date, are drept caracteristic
principal adresarea elementelor sale n timp constant. Regsirea sau modificarea
valorii unui element de tablou sunt operaii care necesit un timp constant,
independent de factori cum ar fi poziia (indicele) elementului, sau faptul c este
neiniializat, iniializat sau chiar modificat de mai multe ori.
Adresarea elementelor pentru citirea sau modificarea valorilor lor, operaie
numit indexare, este considerat operaia fundamental asociat structurii de
tablou. Chiar dac este uor de implementat, cu un timp de execuie constant (vezi
operatorul de indexare din clasa tablou<T>, Seciunea 4.1.3), uneori indexarea se
dovedete a fi costisitoare. Algoritmul nim ne-a pus n faa unei astfel de situaii:
tabloul init este util doar dac poate fi nu numai adresat, ci i iniializat n timp
constant, timp imposibil de atins n condiiile iniializrii fiecrui element prin
operatorul de indexare. Aparent, tabloul nu se preteaz la o astfel de iniializare i
deci ar fi bine s lucrm cu o alt structur. De exemplu, cu o list a elementelor
modificate. Valoarea oricrui element este cea memorat n list, sau este o
valoare implicit, dac el nu apare aici. Iniializarea nu mai depinde, n aceast
situaie, de numrul elementelor, dar nici adresarea nu mai este o operaie cu timp
constant de execuie!
Iniializarea unui tablou n timp constant, mpreun cu accesarea elementelor tot
n timp constant, sunt dou cerine aparent contradictorii pentru structura de
tablou. Eliminarea contradiciei, n caz c este posibil (i este), impune
completarea tabloului cu o nou operaie elementar, iniializarea, precum i
modificarea corespunztoare a operatorului de indexare. Obinem un alt tip de
tablou, n care elementele nu mai sunt iniializate efectiv, fiecare n parte, ci
virtual, printr-un operator de iniializare global.
n continuare, vom prezenta o structur de tablou iniializat virtual
*
, precum i
implementarea corespunztoare n limbajul C++.

*
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 Tablouri iniializate virtual 261

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:
50
1
3 0 4
sb
t
p
b
70 40
0 2

Figura 10.2 Structura de tablou iniializat virtual.
262 Derivare public, funcii virtuale 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.
10.2.2 Implementarea (o variant de nota ase)
Tabloul iniializat virtual cu elemente de tip T, tablouVI<T> (se poate citi chiar
tablou ase), este o clas cu o funcionalitate suficient de bine precizat pentru
a nu pune probleme deosebite la implementare. Vom vedea ulterior c apar totui
anumite probleme, din pcate majore i imposibil de ocolit. Pn atunci, s
stabilim ns structura clasei tablouVI<T>. Fiind, n esen, vorba tot de un
tablou, folosim clasa tablou<T> ca tip public de baz. Altfel spus, tablouVI<T>
este un subtip al tipului tablou<T> i poate fi folosit oricnd n locul acestuia.
Alturi de datele motenite de la tipul de baz, noua clas are nevoie de:
Cele dou tablouri auxiliare p i b.
ntregul sb, contorul locaiilor ocupate din b.
Elementul vi, n care vom memora valoarea implicit a elementelor tabloului.
n privina funciilor membre avem nevoie de:
Un constructor (constructorii nu se motenesc), pentru a dimensiona tablourile
i a fixa valoarea implicit.
O funcie (operator) de iniializare virtual, prin care, n orice moment, s
iniializm tabloul.
Un operator de indexare.
n mare, structura clasei tablouVI<T> este urmtoarea:

template <class T>
class tablouVI: public tablou<T> {
public:
tablouVI( int, T );

tablouVI& operator =( T );
T& operator []( int );

Seciunea 10.2 Tablouri iniializate virtual 263

private:
T vi; // valoarea implicita

tablou<int> p, b; // tablourile auxiliare p si b
int sb; // pointer in b
};

unde operatorul de atribuire este cel care realizeaz inializarea virtual a
tabloului.
Indexarea este operaia cea mai dificil de implementat. Dificultatea provine din
necesitatea de a-i conferi acestui operator o funcionalitate similar celui din clasa
tablou<T>, n sensul de a putea fi folosit att pentru returnarea valorii
elementelor, ct i pentru modificarea lor. Pentru a nu complica n mod inutil
implementarea, convenim ca primul acces la fiecare element s implice i
iniializarea elementului respectiv cu valoarea implicit. n consecin,
modificarea valorii unui element se realizeaz prin simpla returnare a referinei
elementului. Operatorul de indexare este implementat astfel:

template<class T>
T& tablouVI<T>::operator []( int i) {
static T z; // elementul returnat in caz de eroare

// verificarea indicelui i
if ( i < 0 || i >= d ) {
cerr << "\n\ntablouIV -- " << d
<< ": indice eronat: " << i << ".\n\n";
return z;
}

// returnarea valorii elementului i
int k = p[ i ];
if ( 0 <= k && k <= sb && b[ k ] == i )
// element deja initializat
return a[ i ];
else
// elementul se initializeaza cu valoarea implicita
return a[ b[ p[ i ] = ++sb ] = i ] = vi;
}

Operatorul de atribuire implementat mai jos poate fi oricnd invocat pentru
iniializarea virtual. Argumentul lui este valoarea implicit asociat tuturor
elementelor tabloului:

264 Derivare public, funcii virtuale Capitolul 10

template<class T>
tablouVI<T>& tablouVI<T>::operator =( T v ) {
vi = v; sb = -1;
return *this;
}

De asemenea, putem realiza iniializarea virtual i prin intermediul
constructorului clasei tablouVI<T>:

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
}

constructor invocat prin lista de inializare a membrilor din constructorul clasei
tablouVI<T>. Mai exact, expresia new T[ d ] are ca efect invocarea
constructorului implicit al tipului T, pentru fiecare din cele d elemente alocate
dinamic. Acest comportament (absolut justificat) al operatorului new este total
inadecvat unei iniializri n timp constant. Ne ntrebm dac putem doar aloca
spaiul, fr a-l i inializa. Dac tipul T nu are nici un constructor, atunci
iniializarea spaiului alocat este inutil, deoarece acest tip admite obiecte
neiniializate. Dar, dac tipul T are cel puin un constructor, atunci nseamn c
obiectele de tip T nu pot fi neiniializate i, n consecin, este necesar un
constructor implicit (apelabil fr nici un argument) pentru a iniializa spaiul
alocat prin new. Astfel, am ajuns la primul motiv pentru care aceast
implementare este doar de nota ase: tabloul tablouVI<T> este (virtual) iniializat
n timp constant, numai dac tipul T nu are constructor, altfel spus, dac permite
lucrul cu obiecte neiniializate.
Problema pe care ne-o punem acum este n ce msur responsabilitatea verificrii
acestei condiii poate fi preluat de compilator sau de proiectantul clasei
tablouVI<T>. Compilatorul poate semnala, n cel mai bun caz, absena
constructorului implicit. Proiectantul nu este nici el ntr-o situaie mai bun,
deoarece:
Seciunea 10.2 Tablouri iniializate virtual 265

Nu poate modifica comportamentul operatorului new astfel nct s nu mai
invoce constructorul implicit.
Prezena (sau absena) constructorilor clasei T nu poate fi verificat n timpul
rulrii programului.
Soluia este reproiectarea clasei, pentru a se obine o variant mai puin naiv. De
exemplu, n tabloul propiu-zis, se pot memora adresele elementelor, i nu
elementele.
Obiectele de tip tablouVI<T> genereaz necazuri i n momentul n care
nceteaz s mai existe. tim c, n aceste situaii, se vor invoca destructorii
datelor membre i cel al clasei de baz (n aceast ordine). Ajungem din nou la
clasa tablou<T> i la destructorul acesteia:

~tablou( ) { delete [ ] a; }

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 Derivare public, funcii virtuale Capitolul 10

utilizat cu succes pentru tipuri de date predefinite, sau care nu necesit
constructori i destructori. De asemenea, se vor evita operaiile de copiere
(iniializri, atribuiri, transmiteri de parametri prin valoare) ntre obiectele de
acest tip.
10.2.3 tablouVI<T> ca subtip al tipului tablou<T>
Derivarea public instituie o relaie special ntre tipul de baz i cel derivat.
Tipul derivat este un subtip al celui de baz, putnd fi astfel folosit oriunde este
folosit i tipul de baz. Aceast flexibilitate se bazeaz pe o conversie standard a
limbajului C++, i anume conversia de la tipul derivat public ctre tipul de baz.
Prin funcionalitatea lui, tabloul iniializat virtual este o particularizare a
tabloului. Decizia de a construi tipul tablouVI<T> ca subtip al tipului tablou<T>
este deci justificat. Simpla derivare public nu este suficient pentru a crea o
veritabil relaie tip-subtip. De exemplu, s considerm urmtorul program pentru
testarea clasei tablouVI<T>.

#include <iostream.h>
#include "tablouVI.h"

// declaratie necesara pentru a evita
// referirea la sablon - vezi Sectiunea 4.1.3
ostream& operator <<( ostream&, tablou<int>& );

main( ) {
cout << "\nTablou (de intregi) initializat virtual."
<< "\nNumarul elementelor, valoarea implicita ... ";
int n, v; cin >> n >> v;
tablouVI<int> x6( n, v );

cout << "\nIndicele, valoarea (prin indicele -1 se\n"
<< " modifica valoarea implicita) <EOF>:\n...";
while( cin >> n >> v ) {
if ( n == -1 ) x6 = v; else x6[ n ] = v;
cout << "...";
}
cin.clear( );

cout << '\n' << x6 << '\n';
return 1;
}

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 Tablouri iniializate virtual 267

template <class T>
ostream& operator <<( ostream& os, tablou<T>& t ) {
int n = t.size( );

os << " [" << n << "]: ";
for ( int i = 0; i < n; os << t[ i++ ] << ' ' );
return os;
}

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 );
// ...
};

O funcie declarat virtual ntr-o clas de baz este o funcie a crei
implementare depinde de tip, n sensul c va fi reimplementat pentru unele din
tipurile derivate. Atunci cnd se invoc printr-o referin sau pointer la clasa de
baz, funcia virtual permite selectarea variantei sale redefinite pentru tipul
actual al obiectului invocator. n cazul nostru, operatorii de indexare au fost
redefinii n clasa derivat tablouVI<T>. Deci, prin declararea ca funcii virtuale
n clasa de baz, se realizeaz legarea lor dinamic de tipul actual al obiectului
invocator.
Motenirea i legturile dinamice sunt atributele necesare programrii orientate pe
obiect. Limbajul C++ suport aceste faciliti prin mecanismul de derivare al
claselor i prin funciile virtuale. Un alt element util programrii orientate pe
obiect este obinerea de informaii asupra claselor n timpul rulrii programului
(RTTI sau Run-Time Type Information). Iat o situaie simpl, n care avem
nevoie de RTTI. Fie urmtoarea funcie pentru interschimbarea a dou tablouri:

template <class T>
void swap( tablou<T>& a, tablou<T>& b ) {
tablou<T> tmp = a;
a = b;
b = tmp;
}

268 Derivare public, funcii virtuale Capitolul 10

Pentru a o invoca, putem utiliza orice argumente de tip tablou<T> sau
tablouVI<T>. Nu este ns logic s interschimbm un tablouVI<T> cu un
tablou<T>. Detectarea acestei situaii (corect din punct de vedere sintactic) se
poate face numai n momentul rulrii programului, prin RTTI. Limbajul C++ nu
are faciliti proprii pentru RTTI, dar permite implementarea lor prin mecanismul
funciilor virtuale. Multe din bibliotecile C++ profesionale ofer faciliti
sofisticate de RTTI. Pentru exemplul de mai sus, am implementat o variant
primitiv de RTTI. Este vorba de introducerea funciilor virtuale tip() n clasele
tablou<T> i tablouVI<T>, funcii care returneaz codurile de identificare ale
claselor respective.

template <class T>
class tablou {
// ...
public:
// ...
virtual char tip( ) const { return 'T'; }
// ...
};

template <class T>
class tablouVI: public tablou<T> {
public:
// ...
char tip( ) const { return 'V'; }
// ...
};

Deci, vom introduce n funcia swap( tablou<T>&, tablou<T>& ) secvena de
test a tipurilor implicate:

template <class T>
void swap( tablou<T>& a, tablou<T>& b ) {
if ( a.tip( ) != b.tip( ) )
cerr << "\n\nswap -- tablouri de tipuri diferite.\n\n";
else {
tablou<T> tmp = a; a = b; b = tmp;
}
}

Am reuit, astfel, s prevenim anumite operaii corecte sintactic, dar imposibil de
aplicat obiectelor din tipurile derivate.
Mecanismul RTTI trebuie folosit cu mult discernmnt. Este mai bine s prevenim
situaii ca cea de mai sus, dect s le soluionm prin artificii (de tip RTTI) care
pot duce la pierderea generalitii funciilor sau claselor respective.
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>.


271
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.


273
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 Annotated C++ Reference Manual,
Addison-Wesley, Reading, 1991.
Graham, R.L., Knuth, D.E., Patashnik, O. Concrete Mathematics,
Addison-Wesley, Reading, 1989.
Horowitz, E., Sahni, S. Fundamentals of Computer Algorithms, Computer
Science Press, Rockville, 1978.
Knuth, D.E. Tratat de programarea calculatoarelor. Algoritmi fundamentali,
Editura Tehnic, Bucureti, 1974.
Knuth, D.E. Tratat de programarea calculatoarelor. Sortare i cutare,
Editura Tehnic, Bucureti, 1976.
Lippman, S. B. C++ Primer, Addison-Wesley, Reading, 1989.
Livovschi, L., Georgescu, H. Sinteza i analiza algoritmilor, Editura tiinific
i Enciclopedic, Bucureti, 1986.
Sedgewick, R. Algorithms, Addison-Wesley, Reading, 1988.
Sedgewick, R. Algorithms in C, Addison-Wesley, Reading, 1990.
Sethi, R. Programming Languages. Concepts and Constructs, Addison-Wesley,
Reading, 1989.
Smith, J.H. Design and Analysis of Algorithms, PWS-KENT Publishing
Company, Boston, 1989.
Standish, T.A. Data Structure Techniques, Addison-Wesley, Reading, 1979.
Stroustrup, B. The C++ Programming Language, Addison-Wesley, Reading,
1991.
Stroustrup, B. The Design and Evolution of C++, Addison-Wesley, Reading,
1994.