Sunteți pe pagina 1din 126

Capitolul 1

Elaborarea, descrierea şi analiza


algoritmilor

1.1 Definiţii, proprietăţi, clasificări


Etimologic, cuvântul “algoritm” ı̂şi are originea ı̂n numele savantului persan Abu Abdullah Muhammad bin Musa
al-Khwarizmi, astrolog, matematician şi scriitor ce a trăit aproximativ ı̂ntre anii 780–846. Una dintre scrierile
acestuia a fost tradusă ı̂n limba latină cu titlul de “Algoritmi de numero Indorum” (Al-Khwarizmi – citit Al-Horizmi
– latinizat Algoritmi ), autorul descriind diverse operaţii de calcul aritmetic. Aşadar, iniţial, ı̂nţelesul noţiunii de
algoritm era acela de metodă de calcul.
Un algoritm nu rezolvă neapărat o problemă ştiinţifică, ci poate fi privit ca o succesiune de paşi de parcurs ı̂n
vederea ı̂ndeplinirii cu succes a unei sarcini: pornirea unui autoturism de pe loc, construirea unei case, folosirea
unui espressor de cafea, a unui bancomat etc.
În prezent, ı̂n domeniile matematicii sau informaticii, un algoritm desemnează o metodă sau o procedură de calcul
bine definită, compusă dintr-o succesiune finită de paşi elementari care conduc la soluţionarea unei probleme sau
a unei clase de probleme. Aşadar, un algoritm reprezintă o succesiune de operaţii aritmetice şi/sau logice care,
aplicate asupra unor date de intrare, permit rezolvarea unei probleme sau a unei categorii de probleme, rezultând
anumite date de ieşire. Astfel, o problemă presupune existenţa unor date de intrare, iar enunţul acesteia precizează
clar care este legătura dintre acestea şi soluţia problemei.
Algoritmul este noţiunea fundamentală a informaticii, totul fiind construit ı̂n jurul algoritmilor şi a structurilor de
date. Descrierea unui algoritm presupune precizarea datelor de intrare şi a operaţiilor efectuate asupra acestora.
Deşi există şi algoritmi teoretici, ı̂ncă neimplementaţi, ne vom concentra pe algoritmi care pot fi implementaţi pe
calculator. Un limbaj de programare este un limbaj artificial, riguros ı̂ntocmit, care permite descrierea algoritmilor
astfel ı̂ncât să poată fi transmişi calculatorului cu scopul ca acesta să efectueze operaţiile specificate. Un program
este un algoritm tradus ı̂ntr-un limbaj de programare pentru a fi executat şi a se obţine soluţia problemei de
rezolvat. Spre deosebire de program, care depinde de un limbaj de programare, algoritmul este independent de
maşina pe care va fi executat.
Unii dintre cei mai vechi algoritmi sunt consideraţi a fi: algoritmul de extragere a rădăcinii pătrate, algoritmul lui
Euclid prin care se determină cel mai mai mare divizor comun al două numere naturale, algoritmul lui Eratostene
pentru generarea numerelor prime mai mici decât o valoare dată, algoritmul lui Gauss de eliminare pentru rezolvarea
sistemelor algebrice liniare, algoritmii descrişi de Al-Khwarizmi pentru rezolvarea ecuaţiilor algebrice liniare şi
pătratice şi pentru operaţiile cu numere arabe, schema lui Horner etc.
Proiectarea unui algoritm presupune o etapă de elaborare a acestuia şi una de analiză a algoritmului. Înainte de a
trece la elaborarea unui algoritm de rezolvare a unei probleme, trebuie stabilite clar cerinţele problemei, datele de
intrare şi legătura acestora cu soluţia problemei. Prin urmare sunt necesare cunoştinţe temeinice din domeniul din
care provine problema. Apoi urmează stabilirea unui raţionament general de rezolvare a problemei, a tehnicilor
de proiectare adecvate şi identificarea paşilor elementari care compun algoritmul. Acesta va fi reprezentat ı̂ntr-o
formă simplă şi clară, fără ambiguităţi şi verificat pentru diverse instanţe ale problemei (diferite seturi de date de
intrare). În această etapă algoritmul va fi analizat din punct de vedere al corectitudinii şi eficienţei sale. Ultima

1
Algoritmi şi complexitate Note de curs

etapă o constituie implementarea algoritmului ı̂ntr-un limbaj de programare şi obţinerea soluţiei prin execuţia pe
calculator a programului corespunzător. Elaborarea unui algoritm şi analiza acestuia sunt etape foarte importante,
ı̂ntrucât erorile de proiectare nu pot fi reparate prin artificii de implementare ale algoritmului ı̂ntr-un limbaj de
programare.
Există probleme pentru care nu se pot descrie algoritmi de rezolvare. Un exemplu simplu este problema generării
mulţimii multiplilor unui număr natural n (mulţimea fiind infinită). Dacă problema ar cere să se determine multiplii
lui n mai mici decât o valoare dată, atunci aceasta ar fi rezolvabilă algoritmic.
Proprietăţile avute ı̂n vedere ı̂n elaborarea unui algoritm sunt:
ˆ corectitudinea (proprietatea algoritmului de a oferi o soluţie corectă pentru problema de rezolvat);
ˆ generalitatea (proprietatea unui algoritm de a rezolva o clasă de probleme, nu doar o problemă particulară);
ˆ finitudinea (proprietatea algoritmului de a se termina ı̂ntr-un număr finit de paşi);
ˆ eficienţa (proprietatea unui algoritm de a se termina ı̂ntr-un număr finit, dar şi rezonabil de paşi (nu neapărat
optim) şi de a utiliza un spaţiu de memorie cât mai mic);
ˆ claritatea (proprietatea algoritmului de a descrie cu exactitate, fără ambiguităţi, paşii care conduc la rezolvarea
problemei);
ˆ completitudinea (proprietate strâns legată de corectitudine, prin care algoritmul ţine cont de toate cazurile
particulare, speciale ale problemei de rezolvat);
ˆ verificabilitatea (proprietatea algoritmului prin care fiecare pas poate fi verificat ı̂ntr-un timp rezonabil);
ˆ optimalitatea (proprietatea unui algoritm de a se termina după un număr minim de paşi);
ˆ existenţa unei intrări (datele de prelucrat), existenţa unei ieşiri (rezultatele);
ˆ caracterul realizabil (proprietatea unui algoritm de a putea fi codificat intr-un limbaj de programare);
ˆ caracterul determinist (ı̂n cazul algoritmilor determinişti, plecând de la acelaşi set de date iniţiale, fiecare
execuţie a programului care implementează algoritmul conduce la aceleaşi rezultate).

O problemă ce aparţine unei anumite clase de probleme se numeşte instanţă a acesteia. Deşi un algoritm este
dezvoltat să rezolve o categorie de probleme, el este rulat pentru o instanţă particulară a acesteia. De exemplu,
ne propunem să dezvoltăm un algoritm de sortare ı̂n ordine crescătoare a unei secvenţe finite de numere. Definim
formal problema de sortare:
1 Input : O secvenţă de n numere [a1 , a2 , . . . , an ].
2 Output : O permutare a secvenţei de intrare , [a01 , a02 , . . . , a0n ], cu a01 ≤ a02 ≤ · · · ≤ a0n .
Un algoritm dezvoltat pentru a rezolva această problemă va trebui să funcţioneze pentru orice secvenţă finită
de intrare. De exemplu, dacă algoritmul este rulat pentru secvenţa de numere (21, 13, 18, 20, 32, 28, 45), aceasta
reprezintă o instanţă a problemei. Rezultatul corespunzător va trebui să fie (13, 18, 20, 21, 28, 32, 45).
Să presupunem că iniţial algoritmul de rezolvare este următorul: se compară primele două numere din şir. Dacă
sunt ı̂n ordine incorectă, se interschimbă; se trece la compararea celui de-al doilea număr cu al treilea, problema
fiind aceeaşi: dacă sunt ı̂n ordine incorectă se interschimbă, dacă nu, se continuă cu compararea celui de-al treilea
număr cu cel de-al patrulea ş.a.m.d. Să urmărim cum lucrează acest algoritm pentru instanţa considerată:
21, 13, 18, 20, 32, 28, 45
13, 21, 18, 20, 32, 28, 45
13, 18, 21, 20, 32, 28, 45
13, 18, 20, 21, 32, 28, 45
13, 18, 20, 21, 32, 28, 45
13, 18, 20, 21, 28, 32, 45
13, 18, 20, 21, 28, 32, 45
Algoritmul funcţionează corect pentru această instanţă. Să vedem acum cum lucrează pentru o altă instanţă a
problemei, (45, 32, 28, 21, 20, 18, 13):

2
Algoritmi şi complexitate Note de curs

45, 32, 28, 21, 20, 18, 13


32, 45, 28, 21, 20, 18, 13
32, 28, 45, 21, 20, 18, 13
32, 28, 21, 45, 20, 18, 13
32, 28, 21, 20, 45, 18, 13
32, 28, 21, 20, 18, 45, 13
32, 28, 21, 20, 18, 13, 45
Este evident că pentru această instanţă algoritmul nu mai funcţionează corect, singurul lucru pe care ı̂l realizează
cu siguranţă este faptul că cel mai mare element este plasat pe ultima poziţie ı̂n şir, poziţia sa corectă. Se observă
că pentru o secvenţă de n elemente, algoritmul ar trebui reluat de n − 1 ori, pentru ca fiecare element să ajungă
pe poziţia sa corectă.
Ţinând cont că un algoritm este o metodă de rezolvare a unei clase de probleme folosind calculatorul, eficienţa sau
complexitatea unui algoritm reprezintă costul utilizării acestuia pentru a rezolva o problemă aparţinând acelei clase
de probleme. Distincţia ı̂ntre clasa de probleme pe care o rezolvă algoritmul şi instanţele acesteia este esenţială mai
ales dacă ne interesează să studiem eficienţa algoritmului. O metodă care evaluează performanţele unui algoritm
nu trebuie să depindă de instanţele pentru care este executat algoritmul.
Dacă există doi algoritmi care rezolvă aceeaşi problemă, atunci este posibil ca pentru anumite instanţe ale problemei
unul să fie mai rapid, iar pentru alte instanţe, celălalt să fie mai rapid. De exemplu, algoritmul de sortare prin
inserţie este preferat pentru vectori de dimensiune mică, iar algoritmii quicksort sau heapsort pentru vectori de
dimensiune mare. Dacă valorile din vector sunt mici, atunci este preferat algoritmul radixsort. Deci ar trebui găsită
o metodă prin care să putem compara algoritmii ce rezolvă o aceeaşi problemă.
Analiza complexităţii sau a eficienţei unui algoritm are sopul de a aproxima volumul de resurse necesar rulării
algoritmului, acesta reprezentând costul executării unui algoritm. Astfel, putem vorbi despre:
ˆ complexitatea–spaţiu a unui algoritm (determinarea spaţiului de memorie necesar stocării datelor prelucrate
de către algoritm);
ˆ complexitatea–timp a unui algoritm (estimarea timpului de execuţie al paşilor de bază ai algoritmului).
Aşadar, deoarece resursele sunt limitate, un algoritm va fi cu atât mai eficient, cu cât va folosi mai puţine resurse.
De altfel, un algoritm care este corect, dar care utilizează un volum inacceptabil de resurse, nu va putea fi folosit ı̂n
practică. În cazul celor mai multe probleme, costul utilizării unui algoritm depinde de dimensiunea problemei, adică
de volumul datelor de intrare. Dimensiunea spaţiului de memorie necesar stocării datelor folosite de către algoritm
este determinată de modul ı̂n care sunt reprezentate acestea, adică de structurile de date utilizate. De exemplu, o
matrice rară este o matrice cu un număr mare de elemente nule. Reprezentarea sa ı̂n memorie poate fi realizată, ı̂n
mod clasic, folosind un tablou bidimensional sau, alternativ, printr-un tablou unidimensional ı̂n care vom păstra
doar elementele nenule, memorând indicii corespunzători de linie şi coloană. Astfel se reduce semnificativ spaţiul de
memorie alocat. Pe de altă parte, algoritmii care implementează operaţiile definite pe matrice rare stocate folosind
tablouri unidimensionale devin mai complicaţi.
Dintre cele două tipuri de resurse, timpul de execuţie este considerat cel mai important. În cele ce urmează vom
analiza complexitatea unui algoritm doar din perspectiva timpului său de execuţie. Astfel, vom aproxima timpul
ı̂n care operaţiile de bază ale unui algoritm sunt executate pentru fiecare set de date de intrare, deci vom analiza
dependenţa timpului de execuţie al unui algoritm de dimensiunea problemei.
În funcţie de complexitate, putem clasifica algoritmii astfel: unii algoritmi se termină ı̂n timp constant, alţii ı̂n timp
logaritmic, alţii ı̂n timp liniar, pătratic sau cubic, alţii ı̂n timp exponenţial sau factorial, iar alţii nu se termină.
În funcţie de modul de implementare, un algoritm poate fi: recursiv, iterativ (repetitiv), serial sau paralel, deter-
minist sau aleatoriu (probabilistic), exact sau aproximativ. Algoritmii aleatori se ı̂mpart, de regulă, ı̂n două clase:
algoritmi de aproximare, algoritmi genetici şi algoritmi aleatori de tip Las Vegas (soluţia oferită de algoritm este
sigur corectă, dar este una aproximativă, eroarea de aproximare fiind controlată probabilistic) şi algoritmi aleatori
de tip Monte Carlo şi stocastici (soluţia oferită de algoritm nu este sigur corectă, dar se apropie cu o probabilitate
suficient de mare de soluţia exactă).
În funcţie de domeniul de studiu, algoritmii pot fi:
ˆ de căutare: algoritmii de căutare secvenţială, de căutare binară, etc.;

3
Algoritmi şi complexitate Note de curs

ˆ de sortare: algoritmii de sortare prin selecţie, prin inserţie, prin numărare, prin interschimbare, prin intercla-
sare, prin partiţionare etc.;
ˆ numerici : algoritmul lui Euclid, algoritmul lui Eratostene etc.;
ˆ de optimizare combinatorială: problema rucsacului, problema comis voiajorului, problema planificării acti-
vităţilor etc.;
ˆ de geometrie computaţională: problema ı̂nfăşurătorii convexe, problema triangularizării unui domeniu polie-
dral etc.;
ˆ de criptografie: algoritmul Rijndael, funcţii hash criptografice etc.

Algoritmii pot fi elaboraţi prin diverse tehnici de proiectare (paradigme), dintre care amintim:

ˆ “forţa brută” – modalitate intuitivă de abordare directă a problemei; este simplă, uşor de implementat, dar
nu ı̂ntotdeuna eficientă;
ˆ metoda reducerii – modalitatea prin care se exploatează relaţia dintre o soluţie a unei instanţe a problemei
(de acceaşi dimensiune cu problema) şi soluţia unei instanţe de dimensiune mai mică a aceleiaşi probleme;
exploatarea se poate face top-down (recursiv) sau bottom-up (iterativ);
ˆ metoda divizării (algoritmi de tip divide et impera) – presupune descompunerea problemei de rezolvat ı̂n două
sau mai multe probleme independente, similare cu cea iniţială, dar de dimensiuni mai mici, apoi rezolvarea
subproblemelor folosind aceeaşi metodă. Soluţiile subproblemelor sunt apoi recompuse pentru a obţine astfel
soluţia problemei iniţiale;
ˆ metoda căutării cu revenire (backtracking) – permite generarea tuturor soluţiilor unei probleme, bazată pe
construirea incrementală a soluţiilor rezultat şi pe renunţarea imediată la o soluţie posibilă, dacă aceasta nu
respectă condiţiile de validare a soluţiei; această tehnică permite ı̂ntoarcerea la configuraţii anterioare;
ˆ tehnica greedy (tehnica alegerii local optimale) – furnizează o modalitate de rezolvare a problemelor de optim
ı̂n care optimul global se poate obţine prin alegeri succesive ale optimului local; astfel, problema este rezolvată
fără a se reveni la deciziile luate deja. Tehnica greedy nu conduce ı̂ntotdeauna la o soluţie optimă (ceea ce
pare optim la un anumit pas (local), poate fi dezastruos la nivel global);
ˆ metoda programării dinamice – presupune rezolvarea unei probleme prin descompunerea ei ı̂n subprobleme de
dimensiune mai mică. Spre deosebire de metoda Divide et Impera, unde subproblemele rezultate sunt inde-
pendente, ı̂n cazul acestei metode, subproblemele care apar ı̂n descompunere nu sunt independente. Acestea
se suprapun, astfel că soluţia unei subprobleme se utilizează ı̂n construirea soluţiilor altor subprobleme. În ca-
zul ı̂n care subproblemele se suprapun şi s-ar folosi metoda Divide et Impera, s-ar efectua calcule redundante,
rezolvându-se fiecare subproblemă ca şi când aceasta nu a mai fost ı̂ntâlnită până atunci. Folosind această
tehnică se rezolvă fiecare dintre subprobleme o singură dată, memorând soluţia acesteia ı̂ntr-o structură de
date şi evitând astfel rezolvarea redundantă a aceleiaşi probleme.

1.2 Descrierea algoritmilor


Dintre modalităţile de descriere a algoritmilor vom folosi schema logică şi pseudocodul. Schema logică este o
modalitate grafică de a descrie un algoritm, fiecare prelucrare fiind precizată printr-un element (simbol) grafic.
Prelucrările sunt conectate, iar succesiunea lor este indicată prin săgeţi. Pseudocodul este un limbaj apropiat de cel
natural, dar şi de cele de programare, compus din cuvinte cheie şi simboluri folosite pentru a descrie prelucrările
unui algoritm.

1.2.1 Date şi prelucrări


Un algoritm operează cu date. Acestea pot fi elementare (ı̂ntregi, reale, booleene, caractere, pointeri) sau structurate
(tablouri, şiruri de caractere, structuri, liste) – formate din mai multe date elementare (simple). Datele pot fi
constante (date care nu ı̂şi modifică valoarea pe parcursul execuţiei algoritmului) sau variabile (date care ı̂şi modifică
valoarea pe parcursul execuţiei algoritmului). Orice dată este caracterizată prin tipul şi numele său, prin locaţia
de memorie atribuită la alocare şi valoarea păstrată ı̂n acea zonă de memorie. Asupra datelor sunt definite operaţii
specifice acestora folosind operatorii aritmetici (+, −, ∗, /, la care adăugăm operatorul % pentru calculul restului

4
Algoritmi şi complexitate Note de curs

ı̂mpărţirii a două numere ı̂ntregi; precizăm că, la fel ca ı̂n limbajul C++, operatorul de ı̂mpărţire folosit pentru
doi operanzi ı̂ntregi va oferi câtul obţinut prin ı̂mparţirea cu rest a acestora), relaţionali (==, ! =, <, >, <=, >=)
şi logici ( && (and), || (or), ! (not)). Folosind date (operanzi) şi operatori corespunzători se construiesc expresii
numerice, alfanumerice sau logice.
Prelucrările unui algoritm reprezintă operaţiile efectuate asupra datelor şi pot fi clasificate, la fel ca acestea, ı̂n
elementare şi structurate. Cele elementare sunt operaţiile de intrare/ieşire, atribuire şi control. Operaţiile de in-
trare/ieşire se referă la citirea de la dispozitivul de intrare (tastatură, fişier etc.) a datelor de intrare, respectiv
scrierea la dispozitivul de ieşire (consolă, fişier etc.) a rezultatelor. Operaţia de atribuire constă ı̂n asignarea unei
valori unei variabile. Valoarea poate fi rezultatul evaluării unei expresii care apoi va fi memorat ı̂n variabilă. Pre-
lucrările unui algoritm sunt executate secvenţial, aşa cum apar descrise ı̂n algoritm. Dacă se doreşte modificarea
acestei succesiuni se utilizează operaţia elementară de control ce constă ı̂n transferul execuţiei la o anumită prelu-
crare. Prelucrările structurate pot fi secvenţiale, alternative (de decizie) sau repetitive (de ciclare). O prelucrare
secvenţială este o succesiune de paşi ce vor fi executaţi ı̂n ordinea specificării lor. Structura decizională presupune
evaluarea unei anumite expresii (condiţii); ı̂n funcţie de valoarea sa logică de adevăr, se continuă cu o prelucrare
sau o alta. Structura repetitivă este utilizată atunci când o prelucrare elementară sau structurată trebuie executată
de mai multe ori. Aceast tip de prelucrare necesită precizarea unei condiţii de oprire sau a uneia de continuare.
După momentul ı̂n care este testată condiţia, există structuri repetitive condiţionate anterior (condiţia este testată
ı̂naintea execuţiei prelucrării) sau posterior (condiţia este testată după execuţia prelucrării).
Simbolul grafic, ı̂n cazul schemelor logice, sau cuvintele cheie şi simbolurile, ı̂n cazul pseudocodului, utilizate pentru
a descrie o prelucrare a unui algoritm depinde de tipul de operaţie efectuată la pasul respectiv.

1.2.2 Schema logică


Orice schemă logică ı̂ncepe cu un bloc de start şi se ı̂ncheie cu un bloc de stop:

START STOP

În construirea unei scheme logice, blocurile grafice pe care le vom folosi pentru a indica operaţiile de intrare/ieşire
sunt (R – read, W – write):

R: lista variabile W: lista variabile

Pentru operaţia de atribuire vom folosi


variabila = expresie

iar pentru operaţia de decizie vom folosi următorul bloc grafic

no yes
condiţie

Ordinea prelucrărilor secvenţiale o vom indica prin săgeţi, iar structurile de ciclare le vom indica prin următoarele
blocuri grafice:
ˆ structuri repetitive condiţionate anterior

5
Algoritmi şi complexitate Note de curs

no yes
condiţie

instrucţiune

ˆ structuri repetitive condiţionate posterior

instrucţiune

no yes
condiţie

Exemplul 1.1. Suma primelor n numere naturale, n ≥ 1. Formal, problema poate fi definită astfel
1 Input : PNumărul natural n ≥ 1
n
2 Output : i=1 i

Vom descrie algoritmul de calcul al sumei numerelor naturale 1, 2, . . . , n, n ≥ 1, prin următoarea schemă logică:

START

R: n

S = 0

i = 1

no yes
i <= n

S = S + i

W: S
i = i + 1

STOP

Exemplul 1.2. Algoritmul lui Euclid de determinare a celui mai mare divizor comun al două numere naturale
nenule. Formal, problema poate fi definită astfel

6
Algoritmi şi complexitate Note de curs

1 Input : Două numere naturale a şi b, nenule .


2 Output : c . m . m . d . c .( a , b ) .
Aplicăm algoritmul lui Euclid: cât timp b este diferit de 0, ı̂mpărţim pe a la b şi calculăm restul. La pasul următor,
ı̂mpărţitorul devine deı̂mpărţit, iar restul devine ı̂mpărţitor. Ultimul rest diferit de 0 este c.m.m.d.c.(a, b). De
exemplu, să considerăm a = 364 şi b = 124. Etapele algoritmului sunt:
b != 0 ? DA =⇒ iteraţia 1: r = 76, a = 124, b = 76
b != 0 ? DA =⇒ iteraţia 2: r = 48, a = 76, b = 48
b != 0 ? DA =⇒ iteraţia 3: r = 28, a = 48, b = 28
b != 0 ? DA =⇒ iteraţia 4: r = 20, a = 28, b = 20
b != 0 ? DA =⇒ iteraţia 5: r = 8, a = 20, b = 8
b != 0 ? DA =⇒ iteraţia 6: r = 4, a = 8, b = 4
b != 0 ? DA =⇒ iteraţia 7: r = 0, a = 4, b = 0
b != 0 ? NU =⇒ afişează 4 (execuţia se ^ ıncheie)
O schemă logică ce descrie algoritmului lui Euclid este:

START

R: a, b

no yes
b != 0

r = a % b

W: a
a = b

STOP
b = r

1.2.3 Limbajul pseudocod


Limbajul pseudocod utilizat ı̂n acest curs va fi apropiat de limbajele de programare C/C++, evident ceva mai
relaxat ı̂n ceea ce priveşte sintaxa. Vom declara datele elementare ı̂n mod obişnuit, precizând tipul şi numele lor:
1 integer a , b , c // date intregi
2 real d // date reale
3 bool e /* date booleene ;
4 doar doua valori posibile : true , false */
5 char f // date de tip caracter
6 integer * p // date de tip pointer
Printr-o scriere de tipul /*...*/ sunt specificate comentariile pe mai multe linii. Comentariile nu fac parte din
algoritm, ele au doar rolul de a da unele explicaţii celui care parcurge descrierea algoritmului. Comentariile pe o
singură linie pot fi specificate prin dublu slash, //, la ı̂nceputul comentariului.
Pentru declararea tablourilor unidimensionale şi bidimensionale vom folosi sintaxa
1 <tip > < nume_vector >[0.. < dimensiune > -1]
2 <tip > < nume_matrice >[0.. < dimensiune_1 > -1][0.. < dimensiune_2 -1 >]

7
Algoritmi şi complexitate Note de curs

unde <dimensiune> şi <dimensiune 1>, <dimensiune 2> reprezintă dimensiunile efective ale tablourilor. Vom
considera indexarea tablourilor de la 0, la fel ca ı̂n limbajul C/C++. Astfel, dacă avem declaraţii de tipul
1 integer vector [0.. n -1]
2 real matrice [0.. m -1][0.. n -1]
atunci vom ı̂nţelege că vector este un tablou unidimensional (vector) de n numere ı̂ntregi, de elemente vector[0],
vector[1],..., vector[n-1], iar matrice este un tablou bidimensional (matrice cu m linii şi n coloane) de m ∗ n
elemente numere reale, anume matrice[0][0], matrice[0][1],..., matrice[0][n-1], matrice[1][0],...,
matrice[1][n-1],..., matrice[m-1][0],..., matrice[m-1][n-1].
Pentru a defini structuri ı̂n limbajul nostru algoritmic, vom folosi o scriere asemănătoare celei din C/C++:
1 structure < nume_struct >
2 <tip > < nume_c^
a mp >
3 end structure
iar pentru a accesa câmpurile corespunzătoare pentru o variabilă declarată de tipul unei structuri vom folosi
amp. Excepţie fac variabilele de tip pointer, caz ı̂n care se foloseşte
operatorul de selecţie “.”, variabila.nume c^
operatorul “->”.
De asemenea, vom folosi următoarele cuvinte cheie şi simboluri pentru a descrie prelucrările elementare şi structurate
ale unui algoritm:
ˆ pentru operaţiile de citire/scriere
1 read < lista_variabile >
2 write < lista_variabile / expresii >

ˆ pentru operaţia de atribuire


1 < variabila > = < expresie >

ˆ structura secvenţială va fi descrisă ı̂n mod natural, precizând fiecare pas elementar component;
ˆ pentru structura decizională
1 if < expresie > then
2 < secvenţă_instrucţiuni_1 >
3 else
4 < secvenţă_instrucţiuni_2 >
5 end if

Se evaluează <expresie>. Dacă valoarea de adevăr a acesteia este true, atunci se va executa
<secvenţă instrucţiuni 1>, iar dacă este false se va executa <secvenţă instrucţiuni 2>. În ambele
cazuri, execuţia instrucţiunii if se termină, iar controlul execuţiei este transferat următoarei prelucrări.
ˆ pentru structura repetitivă
– condiţionată anterior:
Instrucţiunea while
1 while < expresie > do
2 < secvenţă_instrucţiuni >
3 end while
Se evaluează <expresie>. Dacă valoarea de adevăr a acesteia este true, atunci se va executa
<secvenţă instrucţiuni> şi controlul execuţiei revine pasului ı̂n care se evaluează <expresie>. Dacă
rezulatul evaluării este false execuţia instrucţiunii while se ı̂ncheie.
Instrucţiunea for, cu două sintaxe:
1 for < variabila > = < expresie_1 > to < expresie_2 > do
2 < secvenţă_instrucţiuni >
3 end for

8
Algoritmi şi complexitate Note de curs

1 for < variabila > = < expresie_1 > downto < expresie_2 > do
2 < secvenţă_instrucţiuni >
3 end for
unde <variabila> şi rezultatele evaluării expresiilor <expresie 1> şi <expresie 2> sunt de tip integer.
<secvenţă instrucţiuni> se execută de un număr finit de ori. Dacă presupunem că rezultatele evaluării
expresiilor <expresie 1> şi <expresie 2> sunt v1, respectiv v2, atunci <variabila> ia valori de la v1
la v2, cu pasul pas1 ı̂n primul caz, unde pas1 = succesorul(v1)-v1, şi cu pasul -pas2 ı̂n cel de-al
doilea caz, unde pas2 = v1-predecesorul(v1). Folosind instrucţiunea while, cele două bucle for pot
fi rescrise astfel:
1 < variabila > = < expresie_1 >
2 while < variabila > <= < expresie_2 > do
3 < secvenţă_instrucţiuni >
4 < variabila > = < variabila > + pas1
5 end while
respectiv,
1 < variabila > = < expresie_1 >
2 while < variabila > >= < expresie_2 > do
3 < secvenţă_instrucţiuni >
4 < variabila > = < variabila > - pas2
5 end while

– condiţionată posterior:
Instrucţiunea repeat
1 repeat
2 < secvenţă_instrucţiuni >
3 until < expresie >
unde <secvenţă instrucţiuni> se execută măcar o dată, iar execuţia acesteia se opreşte când rezultatul
evaluării expresiei <expresie> este true. Structura repeat poate fi rescrisă folosind instrucţiunea while:
1 < secvenţă_instrucţiuni >
2 while ! < expresie > do
3 < secvenţă_instrucţiuni >
4 end while

Dacă o problemă poate fi descompusă ı̂n subprobleme ce pot fi rezolvate individual, atunci pentru rezolvarea fiecărei
subprobleme va fi descris un subalgoritm. Avantajele unei astfel de abordări ar fi: subproblemele sunt mai uşor de
rezolvat, iar odată descris un subalgoritm de rezolvare a unei subprobleme, acesta poate fi reutilizat ori de câte
ori este nevoie. Pentru definirea unui subalgoritm vom folosi o sintaxă apropiată de forma generală de declarare şi
definire a unei funcţii ı̂n limbajul C/C++:
1 function < nume_subalgoritm >( < lista_parametri_formali >)
2 begin
3 <declaraţii_date_locale >
4 < secvenţă_instrucţiuni >
5 return < expresie >
6 end
Lista declaraţiilor de date locale şi prelucrările subalgoritmului vor fi plasate ı̂ntre cuvintele cheie begin şi end.
Parametrii formali ai unui subalgoritm sunt variabile locale (vizibile doar subalgoritmului şi cu durata de viaţă
corespunzătoare) prelucrate la nivelul subalgoritmului. Rezultatele prelucrărilor sunt furnizate la apelul subal-
goritmului prin intermediul instrucţiunii return. Parametrii de apel se numesc parametri efectivi sau actuali.
<lista parametri formali> şi return pot lipsi. Algoritmul principal, cel care rezolvă problema iniţială, va
conţine, pe lângă alte prelucrări, o succesiune de apeluri ale subalgoritmilor corespunzători fiecărei suprobleme ı̂n
parte. Sintaxa de apel a unui subalgoritm este:

9
Algoritmi şi complexitate Note de curs

1 < nume_subalgoritm >( < lista_parametri_actuali >)


Putem concluziona că un algoritm de rezolvare a unei probleme descris ı̂n pseudocod are două părţi: o parte de
declaraţii de date şi subalgoritmi utilizaţi şi una de calcul, ı̂n care se citesc datele de intrare, se descriu prelucrările
asupra acestora, se apelează subalgoritmii declaraţi şi se afişează datele de ieşire. Ambele părţi ale algoritmului
vor fi plasate ı̂ntre cuvintele cheie begin şi end. Aşadar, sintaxa generală de descriere a unui algoritm ı̂n limbaj
pseudocod este
1 algorithm < nume_algoritm >
2 begin
3 < declaraţii_date >
4 < declaraţii_subalgoritmi >
5 < prelucrări >
6 end

n
X
Exemplul 1.3. Suma primelor n numere naturale, i, n ≥ 1.
i=1
Descrierea ı̂n pseudocod a algoritmului:
1 algorithm Suma primelor n numere naturale , n ≥ 1
2 begin
3 integer n, i, S
4 read n
5 S = 0
6 for i = 1 to n do
7 S = S + i
8 end for
9 write S
10 end

Exemplul 1.4. Algoritmul lui Euclid ı̂n pseudocod.


1 algorithm Algoritmul lui Euclid
2 begin
3 integer a , b , r
4 read a , b // a !=0 , b !=0
5 while b != 0 do
6 r = a % b
7 a = b
8 b = r
9 end while
10 write a
11 end

Pentru algoritmii simpli, cu puţine prelucrări, schema logică corespunzătoare este uşor de realizat şi vizualizat. Cu
cât algoritmul este mai complicat, cu mai multe prelucrări, cu atât schema logică este mai amplă şi mai greu de
urmărit. Din acest motiv, algoritmii pe care ı̂i vom propune ı̂n continuare vor fi descrişi ı̂n limbajul algoritmic
descris pe scurt mai sus.
Exemplul 1.5. Ne propunem să determinăm cel mai mare divizor comun al unui şir de n numere naturale nenule.
Formal, problema poate fi descrisă prin
1 Input : O secvenţă de n numere naturale [a1 , a2 , . . . , an ], nenule .
2 Output : c.m.m.d.c.(a1 , a2 , . . . , an ).

10
Algoritmi şi complexitate Note de curs

De exemplu, pentru n = 3, c.m.m.d.c.(a1 , a2 , a3 ) = c.m.m.d.c.(c.m.m.d.c.(a1 , a2 ), a3 ). Problema presupune deci


determinarea celui mai mare divizor comun al primelor două numere, apoi a celui mai mare divizor comun al
rezultatului obţinut anterior şi cel de-al treilea număr din şir ş.a.m.d. Vom descrie un subalgoritm corespunzător
algoritmului lui Euclid, dar de data aceasta bazat pe scăderi repetate, subalgoritm ce va fi apelat ı̂n algoritmul
principal. Memorăm secvenţa de numere ı̂n tabloul unidimensional a de elemente a[0], a[1], . . . , a[n-1].
1 algorithm c.m.m.d.c.(a1 , a2 , . . . , an )
2 begin
3 integer n , a [0.. n -1] , dM , i
4 function cmmdc ( integer , integer ) /* declaratia subalgoritmului folosit ; aici
nu este vorba despre apel intrucat lista parametrilor contine doar tipul
lor ! */
5 read n , a
6 dM = cmmmdc ( a [0] , a [1])
7 for i = 2 to n - 1 do
8 dM = cmmdc ( dM , a [ i ]) /* apelul subalgoritmului */
9 end for
10 write dM
11 end

1 function cmmdc ( integer a , integer b )


2 begin
3 while a != b do
4 if a > b then
5 a = a - b
6 else
7 b = b - a
8 end if
9 end while
10 return a
11 end
Observaţie. Prin scrierile de tipul read a, write a, unde a este un tablou, se va ı̂nţelege, pe parcursul acestui
curs, citirea, respectiv afişarea componentelor acestuia.
Exemplul 1.6. Algoritmul lui Eratostene pentru generarea numerelor prime mai mici decât o valoare dată. Ciurul
lui Eratostene este un algoritm vechi creat de matematicianul grec Eratostene. Paşii algoritmului sunt:
1. Se scriu succesiv toate numerele de la 2 la valoarea dată.
2. Se selectează numărul 2, acesta fiind cel mai mic număr prim;
3. Se marchează 2 şi toţi multiplii lui 2 ı̂n lista originală de numere.
4. Se selectează primul număr nemarcat, acesta fiind un număr prim.
5. Se marchează acest număr şi toţi multiplii săi ı̂n lista originală de numere. Marcarea multiplilor poate ı̂ncepe
cu pătratul numărului, deoarece multiplii mai mici au fost deja marcaţi ı̂n paşii anteriori;
6. Se repetă paşii 4 şi 5 până la terminarea tuturor numerelor din lista iniţială.
În cele ce urmează vom descrie algoritmul ı̂n limbaj pseudocod.
1 algorithm Algoritmul lui Eratostene
2 begin
3 integer N , i , j
4 bool p [0.. N ] /* p este un vector de dimensiune N +1 corespunzator listei
initiale de numere ; p [ i ] este true daca i este numar prim si false daca i
nu este numar prim ; p [0] si p [1] raman neinitializate sau p [0]= p [1]= false
*/
5 read N
6 p [0] = false

11
Algoritmi şi complexitate Note de curs

7 p [1] = false
8 for i = 2 to N do
9 p [ i ] = true
10 end for
11 for i = 2 to [ sqrt ( N ) ] do
12 if p [ i ] == true then
13 j = i * i
14 while j <= N do
15 p [ j ] = false
16 j = j + i
17 end while
18 end if
19 end for
20 for i = 2 to N do
21 if p [ i ] == true then
22 write i
23 end if
24 end for
25 end

Exemplul 1.7. Să considerăm o matrice A ∈ Mm×n (R), A = (aij )1≤i≤m . Dorim să calculăm maximul sumelor
1≤j≤n
m
X
pe coloane, max aij . Vom construi un vector de n elemente ce va conţine sumele elementelor de pe fiecare
1≤j≤n
i=1
coloană. Apoi vom determina maximul acestui vector. Vom rezolva mai ı̂ntâi subproblema determinării maximului
unui şir de n elemente, iar subalgoritmul corespunzător va fi apelat ı̂n cadrul algoritmului principal.

1 algorithm Maximul sumelor pe coloane pentru o matrice m × n


2 begin
3 integer m , n , i , j
4 real A [0.. m -1][0.. n -1] , b [0.. n -1] , M
5 function maxim ( integer , real []) /* declaratia subalgoritmului */
6 read m , n , A
7 for j = 0 to n - 1 do
8 b[j] = 0
9 for i = 0 to m - 1 do
10 b [ j ] = b [ j ] + a [ i ][ j ]
11 end for
12 end for
13 M = maxim (n , b ) /* apelul subalgoritmului */
14 write M
15 end

1 function maxim ( integer n , real v [0.. n -1])


2 /* Maximul unui vector cu n elemente reale */
3 begin
4 integer i
5 real max
6 max = v [0]
7 for i = 1 to n - 1 do
8 if max < v [ i ] then
9 max = v [ i ]
10 end if
11 end for
12 return max
13 end

12
Algoritmi şi complexitate Note de curs

Exemplul 1.8. Să considerăm un punct din plan şi un cerc. Ne propunem să determinăm dacă punctul aparţine
sau nu interiorului cercului.
Vom defini tipul abstract de date PUNCT corespunzător noţiunii de punct din plan, dat ı̂ntr-un sistem de coordonate
carteziene; un astfel de punct este caracterizat prin două numere reale reprezentând abscisa, respectiv ordonata
sa. Vom defini, de asemenea, tipul abstract CERC corespunzător noţiunii de cerc de centru O(x, y) şi rază R.
Structura prin care va fi implementat noul tip de date va avea ca membri un punct din plan reprezentând centrul
cercului şi un număr real reprezentând raza cercului. Vom descrie un subalgoritm pentru determinarea distanţei
euclidiene dintre două puncte din plan, pe care ı̂l vom apela pentru a rezolva cerinţa problemei.
1 structure PUNCT
2 real x , y ; /* coordonatele carteziene ale unui punct din plan */
3 end structure
4 structure CERC
5 PUNCT O ; /* centrul cercului */
6 real R ; // raza cercului
7 end structure
8

9 algorithm Test
10 begin
11 PUNCT P
12 CERC C
13 bool raspuns
14 function distanta ( PUNCT , PUNCT ) /* declaratia subalgoritmului */
15 read P .x , P .y , C . O .x , C . O .y , C . R
16 raspuns = false
17 if distanta (P , C . O ) < C . R then /* apelul subalgoritmului */
18 raspuns = true
19 end if
20 write raspuns
21 end

1 function distanta ( PUNCT P , PUNCT Q )


2 begin
3 real d
4 d = sqrt (( P .x - Q . x ) *( P .x - Q . x ) +( P .y - Q . y ) *( P .y - Q . y ) )
5 return d
6 end
Completaţi lista cerinţelor cu umătoarele:
ˆ calculaţi aria şi lungimea unui cerc dat;
ˆ considerându-se trei puncte din plan, precizaţi dacă acestea pot fi vârfurile unui triunghi; ı̂n caz afirmativ
calculaţi aria triunghiului rezultat;
ˆ se consideră două drepte din plan, fiecare fiind determinată de două puncte distincte date; determinaţi poziţia
din plan a celor două drepte.

13
Algoritmi şi complexitate Note de curs

1.3 Analiza corectitudinii algoritmilor


După etapa de elaborare şi descriere a unui algoritm urmează o altă etapă importantă şi anume, analiza acestuia.
Aceasta presupune analiza corectitudinii algoritmului şi apoi a eficienţei acestuia. De altfel, descrierea şi verificarea
corectitudinii unui algoritm se pot realiza simultan. Intuitiv, un algoritm este considerat corect dacă, pornind
de la date de intrare valide ale problemei, se obţin datele de ieşire aşteptate (rezultatele aşteptate) prin execuţia
algoritmului ı̂ntr-un timp finit.
Verificarea corectitudinii unui algoritm elaborat pentru a rezolva o anumită problemă, se poate realiza, ı̂n principal,
prin două metode:
ˆ testarea algoritmului – este metoda prin care algoritmul este executat pentru diverse instanţe ale problemei
(seturi valide de date de intrare). Datorită faptului că, de cele mai multe ori, nu poate fi acoperită complet
ı̂ntreaga mulţime de instanţe ale problemei, această metodă nu garantează corectitudinea algoritmului. Totuşi,
testarea algoritmului pentru instanţe atent selectate poate indica anumite erori ı̂n elaborarea algoritmului.
Găsirea unui contra exemplu (un set de date de intrare care, prin aplicarea paşilor algoritmului, nu conduce
la rezultatele aşteptate), face din algoritm unul incorect. Pe lângă acest avantaj, al detectării erorilor, metoda
este şi una simplu de utilizat.
ˆ demonstraţia formală a corectitudinii algoritmului – este metoda prin care se demonstreză că algoritmul
conduce la rezultatele aşteptate pentru orice instanţă a problemei. Această metodă garantează corectitudinea
algoritmului, dar ı̂n situaţiile unor algoritmi ampli, complecşi, demonstraţia poate deveni greu sau chiar
imposibil de realizat. Soluţia adoptată ı̂n acest caz poate fi ı̂mpărţirea algoritmului ı̂n subalgoritmi de
dimensiune mai mică şi demonstrarea corectitudinii pentru fiecare dintre aceşti subalgoritmi.
De altfel, ı̂n practică, pentru analiza corectitudinii algoritmilor ce rezolvă probleme complexe se adoptă o strategie
ce combină cele două metode.
În analiza corectitudinii unui algoritm (A), se stabilesc mai ı̂ntâi proprietăţile pe care trebuie să le satisfacă datele de
intrare pentru ca problema să aibă sens. Aceste restricţii se numesc precondiţii şi sunt notate cu Pin . Precondiţiile
sunt reunite ı̂ntr-un singur predicat numit aserţiunea de intrare. Apoi se precizează condiţiile pe care trebuie
să le ı̂ndeplinească datele de ieşire, notate cu Pout , numite postcondiţii (conjugate ı̂ntr-un singur predicat numit
aserţiunea de ieşire). Acestea descriu relaţiile care există ı̂ntre datele de intrare şi cele de ieşire conform enunţului
problemei. Spunem că precondiţiile şi postcondiţiile constituie specificaţiile problemei. Apoi trebuie demonstrat că
pornind de la precondiţii şi executând paşii algoritmului, postcondiţiile vor fi satisfăcute (ı̂n timp finit).
Numim starea unui algoritm la un moment dat, mulţimea valorilor variabilelor supuse prelucrărilor algoritmului
la un anumit pas al acestuia. De exemplu, să identificăm starea algoritmului următor după execuţia operaţiei
corespunzătoare fiecărui pas component.

1 algorithm DoesSomething
2 begin
3 integer S , i
4 S = 0
5 i = 10
6 while i <= 16 do
7 S = S + i * i
8 i = i + 2
9 end while
10 write S
11 end

Algoritmul acţionează asupra a două variabile, S şi i.

14
Algoritmi şi complexitate Note de curs

Pas Operaţie S i
1 S=0 0 –
2 i = 10 0 10
3 10 <= 16 0 10
4 S = 0 + 10 ∗ 10 100 10
5 i = 10 + 2 100 12
6 12 <= 16 100 12
7 S = 100 + 12 ∗ 12 244 12
8 i = 12 + 2 244 14
9 14 <= 16 244 14
10 S = 244 + 14 ∗ 14 440 14
11 i = 14 + 2 440 16
12 16 <= 16 440 16
13 S = 440 + 16 ∗ 16 696 16
14 i = 16 + 2 696 18
15 18 <= 16 696 18
16 Afişează S 696 18

Pornind de la precondiţii, se stabilesc stările intermediare ale algoritmului, astfel ı̂ncât, ı̂n final, să fie satisfăcute
postcondiţiile. Trebuie verificat apoi că fiecare prelucrare a algoritmului transformă starea care o precede ı̂n starea
care o succede. În cazul prelucrărilor secvenţiale sau alternative, se verifică efectul fiecărei dintre aceste prelucrări
asupra variabilelor afectate, această verificare nefiind, ı̂n general, complicată. Pentru prelucrările ciclice, verificarea
este ceva mai dificilă; se poate face ı̂n special prin inducţie şi/sau deducţie (deducţie directă sau prin reducere la
absurd), cu atenţie sporită la iniţializări şi la condiţiile de continuare/terminare a ciclului repetitiv.
Formal, corectitudinea unui algoritm poate fi definită astfel:
Definiţia 1.1. (i) Spunem că algoritmul A de rezolvarea a unei probleme este parţial corect, dacă pentru
orice instanţă a problemei care satisface precondiţiile şi pentru care algoritmul A este finit, rezultatul satisface
postcondiţiile.
(ii) Spunem că algoritmul A de rezolvarea a unei probleme este total corect, dacă pentru orice instanţă a problemei
care satisface precondiţiile, algoritmul A este finit şi rezultatul satisface postcondiţiile.
Diferenţa dintre cele două noţiuni este aceea că, ı̂n cazul corectitudinii totale, trebuie ca pentru orice instanţă a
problemei să fie demonstrat faptul că algoritmul se termină ı̂ntr-un număr finit de paşi. Aşadar, corectitudinea totală
ne garantează şi că algoritmul se termină ı̂n timp finit, şi că se termină cu rezultatele aşteptate. Corectitudinea
parţială garantează că algoritmul se termină cu rezultatele dorite, ı̂n ipoteza că algoritmul se termină ı̂n timp finit.
Corectitudinea totală este de multe ori dificil de demonstrat, unele probleme fiind ı̂ncă probleme deschise. De
exemplu, pentru problema căutării succesive ı̂n mulţimea numerelor naturale a unuia care să satisfacă o anumită
proprietate, cum ar fi un număr impar perfect, se poate descrie un algoritm parţial corect. Dar ı̂ncă nu s-a putut
demonstra total corectitudinea algoritmului, deoarece nu s-a demonstrat ı̂ncă că există un astfel de număr, fiind
o problemă deschisă ı̂n teoria numerelor (https://en.wikipedia.org/wiki/Perfect_number). Există situaţii
când terminarea unui algoritm ı̂n timp finit poate fi considerată evidentă, situaţii când poate fi demonstrată
intuitiv/formal sau, după cum am vazut, nu poate fi demonstrată ı̂ncă.
Pentru prelucrările secvenţiale şi pentru cele alternative verificarea corectitudinii totale nu este dificilă.
Exemplul 1.9. Considerăm algoritmul care interschimbă valorile a două variabile. Precondiţii şi postcondiţii:
Pin : x = a, y = b, a, b ∈ R
Pout : x = b, y = a
Vom folosi pentru interschimbare o variabilă auxiliară aux.
1 algorithm Interschimbarea valorilor a două variabile
2 begin
3 real x , y , a , b , aux
4 read a , b
5 x = a
6 y = b

15
Algoritmi şi complexitate Note de curs

7 // S0 = { x = a , y = b }
8 aux = x /* A1 */
9 // S1 = { aux = a , x = a , y = b }
10 x = y /* A2 */
11 // S2 = { aux = a , x = b , y = b }
12 y = aux /* A3 */
13 // S3 = { aux = a , x = b , y = a }
14 write x , y
15 end
S0 , S1 , S2 , S3 corespund stărilor intermediare ale algoritmului. Observăm că Pin coincide cu S0 şi că fiecare
prelucrare a algoritmului, notată cu Ai , transformă starea care o precede, Si-1 , ı̂n starea care o succede, Si , pentru
i = 1, 2, 3. De asemenea, S3 ⇒ Pout . Deci algoritmul este unul total corect (este parţial corect şi se termină
după execuţia unui număr finit de paşi).
Exemplul 1.10. Considerăm algoritmul care determină minimul a trei numere reale distincte. Precondiţii şi
postcondiţii:
Pin : a, b, c ∈ R, distincte
Pout : min = min{a, b, c}
Postcondiţia este echivalentă cu min = min{min{a, b}, c}.
1 algorithm Minimul a trei numere reale distincte
2 begin
3 real a , b , c , min
4 read a , b , c
5 if a < b then
6 min = a /* A1 */
7 // S1 = { a < b , min = a }
8 else
9 min = b /* A2 */
10 // S2 = { a > b , min = b }
11 end if
12 if min > c then
13 min = c /* A3 */
14 // S3 = { min {a , b } > c , min = c }
15 end if
16 write min
17 end
S1 , S2 , S3 corespund stărilor algoritmului la anumite momente ale execuţiei sale. În cazul primei structuri al-
ternative, condiţia testată este a < b. Dacă rezultatul evaluării este true atunci min = a = min{a, b}, altfel
ı̂nseamnă că a > b (a şi b sunt distincte) şi min = b = min{a, b}. A doua structură alternativă are drept
condiţie de test min > c, cu min = min{a, b}, determinat ı̂n pasul precedent. Dacă rezultatul evaluării este true
atunci min = c = min{min, c}. Dacă min < c, variabila min nu ı̂şi modifică valoarea, dar rămâne adevărat că
min = min{min, c} şi deci este satisfăcută postcondiţia. Astfel, algoritmul este total corect.
Pentru a arăta că o prelucrare repetitivă este total corectă trebuie demonstrat că, plecând de la precondiţii, aceasta
conduce la satisfacerea postcondiţiilor, ı̂n ipoteza că se termină (parţial corectitudinea), dar şi că se termină după
execuţia unui număr finit de paşi. Astfel, pentru a arăta că structura
1 while conditie do
2 A
3 end while
este parţial corectă ı̂n raport cu precondiţiile şi postcondiţiile Pin şi Pout (conditie este o expresie ce poate fi
evaluată) se va pune ı̂n evidenţă un invariant la ciclare (loop invariant). Acest lucru presupune identificarea unui
predicat I(n), asociat stării algoritmului (I(n) este un predicat ce este adevărat după n iteraţii ale structurii
while), care să satisfacă proprietăţile:
ˆ Pin ⇒ I(0) (I(n) este adevărat la intrarea ı̂n structura repetitivă);

16
Algoritmi şi complexitate Note de curs

A
ˆ I(k) ∧ conditie −→ I(k+1) (I(n) este invariant la ciclare: dacă I(k) este adevărat şi conditie
este adevărată, atunci I(k+1) este adevărat);
ˆ I(N) ∧ ¬conditie ⇒ Pout (dacă conditie devine falsă şi deci se ı̂ncheie execuţia buclei repetitive, I(N)
conduce la postcondiţii; acest lucru presupune că bucla a fost iterată de N ori).
Identificarea invariantului la ciclare nu este ı̂ntotdeauna o sarcină uşoară. Ceea ce cu siguranţă trebuie urmărit
este ca acesta să reflecte efectul unei iteraţii asupra variabilelor implicate ı̂n rezultatul aşteptat.
Pentru a demonstra că o buclă repetitivă se termină ı̂ntr-un număr finit de paşi este suficient să se găsească o funcţie
t : N −→ N, numită funcţie de terminare, ce depinde de numărul curent al iteraţiei şi este strict descrescătoare la
fiecare iteraţie. Atât timp cât conditie este adevărată, funcţia de terminare ia valori mai mari sau egale cu 1,
iar atunci când funcţia de terminare ia valoarea 0, conditie devine falsă (t fiind descrescătoare şi având valori
naturale, va ajunge să ia valoarea 0).
După cum am văzut, structurile repetitive for şi repeat pot fi rescrise folosind structura condiţionată anterior
while. Deci ideile enunţate rămân valabile şi pentru aceste structuri.
Exemplul 1.11. Considerăm algoritmul care realizează calculul aN , N ∈ N, a ∈ R∗ , prin ı̂nmulţiri repetate.
Precondiţii şi postcondiţii:
Pin : N ∈ N, a ∈ R∗
Pout : p = aN
1 algorithm Calculul puterii naturale a unui număr real nenul
2 begin
3 integer N , i
4 real a , p
5 read a , N
6 i = 0
7 /* { i = 0} */
8 p = 1
9 /* { i = 0 , p = 1} */
10 while i < N do
11 i = i + 1
12 p = p * a
13 end while
14 write p
15 end
Notăm cu ik şi pk valorile variabilelor i şi p la iteraţia k (starea algoritmului la iteraţia k). Conform algoritmului
obţinem:
iteraţia 0: i0 = 0, p0 = 1
iteraţia 1: i1 = i0 + 1, p1 = p0 ∗ a
...
iteraţia k: ik = ik−1 + 1, pk = pk−1 ∗ a
...
iteraţia n: in = in−1 + 1, pn = pn−1 ∗ a
...
Demonstrăm prin inducţie după n următoarea afirmaţie: in = n, n ∈ N.
Pentru n = 0, i0 = 0 (linia 6 a algoritmului), deci afirmaţia este adevărată. Presupunem adevărată afirmaţia
pentru n = k, adică ik = k şi demonstrăm că acest lucru implică ik+1 = k + 1. Din linia 11 a algoritmului
ik+1 = ik + 1 = k + 1. Aşadar, conform principiului inducţiei matematice, afirmaţia in = n este adevărată pentru
orice număr natural n.
Pentru a demonstra corectitudinea totală a ciclului while vom pune ı̂n evidenţă un invariant la ciclare şi o funcţie
de terminare.
Întrucât calculul se realizează prin ı̂nmulţiri repetate, considerăm următorul predicat

I(n) : pn = an , n ∈ N.

17
Algoritmi şi complexitate Note de curs

Se observă că I(0) este adevărat deoarece, la intrarea ı̂n buclă, p0 = a0 = 1 (afirmaţia este adevărată conform
liniei 8 a algoritmului). Vom arăta că I(n) este un invariant la ciclare. Presupunem că I(k) este adevărat (I(n)
este adevărat la iteraţia k, deci pentru n = k) şi condiţia de continuare a ciclului while este evaluată la valoarea
true. Să demonstrăm că aceste două presupuneri implică că I(n) este adevărat pentru n = k + 1 (iteraţia k + 1).
După execuţia liniei 11, variabila i este incrementată, ik+1 = ik + 1 = k + 1, iar ı̂n linia 12, pk este ı̂nmulţit cu
a, deci pk+1 = pk ∗ a = ak ∗ a = ak+1 . Aşadar, dacă I(n) este adevărat pentru n = k şi condiţia de continuare
este adevărată atunci I(n) este adevărat pentru n = k + 1. Rezultă că I(n) este invariant la ciclul while. Dacă
iN = N , condiţia de continuare devine falsă, iar I(N ) : p = aN , reprezintă chiar postondiţia. Deci structura while
este parţial corectă.
Definim funcţia de terminare t : N −→ N, t(k) = N − ik , unde k este contorul iteraţiei. Această funcţie este strict
descrescătoare la fiecare iteraţie: deoarece ik+1 = ik +1, rezultă că t(k +1) = N −ik+1 = N −ik −1 < N −ik = t(k).
Atât timp cât condiţia de continuare este adevărată (ik < N ), t(k) ≥ 1, iar atunci când t(k) = 0, ik = N , condiţia
de continuare devine falsă, iar algoritmul se opreşte. În concluzie, structura while este total corectă, deci algoritmul
este total corect.
Exemplul 1.12. Reluăm algoritmul de calcul al sumei primelor N numere naturale, N ∈ N∗ . Precondiţii şi
postcondiţii:
Pin : N ∈ N∗
XN
Pout : S = i, N ≥ 1
i=1

1 algorithm Suma primelor N numere naturale nenule


2 begin
3 integer N , i , S
4 read N
5 S = 0
6 /* { S = 0} */
7 i = 1
8 /* { i = 1 , S = 0} */
9 while i <= N do
10 S = S + i
11 i = i + 1
12 end while
13 write S
14 end
Notăm cu ik şi Sk valorile variabilelor i şi S la iteraţia k (starea algoritmului la iteraţia k). Conform algoritmului
obţinem:
iteraţia 0: S0 = 0, i0 = 1
iteraţia 1: S1 = S0 + i0 , i1 = i0 + 1
...
iteraţia k: Sk = Sk−1 + ik−1 , ik = ik−1 + 1
...
iteraţia n: Sn = Sn−1 + in−1 , in = in−1 + 1
...
Demonstrăm prin inducţie după n următoarea afirmaţie: in = n + 1, n ∈ N.
Pentru n = 0, i0 = 1 (linia 7 a algoritmului), deci afirmaţia este adevărată. Presupunem adevărată afirmaţia
pentru n = k, adică ik = k + 1 şi demonstrăm că acest lucru implică ik+1 = k + 2. Din linia 11 a algoritmului
ik+1 = ik + 1 = (k + 1) + 1 = k + 2. Aşadar, conform principiului inducţiei matematice, afirmaţia in = n + 1 este
adevărată pentru orice număr natural n.
Deoarece suma se realizează adăugând la fiecare iteraţie n termenul corespunzător, in−1 = n, un predicat adecvat
este
Xn
I(n) : Sn = i, n ∈ N.
i=1

18
Algoritmi şi complexitate Note de curs

0
X
Observăm că I(0) este adevărat deoarece, la intrarea ı̂n buclă, S0 = i, S0 având semnificaţia aici de sumă vidă,
i=1
adică S0 = 0 (adevărat conform liniei 5). Trebuie să demonstrăm că I(n) este invariant la ciclare. Presupunem
că I(n) este adevărat pentru n = k, iar condiţia de continuare a ciclului while este evaluată la valoarea true. Să
X k k+1
X
arătăm că I(n) este adevărat pentru n = k + 1. După execuţia liniei 10, Sk+1 = Sk + ik = i + (k + 1) = i.
i=1 i=1
Aşadar, dacă I(n) este adevărat pentru n = k şi condiţia de continuare este adevărată atunci I(n) este adevărat
pentru n = k + 1 (I(n) este invariant la ciclul while).
Observăm că se obţine postcondiţia dacă condiţia de continuare devine falsă (iN = N + 1 la ieşirea din while):
N
X
I(N ) : SN = i.
i=1

Prin urmare structura while este parţial corectă.


Alegem funcţia de terminare t : N −→ N, t(k) = N +1−ik , unde k este contorul iteraţiei. t este strict descrescătoare
la fiecare iteraţie: deoarece ik+1 = ik + 1, rezultă că t(k + 1) = N + 1 − ik+1 = N + 1 − ik − 1 < N + 1 − ik = t(k).
Atât timp cât condiţia de continuare este adevărată (ik <= N ) avem că t(k) ≥ 1. Atunci când t(k) = 0, ik = N +1,
condiţia de continuare devine falsă, iar algoritmul se opreşte. În concluzie, algoritmul este total corect.
Exemplul 1.13. Să verificăm corectitudinea algoritmului de calcul a sumei elementelor unui tablou unidimensional
v de N numere reale. Precondiţiile şi postcondiţiile sunt:
Pin : N ∈ N∗ (vectorul are măcar un element)
N
X −1
Pout : S = v[i], N ≥ 1
i=0

1 algorithm Suma elementelor unui vector de N numere reale


2 begin
3 integer N , i
4 real S , v [0.. N -1]
5 read N , v
6 S = 0
7 i = 0
8 while i < N do
9 S = S + v[i]
10 i = i + 1
11 end while
12 write S
13 end
Notăm cu ik şi Sk valorile variabilelor i şi S la iteraţia k (starea algoritmului la iteraţia k). Conform algoritmului
obţinem:
iteraţia 0: S0 = 0, i0 = 0
iteraţia 1: S1 = S0 + v[i0 ], i1 = i0 + 1
...
iteraţia k: Sk = Sk−1 + v[ik−1 ], ik = ik−1 + 1
...
iteraţia n: Sn = Sn−1 + v[in−1 ], in = in−1 + 1
...
Se demonstrează prin inducţie că in = n, n ∈ N.
Vom identifica un invariant şi o funcţie de terminare pentru a demonstra corectitudinea totală a structurii while.
Observăm că la fiecare iteraţie n, ı̂n variabila S se memorează suma primelor n elemente din vectorul v. Rescriem
această afirmaţie ca
n−1
X
I(n) : Sn = v[i], n ∈ N
i=0

19
Algoritmi şi complexitate Note de curs

şi demonstram că este invariant la ciclare.


0−1
X
La intrarea ı̂n structura repetitivă, adică la iteraţia 0, I(0) : S0 = v[i] = 0, cu semnificaţia de sumă vidă, ceea
i=0
ce e adevărat (conform liniei 6).
Presupunem că I(n) este adevărat pentru n = k şi că evaluarea condiţiei de continuare din while este true. După
k−1
X k
X
execuţia liniei 9, Sk+1 = Sk + v[ik ] = v[i] + v[k] = v[i], adică S va memora suma primelor k + 1 elemente ale
i=0 i=0
tabloului v. Aşadar, dacă I(k) este adevărat şi condiţia de continuare a structurii repetitive este adevărată, atunci
I(k + 1) adevărat. Deci I(n) este invariant la ciclare.
Dacă condiţia de continuare devine falsă (iN = N )
N
X −1
I(N ) : SN = v[i],
i=0

adică tocmai postcondiţia. Aşadar structura repetitivă este parţial corectă.


Definim funcţia de terminare t : N −→ N, t(k) = N − ik , unde k este contorul iteraţiei. Această funcţie este strict
descrescătoare la fiecare iteraţie: deoarece ik+1 = ik +1, rezultă că t(k +1) = N −ik+1 = N −ik −1 < N −ik = t(k).
Atât timp cât condiţia de continuare este adevărată (ik < N ), t(k) ≥ 1, iar atunci când t(k) = 0, ik = N , condiţia
de continuare devine falsă, iar algoritmul se opreşte. În concluzie, structura while este total corectă şi algoritmul
este total corect.

Exemplul 1.14. Să reluăm problema determinării maximului unei secvenţe de N numere reale pe care le vom me-
mora ı̂n tabloul unidimensional v, de elemente v[0], v[1], . . . , v[N-1]. În acest caz, precondiţiile şi postcondiţiile
sunt:
Pin : N ∈ N∗ (secvenţa are măcar un element)
Pout : max = max v[i]
0≤i≤N −1
Postcondiţia este echivalentă cu afirmaţia {max ≥ v[i], (∀) i ∈ {0, 1, . . . , N − 1}} ∧ {(∃) i ∈ {0, 1, . . . , N −
1} astfel ı̂ncât max = v[i])}.

1 algorithm Maximul unui vector cu N elemente reale


2 begin
3 integer N , i
4 real v [0.. N -1] , max
5 read N , v
6 max = v [0]
7 for i = 1 to N - 1 do
8 if max < v [ i ] then
9 max = v [ i ]
10 end if
11 end for
12 write max
13 end
Avem de demonstrat că pornind de la precondiţii şi aplicând prelucrările algoritmului, postcondiţia va fi satisfăcută.
Notăm cu ik şi maxk valorile variabilelor i şi max la iteraţia k (starea algoritmului la iteraţia k). Conform
algoritmului obţinem:
iteraţia 0: max0 = v[0], i0 = 1
iteraţia 1: max1 = max{max0 , v[i0 ]}, i1 = i0 + 1
...
iteraţia k: maxk = max{maxk−1 , v[ik−1 ]}, ik = ik−1 + 1,
...
iteraţia n: maxn = max{maxn−1 , v[in−1 ]}, in = in−1 + 1
...
Se demonstrează prin inducţie că in = n + 1, n ∈ N∗ .

20
Algoritmi şi complexitate Note de curs

Alegem drept invariant


I(n) : maxn = max{v[0], v[1], . . . , v[n]}, n ∈ N.
Observăm că I(0) este adevărată, I(0) : max0 = v[0], deoarece prin execuţia liniei 6, variabila max este iniţializată
cu v[0]. Demonstrăm că I(n) este invariant. Dacă presupunem că I(n) este adevărat pentru n = k, adică
maxk = max{v[0], v[1], . . . , v[k]}, iar condiţia de continuare este şi ea adevărată, atunci ar trebui să demonstrăm
că predicatul
I(k + 1) : maxk+1 = max{v[0], v[1], . . . , v[k + 1]}
este adevărat. La iteraţia k + 1 a ciclului for variabila maxk este comparată cu elementul v[ik ] = v[k + 1]. Dacă
maxk < v[k + 1], atunci variabilei maxk+1 i se atribuie valoarea v[k + 1], iar dacă maxk ≥ v[k + 1], valoarea lui
maxk+1 rămâne maxk . În ambele situaţii, este adevărată afirmaţia că maxk+1 = max{v[0], v[1], . . . , v[k], v[k + 1]}
şi deci I(n) este adevărat pentru n = k + 1. Dacă execuţia prelucrării repetitive for se ı̂ncheie, iN −1 = N şi deci
I(N − 1) implică postcondiţia. Avem demonstrată corectitudinea parţială a ciclului for.
Considerăm funcţia de terminare t : N −→ N, t(k) = N −ik , unde k este contorul iteraţiei; t este strict descrescătoare
la fiecare iteraţie: deoarece ik+1 = ik + 1, rezultă că t(k + 1) = N − ik+1 = N − ik − 1 < N − ik = t(k). Atât timp
cât condiţia de continuare este adevărată, ik < N , are loc t(k) ≥ 1. Atunci când t(k) = 0, ik = N , condiţia de
continuare devine falsă, iar algoritmul se opreşte. Total corectitudinea algoritmului este demonstrată.
Exemplul 1.15. Verificarea corectitudinii algoritmului lui Euclid. Precondiţii şi postcondiţii:
Pin : a, b ∈ N∗
Pout : B = c.m.m.d.c.(a, b)
1 algorithm Algoritmul lui Euclid
2 begin
3 integer a , b , r , A , B
4 read a , b /* a !=0 , b !=0 */
5 A = a
6 B = b
7 r = A % B
8 while r != 0 do
9 A = B
10 B = r
11 r = A % B
12 end while
13 write B
14 end
Rezultatul fiecărui pas al algoritmului lui Euclid este utilizat ca punct de plecare pentru pasul următor. La
fiecare iteraţie n, n ≥ 0, se folosesc două resturi rn−1 şi rn−2 , numere naturale. Algoritmul asigură că resturile
scad la fiecare sevenţă, deci rn−1 < rn−2 . La fiecare iteraţie n se determină câtul qn şi restul rn astfel ı̂ncât
rn−2 = qn rn−1 + rn , cu rn < rn−1 .
La pasul iniţial (iteraţia 0), resturile utilizate ca punct de plecare sunt r−2 = a şi r−1 = b. Se calculează
r0 = r−2 %r−1 . La pasul următor (iteraţia 1), resturile sunt r−1 = b şi restul r0 din pasul anterior ş.a.m.d.
Algoritmul poate fi rescris astfel:
a = q0 b + r0 , r0 < r−1 = b
b = q1 r0 + r1 , r1 < r0
r0 = q2 r1 + r2 , r2 < r1
r1 = q3 r2 + r3 , r3 < r2
...
Dacă a < b, primul pas al algoritmului schimbă numerele ı̂ntre ele, deoarece q0 = 0, iar r0 = a. Astfel, rk < rk−1
pentru orice k ≥ 0. Cum resturile scad la fiecare pas, dar nu pot fi negative, există un număr natural N astfel
ı̂ncât restul rN = 0, algoritmul oprindu-se. Ultimul rest nenul, rN −1 , este cel mai mare divizor comun al lui a şi b.
N nu poate fi infinit deoarece există un număr finit de numere naturale ı̂ntre restul iniţial r0 şi 0.
Alegem predicatul
I(n) : c.m.m.d.c.(a, b) = c.m.m.d.c.(An , Bn ), n ∈ N
unde An şi Bn sunt valorile variabilelor A şi B la iteraţia n. Astfel, I(0) este adevărat deoarece, la intrarea
ı̂n bucla repetitivă, A0 = a şi B0 = b (liniile 5 şi 6). Pentru a demonstra că I(n) este invariant, este suficient

21
Algoritmi şi complexitate Note de curs

să demonstrăm că pentru orice rk 6= 0, c.m.m.d.c.(Ak , Bk ) = c.m.m.d.c.(Ak+1 , Bk+1 ) = c.m.m.d.c.(Bk , rk ). Fie
f = c.m.m.d.c.(Ak , Bk ) şi g = c.m.m.d.c.(Bk , rk ). Folosim faptul că Ak = qk Bk + rk :

f | Ak , f | Bk ⇒ f | (Ak − qk Bk ) ⇒ f | rk , f | Bk ⇒ f ≤ g,
g | Bk , g | rk ⇒ g | (qk Bk + rk ) ⇒ g | Ak , g | Bk ⇒ g ≤ f.
Din ultimile două relaţii rezultă că f = g, deci I(n) este invariant. Mai trebuie să demonstrăm că ı̂n final este
implicată postcondiţia. Dacă rN = 0 (condiţia de terminare a algoritmului), atunci rezultatul furnizat de algoritm
este ultimul rest nenul, c.m.m.d.c.(AN , BN ) = c.m.m.d.c.(BN , rN ) = BN , deci postcondiţia.
Considerăm funcţia de terminare t : N −→ N, t(k) = rk , unde k este contorul iteraţiei, iar rk este valoarea restului
obţinut la iteraţia k; t este strict descrescătoare la fiecare iteraţie, deoarece rk+1 < rk . Atât timp cât condiţia de
continuare este adevărată, rk 6= 0, are loc t(k) ≥ 1. Atunci când t(k) = 0, condiţia de continuare devine falsă, iar
algoritmul se termină. Total corectitudinea algoritmului este demonstrată.

1.4 Analiza eficienţei algoritmilor


1.4.1 Timpul de execuţie al unui algoritm
Analiza complexităţii–timp a unui algoritm necesită precizarea modelului de calcul utilizat. Presupunem că se
foloseşte o maşină de calcul cu acces aleator, un model cu un singur procesor şi cu următoarele proprietăţi: pre-
lucrările se execută secvenţial, iar operaţiile elementare sunt executate ı̂n timp constant. Operaţii elementare sunt
considerate a fi operaţiile aritmetice, cele logice şi comparaţiile.
Timpul de execuţie al unui algoritm de rezolvare a unei probleme depinde de dimensiunea acesteia, deci de volumul
datelor de intrare. Dacă reluăm problema sortării crescătoare a unui şir de numere, timpul de execuţie al algo-
ritmului ce rezolvă această problemă diferă ı̂n funcţie de numărul de elemente de sortat (timpul necesar sortării
unei secvenţe de 10 numere este mai mic decât timpul necesar pentru a sorta 10000 de numere). Sunt situaţii
când timpul de execuţie depinde nu doar de dimensiunea problemei, ci şi de caracteristicile datelor de intrare. De
exemplu, timpul de execuţie al algoritmului de sortare crescătoare a unui şir aproape ordonat crescător este mult
mai mic decât al unei secvenţe de numere care este aproape ordonată descrescător.
Dimensiunea unei probleme depinde de caracteristicile acesteia; aşadar, atunci când este analizat un algoritm din
punctul de vedere al timpului său de execuţie, trebuie specificat cum este măsurat volumul datelor de intrare. De
exemplu, ı̂n cazul sortării unei secvenţe de n numere, dimensiunea problemei este n. În schimb, ı̂n cazul algoritmului
de sumare a două matrice de aceeaşi dimensiune, m × n, volumul datelor de intrare depinde de două valori, m şi
n.
A determina timpul de execuţie al unui algoritm ı̂nseamnă a număra câte operaţii elementare se execută. Deoarece
scopul analizei timpului de execuţie al unui algoritm este acela de a permite compararea algoritmilor care rezolvă
o aceeaşi problemă, de cele mai multe ori sunt numărate doar operaţiile elementare importante ale algoritmului,
numite operaţii de bază, acestea fiind operaţiile care contribuie cel mai mult la timpul de execuţie. De exemplu,
pentru algoritmii de căutare, operaţiile de bază sunt comparaţiile ı̂ntre elementul căutat şi componentele secvenţei
ı̂n care se face căutarea, iar pentru algoritmii de sortare, operaţiile de bază sunt comparaţiile ı̂ntre elemente
şi mutările acestora. Numărând doar operaţiile de bază ale unui algoritm, obţinem o aproximare (estimare) a
timpului de execuţie al acestuia.
De regulă, atunci când vom analiza timpul de execuţie al unui algoritm, vom estima timpul ı̂n care operaţiile de
bază sunt executate pentru fiecare set de date de intrare, deci vom analiza dependenţa timpului de execuţie al unui
algoritm de dimensiunea problemei pe care o rezolvă. Vom nota cu T (n) timpul de execuţie al unui algoritm ce
rezolvă o problemă de dimensiune n.
În continuare, ne propunem să estimăm timpul de execuţie al unor algoritmi simpli.
Exemplul 1.16. Suma primelor n numere naturale, n ∈ N∗ . Dimensiunea problemei este n.
1 algorithm Suma primelor n numere naturale
2 begin
3 integer n , i , S

22
Algoritmi şi complexitate Note de curs

4 read n
5 S = 0
6 for i = 1 to n do
7 S = S + i
8 end for
9 write S
10 end
Neglijăm din start operaţiile de citire şi scriere (liniile 4 şi 9 ale algoritmului), luând ı̂n calcul pentru aproximarea
timpului de execuţie, doar operaţia de atribuire din linia 5, operaţiile din linia 6 şi cele din linia 7. Presupunem
că toate operaţiile elementare (atribuiri, adunări, comparaţii) au acelaşi cost (= 1 = o unitate de timp). Operaţia
de atribuire din linia 5 se execută o singură dată, având costul 1. În linia 6 avem: o operaţie de atribuire, i = 1,
care se execută o singură dată, o operaţie de de comparaţie, i ≤ n, care se execută de n + 1 ori şi o operaţie de
incrementare (ce este compusă dintr-o adunare şi o atribuire), i = i + 1, ce este executată de n ori, la sfârşitul
fiecărei iteraţii ale ciclului for. Pentru simplificare, considerăm ı̂n cele ce urmează că o operaţie de incrementare
are tot costul 1. Aşadar, costul total al liniei 6 este: 1 + (n + 1) + n = 2n + 2 = 2(n + 1). Prelucrarea din linia
7 se execută de n ori. Acesta constă dintr-o operaţie de adunare şi una de atribuire, deci costul său total este de
2n. Sumând costurile fiecărei prelucrări considerate, obţinem aproximarea T (n) = 4n + 3, deci timpul de execuţie
depinde liniar de dimensiunea problemei, n.
Operaţia Cost Număr de repetări Cost total
5 1 1 1
6 2(n + 1) 1 2(n + 1)
7 2 n 2n
T (n) = 4n + 3
Operaţia de bază care contribuie efectiv cel mai mult la timpul de execuţie este operaţia de adunare din linia 7,
iar aceasta se efectuează de n ori. Dacă ar fi să estimăm timpul de execuţie luând ı̂n considerare doar operaţiile de
bază, atunci T (n) = n, obţinând tot o dependenţă liniară a timpului de execuţie de dimensiunea problemei, n.
Exemplul 1.17. Produsul a două matrice A ∈ Mm×n (R) şi B ∈ Mn×p (R). Dimensiunea problemei depinde de
m, n şi p.
1 algorithm Produsul a două matrice A ∈ Mm×n (R) şi B ∈ Mn×p (R)
2 begin
3 integer m , n , p , i , j , k
4 real A [0.. m -1][0.. n -1] , B [0.. n -1][0.. p -1] , C [0.. m -1][0.. p -1]
5 /* C este matricea produs , C = A * B */
6 read m , n , p , A , B
7 for i = 0 to m - 1 do
8 for j = 0 to p - 1 do
9 C [ i ][ j ] = 0
10 for k = 0 to n - 1 do
11 C [ i ][ j ] = C [ i ][ j ] + A [ i ][ k ] * B [ k ][ j ]
12 end for
13 end for
14 end for
15 write C
16 end
Luăm ı̂n considerare pentru aproximarea timpului de execuţie al algoritmului operaţiile din liniile 7–14. Costul
operaţiilor din linia 7 este: 1 + (m + 1) + m = 2(m + 1). Similar, costul operaţiilor din linia 8 este 2(p + 1), iar
al celor din linia 10 este 2(n + 1). Opearţiile din linia 8 se repetă de m ori, deci costul lor total va fi 2m(p + 1).
Operaţiile din linia 10 se repetă de mp ori, deci costul lor total va fi 2mp(n + 1). Operaţia de atribuire din linia 9
are costul 1 şi este executată de mp ori. Linia 11 este compusă din trei operaţii elementare: o ı̂nmulţire, o adunare
şi o atribuire, deci costul său este 3. Aceste operaţii sunt repetate de mnp ori, deci costul lor total va fi 3mnp.
Sumând, obţinem aproximarea T (m, n, p) = 5mnp + 5mp + 4m + 2. Această estimare are la bază presupunerea că
toate operaţiile elementare (atribuire, comparaţie, adunare, ı̂nmulţire) au acelaşi cost, lucru care nu este neapărat
adevărat ı̂n realitate.

23
Algoritmi şi complexitate Note de curs

Operaţia Cost Număr de repetări Cost total


7 2(m + 1) 1 2(m + 1)
8 2(p + 1) m 2m(p + 1)
9 1 mp mp
10 2(n + 1) mp 2mp(n + 1)
11 3 mnp 3mnp
T (m, n, p) = 5mnp + 5mp + 4m + 2
Dacă ar fi să estimăm timpul de execuţie luând ı̂n considerare doar operaţiile de bază, atunci, ı̂n cazul acestui
algoritm, T (m, n, p) = mnp, datorită faptului că operaţia care contribuie efectiv cel mai mult la timpul de execuţie
este operaţia de ı̂nmulţire din linia 11, iar aceasta se efectuează de mnp ori.

1.4.2 Analiza complexităţii unui algoritm ı̂n cazurile extreme


Până acum am analizat eficienţa unor algoritmi ai căror timpi de execuţie nu depind decât de dimensiunea proble-
mei, nu şi de proprietăţile datelor de intrare. Dacă numărul operaţiilor ce sunt executate depinde de caracteristicile
datelor de intrare, vom obţine timpi de execuţie diferiţi pentru diferite instanţe ale problemei. Deci, dacă timpul
de execuţie depinde de proprietăţile datelor de intrare, este necesară analiza acestuia măcar ı̂n cazurile extreme:
cel mai favorabil caz şi cel mai defavorabil caz.
Cazul cel mai favorabil corespunde situaţiei ı̂n care se execută cel mai mic număr de prelucrări. Prin analiza timpului
de execuţie ı̂n cazul cel mai favorabil se obţine o limită inferioară a acestuia. Această analiză oferă informaţii
valoroase doar ı̂n două situaţii: frecvenţa instanţelor ce corespund cazului cel mai favorabil (sau apropiate de
acesta) este mare sau estimarea obţinută este de neacceptat şi se poate trage concluzia că algoritmul este ineficient.
Cel mai defavorabil caz corespunde situaţiei ı̂n care se execută cel mai mare număr de prelucrări. Prin analiza
timpului de execuţie ı̂n cazul cel mai defavorabil se determină o limită superioară a acestuia. Această analiză
este importantă deoarece orice altă instanţă a problemei va avea un timp de execuţie mai mic sau egal cu acesta.
Singurul caz ı̂n care această analiză nu oferă rezultate importante este atunci când frecvenţa instanţelor ce corespund
celui mai defavorabil caz (sau apropiate de acesta) este mică. Pentru anumiţi algoritmi, cazul cel mai defavorabil
apare destul de des. Spre exemplu, căutând o anumită dată ı̂ntr-o colecţie de date, cazul cel mai defavorabil al
algoritmului de căutare apare atunci când data căutată nu se găseşte ı̂n colecţie.
Exemplul 1.18. Maximul unei secvenţe de n numere naturale, n ∈ N∗ . Dimensiunea problemei este n. Vom
memora secvenţa de numere ı̂ntr-un vector cu n componente, a[0], a[1], ..., a[n-1].
1 algorithm Maximul unui şir de n numere
2 begin
3 integer n , i , a [0.. n -1] , max
4 read n , a
5 max = a [0]
6 for i = 1 to n - 1 do
7 if max < a [ i ] then
8 max = a [ i ]
9 end if
10 end for
11 write max
12 end
13

Luăm ı̂n considerare pentru determinarea timpului de execuţie operaţiile din liniile 5–10. Operaţia de atribuire
din linia 5 se execută o singură dată. Costul execuţiei liniei 6 este : 1 + n + (n − 1) = 2n. Comparaţia din linia 7
se va repeta de n − 1 ori, deci costul total al acestei operaţii este de n − 1. Numărul de repetări ale atribuirii din
linia 8 depinde de caracteristicile secvenţei de numere. De exemplu, dacă a[0] este cea mai mare valoare a şirului,
atunci linia 8 nu va fi executată niciodată, iar dacă şirul este strict crescător, atunci linia 8 va fi executată de n − 1
ori. Dacă notăm cu t(n) numărul de repetări ale operaţiei din linia 8, atunci 0 ≤ t(n) ≤ n − 1. În primul caz, cel
mai favorabil, t(n) = 0 şi deci T (n) = 3n, iar ı̂n cel de-al doilea caz, cel mai defavorabil, t(n) = n − 1, prin urmare
T (n) = 4n − 1. Deci putem scrie că 3n ≤ T (n) ≤ 4n − 1, găsind astfel o limită inferioară şi una superioară pentru
timpul de execuţie. Ambele depind liniar de dimensiunea problemei.

24
Algoritmi şi complexitate Note de curs

Operaţia Cost Număr de repetări Cost total


5 1 1 1
6 2n 1 2n
7 1 n−1 n−1
8 1 t(n) t(n)
T (n) = t(n) + 3n

Dacă vom considera comparaţia din linia 7 ca operaţie de bază a algoritmului, atunci obţinem o estimare a timpului
de execuţie de forma T (n) = n − 1, deci timpul de execuţie al algoritmului depinde liniar de dimensiunea problemei.
Cu această abordare timpul de execuţie al algoritmului nu mai depinde de proprietăţile datelor de intrare, toate
comparaţiile realizându-se oricum.
Exemplul 1.19. Algoritmul de căutare secvenţială a unei anumite valori x ı̂ntr-un set de n numere reale. Dimen-
siunea problemei este n.
Presupunem că şirul are măcar un element. Vom memora secvenţa de numere reale ı̂ntr-un vector cu n compo-
nente, v[0], v[1], ..., v[n-1]. Căutarea secvenţială, numită şi liniară, presupune căutarea valorii x, element
cu element, ı̂ncepând cu primul din vector. Aceasta continuă fie până când valoarea căutată este găsită ı̂n vector,
fie până când vectorul este complet parcurs. Algoritmul nu necesită ca elementele vectorului să fie ı̂ntr-o anumită
ordine.
1 algorithm Algoritmul 1 de căutare secvenţială
2 begin
3 integer n , i
4 bool gasit
5 real v [0.. n -1] , x
6 read n , v , x
7 gasit = false
8 i = 0
9 while gasit == false && i < n do
10 if v [ i ] == x then
11 gasit = true
12 else
13 i = i + 1
14 end if
15 end while
16 write gasit
17 end
18

Atribuirile din liniile 7 şi 8 se execută o singură dată şi costul fiecăreia este 1. Numărul de execuţii ale celorlalte
prelucrări depinde de poziţia valorii x ı̂n şir. De altfel, valoarea căutată x poate să fie sau să nu fie ı̂n vector.
Notăm cu t1 (n) numărul de comparaţii efectuate ı̂ntre valoarea căutată x şi elementele tabloului (linia 10). Costul
execuţiei o singură dată a liniei 9 este 3 (două comparaţii şi o operaţie logică “şi” ). Această linie va fi executată
de t1 (n) + 1 ori.
Dacă x este ı̂n vector, considerăm cel mai mic k astfel ı̂ncât v[k − 1] = x. Deci valoarea căutată este elementul cu
numărul de ordine k ı̂n şir (are poziţia k ∈ {1, 2, . . . , n} ı̂n şir) şi vor fi necesare k operaţii de comparaţie pentru a
fi găsită. Atunci (
k, dacă x = v[k − 1], k ≤ n,
t1 (n) =
n, dacă x nu se află ı̂n şir.
Aşadar, t1 (n) ∈ {1, 2, . . . , n}. Linia 11 se execută o singură dată, doar dacă x se află ı̂n şir. Putem scrie că timpul
de execuţie al acestei operaţii este
(
1, dacă x se află ı̂n şir
t2 (n) =
0, dacă x nu se află ı̂n şir.

25
Algoritmi şi complexitate Note de curs

Linia 13 se execută de t3 (n) ori, unde


(
k − 1, dacă x se află ı̂n şir
t3 (n) =
n, dacă x nu se află ı̂n şir.

Aşadar, mulţimea valorilor posibile ale lui t3 (n) este {0, 1, 2, . . . , n}. Sumând timpii de execuţie pentru fiecare
operaţie, obţinem T (n) = 4t1 (n) + t2 (n) + t3 (n) + 5.

Operaţia Cost Număr de repetări Cost total


7 1 1 1
8 1 1 1
9 3 t1 (n) + 1 3t1 (n) + 3
10 1 t1 (n) t1 (n)
11 1 t2 (n) t2 (n)
13 1 t3 (n) t3 (n)
T (n) = 4t1 (n) + t2 (n) + t3 (n) + 5

În cazul cel mai favorabil, valoarea căutată este pe prima poziţie ı̂n şir şi deci k = 1. Astfel, t1 (n) = 1, t2 (n) = 1,
t3 (n) = 0 şi T (n) = 10. În cazul cel mai defavorabil, valoarea x nu se află ı̂n tablou. Atunci t1 (n) = n, t2 (n) = 0,
t3 (n) = n, iar T (n) = 5n + 5 = 5(n + 1). Putem scrie 10 ≤ T (n) ≤ 5(n + 1), deci limita inferioară este constantă,
iar cea superioară depinde liniar de dimensiunea problemei.
Dacă vom considera comparaţia din linia 10 operaţia de bază a algoritmului, atunci T (n) = t1 (n) şi estimăm
timpul de execuţie prin T (n) = 1, ı̂n cel mai favorabil caz şi prin T (n) = n, ı̂n cel mai defavorabil caz. Astfel,
1 ≤ T (n) ≤ n.
Exemplul 1.20. Analizăm ı̂n continuare o altă variantă a algoritmului de căutare secvenţială.
1 algorithm Algoritmul 2 de căutare secvenţială
2 begin
3 integer n , i
4 bool gasit
5 real v [0.. n -1] , x
6 read n , v , x
7 i = 0
8 while v [ i ] != x && i < n - 1 do
9 i = i + 1
10 end while
11 if v [ i ] == x then
12 gasit = true
13 else
14 gasit = false
15 end if
16 write gasit
17 end
18

Linia 7 are costul 1, linia 8 are costul 3 şi se repetă de t(n) + 1 ori, iar pasul 9 are costul 1 şi se repetă de t(n) ori,
cu (
k − 1, dacă x = v[k − 1], k ≤ n,
t(n) =
n − 1, dacă x nu se află ı̂n şir.
Valorile posibile ale lui t(n) sunt {0, 1, . . . , n−1}, t(n) reprezentând numărul de iteraţii ale ciclului while. Structura
alternativă (liniile 11-15) are costul 2 (comparaţia şi execuţia ramurei corespunzătoare). Sumând, obţinem T (n) =
4t(n) + 6.

26
Algoritmi şi complexitate Note de curs

Operaţia Cost Număr de repetări Cost total


7 1 1 1
8 3 t(n) + 1 3t(n) + 3
9 1 t(n) t(n)
11-15 2 1 2
T (n) = 4t(n) + 6

În cazul cel mai favorabil, t(n) = 0, T (n) = 6. În cazul cel mai defavorabil, valoarea x nu se află ı̂n tablou. Atunci
t(n) = n − 1, iar T (n) = 4n + 2 şi deci 6 ≤ T (n) ≤ 4n + 2.
Dacă se consideră operaţii de bază doar comparaţiile din liniile 8 şi 11 (cele ı̂ntre valoarea căutată x şi elementele
vectorului), atunci T (n) = 2, ı̂n cel mai favorabil caz şi T (n) = n+1, ı̂n cel mai defavorabil caz, deci 2 ≤ T (n) ≤ n+1.
Exemplul 1.21. Folosind tehnica fanionului, obţinem o altă variantă a algoritmului. Tehnica presupune adăugarea
elementului căutat x la sfârşitul vectorului v, deci v[n] = x. Prin execuţia structurii repetitive while se obţine
indexul elementului x ı̂n şir. Dacă valoarea obţinută este n, atunci x nu se află ı̂n şir.
1 algorithm Algoritmul 3 de căutare secvenţială
2 begin
3 integer n , i
4 bool gasit
5 real v [0.. n ] , x
6 read n , v , x
7 v[n] = x
8 i = 0
9 while v [ i ] != x do
10 i = i + 1
11 end while
12 if i == n then
13 gasit = false
14 else
15 gasit = true
16 end if
17 write gasit
18 end

Liniile 7 şi 8 au fiecare costul de execuţie egal cu 1, linia 9 are costul 1 şi se repetă de t(n) + 1 ori, iar linia 10 are
costul 1 şi se repetă de t(n) ori, cu t(n) = k − 1, unde x = v[k − 1], cu k ≤ n + 1.
Structura decizională (liniile 12-16) are costul de execuţie 2 (comparaţia şi execuţia ramurei corespunzătoare).
Sumând, obţinem T (n) = 2t(n) + 5.
Operaţia Cost Număr de repetări Cost total
7 1 1 1
8 1 1 1
9 1 t(n) + 1 t(n) + 1
10 1 t(n) t(n)
12-16 2 1 2
T (n) = 2t(n) + 5

În cazul cel mai favorabil, t(n) = 0, T (n) = 5. În cazul cel mai defavorabil, valoarea x nu se află ı̂n tablou şi atunci
t(n) = n, iar T (n) = 2n + 5. Putem scrie deci 5 ≤ T (n) ≤ 2n + 5.
Coonsiderând ca operaţii de bază comparaţia din linia 9, obţinem T (n) = 1, ı̂n cel mai favorabil caz şi T (n) = n+1,
ı̂n cel mai defavorabil caz, deci 1 ≤ T (n) ≤ n + 1.

1.4.3 Analiza complexităţii unui algoritm ı̂n cazul mediu


Analiza eficienţei unui algoritm ı̂n cazurile extreme poate să nu ofere informaţii importante dacă cel mai favorabil
caz şi cel mai defavorabil caz (sau cazurile asemănătoare acestora) apar foarte rar. În asemenea situaţii se analizează

27
Algoritmi şi complexitate Note de curs

timpul de execuţie al algoritmului ı̂n cazul mediu, adică se analizează eficienţa algoritmului pentru instanţe aleatoare
ale problemei.
Analiza unui algoritm ı̂n cazul mediu necesită cunoştinţe din domeniile probabilităţilor şi statisticii, ı̂ntrucât este
necesară determinarea repartiţiei probabilistice (distribuţiei de probabilitate) a datelor de intrare. Se realizează
mai ı̂ntâi gruparea tuturor instanţelor posibile ı̂n clase, ı̂n funcţie de timpul de execuţie necesar acestora. Astfel,
pentru instanţele care fac parte din aceeaşi clasă se presupune că algoritmul efectuează acelaşi număr de operaţii.
Dacă vom nota cu N numărul claselor ı̂n care au fost grupate instanţele (N reprezintă numărul cazurilor posibile),
cu Tk (n) timpul de execuţie corespunzător cazului k şi pk probabilitatea de apariţie a cazului k, k = 1, 2, . . . , N ,
atunci timpul mediu de execuţie va fi
N
X
Tmediu (n) = T1 (n)p1 + T2 (n)p2 + · · · + TN (n)pN = Tk (n)pk .
k=1

1
Dacă toate cazurile au aceeaşi probabilitate de apariţie (datele sunt repartizate uniform), atunci pk = , k =
N
N
1 X
1, 2, . . . , N şi Tmediu (n) = Tk (n).
N
k=1
De cele mai multe ori vom presupune ca toate datele de intrare având o dimensiune dată sunt la fel de probabile.
În practică, aceasta presupunere poate fi ı̂nsă falsă.
Să calculăm timpul mediu de execuţie pentru algoritmii de căutare secvenţială prezentaţi anterior. Presupunem,
pentru simplitate, că elementele tabloului v sunt distincte. De asemenea, vom considera drept operaţie de bază,
comparaţia unui element din şir cu x.
Exemplul 1.22. Algoritmul 1 de căutare secvenţială.
Considerăm mai ı̂ntâi cazul ı̂n care elementul căutat x se află ı̂n tablou. După cum am văzut, avem nevoie de k
operaţii de bază pentru a-l localiza pe x, dacă acesta se află pe poziţia k. Deci valorile posibile ale timpilor de
execuţie sunt: 1, 2, . . . , k, . . . , n. De asemenea, presupunem că x se găseşte cu aceeaşi probabilitate pe oricare dintre
cele n poziţii, deci probabilitatea ca x să se găsească pe poziţia k ı̂n şir este
1
P (x = v[k − 1]) = ,
n
k = 1, 2, . . . , n. Atunci timpul mediu de execuţie este
n
1X 1 n(n + 1) n+1
Tmediu (n) = k= · = .
n n 2 2
k=1

În acest caz, putem trage concluzia că, ı̂n medie, aproximativ jumătate din şir va fi parcurs, dacă avem certitudinea
că valoarea căutată este ı̂n şir.
În cele ce urmează luăm ı̂n considerare cazul ı̂n care x s-ar putea să nu fie ı̂n şir. Vom nota cu p probabilitatea ca
x să fie ı̂n şir. Dacă elementul căutat se află ı̂n şir, atunci el se va afla pe una din poziţiile de la 1, 2, . . . , k, . . . , n.
Probabilitatea ca x să se afle pe poziţia k ı̂n şir este
1 p
P (x = v[k − 1]) = p · = ,
n n
k = 1, 2, . . . , n. Probabilitatea ca x să nu se fie ı̂n şir este 1 − p. Astfel timpul mediu va fi
n
pX p(n + 1)  p p
Tmediu (n) = k + (1 − p)n = + (1 − p)n = n 1 − + .
n 2 2 2
k=1

n 1
Să observăm că pentru p = 1, obţinem Tmediu (n) = + ; acest rezultat corespunde cazului anterior, ı̂n care x se
2 2
afla cu siguranţă ı̂n şir.
1
Pentru p = , adică evenimentele ca x să fie ı̂n şir şi x să nu fie ı̂n şir au aceeaşi probabilitate, obţinem Tmediu (n) =
2
3n + 1
.
4

28
Algoritmi şi complexitate Note de curs

Exemplul 1.23. Algoritmul 2 de căutare secvenţială.


Dacă elementul căutat x se află ı̂n tablou, am văzut că avem nevoie de k + 1 operaţii de bază pentru a-l localiza, ı̂n
ipoteza că acesta se află pe poziţia k. Deci valorile posibile ale timpilor de execuţie sunt: 2, 3, . . . , k, k + 1, . . . , n + 1.
De asemenea, presupunem că x se găseşte cu aceeaşi probabilitate pe oricare dintre cele n poziţii, deci probabilitatea
1
ca x să se găsească pe poziţia k ı̂n şir este , k = 1, 2, . . . , n. Atunci timpul mediu de execuţie este
n
n+1
1X n+3
Tmediu (n) = k= .
n 2
k=2

În acest caz, se observă că timpul mediu de execuţie nu creşte semnificativ faţă de cel obţinut, ı̂n aceleaşi ipoteze,
pentru Algoritmul 1.
Luăm ı̂n considerare cazul ı̂n care x s-ar putea să nu fie ı̂n şir. Cu notaţiile de mai sus şi ţinând cont că probabilitatea
1 p
ca x să se afle pe poziţia k ı̂n şir este tot P (x = v[k − 1]) = p · = , k = 1, 2, . . . , n, timpul mediu va fi
n n
n+1
pX p(n + 3)  p p
Tmediu (n) = k + (1 − p)(n + 1) = + (1 − p)(n + 1) = n 1 − + + 1,
n 2 2 2
k=2

din nou o valoare nesemnificativ mai mare faţă de cea obţinută pentru Algoritmul 1.
Exemplul 1.24. Algoritmul 3 de căutare secvenţială.
Dacă x se află pe poziţia k ı̂n şir, avem nevoie de k operaţii de bază (comparaţii ı̂ntre x şi elementele tabloului) pentru
a-l localiza. Deci valorile posibile ale timpilor de execuţie sunt: 1, 2, . . . , k, . . . , n + 1. De asemenea, presupunem că
x se găseşte cu aceeaşi probabilitate pe oricare dintre cele n + 1 poziţii, deci probabilitatea ca x să se găsească pe
poziţia k ı̂n sir este
1
P (x = v[k − 1]) = ,
n+1
k = 1, 2, . . . , n + 1. Atunci timpul mediu de execuţie este
n+1
1 X n+2
Tmediu (n) = k= .
n+1 2
k=1

Şi ı̂n acest caz, putem trage concluzia că, ı̂n medie, aproximativ jumătate din şir va fi parcurs.
Timpii medii de execuţie pentru cele trei variante ale algoritmului de căutare secvenţială nu sunt semnificativi
diferiţi, depinzând liniar de dimensiunea problemei, n.

Putem concluziona că etapele pe care le vom parcurge ı̂n analiza eficienţei algoritmilor sunt:
ˆ identificarea dimensiunii problemei;
ˆ stabilirea operaţiilor luate ı̂n calcul;
ˆ estimarea timpului de execuţie prin determinarea numărului de repetări ale operaţiilor stabilite.
ˆ dacă timpul de execuţie depinde de caracteristicile datelor de intare, atunci se vor analiza cel mai defavorabil
caz, cel mai favorabil caz şi cazul mediu, obţinându-se o margine superioară a timpului de execuţie, una
inferioară şi respectiv timpul mediu de execuţie.
În practică, există situaţii ı̂n care estimarea timpului mediu de execuţie este o sarcină dificilă, ca atare se optează
pentru analiza doar a cazurilor extreme, ı̂n special a celui mai defavorabil.

29
Algoritmi şi complexitate Note de curs

1.5 Ordinul de creştere a timpului de execuţie


Pentru analiza eficienţei unui algoritm este important să se descrie modul ı̂n care creşte timpul de execuţie odată
cu creşterea dimensiunii problemei. Atunci când volumul datelor de intrare devine din ce ı̂n ce mai mare, termenul
dominant din expresia timpului de execuţie este cel care ilustrează cel mai bine comportamentul algoritmului. Dacă
dimensiunea problemei este n, termenul dominant este termenul din expresia lui T (n) care creşte semnificativ mai
mult faţă de ceilalţi termeni, atunci când n creşte. În situaţia unui volum mare al datelor de intrare, constantele
multiplicative şi termenii de ordin inferior din expresia timpului de execuţie pot fi neglijaţi. Astfel, ordinul de
creştere (rata de creştere) a timpului de execuţie al unui algoritm reprezintă modul ı̂n care termenul dominant din
expresia acestuia creşte ı̂n raport cu volumul datelor de intrare.
Atunci când se analizează eficienţa unui algoritm de rezolvare a unei probleme de dimensiune suficient de mare ı̂ncât
numai ordinul de creştere a timpului de execuţie este relevant, spunem că studiem eficienţa asimptotică. Aşadar,
suntem interesaţi de cum creşte termenul dominant al timpului de execuţie atunci când dimensiunea problemei
creşte nemărginit (→ ∞). Spunem că, pentru o problemă de dimensiune mare, algoritmul cu ordinul de creştere
a timpului de execuţie cel mai mic este cel mai eficient. Nu acelaşi lucru ı̂l putem spune despre problemele de
dimensiune mică; pentru astfel de probleme este greu de decis care algoritm este mai eficient.

1.5.1 Notaţii asimptotice


În scopul facilitării analizei eficienţei asimptotice a algoritmilor, introducem câteva simboluri : Θ, O, o, Ω, ω, ∼,
numite notaţii asimptotice.
Considerăm două funcţii f, g : N → R+ , depinzând de n (dimensiunea problemei). Ne interesează să comparăm
ordinele de creştere a funcţiilor f şi g, atunci când n → ∞. Motivul pentru care considerăm codomeniul R+ este că
f (n) sau g(n) reprezintă numărul de paşi efectuaţi de doi algoritmi ce rezolvă o aceeaşi problemă pentru o instanţă
de dimensiune n (f (n) şi g(n) sunt timpi de execuţie).
Definiţia 1.2. (Notaţia Θ) f (n) ∈ Θ(g(n)), pentru n → ∞, dacă există constantele c1 , c2 > 0 şi n0 ∈ N astfel
ı̂ncât c1 g(n) ≤ f (n) ≤ c2 g(n), pentru orice n ≥ n0 .
f (n) f (n)
Altfel spus, f (n) şi g(n) au acelaşi ordin de creştere pentru n mare. Dacă există lim , atunci 0 < lim <
n→∞ g(n) n→∞ g(n)
+∞. De exemplu,

(a) 2n + 3 ∈ Θ(n);
Pentru demonstraţie putem folosi definiţia (există c1 = 1, c2 = 5 astfel ı̂ncât c1 n ≤ 2n + 3 ≤ c2 n, pentru orice
2n + 3
n ≥ n0 = 1) sau că lim = 2 > 0.
n→∞ n
(b) (n + 1)2 ∈ Θ(n2 );
Punem ı̂n evidenţă c1 = 0.5, c2 = 4 astfel ı̂ncât c1 n2 ≤ (n + 1)2 ≤ c2 n2 , pentru orice n ≥ n0 = 1. Totodată,
(n + 1)2
se poate utiliza faptul că lim = 1 > 0.
n→∞ n2
n2 +3n+1
n2 + 3n + 1
 
1 5n3 +7n+2 1
(c) 3
∈Θ ( lim 1 = > 0);
5n + 7n + 2 n n→∞
n
5
p √
p √ 2+ n
(d) 2 + n ∈ Θ(n1/4 ) ( lim √ = 1 > 0);
n→∞ 4
n
(e) n2 + 7n ln n + 5 ∈ Θ(n2 )
Pentru c1 = 1, c2 = 4, c1 n2 ≤ n2 + 7n ln n + 5 ≤ c2 n2 , pentru orice n ≥ n0 = 4 (vezi Figura 1.1, unde
n2 + 7n ln n + 5
f (n) = n2 + 7n ln n + 5, iar g(n) = n2 ). În acelaşi timp, lim = 1 > 0.
n→∞ n2

30
Algoritmi şi complexitate Note de curs

f(n) (g(n))
150
f(n)
c1g(n), c 1 = 1
c2g(n), c 2 = 4

100

50

0
0 1 2 3 4 5 6
n

Figura 1.1: Notaţia Θ descrie mărginirea unei funcţii până la factori constanţi. Scriem f (n) ∈ Θ(g(n)) dacă există
constantele pozitive n0 , c1 şi c2 astfel ı̂ncât la dreapta lui n0 , valoarea lui f (n) se află ı̂ntotdeauna ı̂ntre c1 g(n) şi
c2 g(n), inclusiv.

Propoziţia 1.1. Sunt ı̂ndeplinite următoarele relaţii relative la notaţia “Θ”, pentru n → ∞:

(i) f (n) ∈ Θ(f (n)) (reflexivitate).


(ii) Dacă f (n) ∈ Θ(g(n)), atunci g(n) ∈ Θ(f (n)) (simetrie).
(iii) Dacă f (n) ∈ Θ(g(n)) şi g(n) ∈ Θ(h(n)), atunci f (n) ∈ Θ(h(n)) (tranzitivitate).
(iv) Dacă h(n) ∈ Θ(f (n) + g(n)), atunci h(n) ∈ Θ(max{f (n), g(n)}) şi invers.
(v) Dacă f (n) ∈ Θ(cg(n)) (c > 0, constantă), atunci f (n) ∈ Θ(g(n)) şi invers.
(vi) Dacă f (n) ∈ Θ(loga g(n)), atunci f (n) ∈ Θ(logb g(n)), pentru orice a, b ∈ R∗+ , a 6= 1, b 6= 1 şi invers.
(vii) Dacă T (n) este un polinom de gradul m, T (n) = am nm + am−1 nm−1 + · · · + a1 n + a0 , am > 0, atunci
T (n) ∈ Θ(nm ).
Demonstraţie. Proprietăţile (i), (ii), (iii), (iv) şi (v) rezultă prin aplicarea directă a definiţiei.
logb x
Proprietatea (vi) rezultă din regula de schimbare a bazei unui logaritm (loga x = , a, b, x ∈ R∗+ , a, b 6=
logb a
1) şi definiţie. Această proprietate ne spune că pentru un ordin de creştere logaritmic, baza logaritmului este
neimportantă. În cele ce urmează, vom folosi notaţia log pentru a indica un logaritm generic, fără a specifica baza
acestuia.
Proprietatea (vii) rezultă din faptul că lim Tn(n) m = am , am > 0 şi deci pentru orice ε > 0 există n0 (ε) ∈ N astfel
n→∞
ı̂ncât |T (n)/nm − am | < ε pentru orice n ≥ n0 (ε). Aşadar T (n)/nm este mărginit inferior şi superior de două
constante strict pozitive pentru valori suficient de mari ale lui n.
Proprietăţile (i)–(iii) permit definirea unei relaţii de echivalenţă: f (n) şi g(n) sunt echivalente dacă f (n) ∈ Θ(g(n)).
Clasele de echivalenţă corespunzătoare sunt numite clase de complexitate.
Notaţia Θ se foloseşte ı̂n următoarele cazuri: dacă timpul de execuţie al algoritmului se poate determina explicit
(nu depinde de caracteristicile datelor de intrare) sau dacă timpii de execuţie corespunzători cazurilor extreme au
acelaşi ordin de creştere (ı̂n cazul ı̂n care timpul de execuţie al algoritmului depinde de proprietăţile datelor de
intrare).

31
Algoritmi şi complexitate Note de curs

De exemplu, pentru algoritmul de calcul al sumei primelor n numere naturale, am estimat timpul de execuţie prin
T (n) = 4n + 3. Deci, pentru c1 = 4, c2 = 5, c1 n ≤ 4n + 3 ≤ c2 n, pentru orice n ≥ n0 = 3, ceea ce ı̂nseamnă
că T (n) ∈ Θ(n). Apoi, ı̂n cazul algoritmului de determinare a maximului unui secvenţe de numere naturale, prin
analiza cazurilor extreme am obţinut că 3n ≤ T (n) ≤ 4n − 1. Pentru c1 = 3, c2 = 4, c1 n ≤ T (n) ≤ c2 n, pentru
orice n ≥ n0 = 1, deci T (n) ∈ Θ(n).
Nu acelaşi lucru se poate spune despre algoritmul de căutare secvenţială analizat ı̂n cazurile extreme. Reamintim
că am găsit o limită inferioară constantă (ct) pentru timpul de execuţie şi o limită superioară ce depinde liniar de
dimensiunea secvenţei. În această situaţie, T (n) 6∈ Θ(n), deoarece nu poate fi identificată o constantă c1 > 0 şi un
rang n0 astfel ı̂ncât c1 n ≤ ct pentru orice n ≥ n0 .
Putem extinde definiţia pentru cazul ı̂n care dimensiunea problemei depinde de mai multe valori: f (m, n, p) ∈
Θ(g(m, n, p)) dacă există c1 , c2 > 0 şi m0 , n0 , p0 ∈ N astfel ı̂ncât c1 g(m, n, p) ≤ f (m, n, p) ≤ c2 g(m, n, p), pentru
orice m ≥ m0 , n ≥ n0 , p ≥ p0 . Astfel, pentru algoritmul de ı̂nmulţire a două matrice, T (m, n, p) = 5mnp + 4mp +
4m + 2 ∈ Θ(mnp).
Notaţia Θ delimitează o funcţie asimptotic inferior şi superior. Dacă avem doar o margine asimptotică superioară,
vom utiliza notaţia O.
Definiţia 1.3. (Notaţia O) f (n) ∈ O(g(n)), pentru n → ∞, dacă există constantele c > 0, n0 ∈ N, astfel ı̂ncât
f (n) ≤ cg(n), pentru orice n ≥ n0 .

Astfel, putem spune că ordinul de creştere a lui f (n) este cel mult egal cu cel al lui g(n) pentru n mare (f (n) şi
g(n) pot să aibă aceeaşi rată de creştere sau f (n) poate să aibă rata de creştere mai mică decât cea a lui g(n),
f (n) f (n)
dar cu siguranţă nu va fi mai mare); altfel spus, dacă există lim , atunci 0 ≤ lim < +∞. Considerăm
n→∞ g(n) n→∞ g(n)
câteva exemple:
1 1
(a) ∈ O(1) ( lim = 0 >= 0);
1 + n2 n→∞ 1 + n2

(b) n3 + 5n + 1 ∈ O(n3 ) (n3 este termenul dominant);


Conform definiţiei, n3 +5n+1 ∈ O(n3 ), dacă există constantele pozitive c > 0, n0 astfel ı̂ncât n3 +5n+1 ≤ cn3
5 1
pentru orice n ≥ n0 . Inegalitatea din definiţie este echivalentă cu 1 + 2 + 3 ≤ c. Astfel, condiţia este
n n
satisfăcută pentru n ≥ n0 = 1 şi c ≥ 7 (= 1 + 5 + 1). Pentru valori mai mari ale lui n0 se obţin valori mai
mici pentru c, dar oricum relaţia rămâne adevărată. De altfel, aceeaşi concluzie se poate obţine calculând
n3 + 5n + 1
lim = 1 >= 0.
n→∞ n3
(c) 2n + n0.5 + 0.5n1.25 ∈ O(n1.25 ) (0.5n1.25 este termenul dominant);
2n + n0.5 + 0.5n1.25
lim = 0.5 >= 0.
n→∞ n1.25
(d) n log3 n + n log2 n ∈ O(n ln n);
Relaţia este adevărată dacă există constantele pozitive c, n0 astfel ı̂ncât n log3 n + n log2 n ≤ c n ln n pentru
n log3 n n log2 n
orice n ≥ n0 . Inegalitatea din definiţie este echivalentă cu + ≤ c. Astfel, condiţia este
n ln n n ln n
1 1
satisfăcută pentru n ≥ n0 = 1 şi c ≥ + .
ln 3 ln 2
n log3 n + n log2 n ln 6
lim = >= 0.
n→∞ n ln n ln 2 ln 3
(e) n2 log2 n + n(log2 n)2 ∈ O(n2 ln n) (a se vedea Figura 1.2, unde f (n) = n2 log2 n + n(log2 n)2 , iar g(n) =
n2 ln n);
n2 log2 n + n(log2 n)2 1
lim 2
= >= 0.
n→∞ n ln n ln 2
100n log3 n + n3 + 100n
(f) 100n log3 n + n3 + 100n ∈ O(n3 ) ( lim = 1 >= 0).
n→∞ n3

32
Algoritmi şi complexitate Note de curs

f(n) O(g(n))
30
f(n)
cg(n), c= 2
25

20

15

10

-5
0 0.5 1 1.5 2 2.5 3
n

Figura 1.2: Notaţia O descrie mărginirea superioară a unei funcţii până la un factor constant. Scriem f (n) ∈ O(g(n))
dacă există constantele pozitive n0 , c, astfel ı̂ncât la dreapta lui n0 , valoarea lui f (n) este ı̂ntotdeauna mai mică
sau egală cu cg(n).

Propoziţia 1.2. Sunt ı̂ndeplinite următoarele relaţii relative la notaţia “O”, pentru n → ∞:
(i) f (n) ∈ O(f (n)) (reflexivitate).
(ii) Dacă f (n) ∈ O(g(n)) şi g(n) ∈ O(h(n)), atunci f (n) ∈ O(h(n)) (tranzitivitate).
(iii) Dacă h(n) ∈ O(f (n) + g(n)), atunci h(n) ∈ O(max{f (n), g(n)}) şi invers.
(iv) Dacă T (n) este un polinom de gradul m, T (n) = am nm + am−1 nm−1 + · · · + a1 n + a0 , am > 0 , atunci
T (n) ∈ O(nk ), pentru orice k ≥ m.
(v) Dacă f (n) ∈ Θ(g(n)), atunci f (n) ∈ O(g(n)).
Notaţia O se foloseşte atunci când avem informaţii despre timpul de execuţie al unui algoritm ı̂n cazul cel mai defa-
vorabil. Ne reamintim că, ı̂n acest caz, se obţine o margine superioară pentru timpul de execuţie; de regulă trebuie
identificată cea mai mică astfel de margine superioară. De exemplu, pentru algoritmul de căutare secvenţială, am
observat că ı̂n cazul cel mai defavorabil timpul de execuţie depinde liniar de n, deci putem scrie că T (n) ∈ O(n).

Definiţia 1.4. (Notaţia o) f (n) ∈ o(g(n)), pentru n → ∞, dacă pentru orice constantă c > 0, există un rang
n0 ∈ N, astfel ı̂ncât f (n) < cg(n), pentru orice n ≥ n0 .
f (n)
Putem spune deci că f (n) ∈ o(g(n)), pentru n → ∞, dacă există lim şi este egală cu 0. Cu alte cuvinte,
g(n)
n→∞
f (n) creşte mai ı̂ncet decât g(n), pentru n mare. Dăm, ı̂n continuare, câteva exemple:
n2
(a) n2 ∈ o(n5 ) ( lim
= 0);
n→∞ n5

√ 7 n
(b) 7 n ∈ o(n/2) ( lim = 0);
n→∞ n/2

n5
(c) n5 ∈ o(3.5n ) ( lim = 0);
n→∞ 3.5n
ln(n + 1)
(d) ln(n + 1) ∈ o(n2 ) ( lim = 0);
n→∞ n2

33
Algoritmi şi complexitate Note de curs

n2 ln(n)
(e) n2 ln(n) ∈ o(n2.03 ) = 0);
( lim
n→∞ n2.03

(f) n2 + 7n + 10 6∈ o(n2 ), dar n2 + 7n + 10 ∈ O(n2 ) (a se vedea Figura 1.3, unde f (n) = n2 + 7n + 10, iar
g(n) = n2 );
n2 + 7n + 10
De altfel, lim = 1.
n→∞ n2

f(n) O(g(n)) f(n) nu este de ordinul o(g(n))


300 12000
f(n) f(n)
cg(n), c = 3 cg(n), c = 1
250 10000

200 8000

150 6000

100 4000

50 2000

0 0
0 1 2 3 4 5 6 7 8 9 10 0 10 20 30 40 50 60 70 80 90 100
n n

Figura 1.3: o vs. O

Propoziţia 1.3. Dacă f (n) ∈ o(g(n)) şi g(n) ∈ o(h(n)), atunci f (n) ∈ o(h(n)) (tranzitivitate).
În practică, notaţia o este mai puţin folosită decât O. Notaţia “Θ” este mai precisă decât “O” sau “o” deoarece,
f (n)
dacă ştim că f (n) ∈ Θ(g(n)), pentru n → ∞, atunci ştim că este mărginită inferior şi superior de două
g(n)
constante strict pozitive pentru valori suficient de mari ale lui n. Putem atunci determina ordinul de creştere a lui
f (n), acesta fiind ordinul de creştere a lui g(n).

Notaţia Ω oferă o margine asimptotică inferioară.


Definiţia 1.5. (Notaţia Ω) f (n) ∈ Ω(g(n)), pentru n → ∞, dacă există constantele c > 0, n0 ∈ N, astfel ı̂ncât
cg(n) ≤ f (n), pentru orice n ≥ n0 .
f (n)
Putem spune că f (n) creşte asimptotic cel puţin la fel de repede ca g(n) sau că, dacă lim există, atunci
n→∞ g(n)
lim f (n) > 0, dar nu este neapărat finită. De exemplu,
n→∞ g(n)

(a) 6 ∈ Ω(1) (c = 6, n0 = 1);


6
lim = 6 > 0.
n→∞ 1
(b) 3n + 3 ∈ Ω(n) (c = 3, n0 = 1);
3n + 3
lim = 3 > 0.
n→∞ n
(c) 2n ∈ Ω(2n2 − n + 1) (c = 1, n0 = 7) (a se vedea Figura 1.4, unde f (n) = 2n , iar g(n) = 2n2 − n + 1);
2n
De altfel, lim = +∞.
n→∞ 2n2 − n + 1

34
Algoritmi şi complexitate Note de curs

f(n) (g(n))
600
f(n)
cg(n), c = 1
500

400

300

200

100

0
0 1 2 3 4 5 6 7 8 9
n

Figura 1.4: Notaţia Ω descrie mărginirea inferioară a unei funcţii până la un factor constant. Scriem f (n) ∈ Ω(g(n))
dacă există constantele pozitive n0 , c, astfel ı̂ncât la dreapta lui n0 , valoarea lui f (n) este ı̂ntotdeauna mai mare
sau egală cu cg(n).

Propoziţia 1.4. Sunt ı̂ndeplinite următoarele relaţii relative la notaţia “Ω”, pentru n → ∞:
(i) f (n) ∈ Ω(f (n)) (reflexivitate).
(ii) Dacă f (n) ∈ Ω(g(n)) şi g(n) ∈ Ω(h(n)), atunci f (n) ∈ Ω(h(n)) (tranzitivitate).
(iii) Dacă h(n) ∈ Ω(f (n) + g(n)), atunci h(n) ∈ Ω(max{f (n), g(n)}) şi invers.
(iv) Dacă T (n) este un polinom de gradul m, T (n) = am nm + am−1 nm−1 + · · · + a1 n + a0 , am > 0 , atunci
T (n) ∈ Ω(nk ), pentru orice k ≤ m.
(v) Dacă f (n) ∈ Θ(g(n)), atunci f (n) ∈ Ω(g(n)).
(vi) f (n) ∈ O(g(n)) dacă şi numai dacă g(n) ∈ Ω(f (n)).
Teorema 1.1. f (n) ∈ Θ(g(n)) dacă şi numai dacă f (n) ∈ O(g(n)) şi f (n) ∈ Ω(g(n)).
Notaţia Ω se foloseşte atunci când avem informaţii despre timpul de execuţie al unui algoritm ı̂n cazul cel mai
favorabil, obţinându-se o margine inferioară a acestuia. Astfel, pentru algoritmul de căutare secvenţială, T (n) ∈
Ω(1).
Definiţia 1.6. (Notaţia ω) f (n) ∈ ω(g(n)), pentru n → ∞, dacă pentru orice constantă c > 0, există un rang
n0 ∈ N, astfel ı̂ncât cg(n) < f (n), pentru orice n ≥ n0 .
f (n)
Echivalent, f (n) ∈ ω(g(n)), pentru n → ∞, dacă există lim şi este egală cu +∞. Deci, f (n) are un ordin de
n→∞ g(n)
creştere mai mare decât g(n), pentru n → ∞.

De exemplu, 7n2 − 3 ∈ ω(n), 7n − 3 6∈ ω(n), dar 7n − 3 ∈ Ω(n) (a se vedea Figura 2.5, unde f (n) = 7n − 3, iar
7n2 − 3 7n − 3
g(n) = n). De asemenea, putem calcula lim = +∞ şi lim = 1.
n→∞ n n→∞ n

35
Algoritmi şi complexitate Note de curs

f(n) (g(n)) f(n) nu este de ordinul (g(n))


35 800
f(n) f(n)
30 cg(n), c = 3 700 cg(n), c = 8

600
25

500
20
400
15
300
10
200

5
100

0 0

-5 -100
0 0.5 1 1.5 2 2.5 3 3.5 4 4.5 5 0 10 20 30 40 50 60 70 80 90 100
n n

Figura 1.5: ω vs. Ω

Propoziţia 1.5. Sunt ı̂ndeplinite următoarele relaţii relative la notaţia “ω”, pentru n → ∞:
(i) Dacă f (n) ∈ ω(g(n)) şi g(n) ∈ ω(h(n)), atunci f (n) ∈ ω(h(n)) (tranzitivitate).
(ii) f (n) ∈ o(g(n)) dacă şi numai dacă g(n) ∈ ω(f (n)).

Cea mai precisă dintre notaţiile asimptotice este ∼.


Definiţia 1.7. (Notaţia ∼) Spunem că f (n) şi g(n) sunt asimptotic echivalente şi scriem f (n) ∼ g(n), pentru
f (n)
n → ∞, dacă există lim şi este egală cu 1.
n→∞ g(n)

f (n)
Aşadar, nu numai că f (n) şi g(n) au acelaşi ordin de creştere, dar se apropie de 1, când n → ∞.
g(n)
De exemplu, n2 + n ∼ n2 , (3n + 1)4 ∼ 81n4 , (2n3 + 5n + 7)/(n2 + 4) ∼ 2n. Se observă importanţa alegerii corecte
a constantelor multiplicative, atunci când se foloseşte această notaţie asimptotică. De exemplu nu este adevărat
2n2
că 2n2 ∼ n2 , deşi 2n2 ∈ Θ(n2 ) ( lim 2 = 2).
n→∞ n

1.5.2 Clasificarea algoritmilor după ordinul de creştere a timpului de


execuţie
Dacă luăm ı̂n considerare ordinul de creştere a timpului de execuţie ı̂n cel mai defavorabil caz, algoritmii pot fi de
complexitate:

ˆ constantă, dacă T (n) ∈ O(1);


ˆ logaritmică, dacă T (n) ∈ O(log n);
ˆ polilogaritmică, dacă T (n) ∈ O((log n)k );
ˆ liniară, dacă T (n) ∈ O(n);
ˆ cvasiliniară, dacă T (n) ∈ O(n log n);
ˆ pătratică, dacă T (n) ∈ O(n2 )
ˆ cubică, dacă T (n) ∈ O(n3 )
ˆ exponenţială, dacă T (n) ∈ O(2n )
ˆ factorială, dacă T (n) ∈ O(n!)

36
Algoritmi şi complexitate Note de curs

O clasă importantă de algoritmi este clasa algoritmilor polinomiali, pentru care timpul de execuţie este T (n) ∈
O(nk ); aceşti algoritmi pot fi folosiţi pentru probleme de dimensiune mare (pentru k nu foarte mare). Algoritmii
de complexitate exponenţială nu pot fi utilizaţi, ı̂n general, decât pentru probleme de dimensiune relativ mică.
În analiza complexităţii asimptotice a unui algoritm, vom căuta să aproximăm cât mai bine ordinul de creştere a
timpului de execuţie. De exemplu sunt adevărate ambele afirmaţii: dacă T (n) = 3n, atunci T (n) ∈ O(n), dar şi
T (n) ∈ O(n2 ), dar prima afirmaţie oferă o aproximare mai bună.

Putem concluziona că etapele de parcurs ı̂n analiza complexităţii unui algoritm sunt:

ˆ identificarea dimensiunii problemei;


ˆ stabilirea operaţiilor ce vor fi luate ı̂n calcul;
ˆ estimarea timpului de execuţie prin determinarea numărului de repetări ale operaţiilor considerate;
ˆ dacă timpul de execuţie depinde de caracteristicile datelor de intrare, atunci se vor analiza cel mai defavorabil
caz, cel mai favorabil caz şi cazul mediu, obţinându-se o margine superioară a timpului de execuţie, una
inferioară şi respectiv timpul mediu de execuţie;
ˆ se determină clasa de complexitate din care face parte algoritmul.

Exemplul 1.25. Considerăm un polinom de grad n, P (X) = an X n + an−1 X n−1 + · · · + a1 X + a0 , ai ∈ R,


i = 0, 1, . . . , n, an 6= 0. Descrieţi un algoritm pentru calculul valorii polinomului P (X) pentru X = x, cu x ∈ R.
Vom descrie trei algoritmi de rezolvare a problemei pe care ı̂i vom compara din punctul de vedere al eficienţei-timp.
Xn
Dimensiunea problemei depinde de n. Întrucât rezultatul aşteptat este P (x) = ai xi , acesta poate fi obţinut
i=0

(1) direct, calculând fiecare termen al sumei;


(2) folosind schema lui Horner.

(1) Dacă fiecare putere este calculată separat prin ı̂nmulţiri repetate, obţinem următorul algoritm:
1 algorithm PolyVal1
2 begin
3 integer n , i , j
4 real x , a [0.. n ] , produs , suma
5 read n , x , a Operaţia Cost Număr de repetări Cost total
6 suma = a [0] 6 1 1 1
7 i = 1 7 1 1 1
8 while i <= n do 8 1 n+1 n+1
9 produs = 1 9 1 n n
10 j = 1 10 1 n n
11 while j <= i do 11 1 n(n + 3)/2 n(n + 3)/2
12 produs = produs * x 12 2 n(n + 1)/2 n(n + 1)
13 j = j + 1 13 1 n(n + 1)/2 n(n + 1)/2
14 end while 15 3 n 3n
15 suma = suma + a [ i ] * produs 16 1 n n
16 i = i + 1 T1 (n) = 2n2 + 10n + 3
17 end while
18 write suma
19 end
n+1
X n(n + 3)
Numărul de execuţii ale comparaţiei din linia 11 este: 2 + 3 + · · · + (n + 1) = i= , iar liniile 12 şi 13
i=2
2
n X
i n
X X n(n + 1)
se execută de 1= i= ori.
i=1 j=1 i=1
2

Observăm că T1 (n) ∈ Θ(n2 ), deci algoritmul are o complexitate pătratică.

37
Algoritmi şi complexitate Note de curs

În loc să calculăm la fiecare iterţie a ciclului while exterior, valoarea xk , k = 1, 2, . . . , n, vom folosi faptul că
xk = xk−1 x.
1 algorithm PolyVal2
2 begin
3 integer n , i
4 real x , a [0.. n ] , produs , suma Operaţia Cost Număr de repetări Cost total
5 read n , x , a 6 1 1 1
6 suma = a [0] 7 1 1 1
7 i = 1 8 1 1 1
8 produs = 1 9 1 n+1 n+1
9 while i <= n do 10 2 n 2n
10 produs = produs * x 11 3 n 3n
11 suma = suma + a [ i ] * produs 12 1 n n
12 i = i + 1 T2 (n) = 7n + 4
13 end while
14 write suma
15 end

Pentru acest algoritm, T2 (n) ∈ Θ(n), deci am obţinut un algoritm mai eficient ca precedentul, de complexitate
liniară.
(2) Folosind schema lui Horner, observăm că P (x) = (. . . ((an x + an−1 )x + an−2 )x · · · + a1 )x + a0 .
1 algorithm Horner
2 begin
3 integer n , i
4 real x , a [0.. n ] , suma
5 read n , x , a Operaţia Cost Număr de repetări Cost total
6 i = n 7 1 1 1
7 suma = a [ n ] 8 1 1 1
8 while i > 0 do 9 1 n+1 n+1
9 i = i - 1 10 1 n n
10 suma = suma * x + a [ i ] 11 3 n 3n
11 end while T3 (n) = 5n + 3
12 write suma
13 end

Numărul total de operaţii elementare executate este mai mic decât ı̂n cazul algoritmului PolyVal2, dar acest lucru
nu influenţează comportamentul asimptotic deoarece, şi pentru acest algoritm, T3 (n) ∈ Θ(n) (complexitate liniară).
Pentru acest ultim algoritm s-a demonstrat că este optim, ı̂n sensul că este executat cel mai mic număr de operaţii
pentru a rezolva problema.

Exemplul 1.26. Descrieţi un algoritm care generează toate numerele prime mai mici decât o valoare dată N ,
N ≥ 2.
Dimensiunea problemei este N . Propunem două variante de rezolvare:
(1) parcurgerea tuturor valorilor cuprinse ı̂ntre 2 şi N , căutarea divizorilor acestora şi identificarea numerelor
prime;
(2) algoritmul lui Eratostene.

(1) Subalgoritmul prim(n) returnează true dacă n este prim şi false dacă n nu este √ prim, căutând divizorii
acestuia; se observă că este suficient să se caute divizorii ı̂n secvenţa de numere 2, . . . , [ n].
1 function prim ( integer n )
2 begin
3 integer i

38
Algoritmi şi complexitate Note de curs

4 bool p
5 p = true
6 i = 2
7 while i <= [ sqrt ( n ) ] && p == true do
8 if n % i == 0 then
9 p = false
10 else
11 i = i + 1
12 end if
13 end while
14 return p
15 end

1 algorithm Determinarea numerelor prime mai mici dec^ at N


2 begin
3 integer N , n
4 function prim ( integer ) // declararea subalgoritmului ce urmeaza a fi apelat
5 read N
6 for n = 2 to N do
7 if prim ( n ) == true then
8 write n
9 end if
10 end for
11 end
Este evident că timpul de execuţie al algoritmului depinde de caracteristicile datelor de intrare, astfel că vom
considera drept operaţii de bază, cele prin care se determină posibilii divizori ai lui n, n = 2, 3, . . . , N , adică
comparaţiile
√ efectuate ı̂n acest sens ı̂n subalgoritmul prim. Numărul de comparaţii pentru fiecare n este maxim
[ n] − 1. Sumând după n, obţinem că timpul de execuţie al algoritmului satisface
N N N
X √ X √ X √
T1 (N ) ≤ ([ n] − 1) = [ n] − (N − 1) ≤ n − (N − 1)
n=2 n=2 n=2

N ZN √
X √ √ 2 √ 4 2
Dar n≈ xdx = N N − . Obţinem o margine superioară a timpului de execuţie
n=2
3 3
2

2 √ 4 2
T1 (N ) ≤ N N − N − + 1.
3 3
Deci T1 (N ) ∈ O(N 3/2 ). Numărul total de operaţii ar putea fi micşorat dacă se ţine cont că singurul număr par prim
este 2, iar celelalte numere prime până la N se caută doar printre numerele impare, dar acest lucru nu influenţează
comportamentul asimptotic.
(2) Reluăm algoritmul lui Eratostene: se scriu succesiv toate numerele de la 2 la N şi se selectează numărul 2, acesta
fiind primul număr prim; se marchează apoi toţi multiplii lui 2 ı̂n lista originală de numere, acestea fiind numere
compuse; se selectează primul număr nemarcat, acesta fiind un număr prim, apoi se marchează toţi multiplii săi
ı̂n lista originală de numere. Algoritmul se repetă până la parcurgerea completă a a tuturor numerelor din lista
iniţială.
1 algorithm Algoritmul lui Eratostene
2 begin
3 integer N , i , j
4 bool p [ N +1]
5 read N
6 p [0] = false
7 p [1] = false

39
Algoritmi şi complexitate Note de curs

8 for i = 2 to N do
9 p [ i ] = true
10 end for
11 for i = 2 to [ sqrt ( N ) ] do
12 if p [ i ] == true then
13 j = i * i
14 while j <= N do
15 p [ j ] = false
16 j = j + i
17 end while
18 end if
19 end for
20 for i = 2 to N do
21 if p [ i ] == true then
22 write i
23 end if
24 end for
25 end
Putem √ considera drept operaţie de bază, operaţia de marcare a elementelor vectorului p. Pentru fiecare i, i =
2, . . . , [ N ] se marchează cel mult [(N − i2 )/i + 1] elemente. Timpul de execuţie va satisface
√ √ √ √
[ N] [ N]  [ N]
X N − i2 X N − i2  X 1 [X N]

T2 (N ) ≤ [ + 1] ≤ +1 =N − i + [ N ] − 1.
i=2
i i=2
i i=2
i i=2

√ √
[Z N ]
[ N] √ √
X 1 1 [ N] N
Aproximăm prima sumă prin integrala: ≈ dx = ln ≤ ln ,
i=2
i x 2 2
2

[ N] √ √
X [ N ]([ N ] + 1)
iar i= − 1. Obţinem că
i=2
2
√ √ √ √ √ √ √
N [ N ] [ N ]2 N [ N] N N
T2 (N ) ≤ N ln + − ≤ N ln + ≤ N ln + .
2 2 2 2 2 2 2

Aşadar, T2 (N ) ∈ O(N log N ) sau T2 (N ) ∈ O(N log N ). În concluzie, algoritmul lui Eratostene are un ordin mai
mic de complexitate, dar necesită un tablou suplimentar pentru stocare.
De altfel, putem obţine o aproximare mai bună a ordinului de creştere a timpului de execuţie. O margine superioară √
mai potrivită se obţine prin prisma faptului că pentru fiecare număr prim p, căutat ı̂ntre valorile i = 2, . . . , [ N ],
se marchează cel mult [(N − p2 )/p + 1] elemente. Deci timpul de execuţie va satisface
X N − p2
T2 (N ) ≤ [ + 1].
√ p
p≤[ N ]

X 1
Conform unei teoreme din teoria numerelor (a doua teoremă a lui Mertens), ∈ O(log log N ). După cum
p
p≤N
am văzut această sumă (ı̂nmulţită cu N ) este termenul dominant ı̂n estimarea timpului de execuţie, deci T2 (N ) ∈
O(N log log N ).

40
Algoritmi şi complexitate Note de curs

1.6 Analiza algoritmilor elementari de sortare


Considerăm o secvenţă finită, a0 , a1 , . . . , aN −1 , de N elemente dintr-o mulţime total ordonată. Problema sortării
constă ı̂n rearanjarea componentelor secvenţei ı̂ntr-o ordine specifică, printr-o permutare a acestora. Vom presupune
că elementele secvenţei sunt memorate ı̂n ı̂ntregime ı̂n memoria internă a calculatorului, având deci acces aleator
la acestea. Aşadar, vom analiza doar algoritmi de sortare internă. Pentru sortarea unor date memorate pe un
suport ce nu permite accesul aleator la date, sunt necesari algoritmi speciali, de sortare externă. De asemenea, vom
simplifica problema generală a sortării, presupunând că secvenţa dată este memorată ı̂ntr-un tablou unidimensional.
Sortarea poate fi crescătoare sau descrescătoare. Formal, problema sortării crescătoare poate fi definită astfel:
1 Input : O secvenţă de N numere [a0 , a1 , . . . , aN −1 ].
2 Output : O permutare a secvenţei de intrare , [a00 , a01 , . . . , a0N −1 ], cu a00 ≤ a01 ≤ · · · ≤ a0N −1 .
Există mulţi algoritmi care rezolvă problema sortării interne. Vom introduce ı̂n acest curs câţiva algoritmi elemen-
tari de sortare, bazaţi pe compararea elementelor secvenţei de sortat, ı̂n general cu performanţe scăzute pentru
tablouri de dimensiuni mari (sortarea prin selecţie, sortarea prin inserţie, sortarea prin interschimbarea elementelor
vecine), urmând ca, ulterior, atunci când vom studia tehnica divizării, să descriem şi să analizăm algoritmi eficienţi
de sortare (sortarea rapidă şi sortarea prin interclasare). Vom verifica corectitudinea algoritmilor descrişi şi vom
analiza complexitatea asimptotică a acestora. Precondiţia problemei de sortare este
Pin : N ∈ N∗ (vectorul are măcar un element),
iar postcondiţia este
Pout : a[0] ≤ a[1] ≤ · · · ≤ a[N − 1],
cu precizarea că secvenţa de ieşire reprezintă o permutare a secvenţei de intrare. Eficienţa–timp a unei metode de
sortare este determinată de numărul de comparaţii ı̂ntre elementele tabloului şi de numărul de mutări ale acestora ce
sunt executate ı̂n procesul de sortare. Numărul de elemente ale tabloului (dimensiunea problemei) şi caracteristicile
datelor de intrare sunt determinante ı̂n alegerea uneia sau alteia dintre metodele de sortare. Vom nota cu TC (N ) şi
TM (N ) numărul de comparaţii, respectiv numărul de mutări efectuate pentru a sorta crescător secvenţa de numere.
Pentru algoritmii elementari ce vor fi descrişi ı̂n continuare, cazul cel mai favorabil corespunde şirului deja ordonat
crescător, iar cel mai defavorabil caz corespunde şirului ordonat descrescător.

1.6.1 Sortarea prin selecţie


Această metodă de sortare se bazează pe selectarea, la fiecare pas al algoritmului, a unui anumit element din
secvenţă şi apoi plasarea sa pe poziţia corectă ı̂n şir. Algoritmii de sortare ce au la bază această metodă diferă ı̂n
funcţie de criteriul de selecţie a elementului: algoritmul de sortare prin selecţia minimului/maximului, prin selecţia
sistematică (heapsort) etc. Ne vom ocupa ı̂n această secţiune doar de algoritmii de sortare prin selecţia minimului
şi prin selecţia maximului.

1.6.1.1 Sortarea prin selecţia elementului minim


La primul pas, metoda presupune determinarea elementului minim al tabloului, de fapt a indicelui m pentru care
a[m] ≤ a[i], (∀) i = 0, 1, . . . , N − 1. Elementul minim trebuie plasat pe prima poziţie ı̂n şirul sortat. Aşadar
se schimbă ı̂ntre ele (se interschimbă) elementele a[0] şi a[m]. Se repetă operaţia pentru subşirul de elemente
a[1], a[2], . . . , a[N − 1]. Se caută elementul cel mai mic din acest subşir şi se interschimbă cu a[1] ş.a.m.d., până
când subşirul va conţine un singur element.
1 algorithm Sortarea prin selecţia minimului
2 begin
3 integer i , j , m , N
4 real a [0.. N -1] , aux
5 read N , a
6 for i = 0 to N - 2 do
7 m = i
8 for j = i + 1 to N - 1 do

41
Algoritmi şi complexitate Note de curs

9 if a [ m ] > a [ j ] then
10 m = j
11 end if
12 end for
13 if m != i then
14 aux = a [ m ]
15 a[m] = a[i]
16 a [ i ] = aux
17 end if
18 end for
19 write a
20 end
De exemplu, considerăm şirul a[ ] = {9, 4, 8, 10, 2, 3, 7, 0}, pe care ne propunem să-l sortăm crescător folosind
algoritmul propus mai sus. Să observăm ce se execută la fiecare iteraţie a ciclului exterior for.
iteraţia 1: se determină indicele elementului minim, m = 7 şi se interschimbă elementele a[0] şi a[7]; şirul devine
a[ ] = {0, 4, 8, 10, 2, 3, 7, 9}.
iteraţia 2: se determină indicele elementului minim din subşirul a[1..7], m = 4 şi se interschimbă elementele a[1]
şi a[4]; şirul este acum a[ ] = {0, 2, 8, 10, 4, 3, 7, 9}.
iteraţia 3: se determină indicele elementului minim din subşirul a[2..7], m = 5 şi se interschimbă elementele a[2]
şi a[5]; şirul devine a[ ] = {0, 2, 3, 10, 4, 8, 7, 9}.
iteraţia 4: se determină indicele elementului minim din subşirul a[3..7], m = 4 şi se interschimbă elementele a[3]
şi a[4]; şirul este a[ ] = {0, 2, 3, 4, 10, 8, 7, 9}.
iteraţia 5: se determină indicele elementului minim din subşirul a[4..7], m = 6 şi se interschimbă elementele a[4]
şi a[6]; şirul este acum a[ ] = {0, 2, 3, 4, 7, 8, 10, 9}.
iteraţia6: se determină indicele elementului minim din subşirul a[5..7], m = 5; nu are loc nicio interschimbare,
şirul rămâne acelaşi.
iteraţia 7: se determină indicele elementului minim din subşirul a[6..7], m = 7 şi se interschimbă elementele a[6]
şi a[7]; şirul devine a[ ] = {0, 2, 3, 4, 7, 8, 9, 10}, complet sortat crescător.

Verificarea corectitudinii. Să arătăm că predicatul

I1 (n) : a[0] ≤ a[1] ≤ · · · ≤ a[n − 1] şi a[n − 1] ≤ a[i], (∀) i = n, . . . , N − 1, n ∈ N

este invariant la ciclul for exterior. I1 (0) este adevărat (şirul sortat este vid).
Presupunem că I1 (k) este adevărat şi condiţia de continuare a ciclului exterior este, de asemenea, adevărată. La
iteraţia k + 1 a ciclului for exterior, ı̂n ciclul for interior, se caută minimul elementelor a[k], a[k + 1], . . . , a[N − 1],
care apoi se interschimbă cu elementul a[k]. Un invariant la ciclul for interior este deci

I2 (p) : a[m] = min{a[k], a[k + 1], . . . , a[k + p]}, p ∈ N.

Acesta este adevărat la intrarea ı̂n buclă (I2 (0) este adevărat, conform iniţializării din linia 7 a algoritmului). Dacă
I2 (l) este adevărat şi condiţia de continuare din ciclul for interior este adevărată, atunci I2 (l + 1) este adevărat.
Dacă jN −k−1 = N , după N − k − 1 iteraţii ale ciclului interior,

I2 (N − k − 1) : a[m] = min{a[k], . . . , a[N − 1]}.

Apoi are loc interschimbarea elementului a[k] cu a[m]. Aşadar, dacă I1 (k) este adevărat şi condiţia de continuare
din ciclul for exterior este adevărată, atunci I1 (k + 1) este adevărat. Dacă iN −1 = N − 1, după N − 1 iteraţii ale
ciclului exterior for, invariantul conduce la postcondiţie, deoarce

I1 (N − 1) : a[0] ≤ a[1] ≤ · · · ≤ a[N − 2] şi a[N − 2] ≤ a[N − 1].

Pentru fiecare dintre cele două cicluri for, variabila contor de iteraţie este incrementată la fiecare iteraţie, deci
pot fi identificate funcţii de terminare corespunzătoare: pentru ciclul exterior, considerăm funcţia de terminare
t1 (k) = N − 1 − ik , iar pentru ciclul interior, t2 (k) = N − jk , k ∈ N. Astfel, algoritmul estel total corect.

42
Algoritmi şi complexitate Note de curs

Analiza complexităţii. Numărul de comparaţii (din linia 9) nu depinde de distribuţia iniţială a elementelor,
fiind egal cu
N (N − 1)
TC (N ) = (N − 1) + (N − 2) + · · · + 1 = .
2
În schimb, numărul mutărilor depinde de proprietăţile secvenţei iniţiale de numere, adică de repartizarea iniţială
a elementelor acesteia. Astfel, vom analiza cazurile extreme. În cazul cel mai favorabil, numărul interschimbărilor
este 0, deci TM (N ) = 0. În cazul cel mai defavorabil, la fiecare iteraţie a ciclului for exterior se efectuează o
interschimbare (= 3 atribuiri: liniile 14, 15, 16), deci TM (N ) = 3(N − 1). Aşadar,

0 ≤ TM (N ) ≤ 3(N − 1).

Cum T (N ) = TC (N ) + TM (N ), obţinem că

N (N − 1) (N − 1)(N + 6)
≤ T (N ) ≤ ,
2 2
şi deci T (N ) ∈ Θ(N 2 ). Observăm că T (N ) ∈ Ω(N 2 ) şi T (N ) ∈ O(N 2 ).

1.6.1.2 Sortarea prin selecţia elementului maxim


Metoda este similară precedentei, doar că presupune determinarea elementului maxim al tabloului, deci a indi-
celui M pentru care a[i] ≤ a[M ], (∀) i = 0, 1, . . . , N − 1. Apoi sunt interschimbate elementele a[N − 1] şi a[M ]
(elementul maxim este plasat pe ultima poziţie ı̂n şir). Procedeul se repetă pentru subşirul format din elementele
a[0], a[1], . . . , a[N − 2]. Se caută elementul cel mai mare din acest subşir şi se interschimbă cu a[N − 2] ş.a.m.d.,
până când subşirul va conţine un singur element.
1 algorithm Sortarea prin selecţia maximului
2 begin
3 integer i , j , M , N
4 real a [0.. N -1] , aux
5 read N , a
6 for i = N - 1 downto 1 do
7 M = i
8 for j = 0 to i - 1 do
9 if a [ M ] < a [ j ] then
10 M = j
11 end if
12 end for
13 if M != i then
14 aux = a [ M ]
15 a[M] = a[i]
16 a [ i ] = aux
17 end if
18 end for
19 write a
20 end
Desigur, complexitatea algoritmului este tot Θ(N 2 ).

1.6.2 Sortarea prin inserţie


1.6.2.1 Sortarea prin inserţie directă
Metoda constă ı̂n determinarea poziţiei corecte a fiecărui element a[i], ı̂ncepând cu al doilea (adică cu a[1]), ı̂n
subşirul care ı̂l precede, a[0] ≤ a[1] ≤ · · · ≤ a[i − 1], deja ordonat ı̂n paşii anteriori ai algoritmului. Căutarea poziţiei
lui a[i] se realizează folosind algoritmul de căutare secvenţială. Astfel, a[i] se compară succesiv cu a[i−1], a[i−2], . . . ,

43
Algoritmi şi complexitate Note de curs

până când se găseşte un element a[j], ı̂n subşirul deja ordonat, astfel ı̂ncât a[j] ≤ a[i]. Se inserează elementul a[i]
după a[j], nu ı̂nainte de a deplasa elementele a[i − 1], a[i − 2], . . . , a[j + 1] cu o poziţie la dreapta.
Pe parcursul sortării, se observă că şirul este ı̂mpărţit (imaginar) ı̂n două subtablouri: subşirul sortat a[0], a[1],
. . . , a[i − 1], ı̂n care urmează să fie inserat elementul curent a[i] şi secvenţa nesortată a[i + 1], a[i + 2], . . . , a[N − 1],
din care urmează să se extragă elemente ce vor fi inserate pe poziţiile corecte ı̂n secvenţa sortată.
1 algorithm Sortarea prin inserţie directă
2 begin
3 integer N , i , j
4 real a [0.. N -1] , aux
5 read N , a
6 for i = 1 to N - 1 do
7 j = i - 1
8 aux = a [ i ]
9 while j >= 0 && aux < a [ j ] do
10 a [ j + 1] = a [ j ]
11 j = j - 1
12 end while
13 a [ j + 1] = aux
14 end for
15 write a
16 end
Exemplificăm funcţionarea algoritmului pe următorul exemplu: a[ ] = {5, 2, 1, 4, 3, 0}. Să observăm ce se execută
la fiecare iteraţie a ciclului for.
iteraţia 1: Primul element pentru care se caută poziţia corectă este a[1] = 2. Subşirul care ı̂l precede este format
doar din elementul a[0] = 5. Astfel, singura comparaţie ce se execută este cea ı̂ntre cele două elemente. Cum 2 < 5,
se deplasează elementul 5 cu o poziţie la dreapta şi elementul 2 se plasează pe poziţia corectă ı̂n şir (adică pe prima
poziţie). Noul şir este a[ ] = {2, 5, 1, 4, 3, 0}.
iteraţia 2: Se caută poziţia elementului a[2] = 1 ı̂n subşirul format din elementele a[0..1] = {2, 5}, sortat. Cum
ambele elemente ale subşirului sunt mai mari decât 1, acestea sunt deplasate cu o poziţie la dreapta, iar elementul
1 va ocupa prima poziţie ı̂n şir. Şirul devine a[ ] = {1, 2, 5, 4, 3, 0}.
iteraţia 3: Se caută poziţia elementului a[3] = 4 ı̂n subşirul a[0..2] = {1, 2, 5}, sortat. Cum doar elementul 5 al
subşirului este mai mare decât 4, acesta va fi deplasat cu o poziţie la dreapta. Şirul este a[ ] = {1, 2, 4, 5, 3, 0}.
iteraţia 4: Se caută poziţia elementului a[4] = 3 ı̂n subşirul a[0..3] = {1, 2, 4, 5}, sortat. Cum doar elementele
4 şi 5 ale subşirului sunt mai mari decât 3, acestea vor fi deplasate cu o poziţie la dreapta. Şirul este acum
a[ ] = {1, 2, 3, 4, 5, 0}.
iteraţia 5: Se caută poziţia elementului a[5] = 0 ı̂n subşirul a[0..4] = {1, 2, 3, 4, 5}, sortat. Cum toate ele-
mentele subşirului sunt mai mari decât 0, acestea vor fi deplasate cu o poziţie la dreapta. Şirul sortat este
a[ ] = {0, 1, 2, 3, 4, 5}.

Verificarea corectitudinii. Să arătăm că predicatul

I1 (n) : a[0] ≤ a[1] ≤ · · · ≤ a[n], n ∈ N

este invariant la ciclul for după i (elementele şirului sortat, a[0], . . . , a[n], sunt elementele şirului original de la 0 la
n, permutate). Iniţial (n = 0), precondiţia implică faptul că şirul format dintr-un singur element poate fi considerat
sortat crescător. Presupunem că I1 (k) este adevărat şi condiţia de continuare a ciclului for este adevărată. Vom
arăta că I1 (k + 1) este adevărat. Fie predicatul

I2 (p) : a[0] ≤ a[1] ≤ · · · ≤ a[k − p] şi auxk+1 ≤ a[k + 1 − p] = a[k + 2 − p] ≤ · · · ≤ a[k + 1], p ∈ N.

Vom arăta că acesta este invariant la structura repetitivă while. La intrarea ı̂n ciclu (p = 0), auxk+1 = a[k + 1],
iar şirul a[0] ≤ a[1] ≤ · · · ≤ a[k] este deja ordonat, deci I2 (0) este adevărat. Presupunem că I2 (l) este adevărat
şi condiţia de continuare a ciclului while este adevărată. Din auxk+1 < a[k − l] şi linia 10, rezultă că auxk+1 <
a[k − l] = a[k − l + 1] ≤ · · · ≤ a[k + 1] şi deci I2 (l + 1) este adevărat.

44
Algoritmi şi complexitate Note de curs

La sfârşitul buclei while este adevărată una dintre relaţiile:

a[0] ≤ · · · ≤ a[j ∗ ] ≤ aux ≤ a[j ∗ + 1] = a[j ∗ + 2] ≤ · · · ≤ a[k + 1],

dacă ciclul s-a ı̂ncheiat deoarece aux ≥ a[j ∗ ] sau

aux ≤ a[0] = a[1] ≤ · · · ≤ a[k + 1],

dacă ciclul s-a ı̂ncheiat deoarce j = −1. Dar, după atribuirea din linia 13, a[0] ≤ · · · ≤ a[j ∗ ] ≤ a[j ∗ +1] ≤ a[j ∗ +2] ≤
· · · ≤ a[k + 1]. Deci I1 (k + 1) este adevărat. Dacă iN −1 = N , deci după N − 1 iteraţii ale ciclului exterior, I1 (N − 1)
implică postcondiţia.
Considerăm funcţia de terminare pentru ciclul exterior t1 (k) = n − ik , k ∈ N, iar pentru ciclul interior,
(
jk + 1, dacă aux < a[jk ]
t2 (k) =
0, dacă aux ≥ a[jk ], k ∈ N.

Astfel, algoritmul este total corect.

Analiza complexităţii. Atât numărul comparaţiilor, cât şi cel al mutărilor depinde de distribuţia iniţială a
elementelor ı̂n secvenţă. În cazul cel mai favorabil, pentru fiecare i = 1, 2, . . . , N − 1, are loc o singură comparaţie
şi două mutări (aux = a[i], a[j + 1] = a[i]). Luând ı̂n calcul atât comparaţiile, cât şi mutările, se obţine o margine
inferioară pentru timpul de execuţie,
3(N − 1) ≤ T (N ).
În cazul cel mai defavorabil, pentru fiecare i = 1, 2, . . . , N − 1, au loc i comparaţii şi i + 2 mutări. Obţinem o
margine superioară a timpului de execuţie,
N
X −1
T (N ) ≤ (2i + 2) = (N − 1)(N + 2)
i=1

şi deci
3(N − 1) ≤ T (N ) ≤ (N − 1)(N + 2).
Putem spune astfel că T (N ) ∈ Ω(N ) şi T (N ) ∈ O(N 2 ).

1.6.2.2 Sortarea prin inserţie cu pas variabil


Metoda a fost introdusă de Donald L. Shell, ı̂n anul 1959 şi este cunoscută şi sub numele de sortare prin micşorarea
incrementului sau shellsort. Ideea metodei este de a sorta elementele vectorului pe grupe, fiecare grupă având
elementele din şirul original ce sunt distanţate la un anumit pas h. Acest proces poartă numele de h-sortare. Dacă
cei t incremenţi (paşi) sunt h1 , h2 , . . . ht , cu ht = 1 şi hi+1 < hi , fiecare hi -sortare se poate implementa ca o sortate
prin inserţie directă. Există mai multe modalităţi de a alege incremenţii (paşii), de alegerea acestora depinzând
performanţa algoritmului. Incremenţii pot fi aleşi după o putere a lui 2 sau putem folosi un tablou de incremenţi cu
valori descrescătoare, ultima dintre acestea fiind obligatoriu 1. Pentru aumite alegeri ale secvenţei de incremenţi se
pot obţine algoritmi de complexitate mai mică decât cea a algoritmului de sortare prin inserţie directă. Un astfel
de şir de incremenţi este
. . . , 511, 255, 127, 63, 31, 15, 7, 3, 1
adică şirul cu elementele de forma 2k − 1, k ≥ 1, pentru care s-a demonstrat că T (N ) ∈ O(N 3/2 ). D. E. Knuth
recomandă următorul şir de incremenţi, deoarece este uşor de implementat şi are aceeaşi complexitate ı̂n cel mai
defavorabil caz ca precedentul:
. . . , 3280, 1093, 364, 121, 40, 13, 4, 1.
Aşadar, după cum se observă, hi = 3hi+1 + 1, ultimul increment fiind egal cu 1. Alte secvenţe de incremenţi pot
conduce la algoritmi de sortare mai eficienţi. De exemplu, Sedgewick propune secvenţa

. . . , 281, 77, 23, 8, 1

45
Algoritmi şi complexitate Note de curs

adică şirul cu elementele de forma 4k+1 +3·2k +1, k ≥ 0, la care se adaugă incrementul 1. Algoritmul corespunzător
are o performanţă de O(N 4/3 ).
Anumite secvenţe de incremenţi ar trebui evitate, având performanţe slabe. Un exemplu de astfel de secvenţă este
. . . , 256, 128, 64, 32, 16, 8, 4, 2, 1
elementele fiind puteri ale lui 2, deoarece elementele de pe poziţiile pare nu sunt comparate cu cele de pe poziţiile
impare până la pasul final.
Avantajul metodei constă ı̂n faptul că elementele sunt mutate mai repede mai aproape de poziţiile lor corecte,
sortarea realizându-se la distanţe mai mari.
1 algorithm Sortarea prin inserţie cu pas variabil
2 begin
3 integer N , i , j , h
4 real a [0.. N -1] , aux
5 read N , a
6 h = 1
7 while h < N / 3 do
8 h = 3 * h + 1 // sirul de incrementi sugerat de Knuth
9 end while
10 while h >= 1 do
11 for i = h to N - 1 do
12 aux = a [ i ]
13 j = i
14 while j >= h && a [ j - h ] > aux do
15 a[j] = a[j - h]
16 j = j - h
17 end while
18 a [ j ] = aux
19 end for
20 h = h / 3
21 end while
22 write a
23 end
Să considerăm un exemplu: a[ ] = {31, 56, 45, 51, 34, 48, 16, 65, 19, 7, 5, 20, 43, 10, 22}, pentru care ne propunem să
folosim algoritmul de sortare prin micşorarea incrementului, folosind secvenţa de incremenţi 13, 4, 1.
Sortarea elementelor aflate la pasul h = 13. Grupele de sortat, la distanţa de 13 paşi sunt: {31, 10}, {56, 22}.
Sortate prin inserţie directă, la pas h = 13, devin: {10, 31}, {22, 56}. Noul tablou este:

a[ ] = {10, 22, 45, 51, 34, 48, 16, 65, 19, 7, 5, 20, 43, 31, 56}.
Sortarea elementelor aflate la pasul h = 4. Grupele de sortat, la distanţa de 4 paşi sunt: {10, 34, 19, 43},
{22, 48, 7, 31}, {45, 16, 5, 56}, {51, 65, 20}. Grupele sortate prin inserţie directă: {10, 19, 34, 43}, {7, 22, 31, 48},
{5, 16, 45, 56}, {20, 51, 65}. Tabloul devine:
a[ ] = {10, 7, 5, 20, 19, 22, 16, 51, 34, 31, 45, 65, 43, 48, 56}.
Sortarea elementelor aflate la pasul h = 1. Sortarea la distanţa de 1 pas duce la sortarea ı̂ntregului tablou, de fapt
fiind vorba de sortarea prin inserţie directă aplicată ı̂ntregului tablou. Tabloul sortat:
a[ ] = {5, 7, 10, 16, 19, 20, 22, 31, 34, 43, 45, 48, 51, 56, 65}.

1.6.3 Sortarea prin interschimbarea elementelor vecine


Ne vom opri ı̂n această secţiune asupra metodei de sortare prin interschimbări succesive ale elementelor vecine ce nu
sunt ı̂n ordinea corectă. Interschimbările au loc până când elementele vectorului sunt complet ordonate. Această
variantă de sortare mai poartă numele de metoda bulelor sau bubblesort.

46
Algoritmi şi complexitate Note de curs

Metoda constă ı̂n compararea elementelor a[i] şi a[i + 1]; dacă a[i] ≤ a[i + 1], atunci se trece la compararea lui
a[i + 1] cu a[i + 2]; dacă nu, se interschimbă a[i] cu a[i + 1] şi apoi se compară a[i + 1] cu a[i + 2]. După prima
parcurgere a vectorului, pe ultima poziţie va fi cu siguranţă elementul având valoarea cea mai mare, după a doua
parcurgere, pe penultima poziţie va ajunge următorul element ca valoare ş.a.m.d. O singură parcurgere a vectorului
nu este suficientă, ci trebuie continuat până când vectorul va fi complet sortat.
1 algorithm Varianta 1 de sortare prin interschimbarea elementelor vecine
2 begin
3 integer N , i , j
4 real a [0.. N -1] , aux
5 read N , a
6 for i = N - 1 downto 1 do
7 for j = 0 to i - 1 do
8 if a [ j ] > a [ j + 1] then
9 aux = a [ j ]
10 a [ j ] = a [ j + 1]
11 a [ j + 1] = aux
12 end if
13 end for
14 end for
15 write a
16 end
Să considerăm un exemplu, ı̂n care şirul pe care vrem să ı̂l sortăm este a[ ] = {89, 45, 68, 90, 29, 34, 17}. Iteraţiile
ciclului for exterior sunt (prin “↔” am indicat o interschimbare):
iteraţia 1:
89 ↔ 45 68 90 29 34 17
45 89 ↔ 68 90 29 34 17
45 68 89 90 ↔ 29 34 17
45 68 89 29 90 ↔ 34 17
45 68 89 29 34 90 ↔ 17
45 68 89 29 34 17 90
La finalul primei iteraţii, cel mai mare element al şirului (= 90) este ı̂n poziţia finală şi anume, pe ultima poziţie a
şirului. Acesta nu va mai face parte din subsecvenţa ce va fi parcursă ı̂n continuare.
iteraţia 2:
45 68 89 ↔ 29 34 17
45 68 29 89 ↔ 34 17
45 68 29 34 89 ↔ 17
45 68 29 34 17 89
La finalul celei de-a doua iteraţie, cel mai mare element al subşirului (= 89) este ı̂n poziţia finală şi anume, pe
penultima poziţie a şirului. Acesta nu va mai face parte din subsecvenţa ce va fi parcursă ı̂n continuare.
iteraţia 3:
45 68 ↔ 29 34 17
45 29 68 ↔ 34 17
45 29 34 68 ↔ 17
45 29 34 17 68
La finalul acestei iteraţii, 68 se va afla pe poziţia corectă ı̂n şir.
iteraţia 4:
45 ↔ 29 34 17
29 45 ↔ 34 17
29 34 45 ↔ 17
29 34 17 45
Elementul 45 este pe poziţia sa finală ı̂n şirul sortat.
iteraţia 5:
29 34 ↔ 17

47
Algoritmi şi complexitate Note de curs

29 17 34
Elementul 34 este pe poziţia sa finală ı̂n şir.
iteraţia 6:
29 ↔ 17
17 29
În acest moment, ultimele două elemente ale şirului sunt pe poziţiile lor corecte, şirul fiind sortat ı̂n ı̂ntregime:

a[ ] = {17, 29, 34, 45, 68, 89, 90}.

Verificarea corectitudinii. Fie predicatul

I1 (n) : a[N − n] ≤ · · · ≤ a[N − 1], cu a[N − n] ≥ a[i], i = 0, . . . , N − n − 1, n ∈ N.

Să arătăm că acesta este invariant pentru ciclul exterior (for i). La intrarea ı̂n ciclu, n = 0, I1 (0) este adevărat (şir
vid). Presupunem că I1 (k) este adevărat şi condiţia de continuare a ciclului exterior este, de asemenea, adevărată.
Considerăm predicatul
I2 (p) : a[p] ≥ a[j], j = 0, . . . , p.
Să arătăm că acesta este invariant pentru ciclul interior (for j). La intrarea ı̂n ciclu, (p = 0), I2 (0) este adevărat,
deoarece a[0] ≥ a[0]. Presupunem că I2 (l) este adevărat şi condiţia de continuare a ciclului interior este adevărată.
Dacă a[l] > a[l + 1] atunci se realizează interschimbarea elementelor şi acestea vor fi ı̂n ordinea a[l] < a[l + 1], iar
dacă a[l] ≤ a[l + 1], nu se efectuează interschimbări, dar relaţia este adevărată. Aşadar, I2 (l + 1) este adevărat. La
iteraţia k + 1 a ciclului exterior, ı̂n ciclul interior, cel mai mare element din secvenţa a[0], a[1], . . . , a[N − (k + 1)]
este plasat pe poziţia N − (k + 1). Astfel, I1 (k + 1) este adevărat. Dacă iN −1 = 0, deci după N − 1 iteraţii ale
ciclului exterior,
I1 (N − 1) : a[1] ≤ · · · ≤ a[N − 1], cu a[1] ≥ a[0],
ceea ce implică postcondiţia.
Pentru a demonstra că ambele cicluri sunt finite, considerăm următoarele funcţii de terminare: pentru ciclul
exterior, t1 (k) = ik , iar pentru ciclul interior, t2 (k) = ik − jk , k ∈ N.

Analiza complexităţii. Numărul de comparaţii nu depinde de proprietăţile datelor de intrare, acesta fiind

N (N − 1)
TC (N ) = (N − 1) + (N − 2) + · · · + 1 = .
2
Numărul de interschimbări depinde de repartizarea iniţială a elementelor ı̂n şir, deci vom analiza cazurile extreme.
În cazul cel mai favorabil numărul de mutări ale elementelor ı̂n vederea sortării este

TM (N ) = 0,

iar ı̂n cazul cel mai defavorabil se obţine


3N (N − 1)
TM (N ) = .
2
Aşadar, obţinem o limită inferioară şi una superioară pentru numărul de mutări

3N (N − 1)
0 ≤ TM (N ) ≤ .
2
Cum T (N ) = TC (N ) + TM (N ), obţinem că

N (N − 1)
≤ T (N ) ≤ 2N (N − 1),
2
deci T (N ) ∈ Θ(N 2 ).
Algoritmul poate fi ı̂mbunătăţit, implicând o variabilă booleană care să indice dacă la iteraţia precedentă au existat
interschimbări. Scopul este de a reduce numărul de comparaţii.

48
Algoritmi şi complexitate Note de curs

1 algorithm Varianta 2 de sortare prin interschimbarea elementelor vecine


2 begin
3 integer N , i , m
4 bool schimb
5 real a [0.. N -1] , aux
6 read N , a
7 m = N
8 repeat
9 schimb = false
10 for i = 0 to m - 2 do
11 if a [ i ] > a [ i + 1] then
12 aux = a [ i ]
13 a [ i ] = a [ i + 1]
14 a [ i + 1] = aux
15 schimb = true
16 end if
17 end for
18 m = m - 1
19 until schimb == false
20 write a
21 end
Pentru acest algoritm, numărul de comparaţii ı̂n cazul cel mai favorabil este

TC (N ) = N − 1,

iar ı̂n cazul cel mai defavorabil este acelaşi ca ı̂n cazul algoritmului precedent,
N (N − 1)
TC (N ) = .
2
În ceea ce priveşte numărul mutărilor, se obţine aceeaşi estimare:
3N (N − 1)
0 ≤ TM (N ) ≤ .
2
Aşadar,
N − 1 ≤ T (N ) ≤ 2N (N − 1).
Obţinem că T (N ) ∈ Ω(N ) şi T (N ) ∈ O(N 2 ).

O metodă de sortare este stabilă dacă păstrează ordinea relativă a elementelor de aceeaşi valoare. Această pro-
prietate presupune ca elementele de valoare egală să apară ı̂n şirul sortat ı̂n aceeaşi ordine ca ı̂n secvenţa iniţială.
Algoritmii de sortare prin inserţie şi prin interschimbarea elementelor vecine sunt stabile, iar cel de sortare prin
selecţie nu este stabil.
Metodele de sortare prezentate sortează elementele “pe loc”, fără a fi necesare tablouri suplimentare.

1.7 Analiza algoritmilor recursivi


1.7.1 Elaborarea algoritmilor recursivi
În general, o noţiune este definită recursiv dacă ı̂n cadrul definiţiei intervine ı̂nsăşi noţiunea care tocmai se de-
fineşte. Astfel, atunci când ı̂ntre prelucrările unui subalgoritm apare cel puţin un autoapel al acestuia, spunem că
subalgoritmul este recursiv. Un subalgoritm recursiv trebuie să respecte următoarele reguli:
1. să poată fi executat cel puţin ı̂ntr-o situaţie direct, fără a se autoapela; aceste situaţii poartă numele de cazuri
elementare;

49
Algoritmi şi complexitate Note de curs

2. pentru cazurile care nu se rezolvă direct, recursivitatea trebuie să conducă la un caz elementar.
Aşadar, pentru a opri autoapelul la nesfârşit al unui subalgoritm, trebuie prevăzută o cale de ieşire din recursivitate,
adică o condiţie de oprire. Aceasta trebuie să reprezinte situaţia ı̂n care rezultatul se poate obţine direct, fără
autoapel.
Presupunem că subalgoritmul recursiv este implementat ı̂ntr-un limbaj de programare printr-o funcţie sau procedură
recursivă, ı̂n vedererea testării programului corespunzător pe calculator. La fiecare apel recursiv al unui subprogram,
se salvează ı̂n stiva sistem alocată programului de către compilator, starea curentă a execuţiei sale: adresa de
revenire (se memorează starea curentă a programului, pentru a putea fi refăcută, atunci când apelul curent al
funcţiei/procedurii recursive se termină şi urmează să fie reluată execuţia programului) şi contextul programului
(fiecare apel recursiv al funcţiei/procedurii recursive necesită alocarea unui volum de memorie pentru variabilele
sale curente). Dacă nu se prevede o cale de ieşire din recursivitate şi se lansează ı̂n execuţie un subprogram
recursiv, stiva atribuită programului se va umple imediat şi programul va fi oprit de către sistemul de operare.
Aşadar, datorită faptului că la fiecare autoapel se ocupă o zonă de stivă, recursivitatea este eficientă doar dacă
ştim că numărul de autoapeluri nu este mare, astfel ı̂ncât să nu se ajungă ı̂n situaţia umplerii stivei. Dincolo de
acest dezavantaj, al umplerii rapide a stivei alocate programului, recursivitatea oferă avantajul unor soluţii mai
clare pentru anumite probleme, programele fiind de dimensiuni mai reduse şi intuitive.
Recursivitatea poate fi simplă sau multiplă, directă sau indirectă. Un subalgoritm este simplu recursiv dacă are
ı̂ntre prelucrările sale un singur autoapel şi este multiplu recursiv dacă se autoapelează de două sau mai multe ori.
Recursivitatea directă are loc când subalgoritmul se autoapelază, iar cea indirectă, când un subalgoritm este apelat
indirect, prin intermediul altui subalgoritm (de exemplu, subalgoritmul A apelează subalgoritmul B, care la rândul
său ı̂l apelează pe A).

Exemplul 1.27. Vrem să calculăm cel mai mare divizor comun al două numere naturale a şi b, nenule. Definiţia
recursivă ce se deduce din algoritmului lui Euclid este
(
a, b=0
c.m.m.d.c.(a, b) =
c.m.m.d.c.(b, a%b), b > 0.

Subalgoritmul recursiv corespunzător este


1 function cmmdcRecursiv ( integer a , integer b )
2 begin
3 if b == 0 then
4 return a
5 else
6 return cmmdcRecursiv (b , a % b )
7 end if
8 end
Recursivitatea este ı̂n acest caz una simplă şi directă. Ne propunem să determinăm cel mai mare divizor comun al
numerelor naturale 15 şi 25. La apelul subalgoritmului cmmdcRecursiv cu parametrii actuali 15 şi 25, ı̂n parametrii
formali a şi b sunt copiate valorile parametrilor de apel. Deoarece subalgoritmul este recursiv, se declanşează un
lanţ de autoapeluri. La fiecare autoapel, este testat cazul elementar, care conţine condiţia de oprire a recursivităţii.
Astfel, dacă cel de-al doilea parametru al subalgoritmului, b, are valoarea 0, se va returna valoarea primului
parametru, a.
Paşii subalgoritmului sunt:
Pasul 1: se apelează cmmdcRecursiv(15, 25): a ←− 15, b ←− 25; b == 0? NU;
Pasul 2: se apelează cmmdcRecursiv(25, 15): a ←− 25, b ←− 15; b == 0? NU;
Pasul 3: se apelează cmmdcRecursiv(15, 10): a ←− 15, b ←− 10; b == 0? NU;
Pasul 4: se apelează cmmdcRecursiv(10, 5): a ←− 10, b ←− 5; b == 0? NU;
Pasul 5: se apelează cmmdcRecursiv(5, 0): a ←− 5, b ←− 0; b == 0? DA. După ce s-a ajuns la cazul elementar,
rezultatul va fi returnat celui mai recent apel recursiv. Aşadar,
Pasul 6: cmmdcRecursiv(5, 0) returnează 5.

50
Algoritmi şi complexitate Note de curs

Pasul 7: cmmdcRecursiv(10, 5) returnează 5;


Pasul 8: cmmdcRecursiv(15, 10) returnează 5;
Pasul 9: cmmdcRecursiv(25, 15) va returna 5;
Pasul 10: cmmdcRecursiv(15, 25) returnează 5.
Putem urmări lanţul de autoapeluri ı̂n următoarea figură:

Condiţia de oprire a recursivităţii va fi satisfăcută după un număr finit de apeluri recursive, deoarece şirul resturilor
este un şir de numere naturale descrescător.

1.7.2 Verificarea corectitudinii şi analiza complexităţii algoritmilor re-


cursivi
Verificarea corectitudinii. Pentru a verifica corectitudinea unui algoritm recursiv este suficient să se arate că
recurenţa ce descrie relaţia dintre soluţia problemei şi soluţiile altor instanţe ale problemei, dar de dimensiuni mai
mici, este corectă. În acest scop, de cele mai multe ori se foloseşte inducţia matematică.
Exemplul 1.28. (Calculul factorialului) Ştim că n! = 1 · 2 · ... · n, n ∈ N∗ , 0! = 1. Are loc atunci recurenţa
(
1, n=0
n! =
n(n − 1)!, n ≥ 1.

Subalgoritmul recursiv corespunzător este


1 function factorialRec ( integer n )
2 begin
3 if n == 0 then
4 return 1
5 else
6 return n * factorialRec ( n - 1)
7 end if
8 end
De exemplu, să presupunem că subalgoritmul recursiv factorialRec este apelat cu parametrul actual 5. Astfel,
valoarea parametrului de apel este copiată ı̂n parametrul formal al subalgoritmului, n. Paşii algoritmului recursiv
pot fi identificaţi ı̂n următoarea figură:

51
Algoritmi şi complexitate Note de curs

Corectitudinea algoritmului se poate demonstra prin inducţie completă, pornind de la formula de recurenţă. De-
monstrăm aşadar prin inducţie că rezultatul returnat de subalgoritmul factorialRec(n) este n!, pentru orice
număr natural n.
Pentru n = 0, algoritmul returnează 1, deci afirmaţia este adevărată. Presupunem că afirmaţia este adevărată
pentru orice k ∈ N, 0 ≤ k ≤ n − 1, şi demonstrăm că acest lucru implică faptul că afirmaţia este adevărată
pentru k = n > 0. Conform ipotezei inductive, factorialRec(n - 1) returnează (n − 1)!. Cum n > 0, conform
algoritmului (linia 6), rezultă că valoarea returnată de subalgoritm este n · (n − 1)! = n!. Deci algoritmul este
parţial corect. Condiţia de oprire va fi satisfăcută după un număr finit de apeluri recursive, deoarece dimensiunea
instanţei cu care se autoapelează algoritmul descreşte la fiecare pas. Aşadar, algoritmul este total corect.

Analiza complexităţii. Considerăm o problemă de dimensiune n şi următorul subalgoritm recursiv care o
rezolvă:
1 function algRec ( integer n )
2 begin
3 if n == n0 then // cazul elementar
4 <P >
5 else
6 algRec ( f ( n ) )
7 end if
8 end
Cazul elementar este n = n0 . Presupunem că prelucrarea corespunzătoare cazului elementar, P, are costul de
execuţie constant, c0 . Pentru ca subalgorimul recursiv să se termine ı̂n timp finit, funcţia f trebuie să fie des-
crescătoare astfel ı̂ncât, după k apeluri recursive, să fie satisfăcută condiţia de oprire, adică (f ◦ f ◦ · · · ◦ f )(n) = n0 .
| {z }
k ori
Dacă extinderea soluţiei instanţei de dimensiune mai mică (f (n)) pentru a obţine soluţia instanţei de dimensiune
mai mare (n) are costul c, atunci timpul de execuţie al subalgoritmului recursiv satisface relaţia de recurenţă
(
c0 , n = n0
T (n) =
T (f (n)) + c, n > n0 .
Plecând de la relaţia de recurenţă, există două abordări pentru a determina timpul de execuţie al unui subalgoritm
recursiv:

52
Algoritmi şi complexitate Note de curs

ˆ metoda substituţiei directe: se intuieşte expresia lui T (n) şi apoi se demonstrează prin inducţie matematică
veridicitatea acesteia;
ˆ metoda substituţiei inverse: se scrie relaţia de recurenţă pentru dimensiunile n, f (n), f (f (n)), . . . , n0 , apoi se
substituie succesiv T (f (n)), T (f (f (n))), . . . şi după anumite calcule, va rezulta T (n).
Analizăm eficienţa algoritmului factorialRec, dimensiunea problemei fiind n. Notând cu T (n) numărul de operaţii
de ı̂nmulţire efectuate de către subalgoritm, se obţine relaţia de recurenţă
(
0, n=0
T (n) = (1.1)
T (n − 1) + 1, n > 0.

Aplicând metoda substituţiei inverse, obţinem

T (n) = T (n − 1) + 1
T (n − 1) = T (n − 2) + 1
..
.
T (1) = T (0) + 1
T (0) = 0.

Prin sumarea relaţiilor şi reducerea termenilor asemenea, vom obţine că T (n) = n, deci T (n) ∈ Θ(n).
Exemplul 1.29. (Turnurile din Hanoi) Presupunem că avem 3 tije notate prin a, b şi c. Pe tija a se gasesc n
discuri de diametre diferite, aşezate ı̂n ordinea descrescătoare a diametrelor, de jos ı̂n sus. Se cere să se transfere,
cu un număr minim de mutări, cele n discuri de pe tija a pe tija b, utilizând ca tijă intermediară tija c, respectând
următoarele reguli:
1. la fiecare pas se mută un singur disc;
2. pe niciuna dintre tije nu este permis să se aşeze un disc cu diametrul mai mare peste un disc cu diametrul
mai mic.
De exemplu, dacă n = 1 (există un singur disc pe tija a) se face mutarea a −→ b, adică se mută discul de pe tija
a pe tija b. Dacă n = 2 (sunt două discuri pe tija a) se fac mutările a −→ c, a −→ b, c −→ b, adică discul mic se
mută de pe tija a pe tija intermediară c, discul mare se mută de pe tija a pe tija b şi apoi se mută discul mic de pe
tija c pe tija b.
Notăm cu Hn (a, b, c) şirul mutărilor celor n discuri de pe tija a pe tija b, utilizând tija intermediară c. Să observăm
că mutarea celor n discuri de pe tija a pe tija b, utilizând tija intermediară c, este echivalentă cu:
1. mutarea a n − 1 discuri de pe tija a pe tija c , utilizând ca tijă intermediară, tija b;
2. mutarea discului rămas pe tija a pe tija b;
3. mutarea a n − 1 discuri de pe tija c pe tija b, utilizând ca tijă intermediară, tija a.
Ca urmare, şirul Hn (a, b, c) poate fi definit recursiv astfel:
(
a −→ b, n = 1,
Hn (a, b, c) =
Hn−1 (a, c, b), a −→ b, Hn−1 (c, b, a), n > 1.

Pentru n = 2, H2 (a, b, c) = H1 (a, c, b), a −→ b, H1 (c, b, a) = a −→ c, a −→ b, c −→ b.


Pentru n = 3, H3 (a, b, c) = H2 (a, c, b), a −→ b, H2 (c, b, a) = H1 (a, b, c), a −→ c, H1 (b, c, a), a −→ b, H1 (c, a, b),
c −→ b, H1 (a, b, c) = a −→ b, a −→ c, b −→ c, a −→ b, c −→ a, c −→ b, a −→ b.
Pentru n = 4, H4 (a, b, c) = H3 (a, c, b), a −→ b, H3 (c, b, a) = H2 (a, b, c), a −→ c, H2 (b, c, a), a −→ b, H2 (c, a, b),
c −→ b, H2 (a, b, c) = H1 (a, c, b), a −→ b, H1 (c, b, a), a −→ c, H1 (b, a, c), b −→ c, H1 (a, c, b), a −→ b, H1 (c, b, a),
c −→ a, H1 (b, a, c), c −→ b, H1 (a, c, b), a −→ b, H1 (c, b, a) = a −→ c, a −→ b, c −→ b, a −→ c, b −→ a, b −→
c, a −→ c, a −→ b, c −→ b, c −→ a, b −→ a, c −→ b, a −→ c, a −→ b, c −→ b.
Cazul elementar este acela ı̂n care a rămas un singur disc de mutat.

53
Algoritmi şi complexitate Note de curs

1 function hanoi ( integer n , char a , char b , char c )


2 begin
3 if n == 1 then
4 write a " ->" b
5 else
6 hanoi ( n - 1 , a , c , b )
7 write a " ->" b
8 hanoi ( n - 1 , c , b , a )
9 end if
10 end
În acest caz, recursivitatea este multiplă, deoarece, la fiecare pas al algoritmului, există două autoapeluri ale
acestuia. Notăm cu T (n) numărul de mutări ale discurilor efectuate de către subalgoritm. Se obţine recurenţa
(
1, n=1
T (n) =
2T (n − 1) + 1, n > 1.

Prin metoda substituţiei inverse, obţinem

T (n) = 2T (n − 1) + 1
T (n − 1) = 2T (n − 2) + 1/ × 2
T (n − 2) = 2T (n − 3) + 1/ × 22
..
.
T (2) = 2T (1) + 1/ × 2n−2
T (1) = 1/ × 2n−1

Sumând relaţiile şi reducând termenii asemenea, se obţine că T (n) = 1 + 2 + 22 + · · · + 2n−1 = 2n − 1. Deci numărul
de mutări necesare pentru a muta cele n discuri de pe tija a pe tija b, utilizând tija intermediară c, este de 2n − 1,
astfel T (n) ∈ Θ(2n ). Deşi timpul de execuţie al acestui algoritm are un ordin de creştere exponenţial, algoritmul
este optim, ı̂n sensul că problema nu poate fi rezolvată ı̂n mai puţin de 2n − 1 mutări.
Exemplul 1.30. Calculaţi
a) v s
u r q

u
t
3 3 3 3 3...

b) v
u s r

u q
t
3 + 3 + 3 + 3 + 3 + ...

a) Definim ı̂n mod recursiv şirul x = (xn )n∈N


(
1, n=0
xn = √
3xn−1 , n>0

corespunzător expresiei de calculat. Se observă că şirul este crescător şi mărginit superior, deci convergent. Dacă
notăm cu l = lim xn şi folosim recurenţa, obţinem
n→∞

l2 = 3l

şi deci l = 0 sau l = 3. Întrucât şirul este crescător, rezultă că l = 3. Aşadar, valoarea la limită a expresiei este 3.
Subalgoritmul recursiv pentru calculul termenului de rang n ∈ N al şirului considerat este

54
Algoritmi şi complexitate Note de curs

1 function sirRec1 ( integer n )


2 begin
3 if n == 0 then
4 return 1
5 else
6 return sqrt (3 * sirRec1 ( n - 1) )
7 end if
8 end
Numărând ı̂nmulţirile efectuate de către subalgoritm, obţinem relaţia de recurenţă (1.1) pentru timpul de execuţie
al algoritmului. Aşadar, T (n) ∈ Θ(n), deci algoritmul este de complexitate liniară.
b) Regula de recurenţă este (
0, n=0
yn = √
3 + yn−1 , n>0
Şirul este crescător şi mărginit superior, deci convergent. Notăm tot cu l = lim yn . Avem că
n→∞

l2 = 3 + l
√ √
1± 13 1+ 13
şi deci l1,2 = . Cum termenii şirului sunt pozitivi, rezultă că l = . Deci valoarea la limită a
2
√ 2
1+ 13
expresiei este .
2
Subalgoritmul recursiv pentru calculul termenului de rang n ∈ N al şirului y = (yn )n∈N este
1 function sirRec2 ( integer n )
2 begin
3 if n == 0 then
4 return 0
5 else
6 return sqrt (3 + sirRec2 ( n - 1) )
7 end if
8 end
Şi acest subalgoritm are ordinul de complexitate Θ(n), luând ı̂n considerare operaţiile de adunare efectuate (se
obţine tot recurenţa (1.1) pentru timpul de execuţie al algoritmului).

1.7.3 Recursivitate vs. iteraţie


Un algoritm iterativ ce conţine o buclă repetitivă presupune execuţia repetată a unei secvenţe de prelucrări până
când sau atât timp cât este ı̂ndeplinită o anumită condiţie. Un algoritm recursiv implică execuţia repetată a unui
ı̂ntreg modul. În cursul execuţiei se testează o condiţie (de oprire). Dacă condiţia este satisfăcută, se revine ı̂n
ordine inversă ı̂n succesiunea de apeluri, reluându-se şi terminându-se apelurile care au fost puse ı̂n aşteptare. Dacă
condiţia de oprire nu este satisfăcută, atunci se reia execuţia modulului, fără ca execuţia curentă să fie terminată.
Recursivitatea este preferată uneori pentru probleme ce conduc către formule recursive sau pentru prelucrarea unor
structuri de date definite recursiv (liste, arbori) datorită simplităţii descrierii algoritmice şi a uşurinţei verificării
corectitudinii algoritmului corespunzător. Algoritmii iterativi sunt preferaţi datorită vitezei mai mari de execuţie
şi pentru faptul că necesită un spaţiu mai mic de memorie decât cei recursivi. De altfel, orice algoritm recursiv
poate fi transcris ı̂ntr-unul iterativ, dar care nu este ı̂ntotdeauna uşor de urmărit şi ı̂nţeles.
După cum am precizat ı̂ncă de la bun ı̂nceput, datorită faptului că la fiecare autoapel al unui algoritm recursiv
se ocupă o zonă de stivă sistem alocată programului, recursivitatea este eficientă doar dacă se ştie că numărul de
autoapeluri nu este mare, astfel ı̂ncât să nu se ajungă ı̂n situaţia depăşirii spaţiului de memorie alocat stivei sistem.
Deci ar trebui demonstrat nu numai că adâncimea recursivităţii este finită, ci şi faptul că este suficient de mică.

55
Algoritmi şi complexitate Note de curs

Exemplul 1.31. (Şirul lui Fibonacci ) Fiecare termen al şirului reprezintă suma celor doi termeni imediat anteriori,
primii doi termeni ai şirului fiind 0 şi 1. Astfel, şirul ı̂ncepe cu 0 şi 1 şi continuă cu 1, 2, 3, 5, 8, 13, 21, 34, 55, 89,
144, 233, 377, 610 etc. Deci şirul lui Fibonacci (Fn )n∈N ⊂ N poate fi definit prin relaţia de recurenţă

0,
 n=0
Fn = 1, n=1 (1.2)

Fn−1 + Fn−2 , n ≥ 2.

Vom descrie un subalgoritm recursiv pentru calculul termenului de rang n ∈ N al şirului lui Fibonacci, Fn .
1 function Fibo1 ( integer n )
2 begin
3 if n <= 1 then
4 return n
5 else
6 return Fibo1 ( n - 1) + Fibo1 ( n - 2)
7 end if
8 end
Această metodă este ineficientă deoarece conduce la efectuarea de calcule redundante. De exemplu, ne propunem
să evaluăm termenul F6 . În acest scop, se calculează F5 şi F4 , dar la evaluarea lui F5 , se calculează din nou F4 .
Privind arborele de apeluri al algoritmului recursiv Fibo1, observăm că F4 este evaluat de două ori, F3 este evaluat
de trei ori, F2 este evaluat de cinci ori etc.
F6

F5 F4

F3 F2
F4 F3
F2 F1 F1 F0
F3 F2 F2 F1
F1 F0 1 1 0
F1 F0 F1 F0 1
F2 F1
1 0
F1 F0 1 0 1 0
1

1 0
Intenţionăm să obţinem o formulă explicită pentru Fn . Astfel, putem rescrie recurenţa care descrie termenul general
din şirul lui Fibonacci, astfel
Fn − Fn−1 − Fn−2 = 0, n ≥ 2,
cu F0 = 0, F1 = 1. Recurenţa este una liniară omogenă, iar ecuaţia sa caracteristică este

x2 − x − 1 = 0,

1± 5
cu rădăcinile x1,2 = . Atunci, soluţia generală a recurenţei este
2
Fn = c1 xn1 + c2 xn2 .
1
Folosind condiţiile iniţiale, F0 = 0, F1 = 1, obţinem c1 + c2 = 0 şi c1 x1 + c2 x2 = 1. Aşadar, c1,2 = ± √ . Deci,
5
  n 
1 1 1 1 1
Fn = √ (xn1 − xn2 ) = √ xn1 − − = √ (xn1 − (−x1 )−n ) = √ (ϕn − (−ϕ)−n ), (1.3)
5 5 x1 5 5

1+ 5
cu ϕ = . Deci Fn ∈ Θ(ϕn ) (ordin de creştere exponenţial).
2

56
Algoritmi şi complexitate Note de curs

Notăm cu T1 (n) numărul de adunări efectuate (adunările sunt operaţiile de bază) de către algoritmul Fibo1 pentru
a calcula Fn . Atunci (
0, n = 0 sau n = 1
T1 (n) =
T1 (n − 1) + T1 (n − 2) + 1, n ≥ 2.
Această recurenţă este neomogenă, dar o rescriem astfel

(T1 (n) + 1) − (T1 (n − 1) + 1) − (T1 (n − 2) + 1) = 0, n ≥ 2.

Notând cu A(n) = T1 (n) + 1, n ∈ N, obţinem

A(n) − A(n − 1) − A(n − 2) = 0, n ≥ 2,

cu A(0) = T1 (0) + 1 = 0 + 1 = 1 şi A(1) = T1 (1) + 1 = 0 + 1 = 1. Putem rezolva această recurenţă omogenă
ı̂n aceeaşi manieră ca mai sus, sau putem observa că, de fapt e aceeaşi recurenţă, doar că porneşte cu valorile
1, 1, şi nu cu 0 şi 1, ca şirul lui Fibonacci. Deci, A(n) = Fn+1 . Cum T1 (n) = A(n) − 1, rezultă că T1 (n) =
1
Fn+1 − 1 = √ (ϕn+1 − (−ϕ)−(n+1) ) − 1, ceea ce ı̂nseamnă că T1 (n) ∈ Θ(ϕn ), un timp de execuţie cu ordin de
5
creştere exponenţial. Deci Fibo1 este un algoritm de complexitate exponenţială.

Se poate obţine un algoritm de complexitate liniară, ı̂nlocuind dublul apel recursiv printr-unul singur. Pentru
a determina termenul curent, trebuie cunoscuţi cei doi termeni imediat anteriori, pe care ı̂i vom transmite ca
parametri.
1 function Fibo2 ( integer a , integer b , integer n )
2 begin
3 if n == 0 then
4 return 0
5 else
6 if n == 1 then
7 return b
8 else
9 return Fibo2 (b , a + b , n - 1)
10 end if
11 end if
12 end
13

14 algorithm Fibonacci
15 begin
16 integer n
17 read n
18 write Fibo2 (0 , 1 , n )
19 end
Implementat iterativ, algoritmul devine
1 function Fibo3 ( integer n )
2 begin
3 integer a , b , c , i
4 if n == 0 then
5 return 0
6 end if
7 a = 0
8 b = 1
9 for i = 2 to n do
10 c = a + b
11 a = b
12 b = c

57
Algoritmi şi complexitate Note de curs

13 end for
14 return b
15 end
Notăm cu T2 (n) numărul de adunări efectuate (operaţii de bază) de către algoritmii Fibo2 sau Fibo3 pentru
a calcula Fn . Astfel, T2 (n) = n − 1 ∈ Θ(n). În cazul variantei recursive de implementare, fiecare autoapel al
subprogramului presupune memorarea ı̂n stiva sistem a stării curente a execuţiei sale. Astfel, complexitatea spaţiu
a algoritmului recursiv este mai mare, iar viteza de execuţie este mai mică comparativ cu performanţele algoritmului
iterativ.
Un alt algoritm poate fi obţinut implementând formula explicită (1.3) pentru Fn :
1 /* a ^n , a = numar real diferit de 0 , n = numar natural */
2 function putereRec ( real a , integer n )
3 begin
4 if n == 0 then
5 return 1
6 else
7 return a * putereRec (a , n - 1)
8 end if
9 end
10

11 function Fibo4 ( integer n )


12 begin
13 real phi
14 phi = (1 + sqrt (5) ) /2
15 return ( putereRec ( phi , n ) - putereRec ( -1/ phi , n ) ) / sqrt (5)
16 end
Timpul de execuţie al algoritmului Fibo4 este determinat de cel al algoritmului putereRec. Notăm cu T3 (n)
numărul de ı̂nmulţiri (operaţiile de bază) efectuate de către acesta din urmă; T3 (n) satisface relaţia de recureţă
(1.1). Astfel, T3 (n) ∈ Θ(n), pentru algoritmul putereRec. Cum algoritmul Fibo4 apelează de două ori algoritmul
putereRec, ı̂nseamnă că, numărând aceleaşi operaţii de bază, timpul său de execuţie va fi 2T3 (n). Dar 2T3 (n) ∈
Θ(n), rezultă că algoritmul Fibo4 are o complexitate liniară. Folosind un algoritm mai eficient pentru a calcula ϕn
(de exemplu, un algoritm de complexitate logaritmică), vom obţine o complexitate mai bună a algoritmului Fibo4.

58
Capitolul 2

Tehnici de proiectare a algoritmilor

2.1 Tehnica reducerii


2.1.1 Descrierea metodei
Metoda reducerii (decrease and conquer /reduce and conquer ) este o tehnică de proiectare a algoritmilor prin care
se exploatează relaţia dintre soluţia unei instanţe a problemei, de aceeaşi dimensiune cu problema şi soluţia unei
instanţe ale aceleiaşi probleme, dar de dimensiune mai mică. Spre exemplu, dacă ne propunem să calculăm
factorialul unui număr natural nenul n, putem să folosim relaţia care există ı̂ntre soluţia acestei probleme, de
dimensiune n şi soluţia problemei de dimensiune n − 1, deoarece n! = n(n − 1)!. Folosind această tehnică, la fiecare
iteraţie a algoritmului se rezolvă o singură instanţă a problemei iniţiale.
Rezolvarea unei probleme folosind această abordare, presupune parcurgerea următorilor paşi:

1. reducerea instanţei de aceeaşi dimensiune cu a problemei la o instanţă de dimensiune mai mică a aceleiaşi
probleme (decrease);
2. rezolvarea instanţei mai mici a problemei (conquer );
3. extinderea soluţiei instanţei mai mici pentru a obţine soluţia problemei iniţiale.
Există trei modalităţi de a reduce rezolvarea unei probleme de o anumită dimensiune la rezolvarea unei probleme
similare cu cea originală, dar de dimensiune mai mică:
ˆ reducerea cu un termen constant: reducerea unei instanţe se realizează cu acelaşi termen constant la fiecare
iteraţie a algoritmului, de obicei această constantă fiind egală 1 (de exemplu, algoritmul de calcul al factori-
alului, algoritmul de sortare prin inserţie directă, algoritmul de căutare secvenţială, algoritmii de generare a
permutărilor, a coeficienţilor binomiali sau a submulţimilor unei mulţimi date etc.). De altfel, mulţi algoritmi
iterativi pot fi incluşi ı̂n această categorie;
ˆ reducerea printr-un factor constant: reducerea unei instanţe se realizează printr-un acelaşi factor constant la
fiecare iteraţie a algoritmului, de obicei această constantă fiind egală 2 (de exemplu, algoritmul de căutare
binară, algoritmul de ı̂nmulţire à la russe, algoritmul de identificare a monedei false etc.);
ˆ reducerea cu pas variabil : reducerea unei instanţe se realizează cu un pas variabil la fiecare iteraţie a algorit-
mului (de exemplu, algoritmul lui Euclid, algoritmul de selecţie prin partiţionare etc.).
Reducerea dimensiunii continuă până când se ajunge la o problemă de dimensiune suficient de mică pentru a putea
fi rezolvată direct. Este posibil ca o anumită problemă să poată fi rezolvată atât prin varianta de reducere cu un
termen constant, precum şi prin cea de reducere printr-un factor constant (de exemplu, calculul an , a ∈ R∗ , n ∈ N).
Exploatarea relaţiei dintre instanţe se poate face fie top-down (de sus ı̂n jos – abordare descendentă, ı̂n care valoarea
de calculat se exprimă prin valori anterioare, ce trebuie la rândul lor calculate; această abordare se descrie, de regulă,
recursiv), fie bottom-up (de jos ı̂n sus – abordare ascendentă, ı̂n care se porneşte de la cazul elementar şi se generează
noi valori pe baza celor deja calculate; această abordare se descrie, de regulă, iterativ). Astfel, aceste abordări
conduc către două variante de descriere a algoritmilor corespunzători: recursivă şi iterativă.

59
Algoritmi şi complexitate Note de curs

Exemplul 2.1. Calculul an , a ∈ R∗ , n ∈ N.


A aplica tehnica reducerii cu termenul constant 1 presupune, fie rezolvarea directă a acestei probleme utilizând
∗ ∗
definiţia: a0 = 1 şi an = a
| · a ·{z. . . · a}, a ∈ R , n ∈ N , fie rezolvarea problemei plecând de la observaţia că
n ori
an = a · an−1 , n ∈ N∗ . Vom folosi atât varianta recursivă, cât şi cea iterativă pentru descrierea abordărilor de mai
sus.
1 // tehnica reducerii cu termenul constant 1 , varianta recursiva
2 function putereRec1 ( real a , integer n )
3 begin
4 if n == 0 then
5 return 1
6 else
7 return a * putereRec1 (a , n - 1)
8 end if
9 end
10

11 // tehnica reducerii cu termenul constant 1 , varianta iterativa


12 function putereIter1 ( real a , integer n )
13 begin
14 real p
15 p = 1
16 for i = 1 to n do
17 p = p * a
18 end for
19 return p
20 end
Vom demonstra prin inducţie completă corectitudinea subalgoritmului recursiv putereRec1, plecând de la formula
de recurenţă. Astfel, vom demonstra că rezultatul returnat de subalgoritmul putereRec1(n) este an , pentru orice
număr real a 6= 0 şi pentru orice n ≥ 0, natural. Pentru n = 0, algoritmul returnează 1, deci afirmaţia este adevărată
(a0 = 1). Presupunem că afirmaţia este adevărată pentru orice k ∈ N, 0 ≤ k ≤ n − 1, şi demonstrăm că acest lucru
implică faptul că afirmaţia este adevărată pentru k = n > 0. Conform ipotezei inductive, putereRec1(a, n - 1)
returnează an−1 . Cum n > 0, rezultă că valoarea returnată de subalgoritm este a · an−1 = an (linia 7). Deci
algoritmul este corect, ı̂n ipoteza că se termină ı̂n timp finit. Condiţia de oprire va fi satisfăcută după un număr
finit de apeluri recursive, deoarece dimensiunile instanţelor cu care se autoapelează algoritmul descresc la fiecare
pas.
În cazul subalgoritmului iterativ putereIter1, numărul de ı̂nmulţiri (operaţii de bază) este n, deci T (n) ∈ Θ(n).
Analizăm eficienţa asimptotică a algoritmului putereRec1. Considerăm că operaţia de bază a algoritmului este
ı̂nmulţirea. Notând cu T1 (n) numărul ı̂nmulţirilor efectuate, obţinem:
(
0, n=0
T1 (n) =
T1 (n − 1) + 1, n > 0.

Prin metoda substituţiei inverse se obţine:

T1 (n) = T1 (n − 1) + 1
T1 (n − 1) = T1 (n − 2) + 1
..
.
T1 (1) = T1 (0) + 1
T1 (0) = 0.

Prin sumarea relaţiilor şi reducerea termenilor asemenea, se obţine că T1 (n) = n, deci T1 (n) ∈ Θ(n), obţinând deci
tot o complexitate asimptotică liniară ca ı̂n cazul algoritmului putereIter1.

60
Algoritmi şi complexitate Note de curs

Se pune ı̂ntrebarea dacă se poate proiecta un algoritm mai eficient. Se pleacă de la observaţia că a0 = 1, iar pentru
n ∈ N∗ , (
n (an/2 )2 , dacă n este par
a =
a · (a(n−1)/2 )2 , dacă n este impar.
Astfel, calculele se ı̂njumătăţesc la fiecare nivel al recurenţei, obţinându-se o complexitate mai bună. Vom descrie
această abordare atât ı̂n variantă recursivă , cât şi ı̂n cea iterativă.
1 // tehnica reducerii prin factorul constant 2 , varianta recursiva
2 function putereRec2 ( real a , integer n )
3 begin
4 real p
5 if n == 0
6 return 1
7 end if
8 p = putereRec2 (a , n / 2)
9 if n % 2 == 1 then
10 return a * p * p
11 else
12 return p * p
13 end if
14 end
15

16 // tehnica reducerii prin factorul constant 2 , varianta iterativa


17 function putereIter2 ( real a , integer n )
18 begin
19 real p
20 p = 1
21 while n > 0 do
22 if n % 2 == 1 then
23 p = p * a
24 end if
25 a = a * a
26 n = n / 2
27 end while
28 return p
29 end
Verificăm corectitudinea algoritmului putereRec2. Demonstrăm prin inducţie completă că rezultatul returnat de
subalgoritmul putereRec2(n) este an , pentru orice număr real a 6= 0 şi pentru orice n ≥ 0, natural. Pentru n = 0,
algoritmul returnează 1 (cazul elementar), deci afirmaţia este adevărată. Presupunem că afirmaţia este adevărată
pentru orice k ∈ N, 0 ≤ k ≤ n − 1, şi demonstrăm că acest lucru implică faptul că afirmaţia este adevărată pentru
k = n > 0. Dacă n este par, putereRec2(a, n) returnează p ∗ p (linia 12), unde p este valoarea returnată
de putereRec2(a, n/2). Cum pentru n > 0 număr par, este ı̂ndeplinită condiţia 0 < n/2 ≤ n − 1, atunci,
conform ipotezei inductive putereRec2(a, n/2) va returna an/2 . Deci algoritmul putereRec2(a, n) returnează
p ∗ p = an/2 · an/2 = an . După cum a fost definit operatorul de ı̂mpărţire ı̂ntre doi ı̂ntregi, pentru n impar,
n/2 = (n − 1)/2. Dacă n > 0 este impar, are loc 0 ≤ (n − 1)/2 ≤ n − 1. Ţinând cont de ipoteza inductivă, valoarea
returnată de putereRec2(a, n/2) este p = a(n−1)/2 , iar pentru n impar, algoritmul returnează a ∗ p ∗ p (linia
10). Deci valoarea returnată de algoritmul putereRec2(a, n) este a ∗ a(n−1)/2 ∗ a(n−1)/2 = an . Astfel, algoritmul
este corect, dacă acesta este finit. Dar, analog cazului precedent, şirul autoapelurilor recursive este finit.
Vom enunţa un rezultat important pentru analiza complexităţii asimptotice a algoritmilor elaboraţi prin metoda
reducerii printr-un factor constant şi prin tehnica divizării numit Teorema Master . Plecăm de la presupunerea
că, pentru a fi rezolvată, o problemă de dimensiune n este descompusă ı̂n b subprobleme de aceeaşi dimensiune,
egală cu n/b. De asemenea, presupunem că a subprobleme de acest tip necesită să fie rezolvate (a şi b sunt
constante naturale, a ≥ 1, b > 1). Considerăm că divizarea problemei iniţiale ı̂n b subprobleme de dimensiune n/b
şi compunerea soluţiilor subproblemelor rezolvate au ı̂mpreună costul f (n), iar costul rezolvării cazului elementar

61
Algoritmi şi complexitate Note de curs

(n ≤ n0 ) este o constantă T0 . Astfel, timpul de execuţie al algoritmului satisface recurenţa


(
T0 , dacă n ≤ n0
T (n) =
aT (n/b) + f (n), dacă n > n0.

În mod evident, ordinul de creştere al lui T (n) depinde de ordinul de creştere a lui f (n) şi de valorile constantelor
a şi b.
Teorema 2.1. (Teorema Master) Dacă f (n) ∈ Θ(nd ), d ≥ 0, atunci

d
Θ(n ),
 dacă a < bd
T (n) ∈ Θ(nd log n), dacă a = bd

Θ(nlogb a ), dacă a > bd .

Teorema rămâne valabilă şi pentru notaţiile asimptotice O şi Ω.


De asemenea, un alt rezultat important pentru estimarea ordinului de creştere a timpului de execuţie T (n) este
următoarea propoziţie, cunoscută ı̂n literatura de specialitate sub numele de “Smoothness rule”.
Propoziţia 2.1. Presupunem că există n0 ∈ N astfel ı̂ncât T (n) ≤ T (n + 1) pentru orice n ≥ n0 şi că T (n) ∈
Θ(g(n)) pentru n = cm , cu m ≥ 0 şi c ≥ 2, naturale. De asemenea, presupunem că există n1 ∈ N astfel ı̂ncât
g(n) ≤ g(n + 1) pentru orice n ≥ n1 şi că g(kn) ∈ Θ(g(n)) pentru orice constantă k > 0. Atunci T (n) ∈ Θ(g(n)),
pentru n arbitrar.
Acelaşi rezultat se păstrează şi pentru celelalte două notaţii asimptotice uzuale, O şi Ω.
Folosind rezultatele de mai sus, studiem eficienţa asimptotică a algoritmului putereRec2. Luând ı̂n calcul doar
operaţiile de ı̂nmulţire, timpul de execuţie al algoritmului satisface recurenţa

0,
 n=0
T2 (n) = T2 (n/2) + 1, n > 0, n par

T2 ((n − 1)/2) + 2, n > 0, n impar.

Să considerăm pentru ı̂nceput că dimensiunea problemei este o putere a lui 2, n = 2m , m ∈ N. Astfel, timpul de
execuţie al algoritmului va satisface recurenţa
(
0, n=0
T2 (n) =
T2 (n/2) + Θ(1), n > 0.

Aplicăm Teorema 2.1: cum a = 1, b = 2 şi d = 0, rezultă că ne aflăm ı̂n cazul al 2-lea al teoremei (1 =
a = bd = 20 = 1) şi deci T2 (n) ∈ Θ(n0 log n) = Θ(log n), pentru n o putere a lui 2. Folosind Propoziţia 2.1,
deoarece T2 (n) şi g(n) = log2 (n) sunt crescătoare pentru n suficient de mare, iar pentru orice constantă k > 0,
g(kn) = log2 (kn) = log2 n + log2 k ∈ Θ(log2 n), rezultă că rezultatul obţinut pentru cazul particular n = 2m este
valabil pentru n arbitrar. Aşadar, T2 (n) ∈ Θ(log n).
Observăm că, folosind tehnica reducerii prin factorul constant 2 se obţine un algoritm mai eficient, de complexitate
logaritmică.

2.1.2 Aplicaţii
Exemplul 2.2. (Căutarea binară) Considerăm problema căutării unei valori x ı̂ntr-un tablou v cu n elemente reale.
Se pleacă de la presupunerea că vectorul este sortat crescător. Se alege un element al tabloului, v[k]. Valoarea
căutată se compară cu v[k]. Dacă acestea coincid, căutarea se opreşte. Dacă valoarea x este mai mică decât
elementul v[k], căutarea va continua la stânga elementului v[k], ı̂ntre elementele v[0], v[1], . . . , v[k − 1]; altfel, dacă
x este mai mare decât elementul v[k], căutarea va continua la dreapta elementului v[k], ı̂n subtabloul v[k + 1], v[k +
2], . . . , v[n − 1]. Pentru ca dimensiunea problemei să fie redusă prin factorul constant 2, alegerea naturală pentru
v[k] este ca acesta să fie cât mai aproape de mijlocul vectorului. Metoda de căutare poartă numele de căutare
binară.

62
Algoritmi şi complexitate Note de curs

1 function cautareBin ( real v [] , integer stang , integer drept , real x )


2 begin
3 integer mijloc
4 if stang > drept then
5 return false
6 else
7 mijloc = ( stang + drept ) / 2
8 if v [ mijloc ] == x then
9 return true
10 else
11 if x < v [ mijloc ] then
12 return cautareBin (v , stang , mijloc - 1 , x )
13 else
14 return cautareBin (v , mijloc + 1 , drept , x )
15 end if
16 end if
17 end if
18 end
19

20 algorithm Cautare Binara


21 begin
22 integer n
23 real v [0.. n -1] , x
24 read n , v , x
25 write cautareBin (v , 0 , n - 1 , x )
26 end
De exemplu, considerăm vectorul v de lungime 10, sortat crescător, v[ ] = {4, 9, 12, 19, 25, 36, 42, 56, 63, 78}.
Presupunem că valoarea căutată este x = 63. La fiecare pas al algoritmului, căutarea se realizează ı̂n subtabloul
v[stang..drept], unde stang şi drept sunt actualizate la fiecare nivel al recursivităţii.
Pasul 1: se apelează cautareBin(v, 0, 9, 63). Astfel, iniţial, stang = 0, iar drept = n − 1 = 10 − 1 = 9. Cum
0 < 9, se calculează indexul elementului din “mijlocul” vectorului după formula mijloc = (stang + drept)/2, deci
mijloc = (0 + 9)/2 = 4, mijloc fiind un număr natural. Apoi, se compară valoarea căutată 63 cu elementul aflat
pe poziţia din “mijlocul” vectorului, v[4] = 25. Pentru că 63 > 25, căutarea se mută la dreapta elementului v[4],
adică ı̂n subtabloul format din elementele v[5..9] = {36, 42, 56, 63, 78} (stang = mijloc + 1 = 4 + 1 = 5).
Pasul 2: se apelează cautareBin(v, 5, 9, 63). Cum 5 < 9, se determină mijloc = (5 + 9)/2 = 7, deci
v[7] = 56 < 63. Căutarea continuă la dreapta elementului v[7], adică ı̂n subtabloul v[8..9] = {63, 78} (stang =
mijloc + 1 = 7 + 1 = 8).
Pasul 3: se apelează cautareBin(v, 8, 9, 63). Cum 8 < 9, se calculează mijloc = (8 + 9)/2 = 8. Cum
v[8] = 63, apelul curent al subalgoritmului recursiv se opreşte, returnând valoarea true.
Lanţul de autoapeluri este reluat ı̂n sens invers, ı̂ncheind-se apelurile care au fost puse ı̂n aşteptare.
Vom analiza eficienţa algoritmului de căutare binară numărând de câte ori valoarea căutată x a fost comparată cu
un element al tabloului. Numărul de astfel de comparaţii depinde nu numai de dimensiunea n a problemei, ci şi de
proprietăţile datelor de intrare. Notăm cu T (n) numărul de comparaţii efectuate de către algoritm. În cazul cel
mai defavorabil, obţinem relaţia de recurenţă
(
1, n=1
T (n) =
T ([n/2]) + Θ(1), n > 1.

Să considerăm pentru ı̂nceput că n = 2m , m ∈ N. Aplicăm Teorema 2.1: a = 1, b = 2, iar d = 0 (f (n) ∈ Θ(1)).
Aşadar, a = bd şi deci T (n) ∈ O(nd log n) = O(log n), pentru n o putere a lui 2. Deoarece T (n) şi g(n) = log2 (n)
sunt crescătoare pentru n suficient de mare, iar pentru orice constantă k > 0, g(kn) = log2 (kn) = log2 n + log2 k ∈
Θ(log2 n), rezultă că rezultatul obţinut pentru cazul particular n = 2m , este valabil pentru n arbitrar, conform
Propoziţiei 2.1. Rezultă că T (n) ∈ O(log n).
Desigur că algoritmul de căutare binară poate fi implementat şi iterativ:

63
Algoritmi şi complexitate Note de curs

1 function cautareBinIter ( integer n , real v [] , real x )


2 begin
3 integer stang , drept , mijloc
4 bool gasit
5 stang = 0
6 drept = n - 1
7 gasit = false
8 while stang <= drept && gasit == false do
9 mijloc = ( stang + drept ) / 2
10 if v [ mijloc ] == x then
11 gasit = true
12 else
13 if x < v [ mijloc ] then
14 drept = mijloc - 1
15 else
16 stang = mijloc + 1
17 end if
18 end if
19 end while
20 return gasit
21 end

Exemplul 2.3. (Aproximarea numerică a soluţiilor ecuaţiilor algebrice şi a celor transcendente folosind metoda
bisecţiei, numită şi metoda ı̂njumătăţirii intervalului) O ecuaţie de forma

ax + b = 0

se numeşte liniară. Orice ecuaţie care nu are această formă se numeşte neliniară, putând fi scrisă sub forma

f (x) = 0.

Dacă funcţia f (x) are forma unui polinom sau poate fi adusă la această formă, ecuaţia se numeşte algebrică. În
caz contrar, ecuaţia se numeşte transcendentă. De exemplu, ecuaţiile:

x2 − 2 = 0,

x3 − 2x2 + 3x − 1 = 0,
sunt ecuaţii algebrice, iar ecuaţiile: x x x
4.5 cos cos − = 0, (2.1)
3 3 4
x3 − x2 − 3 arctg(x) + 1 = 0, (2.2)
x
e − 1 = 0, (2.3)
sunt transcendente.
Un punct ξ din intervalul de definiţie al lui f cu proprietatea că f (ξ) = 0 se numeşte zerou al funcţiei f sau rădăcină
a ecuaţiei f (x) = 0. Metodele de rezolvare numerică a ecuaţiilor neliniare se ı̂mpart ı̂n două mari categorii: metode
de partiţionare şi metodele aproximaţiilor succesive.
În cazul metodelor de partiţionare, intervalul curent ı̂n care se caută rădăcina, este micşorat progresiv, până când
lungimea acestuia este suficient de mică pentru a satisface o anumită precizie impusă. Dintre aceste metode
amintim: metoda bisecţiei (sau a ı̂njumătăţirii intervalului) şi metoda secantei. Acestea se caracterizează printr-o
convergenţă lentă, dar rădăcina este ı̂ntotdeauna izolată ı̂ntr-un interval de lungime suficient de mică. Metodele
aproximaţiilor succesive pornesc de la o aproximaţie iniţială, care este apoi ı̂mbunătăţită ı̂n paşi succesivi şi, ı̂n
anumite condiţii, converge către soluţia exactă. Aceste metode sunt, de regulă, mai rapide decât metodele de
partiţionare, dar, ı̂n anumite situaţii, pot să nu conveargă către soluţia exactă.

64
Algoritmi şi complexitate Note de curs

Metodele de partiţionare se pretează utilizării tehnicii reducerii. Pentru exemplificare, ne oprim asupra metodei
bisecţiei, cea mai simplă metodă de rezolvare numerică a ecuaţiilor algebrice şi transcendente. Presupunem că f
este continuă pe [a, b] (interval ce este inclus ı̂n intervalul de definiţie al funcţiei f ) şi că, ı̂n extremităţile intervalului,
funcţia ia valori de semne contrare, adică f (a)f (b) < 0. Atunci există o rădăcină exactă ξ a ecuaţiei f (x) = 0 ce
aparţine intervalului (a, b). Folosirea metodei bisecţiei presupune parcurgerea următorilor paşi:

ˆ intervalul [a, b] se ı̂njumătăţeşte: se calculează c = (a + b)/2, mijlocul intervalului. Dacă f (c) = 0, rădăcina
căutată este c şi algoritmul se opreşte;
ˆ dacă produsul f (a)f (c) < 0 atunci rădăcina se găseşte ı̂ntre a şi c. Astfel, c devine extremitatea dreaptă a
intervalului şi procedeul se reia;

ˆ dacă f (c)f (b) < 0, rădăcina se găseşte ı̂ntre c şi b. În acest caz, c devine extremitatea stângă a intervalului
şi se reia procedeul.
Algoritmul este unul aproximativ, adică nu furnizează neapărat soluţia exactă a problemei, ci doar una aproximativă.
Ne propunem determinarea aproximaţiei ξaprox a rădăcinii exacte ξ, cu o precizie prestabilită ε > 0 (ε reprezintă
valoarea maximă tolerată a erorii de aproximare).
Notăm cu c1 = (a + b)/2 mijlocul intervalului iniţial, cel de la iteraţia 1, [a, b] = [a1 , b1 ] ş.a.m.d., cu cn mijlocul
intervalului de la iteraţia n, [an , bn ]. Cum nu ne aşteptăm să găsim neapărat soluţia exactă, nu putem folosi drept
criteriu de oprire condiţia f (cn ) = 0, ci mai degrabă |f (cn )| < ε. Această schemă se aplică ı̂n mod repetat până
cand lungimea intervalului [an , bn ] este suficient de mică, astfel ı̂ncât eroarea absolută a aproximării soluţiei exacte
ξ, prin ξaprox = cn , să fie mai mică decât ε > 0. Cum cn este mijlocul intervalului [an , bn ], iar ξ ∈ [an , bn ], rezultă
că
bn − an
|cn − ξ| ≤ .
2
bn − an
Aşadar, am identificat un alt criteriu de oprire: < ε.
2
Presupunem că funcţia f este descrisă algoritmic şi că datele de intrare sunt validate prin testare (datele de intrare
a şi b, a < b, sunt furnizate astfel ı̂ncât f (a)f (b) < 0).
Descriem algoritmul recursiv:
1 function bisectieRec ( real a , real b , real eps )
2 begin
3 real c
4 c = (a + b) / 2
5 if abs ( f ( c ) ) < eps || ( b - a ) /2 < eps then
6 return c
7 end if
8 if f ( a ) * f ( c ) < 0 then
9 return bisectieRec (a , c , eps ) ;
10 else
11 return bisectieRec (c , b , eps ) ;
12 end if
13 end
Acelaşi algoritm descris iterativ:
1 function bisectieIter ( real a , real b , real eps )
2 begin
3 real c
4 repeat
5 c = (a + b) / 2
6 if abs ( f ( c ) ) < eps then
7 return c
8 end if
9 if f ( a ) * f ( c ) < 0 then

65
Algoritmi şi complexitate Note de curs

10 b = c
11 else
12 a = c
13 end if
14 until ( b - a ) /2 < eps
15 return c
16 end
Se observă că
b−a
|cn − ξ| ≤ , n = 1, 2, . . . .
2n
Deci vom obţine o aproximare a soluţiei exacte cu atât mai bună, cu cât n, numărul de iteraţii, este
 mai mare.
b−a
Numărul de iteraţii necesar pentru a obţine o soluţie aproximativă cu precizia ε, satisface n > log2 .
ε
Observaţie. Algoritmul poate fi testat pentru ecuaţia transcendentă (2.1) pentru [a, b] = [2, 4], ecuaţia (2.2)
pentru [a, b] = [−2, −1], respectiv ecuaţia (2.3) pentru [a, b] = [−1, 1].
Exemplul 2.4. (Problema selecţiei prin partiţionare) Problema constă ı̂n determinarea celui mai mic element de
rang k, k = 1, 2, . . . , n, dintr-un tablou cu n elemente. De exemplu, cel mai mic element de rang 1 din tablou
coincide cu cel mai mic element al tabloului, cel mai mic element de rang 2 este al doilea cel mai mic element din
tablou, cel mai mic element de rang 3 este al treilea cel mai mic element din tablou ş.a.m.d., cel mai mic element
de rang n din tablou coincide cu cel mai mare element al tabloului. Acest element poartă numele de statistică de
ordine de rang k. O problemă importantă este aceea a determinării elementului din tablou care este mai mare sau
egal cu jumătate dintre elementele tabloului şi mai mic sau egal cu cealaltă jumătate, adică a celui mai mic element
de rang k, unde k = n/2, dacă n este număr par şi k = (n + 1)/2, dacă n este impar. Un astfel de element ı̂l vom
numi elementul median al tabloului dat.
Într-o primă abordare, putem rezolva problema determinării celui mai mic element de rang k al unui tablou cu n
elemente, sortând crescător tabloul şi selectând apoi cel mai mic element de rang k din tabloul sortat. Timpul de
execuţie al unui astfel de algoritm depinde de eficienţa algoritmului de sortare utilizat.
O altă idee ar fi ca tabloul dat să fie partiţionat ı̂n două părţi, ı̂n jurul unui element numit pivot. Vom considera
pivotul ca fiind primul element al tabloului.

În general, această idee constă ı̂ntr-o rearanjare a elementelor tabloului astfel ı̂ncât prima parte a acestuia va
conţine elemente mai mici sau egale cu pivotul, urmează apoi pivotul ı̂nsuşi, iar după acesta se vor regăsi elemente
care sunt mai mari sau egale cu pivotul.

Vom utiliza algoritmul lui Lomuto de partiţionare: un subtablou a[stang..drept], cu 0 ≤ stang ≤ drept ≤ n − 1,
este privit ca fiind format din trei zone contigue de elemente ce urmează după pivot – prima parte conţine toate
elementele strict mai mici decât pivotul, a doua parte, elementele mai mari sau egale cu pivotul, iar ultima zonă
conţine elementele ce nu au fost ı̂ncă comparate cu pivotul (ı̂n continuare o vom numi zona test). Să remarcăm
faptul că oricare dintre aceste zone poate fi vidă; de exemplu, ı̂nainte de a ı̂ncepe execuţia algoritmului, primele
două zone sunt vide, iar zona test conţine toate elementele tabloului, cu excepţia pivotului.
Iniţial, pivotul este a[stang]. Începând cu i = stang + 1, algoritmul presupune parcurgerea tabloului de la stânga
la dreapta, până când se obţine o partiţionare ı̂n jurul pivotului. La fiecare iteraţie, se compară primul element
din zona test (acest element are indicele i) cu pivotul. Dacă a[i] ≥ pivot, se incrementează i pentru a extinde zona
elementelor mai mari sau egale cu pivotul, ı̂n timp ce zona test se micşorează. Dacă a[i] < pivot, atunci trebuie
extinsă zona ce conţine elementele strict mai mici decât pivotul. Astfel, va fi incrementat indicele s, al ultimului
element din prima zonă, apoi vor fi interschimbate elementele a[i] şi a[s] şi va fi incrementat indicele i, pentru a
indica spre noul prim element din zona test.

66
Algoritmi şi complexitate Note de curs

După ce zona test devine vidă, algoritmul interschimbă pivotul a[stang] cu elementul a[s], obţinându-se astfel
partiţionarea tabloului ı̂n jurul pivotului.

Algoritmul Lomuto de partiţionare este:


1 function LomutoPartition ( integer a [] , integer stang , integer drept )
2 begin
3 integer aux , p , s , i
4 p = a [ stang ] // pivotul
5 s = stang
6 for i = stang + 1 to drept do
7 if a [ i ] < p then
8 s = s + 1
9 aux = a [ s ]
10 a[s] = a[i]
11 a [ i ] = aux
12 end if
13 end for
14 aux = a [ stang ]
15 a [ stang ] = a [ s ]
16 a [ s ] = aux
17 return s // indexul pivotului
18 end
După cum se observă, după partiţionare, pivotul are indicele s (s poartă numele de poziţie de partiţionare). Notăm
cu m = s − stang, numărul de elemente din prima zonă, a elementelor mai mici decât pivotul. Dacă m = k − 1,
atunci, ı̂n mod evident, pivotul este cel mai mic element de rang k al subtabloului a[stang..drept]. Dacă m > k − 1,
cel mai mic element de rang k al subtabloului a[stang..drept] poate fi determinant ca fiind cel mai mic element de
rang k din partea stângă a subtabloului partiţionat, a[stang..s − 1]. Dacă m < k − 1, cel mai mic element de rang
k al subtabloului a[stang..drept] poate fi determinant ca fiind cel mai mic element de rang (k − m − 1) din partea
dreaptă a subtabloului partiţionat, a[s + 1..drept]. Acesta este algoritmul de selecţie prin partiţionare, ce poartă
numele şi de quickselect.
1 function quickselect ( integer a [] , integer stang , integer drept , integer k )
2 // 1 <= k <= ( drept - stang + 1)
3 begin
4 integer s
5 s = LomutoPartition (a , stang , drept ) // determinarea pozitiei de partitionare
6 if stang == drept then
7 return a [ stang ]
8 if s - stang == k - 1 then
9 return a [ s ]
10 else
11 if s - stang > k - 1 then

67
Algoritmi şi complexitate Note de curs

12 return quickselect (a , stang , s - 1 , k )


13 else
14 return quickselect (a , s + 1 , drept , k - s + stang - 1)
15 end if
16 end if
17 end
18

19 algorithm Cel mai mic element de rang k


20 begin
21 integer n , k
22 real a [0.. n -1]
23 read n , k , a
24 write quickselect (a , 0 , n - 1 , k )
25 end
De exemplu, ne propunem să determinăm elementul median al tabloului a[ ] = {4, 3, 11, 8, 7, 12, 9, 0, 13}. Cum
tabloul are 9 elemente, trebuie să determinăm cel mai mic element de rang k = 5 = (9 + 1)/2 din tablou. Acest
element va fi mai mare sau egal cu 4 elemente ale tabloului şi mai mic sau egal tot cu 4 elemente.
Se va apela deci quickselect(a, 0, 8, 5). Astfel, stang = 0, drept = 8 şi se va determina poziţia de partiţionare
prin apelul algoritmului LomutoPartition(a, 0, 8). Urmărim mai jos paşii algoritmului:
4 3 11 8 7 12 9 0 13 (pivot = a[0] = 4)
s i
4 3 11 8 7 12 9 0 13
si
4 3 11 8 7 12 9 0 13
s i
4 3 11 8 7 12 9 0 13
s i
4 3 11 8 7 12 9 0 13
s i
4 3 11 8 7 12 9 0 13
s i
4 3 11 8 7 12 9 0 13
s i
4 3 11 8 7 12 9 0 13
s i
4 3 11 8 7 12 9 0 13
s i
4 3 0 8 7 12 9 11 13
s i
4 3 0 8 7 12 9 11 13
s i
Se interschimbă pivotul a[0] cu a[2] şi se returnează poziţia de partiţionare, s = 2. Tabloul devine:
0 3 4 8 7 12 9 11 13
Cum m = s−stang = 2−0 = 2 şi k−1 = 4, continuăm cu partea dreaptă a tabloului, cea aflată la dreapta pivotului.
Astfel, se va apela quickselect(a, 3, 8, 2) (k − s + stang − 1 = 5 − 2 + 0 − 1 = 2). Astfel, stang = 3, drept = 8
şi se va determin poziţia de partiţionare prin apelul algoritmului LomutoPartition(a, 3, 8). Paşii algoritmului
ı̂i urmărim mai jos:
8 7 12 9 11 13 (pivot = a[3] = 8)
s i
8 7 12 9 11 13
si
8 7 12 9 11 13
s i

68
Algoritmi şi complexitate Note de curs

8 7 12 9 11 13
s i
8 7 12 9 11 13
s i
8 7 12 9 11 13
s i
Se interschimbă pivotul a[3] cu a[4] şi se returnează poziţia de partiţionare, s = 4. Subtabloul devine:
7 8 12 9 11 13
Cum m = s − stang = 4 − 3 = 1 şi k − 1 = 2 − 1 = 1, rezultă că apelul curent al algoritmului quickselect
returnează a[4] = 8. Într-adevăr, elementul 8 este mai mare decât 4 elemente ale tabloului a, {0, 3, 4, 7} şi este mai
mic tot decât 4 valori ale acestuia, {9, 11, 12, 13}.
Pentru a partiţiona un vector cu n componente sunt necesare n − 1 comparaţii ı̂ntre elementele tabloului. Dacă
după partiţionare nu mai sunt necesare iteraţii pentru a returna elementul aşteptat, se obţine o complexitate liniară,
T (n) ∈ Ω(n) (cazul cel mai favorabil). Cazul cel mai defavorabil corespunde partiţionării dezechilibrate a tabloului
ı̂n toate cele n − 1 iteraţii, adică o parte nu conţine niciun element, iar cealaltă conţine toate celelalte elemente ale
subtabloului, mai puţin pivotul (de exemplu, pentru un tablou strict crescător şi k = n). Atunci

(n − 1)n
T (n) = (n − 1) + (n − 2) + · · · + 1 = .
2
Deci T (n) ∈ O(n2 ). Am obţinut deci o eficienţă comparabilă cu a algoritmilor elementari de sortare studiaţi.
Utilitatea practică a algoritmului quickselect este dată de eficienţa algoritmului ı̂n cazul mediu, care s-a demonstrat
că este una liniară. De asemenea, există alte metode mai eficiente de a alege pivotul care conduc la o complexitate
liniară chiar şi ı̂n cel mai defavorabil caz.

2.2 Tehnica divizării


2.2.1 Descrierea metodei
Tehnica divizării (divide and conquer /divide et impera/divide şi stăpâneşte) este una dintre cele mai cunoscute
strategii de proiectare a algoritmilor. Această metodă presupune descompunerea problemei de rezolvat ı̂n cel puţin
două subprobleme independente, de regulă similare cu cea iniţială, dar de dimensiuni mai mici. Subproblemele
rezultate sunt rezolvate, de regulă, recursiv. Apoi, dacă este cazul, soluţiile subproblemelor sunt combinate pen-
tru a obţine astfel soluţia problemei iniţiale. Rezolvarea unei probleme folosind această metodă, presupune deci
parcurgerea următorilor paşi:
ˆ problema este descompusă ı̂n două sau mai multe subprobleme independente (fiecare problemă este rezolvată
cel mult o dată), de aproximativ aceeaşi dimensiune (divide);
ˆ fiecare dintre subproblemele independente este rezolvată; ı̂n măsura ı̂n care acestea sunt similare problemei
iniţiale, se aplică din nou tehnica divizării, până când se ajunge la subprobleme de dimensiune suficient de
mică ce pot fi rezolvate direct (conquer );
ˆ dacă este necesar, soluţiile subproblemelor sunt combinate pentru a obţine soluţia problemei iniţiale.

Metoda divizării este asemănătoare metodei reducerii, doar că rezolvarea problemei iniţiale presupune rezolvarea,
la fiecare pas, a cel puţin două subprobleme independente de dimensiune relativ egală şi, eventual, compunerea
rezultatelor acestora ı̂n scopul obţinerii soluţiei problemei iniţiale. În cazul metodei reducerii, rezolvarea unei
probleme se reducea la rezolvarea unei singure subprobleme de dimensiune mai mică. Există autori care consideră
tehnica reducerii printr-un factor constant un caz particular al tehnicii divizării, ı̂n care doar o subproblemă este
necesar să fie rezolvată. În abordările moderne sunt considerate ı̂nsă a fi două paradigme diferite (vezi, de exemplu,
[7]). De altfel, există mult mai mulţi algoritmi care au la bază tehnica reducerii decât tehnica divizării.
Algoritmul general de rezolvare a unei probleme de dimensiune n, aplicând tehnica divizării:

69
Algoritmi şi complexitate Note de curs

1 function DeI ( P ( n ) ) // DeI = divide et impera


2 begin
3 if n <= n0 then // cazul elementar
4 rez = < rezolva direct P ( n ) >
5 else
6 // pasul de divizare
7 < descompune P ( n ) in b subprobleme P (n1 ) ,... , P (nb ) independente >
8 // pasul de rezolvare
9 for i = 1 to b do
10 rez i = DeI ( P (ni ) ) // rezolva subproblema P ( ni )
11 end for
12 // pasul de combinare
13 rez = < combina solutiile rez 1 ,... , rez b >
14 end if
15 return rez
16 end
Paradigma divide et impera stă la baza proiectării unor algoritmi eficienţi, cum ar fi algoritmii de sortare quicksort
şi mergesort. De asemenea, tehnica divizării este utilizată ı̂n programarea paralelă pe mai multe procesoare,
subproblemele fiind rezolvate pe maşini diferite. Pentru algoritmii proiectaţi folosind această metodă, există atât
variante recursive, cât şi iterative de descriere, cea mai utilizată fiind varianta recursivă.
Exemplul 2.5. (Maximul unui şir de numere ı̂ntregi) Să considerăm un tablou a de n numere ı̂ntregi. Vrem să
determinăm cel mai mare element al vectorului a. Algoritmul DeI se bazează pe faptul că maximul unei secvenţe
de elemente {ai , ai+1 , ..., am , am+1 , ..., aj } este egal cu cel mai mare dintre maximul subsecvenţei {ai , ai+1 , ..., am }
şi maximul subsecvenţei {am+1 , ..., aj }, unde m indică aproximativ mijlocul secvenţei iniţiale.
1 function maximVectorDeI ( integer a [] , integer stang , integer drept )
2 begin
3 integer max , mijloc , maxstang , maxdrept
4 if stang == drept then // cazul elementar
5 max = a [ stang ]
6 else
7 mijloc = ( stang + drept ) /2 // divizarea
8 // rezolvarea subproblemelor
9 maxStang = maximVectorDeI (a , stang , mijloc )
10 maxDrept = maximVectorDeI (a , mijloc + 1 , drept )
11 if maxStang < maxDrept then // combinarea rezultatelor
12 max = maxDrept
13 else
14 max = maxStang
15 end if
16 end if
17 return max
18 end
Subalgoritmul maximVectorDeI, proiectat folosind metoda divide et impera, se apelează cu stang = 0 şi drept =
n − 1, dimensiunea vectorului fiind n. Problema iniţială este ı̂mpărţită ı̂n două subprobleme independente (b = 2),
ce vor fi rezolvate recursiv, până când se ajunge ca secvenţa de elemente pentru care se caută maximul să aibă un
singur element (cazul elementar).
În figura de mai jos, se pot urmări paşii algoritmului apelat pentru tabloul a[ ] = {8, 2, 1, 7, 5, 0, 4, 3}, cu 8 elemente
(maximVectorDeI(a,0,7)):

70
Algoritmi şi complexitate Note de curs

Teorema Master (Teorema 2.1) este principalul rezultat utilizat pentru analiza eficienţei asimptotice a algoritmi-
lor elaboraţi folosind tehnica divizării. Astfel, pentru algoritmul maximVectorDeI, presupunem că n, dimensiunea
problemei, este o putere a lui 2. Relaţia de recurenţă satisfăcută de timpul de execuţie al algoritmului este:
(
T0 , dacă n = 1
T (n) =
2T (n/2) + f (n), dacă n > 1.

f (n) ∈ Θ(1), deoarece pasul de divizare presupune calculul unui indice care ı̂mparte tabloul ı̂n doi subvectori, iar
timpul necesar combinării soluţiilor este tot constant, deci d = 0. Cum b = 2 (problema este divizată ı̂n două
subprobleme independente) şi a = 2 (ambele subprobleme sunt rezolvate la fiecare pas), atunci 2 = a > bd = 1,
iar T (n) ∈ Θ(nlog2 2 ) = Θ(n), pentru n o putere a lui 2. Cum ipotezele Propoziţiei 2.1 sunt ı̂ndeplinite, rezultă că
T (n) ∈ Θ(n), pentru n arbitrar. Nu se justifică deci utilizarea tehnicii divizării pentru această problemă, deoarece
se obţine aceeaşi complexitate asimptotică ca ı̂n cazul algoritmului elaborat prin forţa brută.
Exemplul 2.6. (Cel mai mare divizor comun al n numere naturale nenule) Vom păstra cele n numere naturale
ı̂ntr-un vector a şi ne vom folosi de observaţia că, pentru o subsecvenţă {ai , ai+1 , ..., am , am+1 , ..., aj }, este adevărată
relaţia

cmmdc(ai , ai+1 , ..., am , am+1 , ..., aj ) = cmmdc(cmmdc(ai , ai+1 , ..., am ), cmmdc(am+1 , am+2 , ..., aj )),

unde m reprezintă aproximativ mijlocul secvenţei iniţiale.


1 function Euclid ( integer x , integer y )
2 begin
3 if x == y then
4 return x
5 else
6 if x > y then
7 return Euclid ( x - y , y )
8 else
9 return Euclid (x , y - x )
10 end if
11 end if
12 end
13

14 function cmmdcDeI ( integer a [] , integer stang , integer drept )

71
Algoritmi şi complexitate Note de curs

15 begin
16 integer mijloc , c , c1 , c2
17 if drept - stang <= 1 then
18 c = Euclid ( a [ stang ] , a [ drept ])
19 else
20 mijloc = ( stang + drept ) /2 // divizarea
21 // rezolvarea subproblemelor
22 c1 = cmmdcDeI (a , stang , mijloc )
23 c2 = cmmdcDeI (a , mijloc +1 , drept )
24 c = Euclid ( c1 , c2 ) // combinarea solutiilor
25 end if
26 return c
27 end
Subalgoritmul cmmdcDeI, proiectat folosind metoda divide et impera, se apelează cu stang = 0 şi drept = n − 1, n
fiind dimensiunea vectorului a.

2.2.2 Algoritmi de sortare bazaţi pe tehnica divizării


Revenim la problema sortării ı̂n ordine crescătoare a unei secvenţe de n numere reale. Reamintim faptul că
algoritmii elementari de sortare au ordinul de complexitate O(n2 ). Ne propunem să folosim tehnica divizării
pentru a elabora noi algoritmi de sortare: sortarea prin interclasare (mergesort) şi sortarea rapidă (quicksort).
Tehnica presupune descompunerea şirului iniţial ı̂n două subsecvenţe, aplicarea aceleiaşi tehnici de sortare fiecărei
subsecvenţe ı̂n parte şi, ı̂n final, combinarea subsecvenţelor obţinute pentru a obţine ordonarea şirului iniţial. Vom
continua cu descompunerea subsecvenţelor până când se ajunge la cazul elementar al unui singur element sau al
unui subşir vid, care sunt implicit ordonate şi problema poate fi rezolvată direct. Există şi variante de algoritmi ı̂n
care descompunerea subsecvenţelor continuă până când se obţin probleme de dimensiune suficient de mică, pentru
care se aplică algoritmi de sortare elementari (de exemplu, sortarea prin inserţie directă).

2.2.2.1 Sortarea prin interclasare (mergesort)


Descrierea algoritmului. Algoritmul a fost propus de John von Neumann ı̂n 1945, având la bază tehnica divide
et impera. Paşii algoritmului sunt: se ı̂mparte tabloul de elemente ı̂n două subşiruri de lungimi aproximativ egale.
Cele două subşiruri vor fi ordonate recursiv prin aplicarea aceleiaşi tehnici de sortare mergesort. Apoi acestea
vor fi combinate (interclasate), rezultând astfel tabloul iniţial sortat. Interclasarea presupune parcurgerea celor
două subsecvenţe ordonate şi preluarea de elemente din acestea astfel ı̂ncât să rezulte un şir sortat. Subsecvenţele
sunt parcurse simultan cu două contoare şi se compară elementele curente. În şirul final se plasează elementul
mai mic dintre cele două, iar contorul utilizat pentru parcurgerea subşirului din care a fost preluat elementul este
incrementat. Procesul continuă până când una dintre subsecvenţe a fost preluată ı̂n ı̂ntregime. Apoi elementele
rămase ı̂n cealaltă subsecvenţă sunt plasate direct ı̂n şirul final. O modalitate de a descrie algoritmul de interclasare
este ilustrată ı̂n cele ce urmează. Se foloseşte o zonă suplimentară de memorie, un tablou temp.
1 // algoritmul de interclasare
2 // a [ stang .. mijloc ] si a [ mijloc +1.. drept ] sunt sortate
3 function merge ( integer a [] , integer stang , integer mijloc , integer drept )
4 begin
5 integer i , j , k
6 i = stang
7 j = mijloc + 1
8 k = 0
9 integer temp [0.. drept - stang ]
10 while i <= mijloc && j <= drept do
11 if a [ i ] < a [ j ] then
12 temp [ k ] = a [ i ]
13 i = i + 1
14 else

72
Algoritmi şi complexitate Note de curs

15 temp [ k ] = a [ j ]
16 j = j + 1
17 end if
18 k = k + 1
19 end while
20 while i <= mijloc do
21 temp [ k ] = a [ i ]
22 i = i + 1
23 k = k + 1
24 end while
25 while j <= drept do
26 temp [ k ] = a [ j ]
27 j = j + 1
28 k = k + 1
29 end while
30 for k = 0 to drept - stang do
31 a [ stang + k ] = temp [ k ]
32 end for
33 end
34

35 // algoritmul recursiv de sortare


36 function mergesort ( integer a [] , integer stang , integer drept )
37 begin
38 integer mijloc
39 if stang < drept then
40 mijloc = ( stang + drept ) / 2
41 // sortarea sublistelor
42 mergesort (a , stang , mijloc )
43 mergesort (a , mijloc + 1 , drept )
44 // interclasarea
45 merge (a , stang , mijloc , drept )
46 end if
47 end
48

49 algorithm Sortare prin interclasare


50 begin
51 integer n , a [0.. n -1]
52 read n , a
53 mergesort (a , 0 , n - 1)
54 write a
55 end
De exemplu, considerăm tabloul a[ ] = {8, 2, 1, 7, 5, 0, 4, 3} cu 8 elemente. În figura de mai jos sunt ilustraţi paşii
algoritmului mergesort(a,0,7).

73
Algoritmi şi complexitate Note de curs

Verificarea corectitudinii. Verificarea corectitudinii algoritmului de interclasare merge, porneşte de la stabilirea


precondiţiilor şi a postcondiţiilor:
Pin : a[stang..mijloc] este crescător şi a[mijloc + 1..drept] este crescător
Pout : a[stang..drept] este crescător.
Să presupunem că, după iteraţia k, indicii ı̂n cele două părţi ale vectorului a sunt ik , ik ≥ stang, respectiv jk ,
jk ≥ mijloc + 1, unde k = ik + jk − stang − mijloc − 1. Astfel, vom considera predicatul pentru prima structură
while:
I(k) : temp[0..k − 1] este crescător, temp[k − 1] ≤ a[ik ], temp[k − 1] ≤ a[jk ],
temp[0..k−1] având componentele egale cu elementele subtablourilor a[stang..ik−1 ] şi a[mijloc+1..jk−1 ], rearanjate.
I(0) este adevărat (temp[0.. − 1] corespunde tabloului vid, pe care ı̂l considerăm sortat). Presupunem că I(l)
este adevărat şi condiţia de continuare a structurii repetitive este şi ea adevărată. Vom demonstra că I(l + 1)
este adevărat. La iteraţia l + 1 se compară a[il ] cu a[jl ]. Reamintim că cele două subsecvenţe ale lui a, de la
stang la mijloc şi de la mijloc + 1 la drept, sunt deja sortate. Deci, a[il ] ≤ a[m], m = il + 1, . . . , mijloc şi
a[jl ] ≤ a[m], m = jl + 1, . . . , drept. Aşadar, a[il ] şi a[jl ] sunt cele mai mici elemente rămase netransferate. Dacă
a[il ] ≤ a[jl ], atunci temp[l] va primi valoarea a[il ] şi deci va fi adevărată relaţia a[il ] ≤ a[m], m = jl , . . . , drept.
La fel şi pentru celălalt caz, când a[il ] ≥ a[jl ]. Aşadar, I(l + 1) este adevărat. Dacă ik = mijloc + 1 sau
jk = drept+1, structura while se va opri, iar tabloul sortat crescător temp va conţine toate elementele subtabloului
a[stang..mijloc] sau a[mijloc + 1..drept], după caz. Funcţia de terminare pentru această buclă este t : N −→ N,
t(k) = (mijloc + 1 − ik )(drept + 1 − jk ). Dacă au rămas elemente ı̂n a[stang..mijloc], acestea vor fi copiate ı̂n
temp ı̂n cea de-a doua structură while, iar dacă au rămas elemente ı̂n a[mijloc + 1..drept], acestea vor fi copiate
ı̂n temp ı̂n cea de-a treia structură while. În ultima structură repetitivă a algoritmului se copie tabloul sortat
temp[0..drept − stang] ı̂n tabloul a[stang..drept], deci a[stang..drept] va fi sortat.
Demonstrarea corectitudinii algoritmului recursiv mergesort se face prin inducţie: cazul de bază este corect,
deoarce un vector cu un singur element este implicit considerat sortat. Presupunem că algoritmul mergesort
sortează corect orice vector de lungime strict mai mică decât n. Presupunem că se apelează algoritmul pentru un
vector de dimensiune n. Astfel, algoritmul mergesort se apelează pentru doi subvectori de lungimi [n/2] şi n−[n/2].
Conform ipotezei inductive, aceste două subtablouri vor fi sortate corect şi, cum algoritmul de interclasare merge
este corect, rezultă că, după apelul acestuia, va rezulta un tablou corect sortat.

74
Algoritmi şi complexitate Note de curs

Analiza complexităţii. Considerăm drept operaţii de bază comparaţiile şi transferurile de elemente. Notăm cu
T (n) numărul de astfel de operaţii efectuate de către algoritmul de sortare mergesort şi cu Tmerge (n) numărul de
operaţii de bază efectuate de algoritmul de interclasare merge. Presupunem, de asemenea, că dimensiunea problemei
este o putere a lui 2. Sortarea prin interclasare a unui singur element nu consumă timp din punctul de vedere al
comparaţiilor şi al transferurilor de elemente, deci T (1) = 0. Pentru n > 1, se parcurg paşii corespunzători metodei
divide şi stăpâneşte. În pasul de divizare, se calculează doar mijlocul subvectorului curent, deci nu se efectuează
nici o operaţie de bază. La pasul “stăpâneşte” rezolvăm recursiv două subprobleme, fiecare de dimensiune n/2.
Deci acest pas contribuie cu 2T (n/2) la timpul de execuţie. La pasul ı̂n care se combină soluţiile subproblemelor are
loc, de fapt, interclasarea subvectorilor ordonaţi. Timpul necesar interclasării unei secvenţe de n elemente satisface
Tmerge (n) ∈ Θ(n). Astfel, obţinem recurenţa
(
0, dacă n = 1
T (n) =
2T (n/2) + Θ(n), dacă n > 1,

pentru timpul de execuţie al algoritmului mergesort. Aplicăm Teorema Master, cu a = b = 2, d = 1. Cum a = bd ,


rezultă că se va aplica cazul doi al teoremei şi se va obţine că T (n) ∈ Θ(n log n), pentru n o putere a lui 2. Conform
Propoziţiei 2.1, T (n) ∈ Θ(n log n), pentru n arbitrar.
Metoda de sortare este una stabilă, dar prezintă dezavantajul că nu sortează elementele “pe loc”, deoarece necesită
un spaţiu de memorie suplimentar ı̂n etapa de interclasare.

2.2.2.2 Sortarea rapidă (quicksort)


Descrierea algoritmului. Algoritmul quicksort este unul dintre cei mai utilizaţi algoritmi de sortare; acesta
are la bază metoda divide et impera şi metoda interschimbării, interschimbările făcându-se pe distanţe mai mari
dacât ı̂n cazul algoritmului bubblesort. A fost dezvoltat de C.A.R. Hoare ı̂n anul 1960. Dacă ı̂n cazul algoritmului
de sortare prin interclasare, divizarea vectorului ı̂n două subsecvenţe se face ţinând cont de poziţia elementelor, ı̂n
algoritmul de sortare rapidă se ţine cont şi de valoarea acestora. Algoritmul mergesort are nevoie de o procedură
de interclasare tocmai pentru că ı̂n fiecare dintre cele două subsecvenţe se găsesc şi elemente de valoare mică şi
elemente de valoare mare. Algoritmul de sortare rapidă se bazează pe partiţionarea şirului iniţial. Astfel, având
tabloul a, partiţionarea presupune o rearanjare a elementelor vectorului iniţial astfel ı̂ncât toate elementele aflate
la stânga unui element a[s] sunt mai mici sau egale cu a[s], iar toate elementele aflate la dreapta a[s] sunt mai mari
sau egale cu acesta. Aşadar, a[0], . . . , a[s − 1] ≤ a[s], iar a[s + 1], . . . , a[n − 1] ≥ a[s]. Elementul a[s] poartă numele
pivot.

Astfel, după ce se realizează o partiţionare, elementul a[s] se va afla pe poziţia finală ı̂n şirul sortat. Se continuă
apoi recursiv, folosind aceeaşi tehnică de sortare, cu cei doi subvectori aflaţi la stânga şi la dreapta elementului
a[s], independent.
Să remarcăm faptul că, ı̂n cazul algoritmului mergesort, divizarea problemei iniţiale ı̂n două subprobleme este
imediată şi ı̂ntregul efort constă ı̂n combinarea soluţiilor prin interclasarea celor două subşiruri sortate, pe când, ı̂n
cazul algoritmului quicksort, dificultatea stă ı̂n pasul de divizare (partiţionare), iar etapa de combinare a soluţiilor
nu necesită niciun efort.
Astfel, partiţionarea este prelucrarea cea mai importantă a algoritmului. Aceasta presupune identificarea pivotului,
adică a acelui element a[s] pentru care a[k] ≤ a[s], cu k < s şi a[k] ≥ a[s], cu k > s. Această metodă de sortare
mai poartă numele de sortare prin partiţionare. Dacă nu există un pivot, atunci strategia este să se creeze unul.
Există mai multe strategii de a alege pivotul. Vom reveni la această problemă atunci când vom analiza eficienţa
algoritmului. Pentru moment, vom selecta primul element al subtabloului drept pivot, deci pivot = a[stang],
presupunând că algoritmul de partiţionare este apelat pentru subtabloul a[stang..drept]. Se parcurge subtabloul
pornind din ambele capete ale sale şi se compară elementele cu pivotul. Parcurgerea de la stânga la dreapta
porneşte cu cel de-al doilea element. Pentru aceasta vom folosi un indice i. Din moment ce dorim ca elementele
mai mici decât pivotul să fie rearanjate ı̂n partea stângă a subtabloului, parcurgerea sare peste elementele mai mici
decât pivotul şi se opreşte la primul element mai mare sau egal cu acesta, a[i]. Parcurgerea de la dreapta la stânga

75
Algoritmi şi complexitate Note de curs

porneşte cu ultimul element al subtabloului, folosind un indice j. Din moment ce dorim ca elementele mai mari
decât pivotul să fie aranjate ı̂n partea dreaptă a subtabloului, parcurgerea sare peste elementele mai mari decât
pivotul şi se opreşte la primul element mai mic sau egal cu pivotul, a[j]. După ce ambele parcurgeri se opresc, sunt
trei situaţii ce pot să apară: dacă i < j, se interschimbă elementele a[i] şi a[j] şi se reia parcurgerea incrementând
indicele i şi decrementând indicele j; dacă i > j, atunci vom obţine partiţionarea subtabloului după interschimbarea
pivotului cu elementul a[j]; dacă i = j, atunci i = j = s şi am obţinut partiţionarea. De altfel, ultimele două cazuri
pot fi combinate, interschimbând pivotul cu a[j], pentru i ≥ j.
1 // algoritmul de partitionare
2 function partition ( integer a [] , integer stang , integer drept )
3 begin
4 integer i , j , pivot , aux
5 i = stang
6 j = drept + 1
7 pivot = a [ stang ]
8 while true do
9 repeat
10 i = i + 1
11 until i > drept || a [ i ] >= pivot
12 repeat
13 j = j - 1
14 until j < stang || a [ j ] <= pivot
15 if i < j then
16 aux = a [ i ]
17 a[i] = a[j]
18 a [ j ] = aux
19 else
20 aux = a [ stang ]
21 a [ stang ] = a [ j ]
22 a [ j ] = aux
23 return j
24 end if
25 end while
26 end
Se aplică acelaşi procedeu ı̂n mod repetitiv subşirurilor de la stânga şi de la dreapta pivotului, până când aceste
subşiruri sunt suficient de mici (se reduc la un singur element sau la un subşir vid).
1 // algoritmul recursiv de sortare
2 function quicksort ( integer a [] , integer stang , integer drept )
3 begin
4 integer s
5 if stang < drept then
6 s = partition (a , stang , drept )
7 quicksort (a , stang , s - 1)
8 quicksort (a , s + 1 , drept )
9 end if
10 end
11

12 algorithm Sortare prin partitionare


13 begin
14 integer n , a [0.. n -1]
15 read n , a
16 quicksort (a , 0 , n - 1)
17 write a
18 end

76
Algoritmi şi complexitate Note de curs

Să considerăm vectorul a[ ] = {5, 3, 1, 9, 8, 2, 4, 7} pe care ne propunem să-l sortăm. În acest sens, se apelează
algoritmul quicksort(a, 0, 7). Conform algoritmului de partiţionare, partition(a, 0, 7), pivot = a[stang] =
a[0] = 5, astfel că vectorul va fi partiţionat ı̂n două subtablouri: unul a cărui elemente sunt mai mici sau egale cu 5,
iar celălalt va avea elementele mai mari sau egale cu 5. Pornim cu doi indici i şi j ce vor fi deplasaţi spre mijlocul
vectorului, până când se returnează poziţia de partiţionare s, iar pivotul este interschimbat cu a[s]:
5 3 1 9 8 2 4 7 (pivot = 5)
i j
5 3 1 9 8 2 4 7
i j
Elementele a[i] şi a[j] vor fi interschimbate, rezultând vectorul:
5 3 1 4 8 2 9 7
i j
5 3 1 4 8 2 9 7
i j
5 3 1 4 2 8 9 7
i j
5 3 1 4 2 8 9 7
j i
Pentru că am ajuns ı̂n situaţia i ≥ j, se interschimbă pivotul cu a[s], unde s = j = 4 este poziţia de partiţionare.
Tabloul devine:
2 3 1 4 5 8 9 7
Să remarcăm că toate elementele mai mici decât pivotul sunt la stânga acestuia, iar cele mai mari, la dreapta. Se
aplică recursiv algoritmul de sortare pentru subşirurile a[0..3] = {2, 3, 1, 4} şi a[5..7] = {8, 9, 7}.
2 3 1 4 (pivot = 2)
i j
2 3 1 4
i j
2 1 3 4
j i
Se interschimbă pivotul cu a[s], unde s = j = 1 este poziţia de partiţionare:
1 2 3 4
Se aplică recursiv algoritmul de sortare pentru subşirurile a[0..0] = {1} şi a[2..3] = {3, 4}. Primul caz este elementar,
şirul format dintr-un singur element fiind implicit sortat.
3 4 (pivot = 3)
ij
3 4
j i
Se returnează poziţia de interschimbare, s = j = 2. Apoi se apelează recursiv algoritmul de sortare pentru
subşirurile: a[2..1] şi a[3..3]. Primul caz este corespunzător unui subşir vid, iar al doilea este un caz elementar.
Apelul algoritmului de sortare pentru subsecvenţa a[5..7] = {8, 9, 7}:
8 9 7 (pivot = 8)
i j
Elementele a[i] şi a[j] vor fi interschimbate, rezultând vectorul:
8 7 9
i j
Indicii vor fi iarăşi deplasaţi,

77
Algoritmi şi complexitate Note de curs

8 7 9
j i
Se interschimbă pivotul cu elementul a[s] şi se returnează poziţia de interschimbare, s = j = 6. Tabloul devine
7 8 9
Se apelează recursiv algoritmul de sortare pentru subşirurile: a[5..5] şi a[7..7]. Ambele sunt cazuri elementare şi
algoritmul se opreşte.

Verificarea corectitudinii. În ceea ce priveşte algoritmul partition, precondiţia şi postcondiţiile sunt:
Pin : stang < drept
Pout : a[m] ≤ a[s] pentru m = stang, . . . , s − 1 şi a[m] ≥ a[s] pentru m = s + 1, . . . , drept.
Considerăm

I(k) : dacă ik < jk , atunci a[m] ≤ pivot pentru m = stang, . . . , ik , iar a[m] ≥ pivot pentru m = jk , . . . , drept, iar
dacă ik ≥ jk , atunci a[m] ≤ pivot pentru m = stang, . . . , ik − 1, iar a[m] ≥ pivot pentru m = jk + 1, . . . , drept.

Cum precondiţia implică ik < jk , I este invariant ı̂n raport cu structura repetitivă while, iar după ultima in-
terschimbare a pivotului cu elementul a[s], cu s = jk , se obţine postcondiţia. Datorită condiţiilor de oprire din cele
două structuri repeat, algoritmul asigură că indicii ik şi jk rămân ı̂ntre limitele stang şi drept.
Verificarea corectitudinii algoritmului recursiv se face prin inducţie: cazul de bază este corect, deoarece un vector
cu un singur element sau unul vid sunt implicit considerate sortate, deci algoritmul quicksort nu are niciun efect
ı̂n acest caz. Presupunem că algoritmul quicksort sortează corect orice vector de lungime strict mai mică decât
n. De asemenea, presupunem că algoritmul se apelează pentru un vector de dimensiune n. Astfel, se obţine poziţia
de partiţionare s. Să verificăm că oricare două elemente de indici i şi j sunt ı̂n ordinea corectă: dacă i < j ≤ s − 1
sau s + 1 ≤ i < j, atunci a[i] ≤ a[j], conform ipotezei inductive; dacă i < s < j, atunci a[i] ≤ pivot ≤ a[j], datorită
faptului că algoritmul de partiţionare este corect.

Analiza complexităţii. Considerăm drept operaţii de bază doar comparaţiile ı̂ntre elementele tabloului, deoarece
ı̂n algoritmul de partiţionare, numărul acestora este mai mare, ı̂n general, decât numărul interschimbărilor. Pentru
un vector de dimensiune n numărul de comparaţii este n + 1, dacă i > j, şi n, dacă i = j (numărul de comparaţii
este n + i − j, unde i şi j sunt valorile finale ale indicilor cu care se parcurge subşirul de la stânga la dreapta,
respectiv de la dreapta la stânga).
Eficienţa algoritmului quicksort depinde de dimensiunile celor două subşiruri obţinute prin partiţionare. Cu cât
cele două dimensiuni sunt mai apropiate, cu atât se realizează mai puţine comparaţii. Astfel, cazul cel mai favorabil
se obţine când toate partiţionarile se realizează ı̂n mijlocul subşirurilor corespunzătoare (partiţionări echilibrate).
În acest caz, cel mai favorabil, pentru n o putere a lui 2, numărul de comparaţii satisface recurenţa
(
0, dacă n = 1
T (n) =
2T (n/2) + n, dacă n > 1.

Conform Teoremei Master, T (n) ∈ Ω(n log n) (a = b = 2, d = 1).


Cazul cel mai defavorabil se obţine când toate partiţionările sunt dezechilibrate: unul dintre subşiruri este vid,
iar celălalt are dimensiunea cu 1 mai mică decât dimensiunea subşirului pentru care este apelat algoritmul de
partiţionare. Această situaţie se ı̂ntâlneşte când algoritmul de sortare este apelat pentru şiruri deja ordonate.
Într-adevăr, dacă folosim drept pivot pe a[0] şi vectorul este strict crescător, parcurgerea de la stânga la dreapta se
opreşte pe a[1], ı̂n timp ce parcurgerea de la dreapta la stânga se opreşte pe a[0], returnând poziţia de partiţionare 0.
Deci, după ce efectuează n + 1 comparaţii pentru a realiza această partiţionare şi interschimbă pivotul cu el ı̂nsuşi,
continuă cu sortarea şirului deja ordonat strict crescător a[1..n − 1]. Algoritmul se opreşte după ce procesează şi
ultimul subşir, a[n − 2..n − 1]. Numărul total de comparaţii efectuate este egal cu

(n + 1)(n + 2)
T (n) = (n + 1) + n + · · · + 3 = − 3.
2
Deci, ı̂n cazul cel mai defavorabil, algoritmul are o complexitate pătratică şi deci, T (n) ∈ O(n2 ).

78
Algoritmi şi complexitate Note de curs

Vom analiza şi cazul mediu, plecând de la ipoteza că şirul este unul aleator. Presupunem că partiţionarea se
poate realiza ı̂n orice poziţie s cu aceeaşi probabilitate egală cu 1/n, 0 ≤ s ≤ n − 1, şi că se realizează n + 1
comparaţii pentru a obţine o partiţionare. Numărul de comparaţii, ı̂n cazul ı̂n care poziţia pivotului este s,
satisface Ts (n) = T (s) + T (n − 1 − s) + n + 1, deoarece, după partiţionare, cele două subşiruri rezultate au s,
respectiv, n − 1 − s, elemente. Prin urmare timpul mediu de execuţie satisface

0,
 dacă n = 0 sau n = 1
n−1
Tmediu (n) = 1 X

n Ts (n), dacă n > 1.
s=0

Obţinem,
n−1 n−1
1X 1X
Tmediu (n) = [(n + 1) + Tmediu (s) + Tmediu (n − 1 − s)] = (n + 1) + [Tmediu (s) + Tmediu (n − 1 − s)]
n s=0 n s=0

şi deci
n−1
2X
Tmediu (n) = (n + 1) + Tmediu (s), n > 1
n s=0
n−1
X
nTmediu (n) = 2 Tmediu (s) + n(n + 1), n > 1.
s=0

Deci este adevărată şi relaţia următoare


n−2
X
(n − 1)Tmediu (n − 1) = 2 Tmediu (s) + n(n − 1).
s=0

Scăzând ultimele două relaţii se obţine formula de recurenţă pentru T (n):


n+1
Tmediu (n) = Tmediu (n − 1) + 2, n > 1.
n
Astfel, prin substituţie inversă obţinem:
n+1
Tmediu (n) = Tmediu (n − 1) + 2
n
n
Tmediu (n − 1) = Tmediu (n − 2) + 2
n−1
n−1
Tmediu (n − 2) = Tmediu (n − 3) + 2
n−2
..
.
3
Tmediu (2) = Tmediu (1) + 2
2

Tmediu (1) = 0.
n+1 n+1 n+1 n+1
Înmulţim a doua relaţie cu , a treia cu ş.a.m.d., penultima relaţie cu , iar ultima cu , apoi
n n−1 3 2
sumăm relaţiile:
  n
1 1 1 X 1
Tmediu (n) = 2(n + 1) + + ··· + + 2 = 2(n + 1) + 2.
n n−1 3 i=3
i
n Z n
X 1 1 n n
Aproximăm suma ≈ dx = ln şi atunci Tmediu (n) ≈ 2(n + 1) ln + 2 ≈ 2n ln n ≈ 1.38n log2 n.
i=3
i 3 x 3 3
Rezultatul obţinut sugerează că, ı̂n cazul mediu, algoritmul de sortare efectuează doar cu aproximativ 38% mai
multe comparaţii decât ı̂n cazul cel mai favorabil.

79
Algoritmi şi complexitate Note de curs

Algoritmul de sortare rapidă a fost şi este ı̂ndelung studiat, algoritmul iniţial fiind ı̂mbunătăţit. Printre aceste
ı̂mbunătăţiri amintim: o metodă mai eficientă de a alege pivotul (de exemplu, alegerea aleatoare a pivotului sau
folosind metoda “medianei celor 3 valori”, ı̂n care se alege elementul mijlociu ca mărime, dintre trei elemente alese la
ı̂ntâmplare din vector etc.), utilizarea algoritmului de sortare prin inserţie directă pentru subşirurile de dimensiune
mică (ı̂ntre 5 şi 15 elemente) sau renunţarea la sortarea subşirurilor de dimensiune mică şi ı̂ncheierea algoritmului
cu sortarea prin inserţie directă a ı̂ntregului şir ce este aproape sortat, modificarea algoritmului de partiţionare (de
exemplu, partiţionarea vectorului ı̂n trei părţi: elementele mai mici decât pivotul ı̂ntr-o parte, cele egale cu pivotul
ı̂n cea de-a doua parte şi cele mai mari ı̂n cea de-a treia parte).
Algoritmul quicksort sortează elementele tabloului “pe loc”, fără a fi necesar spaţiu suplimentar, dar, un deza-
vantaj al său este faptul că nu este stabil (nu păstrează ordinea relativă a elementelor de aceeaşi valoare).

2.3 Tehnica căutării cu revenire


2.3.1 Descrierea metodei
Tehnica căutării cu revenire, numită şi tehnica backtracking, este o metodă de căutare sistematică ı̂n spaţiul soluţiilor
unei probleme. Astfel, sunt căutate soluţiile care respectă anumite restricţii impuse de enunţul problemei, permiţând
renunţarea pe parcurs la acele configuraţii care nu conduc la o soluţie validă a problemei.
Metoda backtracking se aplică rezolvării problemelor pentru care:

(1) se cere generarea tuturor soluţiilor care respectă cerinţele problemei de rezolvat;
(2) fiecare soluţie poate fi reprezentată sub forma unui vector x = (x0 , x1 , . . . , xn−1 ) ∈ A0 × A1 × · · · × An−1 ;
(3) Ak , k = 0, 1, . . . , n − 1, sunt mulţimi finite.
Mulţimea A = A0 × A1 × · · · × An−1 este spaţiul tuturor soluţiilor posibile. De obicei, ı̂n funcţie de specificul
problemei, o soluţie x trebuie să verifice anumite condiţii induse de restricţiile acesteia. Aceste condiţii poartă
numele de condiţii interne sau condiţii de continuare. Elementele x ∈ A care satisfac condiţiile interne se numesc
soluţii rezultat.
Metoda backtracking evită generarea tuturor soluţiilor posibile, adică a tuturor elementelor produsului cartezian
A0 × A1 × · · · × An−1 , fiind generate doar soluţiile rezultat. Metoda presupune o anumită modalitate de parcurgere
a spaţiului soluţiilor. Structura de date care stă la baza utilizării metodei backtracking este stiva. Soluţia se
construieşte gradual, de la bază spre vârf, iar eliminarea elementelor care nu conduc la o soluţie rezultat se face din
vârf. Astfel, elementului xk ∈ Ak , k = 0, . . . , n − 1, i se va atribui o valoare doar după ce au fost atribuite valori
pentru componentele x0 ∈ A0 , x1 ∈ A1 , . . . , xk−1 ∈ Ak−1 . Dacă se constată că valoarea este convenabilă, adică xk ,
ı̂mpreună cu x0 , x1 , ..., xk−1 verifică condiţiile de continuare, atunci ea este introdusă ı̂n stiva soluţiei şi algoritmul
continuă prin căutarea unui element convenabil xk+1 ∈ Ak+1 , pentru a umple următorul nivel al stivei. Dacă se
constată că valoarea aleasă pentru componenta xk , nu conduce la o soluţie rezultat, se renunţă la acea valoare şi se
ı̂ncearcă introducerea unei alte valori pe nivelul curent, ı̂n cazul ı̂n care mai există o valoare acceptabilă netestată.
Dacă toate valorile posibile au fost testate şi ı̂ncă nu a fost găsită o valoare care să conducă către o soluţie rezultat
sau dacă se doreşte determinarea unei noi soluţii, atunci se coboară pe nivelul anterior, revenindu-se la componenta
xk−1 şi se ı̂ncearcă următoarea valoare posibilă pentru aceasta. Algoritmul continuă până când au fost testate toate
valorile care sunt acceptabile pentru fiecare nivel al stivei. Astfel se explică şi numele metodei: back = ı̂napoi, track
= urmă.
Etapele necesare pentru a rezolva o problemă concretă folosind metoda backtracking sunt:
(1) se alege o reprezentare a soluţiei sub forma unui vector cu n componente;
(2) se identifică mulţimile A0 , A1 , . . . , An−1 ale valorilor admisibile pentru fiecare componentă a soluţiei rezultat;
se stabileşte o ordine ı̂ntre elementele fiecărei mulţimi care să indice modul de parcurgere a acesteia;
(3) pornind de la restricţiile problemei se precizează condiţiile de continuare.
Putem spune că, ı̂n general, algoritmii obţinuţi folosind tehnica backtracking sunt mai eficienţi decât cei obţinuţi
prin tehnica forţei brute. Totuşi, complexitatea lor este una ridicată şi nu pot fi utilizaţi decât pentru probleme

80
Algoritmi şi complexitate Note de curs

de dimensiune redusă. Chiar dacă este finit, spaţiul soluţiilor creşte cel puţin exponenţial odată cu dimensiunea
problemei.
Un algoritm proiectat folosind această tehnică poate fi descris atât ı̂n variantă iterativă, cât şi recursivă. Prezentăm
ı̂n continuare, structura generală a algoritmului iterativ:
1 function btrIter ()
2 begin
3 k = 0 // pozitionarea pe primul nivel al stivei solutiei x
4 /* A_k = { vmin [ k ] ,... , vmax [ k ]} multimea valorilor posibile pentru nivelul k */
5 x [ k ] = < vmin [ k ] - 1 > // initializare cu o valoare virtuala
6 while k >= 0 do
7 if <x [ k ] < vmax [ k ] > then // daca mai sunt valori posibile netestate
8 x [ k ] = <x [ k ] + 1 > // trecerea la urmatoarea valoare posibila din A_k
9 if valid ( k ) == true then // se testeaza conditiile interne
10 if solutie ( k ) then // daca solutia este completata
11 scrie_solutie ()
12 else
13 k = k + 1 // urca un nivel in stiva
14 x [ k ] = < vmin [ k ] - 1 > // reinitializare
15 end if
16 end if
17 else
18 k = k - 1 // coboara un nivel in stiva
19 end if
20 end while
21 end
Variabila k reţine indicele elementului ce urmează a fi adăugat ı̂n stiva soluţiei, deci indică vârful stivei. Pentru
fiecare nivel k (k ≥ 0), se testează pe rând, toate valorile posibile din Ak şi, dacă este cazul, acestea se vor adăuga
ı̂n soluţie. Atâta timp cât nu s-a golit stiva (k ≥ 0), se verifică dacă pentru elementul de pe nivelul k mai există o
valoare admisibilă. Dacă răspunsul este afirmativ, va fi selectată prima valoare admisibilă din Ak ce nu a fost testată
ı̂ncă. Această valoare va fi atribuită elementului de pe nivelul k, xk . Se verifică dacă xk este valid, adică dacă verifică
condiţiile de continuare ı̂mpreună cu elementele deja introduse ı̂n soluţie pe nivelele anterioare (x0 , x1 , . . . , xk−1 ).
Aceaste condiţii depind de specificul problemei de rezolvat. Dacă elementul este valid, se testează dacă s-a ajuns
la o soluţie rezultat; dacă răspunsul este afirmativ, se afişează această soluţie. În caz contrar, se trece la nivelul
următor, k + 1 şi se continuă printr-o reiniţializare. Dacă pentru elementul curent xk nu mai există o valoare
posibilă ı̂n Ak , atunci se coboară pe nivelul anterior, k − 1.
În cazul variantei recursive, subalgoritmul va avea ca parametru variabila k, nivelul curent din stiva soluţie. Urcarea
cu un nivel ı̂n stivă se face prin autoapelul subalgoritmului btrRec cu parametrul k + 1. Generarea posibililor
candidaţi la soluţie se face prin parcurgerea valorile posibile din Ak , respectând ordinea precizată.
1 function btrRec ( integer k )
2 begin
3 for < fiecare element i din A_k = { vmin [ k ] ,... , vmax [ k ]} > do
4 x[k] = i
5 if valid ( k ) == true then
6 if solutie ( k ) then
7 scrie_solutie ()
8 else
9 btrRec ( k + 1)
10 end if
11 end if
12 end for
13 end

81
Algoritmi şi complexitate Note de curs

2.3.2 Aplicaţii
Exemplul 2.7. Generarea produsului cartezian a n mulţimi A0 , A1 , . . . , An−1 , cu Ak = {1, 2, . . . , mk }, n, mk ∈ N∗ ,
k = 0, 1, . . . , n − 1.
Soluţiile rezultat au n componente, x = (x0 , x1 , . . . , xn−1 ), cu xk ∈ Ak , k = 0, 1, . . . , n − 1. Problema este una ı̂n
care elementele soluţiei rezultat nu sunt neapărat distincte, deci nu este necesară scrierea unei funcţii de validare.
Mulţimile Ak , k = 0, 1, . . . , n − 1, sunt parcurse de la cel mai mic element (care este 1), la cel mai mare (care
este mk ). Se obţine o soluţie rezultat dacă a fost completat ultimul nivel al stivei, adică k = n − 1. După ce este
determinată o soluţie rezultat, aceasta este afişată.
Vom considera un vector m, declarat global, ı̂n care vom păstra numărul elementelor fiecărei mulţimi Ak , k =
0, 1, . . . , n − 1. . De asemenea, se vor declara global vectorul x al soluţiei rezultat şi variabila n.
1 integer n , m [0.. n -1] , x [0.. n -1]
2

3 function scrie_solutie ()
4 begin
5 integer k
6 for k = 0 to n - 1 do
7 write x [ k ]
8 end for
9 end
10

11 function btrIter ()
12 begin
13 integer k
14 /* x [ k ] contine un element din multimea A_k = {1 ,2 ,... , m [ k ]}
15 vmin [ k ] = 1 , vmax [ k ] = m [ k ] */
16 k = 0
17 x[k] = 0
18 while k >= 0 do
19 if x [ k ] < m [ k ] then
20 x[k] = x[k] + 1
21 if k == n - 1 then
22 scrie_solutie ()
23 else
24 k = k + 1
25 x[k] = 0
26 end if
27 else
28 k = k - 1
29 end if
30 end while
31 end
32

33 function btrRec ( integer k )


34 begin
35 integer i
36 for i = 1 to m [ k ] do
37 x[k] = i
38 if k == n - 1 then
39 scrie_solutie ()
40 else
41 btrRec ( k + 1)
42 end if
43 end for
44 end

82
Algoritmi şi complexitate Note de curs

45

46 algorithm Produs Cartezian


47 begin
48 read n , m
49 btrIter () // btrRec (0)
50 end
Dimensiunea spaţiului soluţiilor este m0 m1 . . . mn−1 . Ordinul de creştere al timpului de execuţie al algoritmului
este O(m0 m1 . . . mn−1 ). Dacă m0 = m1 = · · · = mn−1 = m, T (n, m) ∈ O(mn ).

Exemplul 2.8. Generarea permutărilor de ordin n ale mulţimii A = {1, 2, ..., n}, n ∈ N∗ .
O permutare de ordin n este un tablou de n elemente distincte din mulţimea {1, 2, ..., n}. Aşadar, ı̂n cazul acestei
probleme, elementele soluţiei rezultat trebuie să fie distincte, deci pentru fiecare element al stivei soluţiei, xk , k =
0, 1, . . . n − 1, trebuie verificată condiţia xi 6= xk , ∀i < k.
Variabilele n şi x sunt declarate global.
1 integer n , x [0.. n -1]
2

3 function valid ( integer k )


4 begin
5 integer i
6 for i = 0 to k - 1 do
7 if x [ i ] == x [ k ] then
8 return false
9 end if
10 end for
11 return true
12 end
13

14 function scrie_solutie ()
15 begin
16 integer k
17 for k = 0 to n - 1 do
18 write x [ k ]
19 end for
20 end
21

22 function btrIter ()
23 begin
24 integer k
25 /* x [ k ] contine un element din multimea A = {1 ,2 ,... , n }
26 vmin [ k ] = 1 , vmax [ k ] = n */
27 k = 0
28 x[k] = 0
29 while k >= 0 do
30 if x [ k ] < n then
31 x[k] = x[k] + 1
32 if valid ( k ) == true then
33 if k == n - 1 then
34 scrie_solutie ()
35 else
36 k = k + 1
37 x[k] = 0
38 end if
39 end if
40 else

83
Algoritmi şi complexitate Note de curs

41 k = k - 1
42 end if
43 end while
44 end
45

46 function btrRec ( integer k )


47 begin
48 integer i
49 for i = 1 to n do
50 x[k] = i
51 if valid ( k ) == true then
52 if k == n - 1 then
53 scrie_solutie ()
54 else
55 btrRec ( k + 1)
56 end if
57 end if
58 end for
59 end
60

61 algorithm Permutari
62 begin
63 read n
64 btrIter () // btrRec (0)
65 end

Exemplul 2.9. (Problema celor n regine) Problema constă ı̂n plasarea a n regine pe o tablă de şah de dimensiune
n × n astfel ı̂ncât oricare două regine să nu se atace reciproc.
Pentru ca două regine să nu se atace, ele nu trebuie să fie pe aceeaşi linie, coloană sau diagonală. Cum numărul
reginelor este egal cu n, iar tabla de şah are n linii şi n coloane, deducem că pe fiecare linie trebuie plasată o singură
regină. Pentru ca poziţia reginelor să fie complet determinată, trebuie să reţinem pentru fiecare linie, coloana ı̂n
care este plasată regina. Astfel, componenta de indice k a vectorului x ı̂n care se păstreză soluţia rezultat, x[k],
reprezintă coloana ı̂n care este plasată regina de pe linia k + 1. Condiţiile pe care trebuie să le ı̂ndeplinească fiecare
componentă a vectorului soluţiei sunt:

(1) x[k] ∈ {1, 2, ..., n}, ∀k = 0, . . . , n − 1 (elementele vectorului x reprezintă numerele de ordine ale coloanelor);
(2) x[i] 6= x[k], ∀i < k, k = 0, . . . , n − 1 (două regine nu pot fi plasate pe aceeaşi coloană);
(3) |x[k] − x[i]| =
6 |k − i|, ∀i < k (două regine nu pot fi plasate pe aceeaşi diagonală).
Variabilele n şi x sunt declarate global. Subalgoritmii btrIter(), btrRec(integer) şi scrie solutie() de la
problema precedentă rămân valabili şi pentru această problemă. Modificăm doar subalgoritmul de validare:
1 function valid ( integer k )
2 begin
3 integer i
4 for i = 0 to k - 1 do
5 if x [ i ] == x [ k ] || abs ( x [ k ] - x [ i ]) == abs ( k - i ) then
6 return false
7 end if
8 end for
9 return true
10 end
De exemplu, pentru n = 4 se obţin 2 soluţii, ce pot fi observate ı̂n figura de mai jos.

84
Algoritmi şi complexitate Note de curs

Exemplul 2.10. (Problema comisului voiajor ) Un comis voiajor trebuie să viziteze n oraşe identificate prin nu-
merele 0, 1, 2, . . . , n − 1. Presupunem că oraşul din care pleacă este cel cu indicativul 0. În drumul său, comisul
voiajor nu trebuie să treacă de două ori prin acelaşi oraş, iar la ı̂ntoarcere trebuie să revină ı̂n oraşul din care a
plecat. Cunoscând legăturile existente ı̂ntre oraşe, se cere să se afişeze toate drumurile posibile pe care le poate
efectua comisul voiajor. Drumurile directe dintre oraşe sunt date sub forma unei matrice de adiacenţă, ale cărei
elemente sunt: (
1, dacă există drum direct ı̂ntre oraşele i şi j,
a[i][j] =
0, dacă nu există drum direct ı̂ntre oraşele i şi j.
Vor fi furnizate doar componentele de deasupra diagonalei principale a matricei de adiacenţă (a[i][j], i = 0, . . . , n −
2, j = i + 1, . . . , n − 1) şi se va impune condiţia că matricea este simetrică (a[j][i] = a[i][j]).
Componenta de indice k a vectorului x ı̂n care se păstreză soluţia rezultat, x[k], reprezintă oraşul ce este plasat pe
nivelul k al stivei, k = 0, 1, . . . , n − 1. Cum toate drumurile pleacă din oraşul 0, componenta corespunzătoare va fi
setată la 0, adică x[0] = 0. Următoarele componente x[k] ∈ {1, . . . , n − 1}, ∀k ∈ {1, . . . , n − 1}. Între oraşele x[k]
şi x[i] există drum dacă a[x[k]][x[i]] = 1. Mai trebuie să impunem ca oraşul x[k] să fie diferit de oraşul x[i] pentru
i < k şi oraşul x[n − 1] să fie legat cu oraşul x[0], adică a[x[n − 1]][x[0]] = 1.
În subalgoritmul de validare trebuie realizate următoarele verificări:
(1) există drum ı̂ntre oraşele x[k − 1] şi x[k], adică trebuie verificat dacă a[x[k − 1]][x[k]] = 1;
(2) oraşul x[k] este diferit de toate oraşele prin care a trecut deja comisul voiajor, adică x[k] 6= x[i], ∀ i < k;
(3) odată ajuns la cel de-al n-lea oraş, există drum ı̂ntre acesta şi primul oraş; adică, dacă k = n − 1, atunci
trebuie ca a[x[k]][x[0]] = 1.
Matricea de adiacenţă este declarată global. Pe lângă matricea a, sunt declarate globale şi variabilele n şi x.
1 integer n , x [0.. n -1] , a [0.. n -1][0.. n -1]
2

3 function scrie_solutie ()
4 begin
5 integer k
6 for k = 0 to n - 1 do
7 write x [ k ]
8 end for
9 end
10

11 function valid ( integer k )


12 begin
13 integer i
14 if a [ x [k -1]][ x [ k ]] == 0 then
15 return false
16 end if

85
Algoritmi şi complexitate Note de curs

17 for i = 0 to k - 1 do
18 if x [ i ] == x [ k ] then
19 return false
20 end if
21 end for
22 if k == n - 1 && a [ x [0]][ x [ k ]] == 0 then
23 return false
24 end if
25 return true
26 end
27

28 function btrIter ()
29 begin
30 integer k
31 // primul element din stiva solutiei este deja setat la 0
32 k = 1
33 /* x [ k ] contine un element din multimea {1 , ... , n - 1}
34 vmin [ k ] = 1 , vmax [ k ] = n - 1 */
35 x[k] = 0
36 while k >= 1 do
37 if x [ k ] < n - 1 then
38 x[k] = x[k] + 1
39 if valid ( k ) == true then
40 if k == n - 1 then
41 scrie_solutie ()
42 else
43 k = k + 1
44 x[k] = 0
45 end if
46 end if
47 else
48 k = k - 1
49 end if
50 end while
51 end
52

53 algorithm Comis Voiajor


54 begin
55 read n , a
56 x [0] = 0
57 btrIter ()
58 end
Pentru exemplul din figură

algoritmul furnizează următoarea listă a traseelor:

86
Algoritmi şi complexitate Note de curs

0 1 2 3 4 5
0 1 4 3 2 5
0 5 2 3 4 1
0 5 4 3 2 1
Exemplul 2.11. Fiind dat un şir de bancnote de valori b0 , b1 , ..., bn−1 , să se afişeze toate modalităţile de plată a
unei sume de bani S ∈ N, folosind aceste bancnote. Se presupune că bancnotele se găsesc ı̂n cantităţi suficiente
pentru plata sumei S şi că nu trebuie folosite neapărat toate tipurile de bancnote.
Problema revine la a genera şirul {x0 , x1 , ..., xn−1 }, cu proprietatea că
n−1
X
S= xk bk ,
k=0

unde componenta xk a vectorului soluţie reprezintă numărul de bancnote de tipul bk folosite pentru plata sumei S.
Numărul maxim de bancnote de tipul bk , k = 0, 1, . . . , n−1, ce pot fi folosite pentru plata sumei S, este mk = [S/bk ].
Vom păstra toate aceste valori ı̂ntr-un tablou m. Astfel, xk ∈ {0, 1, . . . , mk }. Pentru fiecare componentă xk a stivei
soluţiei rezultat, se va calcula suma obţinută până ı̂n acel moment prin folosirea a x0 bancnote de tipul b0 , x1
bancnote de tipul b1 , . . . , xk bancnote de tipul bk . Toate aceste sume se vor păstra ı̂ntr-un vector suma, de
componente sumak = sumak−1 + bk xk , pentru k ≥ 1 şi suma0 = b0 ∗ x0 . Condiţiile de continuare sunt:
(1) sumak <= S;
(2) dacă s-a ajuns la ultima componetă a vectorului soluţie, atunci suma obţinută până ı̂n acel moment trebuie
să coincidă cu S.
Vom considera că n, S şi tablourile x, b, m, suma sunt declarate ca variabile globale. Cum problema poate să nu
aibă soluţie, vom considera o variabilă globală de tip bool, f ara solutie. Aceasta este iniţializată cu valoarea true
şi rămâne la această valoare dacă nu este determinată o soluţie rezultat. Dacă este identificată o soluţie, atunci
variabila booleană este schimbată la valoarea false.
1 integer n , S , x [0.. n -1] , b [0.. n -1] , m [0.. n -1] , suma [0.. n -1]
2 bool fara_solutie
3

4 function citire ()
5 begin
6 integer k
7 read n , S
8 for k = 0 to n - 1 do
9 read b [ k ]
10 m[k] = S/b[k]
11 end for
12 end
13

14 function scrie_solutie ()
15 begin
16 integer k
17 fara_solutie = false
18 for k = 0 to n - 1 do
19 if x [ k ] != 0 then
20 write x [ k ] , " bancnote de valoare " , b [ k ]
21 end if
22 end for
23 end
24

25 function valid ( integer k )


26 begin
27 if k >= 1 then
28 suma [ k ] = suma [ k - 1] + b [ k ] * x [ k ]

87
Algoritmi şi complexitate Note de curs

29 else
30 suma [ k ] = b [ k ] * x [ k ]
31 end if
32 if suma [ k ] > S then
33 return false
34 end if
35 if k == n - 1 && suma [ k ] != S then
36 return false
37 end if
38 return true
39 end
40

41 function btrIter ()
42 begin
43 integer k
44 // x [ k ] contine un element din multimea {0 ,1 ,... , m_k }
45 k = 0
46 x [ k ] = -1
47 while k >= 0 do
48 if x [ k ] < m [ k ] then
49 x[k] = x[k] + 1
50 if valid ( k ) == true then
51 if k == n - 1 then
52 scrie_solutie ()
53 else
54 k = k + 1
55 x [ k ] = -1
56 end if
57 end if
58 else
59 k = k - 1
60 end if
61 end while
62 end
63

64 function btrRec ( integer k )


65 begin
66 integer i
67 for i = 0 to m [ k ] do
68 x[k] = i
69 if valid ( k ) == true then
70 if k == n - 1 then
71 scrie_solutie ()
72 else
73 btrRec ( k + 1)
74 end if
75 end if
76 end for
77 end
78

79 algorithm Suma
80 begin
81 citire ()
82 fara_solutie = true
83 btrIter () // btrRec (0)
84 if fara_sol == true

88
Algoritmi şi complexitate Note de curs

85 write " Problema nu are solutie . "


86 end if
87 end
Tehnica backtracking poate fi utilizată şi ı̂n cazul ı̂n care vectorul soluţie are un număr variabil de componente.
Pentru problema precedentă, aceasta ı̂nseamnă că x = {x0 , x1 , . . . , xk }, k < n, xi ∈ {0, 1, . . . , mi }, i = 0, 1, . . . , k.
Se declară o variabilă globală suma ı̂n care se va reţine la fiecare pas suma plătită până la acel moment. Vectorul
k
X
soluţie satisface xi bi = S, k < n. Rescriem ı̂n cele ce urmează algoritmul recursiv.
i=0

1 integer n , S , suma , b [0.. n -1] , m [0.. n -1] , x [0.. n -1]


2

3 function citire ()
4 begin
5 integer i
6 read n , S
7 for i = 0 to n - 1 do
8 read b [ i ]
9 m[i] = S/b[i]
10 end for
11 end
12

13 function scrie_solutie ( integer k )


14 begin
15 integer i
16 fara_sol = false
17 for i = 0 to k do
18 if x [ i ] != 0 then
19 write x [ i ] , " bancnote de valoare " , b [ i ]
20 end if
21 end for
22 end
23

24 function btrRec ( integer k )


25 begin
26 integer i
27 if suma == S then
28 scrie_solutie (k -1)
29 end if
30 if k < n && suma < S then
31 for i = 0 to m [ k ] && suma + b [ k ] * i <= S do
32 x[k] = i
33 suma = suma + x [ k ] * b [ k ]
34 btrRec ( k + 1)
35 suma = suma - x [ k ] * b [ k ]
36 end for
37 end if
38 end

Exemplul 2.12. (Colorarea hărţilor ) Orice hartă din plan poate fi colorată folosind cel mult patru culori astfel
ı̂ncât oricare două regiuni conexe care au o frontieră comună (au ı̂n comun nu doar un număr discret de puncte)
au culori diferite (Teorema celor patru culori, F. Guthrie, 1852). Se cer toate soluţiile de colorare a unei hărţi date
din plan.
Presupunem că harta are n regiuni, cu indicativele 0, 1, 2, . . . , n − 1. Harta este dată sub forma unei matrice de

89
Algoritmi şi complexitate Note de curs

adiacenţă, a = (aij ) , i, j = 0, 1, . . . , n − 1, definită prin:


(
1, dacă regiunile i şi j sunt vecine,
a[i][j] =
0, altfel.

Vom declara următoarele variabile globale: numărul de regiuni n, matricea de adiacenţă a şi vectorul x al soluţiei
rezultat. Vor fi citite doar componentele de deasupra diagonalei principale a matricei de adiacenţă (a[i][j], i =
0, . . . , n − 2, j = i + 1, . . . , n − 1), apoi se va folosi faptul că matricea este simetrică (a[j][i] = a[i][j]); pe diagonala
principală elementele sunt nule.
Memorăm cele patru culori ı̂ntr-un tablou de caractere, char culori[] = {’A’,’B’,’C’,’D’}. Soluţia rezultat
x are n componente, fiecare componentă x[k] având valori ı̂n mulţimea {0, 1, 2, 3}, reprezentând indicele culorii
din tabloul culori utilizată pentru colorarea regiunii k. Soluţia nu are continuare dacă două regiuni vecine sunt
colorate la fel. Aşadar, ı̂n subalgoritmul de validare trebuie realizată verificarea: x[k] 6= x[i], pentru i < k, dacă
a[k][i] = 1.
1 integer n , x [0.. n -1] , a [0.. n -1][0.. n -1] , nrsol
2 char culori [] = { ’A ’ , ’B ’ , ’C ’ , ’D ’}
3

4 function scrie_solutie ()
5 begin
6 integer k
7 for k = 0 to n - 1 do
8 write culori [ x [ k ]]
9 end for
10 end
11

12 function valid ( integer k )


13 begin
14 integer i
15 for i = 0 to k - 1 do
16 if x [ i ] == x [ k ] && a [ i ][ k ] == 1 then
17 return false
18 end if
19 end for
20 return true
21 end
22

23 function btrIter ()
24 begin
25 integer k
26 // x [ k ] contine un element din multimea {0 ,1 ,2 ,3}
27 k = 0
28 x [ k ] = -1
29 while k >= 0 do
30 if x [ k ] < 3 then
31 x[k] = x[k] + 1
32 if valid ( k ) == true then
33 if k == n - 1 then
34 scrie_solutie ()
35 nrsol = nrsol + 1
36 else
37 k = k + 1
38 x [ k ] = -1
39 end if
40 end if
41 else

90
Algoritmi şi complexitate Note de curs

42 k = k - 1
43 end if
44 end while
45 end
46

47 function btrRec ( integer k )


48 begin
49 integer i
50 for i = 0 to 3 do
51 x[k] = i
52 if valid ( k ) == true then
53 if k == n - 1 then
54 scrie_solutie ()
55 nrsol = nrsol + 1
56 else
57 btrRec ( k + 1)
58 end if
59 end if
60 end for
61 end
62

63 algorithm Colorarea hartilor


64 begin
65 read n , a
66 btrIter () // btrRec (0)
67 write nrsol
68 end
De exemplu, pentru harta din figură

algoritmul returnează 432 soluţii de colarare ce respectă restricţiile impuse: {A B A B C A}, {A B A B C B}, {A
B A B C D}, {A B A B D A}, {A B A B D B}, {A B A B D C}, {A B A C D A}, {A B A C D B} ş.a.m.d.

2.4 Tehnica alegerii local optimale


2.4.1 Descrierea metodei
Tehnica alegerii local optimale este utilizată, de regulă, pentru rezolvarea problemelor de optimizare. Cu toate
acestea, este totuşi considerată o tehnică generală de proiectare a algoritmilor. Metoda este aplicabilă problemelor
ı̂n care se cere să se determine un subset B al unui set finit A, de n elemente, nu neapărat distincte, care respectă
anumite restricţii specifice problemei. B se numeşte soluţie posibilă. Se cere apoi să se determine o soluţie posibilă
care să optimizeze (maximizeze sau minimizeze) o funcţie obiectiv dată. Această soluţie se numeşte soluţie optimă.
Această tehnică furnizează o modalitate de rezolvare a problemelor de optimizare ı̂n care se poate obţine optimul
global prin alegeri succesive ale optimului local (de aici şi numele metodei). Astfel, problema este rezolvată fără

91
Algoritmi şi complexitate Note de curs

a se reveni la deciziile luate deja. Ideea de bază a tehnicii este următoarea: pornind de la subsetul vid, B este
construit succesiv, la fiecare pas fiind selectat un nou element din A (cel care pare a fi cel mai potrivit conform
criteriului de optim) şi adăugat ı̂n B. Odată ce s-a făcut o alegere, aceasta nu mai poate fi schimbată – nu se mai
poate reveni şi ı̂nlocui un element al lui B cu o altă valoare din A. Metoda mai poartă numele de tehnica greedy
(greedy = lacom). Numele metodei este dat de modalitatea “lacomă” de a selecta fiecare componentă a soluţiei.
Tehnica greedy nu conduce ı̂ntotdeauna la o soluţie optimă – ceea ce pare optim la un anumit pas (local), poate
fi dezastruos la nivel global. Uneori soluţiile obţinute cu această tehnică sunt sub–optimale, adică aproximează
suficient de bine soluţia optimă. Algoritmii de tip greedy sunt simpli, intuitivi şi eficienţi. În practică, există situaţii
când o soluţie aproximativă a unei probleme poate fi mulţumitoare, ı̂ntrucât algoritmul greedy care furnizează
soluţia este uşor de implementat şi eficient. Celelalte variante de rezolvare: căutarea exhaustivă – parcurgerea
completă a spaţiului soluţiilor şi alegerea elementului care satisface restricţiile şi optimizează funcţia obiectiv sau
generarea prin tehnica backtracking a tuturor soluţiilor posibile şi apoi a selectării soluţiei optime, pot fi extrem de
ineficiente datorită complexităţii mari a algoritmilor de rezolvare (mai ales dacă dimensiunea problemei este mare).

Metodele greedy şi backtracking se deosebesc esenţial prin:


ˆ tehnica backtracking oferă toate soluţiile posibile ale problemei, ı̂n timp ce metoda greedy oferă o singură
soluţie;
ˆ tehnica greedy nu dispune de mecanismul ı̂ntoarcerii, specific metodei backtracking.

Presupunem că este descris un subalgoritm solutie() care testează dacă un anumit subset al lui A este o soluţie
posibilă. Descriem algoritmul general greedy:
1 function greedy ( A )
2 begin
3 B = ∅
4 while ! solutie ( B ) && A 6= ∅ do
5 a = selecteaza ( A ) // se selecteaza cel mai promitator element din A
6 A = A \ {a}
7 if B ∪ { a } satisface restrictiile then
8 B = B ∪ { a } // se adauga elementul a la B
9 end if
10 end while
11 if solutie ( B ) then
12 return B
13 else
14 return ∅
15 end if
16 end
Selectarea unui nou element la fiecare pas, ı̂n scopul introducerii sale ı̂n soluţie, este etapa cea mai importantă a
unui algoritm greedy. În cazul anumitor probleme, poate fi convenabil ca elementele setului A să fie sortate (dacă
acest lucru este posibil), după un anumit criteriu ce depinde de criteriul de optim. Astfel, elementele soluţiei vor
fi selectate ı̂n ordinea ı̂n care apar ı̂n setul sortat.
De cele mai multe ori soluţia obţinută cu metoda greedy (ca şi ı̂n metoda backtracking) este reţinută ı̂ntr-un tablou.
Întrucât tehnica alegerii local optimale nu garantează determinarea soluţiei optime, pentru fiecare algoritm ı̂n
parte trebuie demonstrată corectitudinea acestuia. Pe cât de simpli şi intuitivi sunt algoritmii greedy, pe atât de
complicate pot fi demonstraţiile de corectitudine a acestora.
Exemplul 2.13. Fie A un set de n elemente. Se cere să se determine un subset B de m elemente din A, cu m < n,
astfel ı̂ncât suma elementelor selectate să fie maximă.
De exemplu, dacă A = {3, 1, 5, 7, 2, 9} şi m = 3, atunci B = {9, 7, 5}.
Aşadar, strategia alegerii local optimale presupune selectarea din A, la fiecare pas, a celui mai mare element rămas
neselectat. În acest scop, A poate fi sortat descrescător, folosind un anumit algoritm de sortare, pe care ı̂l vom
numi generic sortare descresc.

92
Algoritmi şi complexitate Note de curs

1 function subsetGreedy1 ( integer A [] , integer n , integer B [] , integer m )


2 begin
3 integer i
4 sortare_descresc (A , n )
5 for i = 0 to m - 1 do
6 B[i] = A[i]
7 end for
8 end
9

10 // sau
11

12 function subsetGreedy2 ( integer A [] , integer n , integer B [] , integer m )


13 begin
14 integer i , j , k , aux
15 for i = 0 to m - 1 do
16 k = i
17 for j = i + 1 to n - 1 do
18 if A [ k ] < A [ j ] then
19 k = j
20 end if
21 end for
22 if k != i then
23 aux = A [ k ]
24 A[k] = A[i]
25 A [ i ] = aux
26 end if
27 B[i] = A[i]
28 end for
29 end

Verificarea corectitudinii. Problemele ce se rezolvă folosind tehnica greedy satisfac următoarele două pro-
prietăţi:
ˆ proprietatea de substructură optimă – o soluţie optimă a problemei iniţiale conţine soluţiile optime ale sub-
problemelor (probleme similare celei originale, dar de dimensiuni mai mici). Demonstraţia acestei proprietăţi
se face, de regulă, prin reducere la absurd.
ˆ proprietatea alegerii locale – soluţia problemei se obţine prin alegeri local optimale. Demonstraţia acestei
proprietăţi se face, de regulă, prin reducere la absurd pentru prima componentă a soluţiei optime, apoi prin
inducţie pentru celelalte sau folosind proprietatea precedentă.
Demonstrăm proprietatea de substructură optimă pentru problema anterioară, de dimensiune n. Presupunem că
A = {a1 , a2 , . . . , an } este sortat descrescător, a1 ≥ a2 ≥ · · · ≥ an . Soluţia problemei folosind tehnica greedy va fi
B = {a1 , a2 , . . . , am }.
Fie S = {s1 , s2 , . . . , sm }, o soluţie optimă a problemei, ı̂n care elementele sunt ordonate descrescător. Să demon-
străm că subsetul S2 = {s2 , . . . , sm } este o soluţie optimă a subproblemei de dimensiune n − 1, A2 = {a2 , . . . , an }.
Presupunem prin reducere la absurd că S2 nu este soluţie optimă pentru A2 şi că S20 = {s02 , . . . , s0m } este soluţie
optimă pentru această subproblemă. Completând soluţia cu elementul a1 , cel mai mare element din A, vom obţine
o soluţie mai bună decât S, şi anume S 0 = {a1 , s02 , . . . , s0m }, ceea ce este o contradicţie.
Demonstrăm proprietatea de alegere locală pentru această problemă, adică vom demonstra că soluţia optimă a
problemei se obţine prin alegeri local optimale (sau poate fi transformată ı̂ntr-o soluţie optimă ce a fost obţinută
printr-o strategie greedy). Astfel, vom demonstra că ı̂nlocuind primul element al lui S cu un element selectat prin
metoda greedy, soluţia rămâne optimă. Apoi, se demonstrează acelaşi lucru pentru celelalte elemente, fie folosind
metoda inducţiei matematice, fie folosind proprietatea de substructură optimă deja demonstrată.

93
Algoritmi şi complexitate Note de curs

Aplicând tehnica greedy, prima componentă a soluţiei ar trebui să fie a1 . Presupunem prin reducere la absurd că
s1 6= a1 . Cum a1 este cel mai mare element din A, rezultă că s1 < a1 . Atunci a1 + s2 + · · · + sm > s1 + s2 + . . . sm
şi atunci soluţia S ∗ = {a1 , s2 , . . . , sm } este mai bună decât S, obţinându-se o contradicţie. Deci s1 = a1 şi folosind
proprietatea de substructură optimă se demontrează că şi celelalte componente ale soluţiei S satisfac proprietatea
de alegere locală.

Analiza complexităţii. Algoritmii proiectaţi prin tehnca alegerii local optimale sunt eficienţi. Operaţia cea mai
importantă este selectarea, la fiecare pas, a unui nou element ce urmează să facă parte din soluţia problemei. Dacă
are loc sortarea prealabilă a elementelor lui A, atunci aceasta este cea mai costisitoare operaţie şi va da complexitatea
algoritmului: O(n2 ) sau O(n log n), ı̂n funcţie de algoritmul de sortare utilizat. Dacă nu este necesară sortarea lui
A, atunci se pot obţine algoritmi de complexitate O(n).

2.4.2 Aplicaţii
Exemplul 2.14. (Minimizarea timpului mediu de aşteptare) O singură staţie de servire (de exemplu, un ghişeu,
un procesor, o pompă de benzină, un frizer etc.) trebuie să satisfacă cererile a n clienţi. Cunoscând timpul de
servire necesar fiecărui client (pentru clientul i este necesar un timp de servire ti , i = 1, . . . , n), să se determine o
ordine de servire a clienţilor astfel ı̂ncât timpul total de aşteptare să fie minim.
Vom identifica clienţii prin numere de la 1 la n. Timpul total de aşteptare este suma timpilor de aşteptare pentru
fiecare client ı̂n parte. Timpul de aşteptare pentru un client include şi timpul propriu de servire. Dacă notăm cu
n
X
ai , timpul de aşteptare pentru clientul i, i = 1, . . . , n, iar timpul total de aşteptare cu TA , atunci TA = ai . A
i=1
minimiza timpul total de aşteptare este acelaşi lucru cu a minimiza timpul mediu de aşteptare, care este TA /n.
Problema poate fi rezolvată folosind tehnica backtracking, generând astfel toate soluţiile posibile de servire şi
alegând apoi pe cea optimă, adică pe cea care minimizează timpul total de aşteptare. De fapt, prin această metodă
se vor generara toate permutările de ordin n ale mulţimii {1, 2, 3, . . . , n} şi, pentru fiecare soluţie ı̂n parte, se va
calcula timpul total de aşteptare. Se va alege soluţia pentru care TA este minim. De exemplu, presupunem că sunt
3 clienţi având timpii de servire t1 = 3, t2 = 7 şi t3 = 2. Sunt posibile 3! = 6 modalităţi de servire. Acestea sunt:
(1, 2, 3) TA = 3 + (3 + 7) + (3 + 7 + 2) = 25 (a1 = 3, a2 = 10, a3 = 12)
(1, 3, 2) TA = 3 + (3 + 2) + (3 + 2 + 7) = 20 (a1 = 3, a3 = 5, a2 = 12)
(2, 1, 3) TA = 7 + (7 + 3) + (7 + 3 + 2) = 29 (a2 = 7, a1 = 10, a3 = 12)
(2, 3, 1) TA = 7 + (7 + 2) + (7 + 2 + 3) = 28 (a2 = 7, a3 = 9, a1 = 12)
(3, 1, 2) TA = 2 + (2 + 3) + (2 + 3 + 7) = 19 (a3 = 2, a1 = 5, a2 = 12)
(3, 2, 1) TA = 2 + (2 + 7) + (2 + 7 + 3) = 23 (a3 = 2, a2 = 9, a1 = 12)
În prima soluţie generată cu algoritmul backtracking, clientul 1 este servit primul, deci a1 = 3, timpul de aşteptare
incluzând şi timpul său de servire. Clientul 2 aşteaptă până este servit clientul 1 şi apoi este servit şi el, deci
a2 = 3 + 7 = 10. Clientul 3 aşteaptă până sunt serviţi clienţii 1 şi 2 şi apoi este servit şi el, deci a3 = 3 + 7 + 2 = 12.
Aşadar, timpul total de aşteptare este T A = 3 + 10 + 12 = 25. Observăm că timpul cel mai mic de aşteptare este
19 şi este oferit de soluţia (3, 1, 2). Aşadar aceasta este soluţia optimă: mai ı̂ntâi va fi servit clientul 3, apoi clientul
1 şi la final, clientul 2.
Tehnica greedy este clar mai eficientă din punctul de vedere al timpului de execuţie şi al simplităţii. Algoritmul
bazat pe această tehnică este foarte simplu: la fiecare pas se selectează clientul cu timpul minim de servire din
mulţimea de clienţi rămasă. În scopul obţinerii soluţiei optime, vom sorta crescător şirul clienţilor după timpul
acestora de servire, folosind un anumit algoritm de sortare, sortare cresc. Complexitatea algoritmului greedy
este dată de complexitatea algoritmului de sortare.
Un client este caracterizat prin numărul său de ordine şi prin timpul de servire, astfel că vom defini o structură
corespunzătoare.
1 structure Client
2 // t = timpul de servire
3 // nr = numarul de ordine
4 integer t , nr

94
Algoritmi şi complexitate Note de curs

5 end structure
6

7 function timpGreedy ( Client C [] , integer n , integer B [])


8 begin
9 sortare_cresc (C , n ) // sortare dupa timpul de servire
10 integer i , j
11 real T_A
12 T_A = 0
13 for i = 0 to n - 1 do
14 B [ i ] = C [ i ]. nr /* numarul de ordine al clientului cu timpul de servire
15 C [ i ]. t */
16 for j = 0 to i
17 T_A = T_A + C [ j ]. t
18 end for
19 end for
20 write T_A , T_A / n
21 end

Proprietatea de substructură optimă. Presupunem că C = {c1 , c2 , . . . , cn } este şirul clienţilor sortat crescător
după timpul de servire. Deci c1 .t ≤ c2 .t ≤ · · · ≤ cn .t. Soluţia problemei folosind tehnica greedy este B =
{c1 .nr, c2 .nr, . . . , cn .nr}.
Fie S = {s1 .nr, s2 .nr, . . . , sn .nr} o soluţie optimă a problemei, ı̂n care s1 .t ≤ s2 .t ≤ · · · ≤ sn .t. Să demonstrăm că
subsetul S2 = {s2 .nr, . . . , sn .nr} este o soluţie optimă a subproblemei de dimensiune n − 1, C2 = {c2 , . . . , cn }.
Presupunem prin reducere la absurd că S2 nu este soluţie optimă pentru C2 şi că S20 = {s02 .nr, . . . , s0n .nr} este
soluţie optimă pentru această subproblemă. Completând soluţia cu elementul c1 .nr, clientul cu cel mai mic timp
de servire, obţinem soluţia S 0 = {c1 .nr, s02 .nr, . . . , s0n .nr}, mai bună decât S. Aceasta este o contradicţie.
Proprietatea de alegere locală. Presupunem prin reducere la absurd că s1 6= c1 . Cum c1 .t este cel mai mic
timp de servire, rezultă că s1 .t > c1 .t. Atunci c1 .t + s2 .t + · · · + sn .t < s1 .t + s2 .t + · · · + sn .t şi soluţia S ∗ =
{c1 .nr, s2 .nr, . . . , sn .nr} este mai bună decât S, obţinându-se o contradicţie. Deci s1 = c1 şi folosind proprietatea
de substructură optimă se demonstrează că şi celelalte componente ale soluţiei S satisfac proprietatea de alegere
locală.
Exemplul 2.15. (Problema spectacolelor ) În singura sală de spectacole a unui teatru trebuie programate cât mai
multe spectacole dintre cele n posibile. Pentru fiecare spectacol i, i = 1, 2, . . . , n, se cunoaşte intervalul ı̂n care se
poate desfăşura [si , fi ) (si reprezintă ora şi minutul de ı̂nceput, iar fi ora şi minutul de ı̂ncheiere a spectacolului i).
Să se determine un program care să permită spectatorilor participarea la un număr cât mai mare de spectacole.
Spectacolele le vom identifica prin numere de la 1 la n. Vom sorta spectacolele crescător după momentul acestora
de ı̂ncheiere. Algoritmul greedy va fi următorul: mai ı̂ntâi selectăm spectacolul care se termină cel mai devreme.
La fiecare pas selectăm primul spectacol disponibil, care nu se suprapune cu cele deja selectate, deci care ı̂ncepe
după ce se termină ultimul spectacol selectat.
1 structure Spectacol
2 // s = momentul inceperii spectacolului
3 // f = momentul incheierii spectacolului
4 // nr = numarul de ordine
5 integer s , f , nr
6 end structure
7

8 function spectacoleGreedy ( Spectacol A [] , integer n , integer B [])


9 begin
10 sortare_cresc (A , n ) /* sortare dupa momentul incheierii spectacolelor */
11 integer i , u , k
12 B [0] = A [0]. nr // se selecteaza primul spectacol
13 u = 0 // indicele ultimului spectacol adaugat
14 k = 1

95
Algoritmi şi complexitate Note de curs

15 for i = 1 to n - 1 do
16 if A [ i ]. s >= A [ u ]. f then
17 B [ k ] = A [ i ]. nr /* nr . de ordine al spectacolului cu timpul de inceput A [
i ]. s */
18 k = k + 1
19 u = i
20 end if
21 end for
22 return k
23 end
Pentru sortare, se recomandă transformarea orelor ı̂n minute. De exemplu, pentru următoarele date de intrare
n = 5
Spectacolul 1 12:30 - 16:30
Spectacolul 2 15:00 - 18:00
Spectacolul 3 10:00 - 18:30
Spectacolul 4 18:00 - 20:45
Spectacolul 5 12:15 - 13:00
spectacolele selectate sunt:
5 2 4
Aşadar, primul spectacol programat este cel cu indicativul 5, apoi cel cu indicativul 2, iar ultimul spectacol
programat este cel cu indicativul 4.
Proprietatea de substructură optimă. Presupunem că A = {a1 , a2 , . . . , an } este setul de spectacole sortat crescător
după timpul de ı̂ncheiere a acestora, a1 .f ≤ a2 .f ≤ · · · ≤ an .f .
Fie S = {s1 .nr, s2 .nr, . . . , sm .nr} = {ai1 .nr, . . . , aim .nr} o soluţie optimă a problemei, ı̂n care s1 .f ≤ s2 .f ≤ · · · ≤
sm .f . Să demonstrăm că subsetul S2 = {s2 .nr, . . . , sm .nr} este o soluţie optimă a subproblemei de dimensiune
n − 1, A2 = {a2 , . . . , an }.
Presupunem prin reducere la absurd că S2 nu este soluţie optimă pentru A2 şi că S20 = {s02 .nr, . . . , s0m0 .nr}, m0 > m,
este soluţie optimă pentru această subproblemă. În ipoteza că a1 .f ≤ s02 .s, completând soluţia cu elementul a1 .nr,
vom obţine o soluţie mai bună decât S, şi anume S 0 = {a1 .nr, s02 .nr, . . . , s0m0 .nr}, ceea ce este o contradicţie.
Proprietatea de alegere locală. ai1 .f ≥ a1 .f , prin urmare ı̂nlocuind pe ai1 .nr cu a1 .nr ı̂n S, se obţine o soluţie
greedy cu acelaşi număr de activităţi.
Problema poate fi generalizată la orice set de n activităţi ce partajează aceeaşi resursă, ı̂n care fiecare activitate
ai necesită un interval de timp [si , fi ) pentru a fi executată. Cerinţa problemei este să se selecteze cât mai multe
activităţi ale căror intervale de execuţie sunt disjuncte. Această problemă poartă numele de problema selecţiei
activităţilor.
Exemplul 2.16. Un vânzător trebuie să dea rest unui client o sumă de bani S ∈ N, având la dispoziţie bancnote
de valori b1 , b2 , ..., bn , pe care le deţine ı̂n număr nelimitat. Ştiind că vânzătorul intenţionează să folosească un
număr cât mai mic de bancnote, să se afişeze o modalitate de plată a restului.
Cum vânzătorul vrea să folosescă un număr cât mai mic de bancnote, strategia lui (greedy) ar trebui să fie
următoarea: mai ı̂ntâi, să aleagă, pe cât posibil, bancnota cea mai mare, ı̂n număr maxim posibil. În acest fel va
acoperi o mare parte a sumei cu bancnote puţine. Apoi, la fiecare pas, pentru plata sumei rămase ar trebui să
urmeze aceeaşi regulă: să aleagă bancnota de valoare cea mai mare posibilă, ı̂n număr maxim.
Vom ordona şirul bancnotelor ı̂n ordinea descrescătoare a valorilor acestora. Notăm cu xi numărul de bancnote de
Xn
valoare bi folosite pentru plata restului. Atunci S = xi bi . Dacă Sr este suma rămasă de plată la un moment
i=1
dat şi cea mai mare bancnota disponibilă este de valoare bi (bi ≤ Sr ), atunci vor fi folosite un număr de [Sr /bi ]
bancnote de acest fel.
Problema nu are ı̂ntotdeauna soluţie. De exemplu, dacă restul de plată este S = 19 şi sunt disponibile bancnote
de valoare 5 şi 10, atunci nu se poate găsi nici o combinaţie de bancnote pentru plata restului.

96
Algoritmi şi complexitate Note de curs

1 algorithm restGreedy
2 begin
3 integer n , S , b [0.. n -1] , x [0.. n -1] , i
4 read n , S
5 for i = 0 to n - 1 do
6 read b [ i ]
7 x[i] = 0
8 end for
9 sortare_descresc (b , n ) // sortare dupa valoare a bancnotelor
10 i = 0
11 while S > 0 && i < n do
12 if b [ i ] <= S then // folosim bancnota b [ i ]
13 x[i] = S / b[i]
14 S = S - x[i] * b[i]
15 end if
16 i = i + 1
17 end while
18 if S == 0 then
19 for i = 0 to n - 1 do
20 if x [ i ] != 0 then
21 write x [ i ] , " bancnote de valoare " , b [ i ]
22 end if
23 end for
24 else
25 write " Fara solutie ! "
26 end if
27 end
Pentru datele de intrare
n = 2, S = 65
valori: 10, 25
algoritmul de tip greedy nu furnizează o soluţie. Observăm că totuşi aceasta există: plata sumei cu un număr
minim de bancnote se face folosind o bancnotă de valoare 25 şi 4 bancnote de valoare 10. În schimb, algoritmul
selectează 2 bancnote de 25 (acesta e numărul maxim: 65/25) şi rămâne fără nicio posibilitate de a plăti suma
rămasă (= 15).
Dacă ı̂nsă printre bancnote există şi unele de valoare 1, atunci acest algoritm furnizează ı̂ntotdeauna o soluţie (chiar
dacă nu neapărat pe cea optimă). De exemplu, dacă adăugăm şirului de mai sus, bancnotele de valoare 1,
n = 3, S = 65
valori: 10, 25, 1
pentru plata restului se folosesc:
2 bancnote de valoare 25
1 bancnotă de valoare 10
5 bancnote de valoare 1
Deci, chiar dacă suma S poate fi plătită cu bancnotele disponibile, această metodă nu asigură obţinerea optimului
global pentru orice şir de bancnote care conţine bancnote de valoare 1.
Tehnica greedy furnizează o soluţie optimă pentru această problemă dacă valorile bancnotelor satisfac anumite
condiţii. De exemplu, dacă şirul bancnotelor, b1 , b2 , ..., bn , este ordonat descrescător, iar bi−1 este multiplu al lui
bi , pentru toţi i ≥ 2, se poate demonstra că soluţia greedy este optimă. De exemplu, pentru datele de intrare
n = 6, S = 139
valori: 1, 4, 2, 16, 8, 32
obţinem soluţia optimă:

97
Algoritmi şi complexitate Note de curs

4 bancnote de valoare 32
1 bancnotă de valoare 8
1 bancnotă de valoare 2
1 bancnotă de valoare 1.

Exemplul 2.17. (Problema rucsacului ) O persoană deţine un rucsac ı̂n care poate ı̂ncărca marfă cu masa maximă
M . Persoana are la dispoziţie n obiecte; pentru fiecare obiect cunoaşte masa sa şi câştigul pe care l-ar obţine
dacă obiectul ar fi transportat ı̂n ı̂ntregime. Să se determine ce obiecte ar trebui să aleagă persoana pentru a le
transporta astfel ı̂ncât câştigul total să fie maxim, fără a depăşi cantitatea maximă ce poate fi ı̂ncărcată ı̂n rucsac.
Obiectele vor fi identificate prin numere de la 1 la n. Facem următoarele notaţii pentru fiecare obiect i, i = 1, n:
ˆ mi – masa obiectului;

ˆ ci – câştigul ce s-ar obţine dacă obiectul ar fi transportat ı̂n ı̂ntregime;


ci
ˆ si = – câştigul unitar.
mi
Vom considera următoarele două cazuri:

1. toate obiectele pot fi fracţionate (se poate lua orice parte din obiectul i);
2. obiectele nu pot fi fracţionate (fiecare obiect i poate fi transportat sau ı̂n ı̂ntregime sau deloc).
Vom nota cu xi cantitatea din obiectul i ce este ı̂ncărcată ı̂n rucsac pentru a fi transportată. Notăm cu x =
(x1 , x2 , ..., xn ) vectorul soluţiei. Soluţia optimă va trebui să satisfacă următoarele condiţii, ı̂n funcţie de cazul
considerat:
n
X
1. mi xi = M , unde xi ∈ [0, 1], i = 1, n (se permite fracţionarea obiectelor);
i=1
Xn
2. mi xi ≤ M , unde xi ∈ {0, 1}, i = 1, n (nu se permite fracţionarea obiectelor).
i=1

n
X
Pentru ambele cazuri, soluţia optimă trebuie să maximizeze câştigul total, CT = ci xi . Vom sorta obiectele ı̂n
i=1
ordinea descrescătoare a câştigurilor unitare.
Pentru primul caz, ı̂n care se permite fracţionarea obiectelor, se vor ı̂ncărca obiectele pe cât posibil ı̂n ı̂ntregime,
ı̂ncepând cu obiectul de câştig unitar maxim, continuând apoi cu următorul, respectând ordinea considerată, până
se ajunge la masa maximă a rucsacului. În acest caz se poate demonstra că algoritmul greedy conduce la soluţia
optimă a problemei.
1 structure Obiect
2 integer nr ; // numarul curent al obiectului
3 real m , c , s ;
4 end structure
5

6 algorithm rucsacGreedy ( cazul continuu )


7 begin
8 integer n , i
9 real M , C , part
10 Obiect A [0.. n -1]
11 read n , M
12 for i = 0 to n - 1 do
13 read A [ i ]. m , A [ i ]. c
14 A [ i ]. s = A [ i ]. c / A [ i ]. m
15 A [ i ]. nr = i + 1
16 end for
17 sortare_descresc (A , n ) // sortare dupa castigul unitar

98
Algoritmi şi complexitate Note de curs

18 C = 0
19 i = 0
20 while M > 0 && i < n do
21 if M >= A [ i ]. m then
22 // incarcam integral obiectul A [ i ]. nr
23 M = M - A [ i ]. m
24 C = C + A [ i ]. c
25 write A [ i ]. nr , " complet "
26 else
27 // incarcam partial obiectul A [ i ]. nr
28 part = M / A [ i ]. m
29 C = C + A [ i ]. c * part
30 write A [ i ]. nr , " partial " , 100 * part , " % "
31 M = 0
32 end if
33 i = i + 1
34 end while
35 write C
36 end
De exemplu, pentru datele de intrare
n = 5, M = 10
masa: 1, 4, 3, 5, 2
castigul: 2, 9, 10, 15, 1
trebuie calculat, mai ı̂ntâi, câştigul unitar: 2, 2.25, 3.33, 3, 0.5, pentru fiecare dintre cele 5 obiecte. Se
ordonează obiectele după câştigul unitar şi, conform strategiei greedy, obiectul 3 este ı̂ncărcat complet, obiectul 4
este selectat complet, iar obiectul 2 este ı̂ncărcat parţial, ı̂n proporţie de 50%. Câştigul total este egal cu 29.5.
Pentru al doilea caz, se adaugă ı̂n rucsac obiectele după aceeaşi regulă, doar că nu se permite fracţionarea acestora.
Încărcarea ı̂n rucsac a obiectelor are loc fie până când masa totală a obiectelor alese este egală cu cantitatea maximă
ce poate fi ı̂ncărcată ı̂n rucsac (soluţia este optimă), fie mai există loc ı̂n rucsac, dar nu mai pot fi ı̂ncărcate alte
obiecte, deoarece masa individuală a acestora depăşeşte cantitatea ce mai poate fi adăugată ı̂n rucsac (soluţia
obţinută nu este ı̂ntotdeauna optimă).
1 algorithm rucsacGreedy ( cazul discret )
2 begin
3 integer n , i
4 real M , C , M1
5 Obiect A [0.. n -1]
6 read n , M
7 for i = 0 to n - 1 do
8 read A [ i ]. m , A [ i ]. c
9 A [ i ]. s = A [ i ]. c / A [ i ]. m
10 A [ i ]. nr = i + 1
11 end for
12 sortare_descresc (A , n ) // sortare dupa castigul unitar
13 C = 0
14 M1 = 0
15 for i = 0 to n - 1 do
16 if M1 + A [ i ]. m <= M then
17 // incarcam integral obiectul A [ i ]. nr
18 M1 = M1 + A [ i ]. m
19 C = C + A [ i ]. c
20 write A [ i ]. nr , " complet "
21 end if
22 end for

99
Algoritmi şi complexitate Note de curs

23 write C
24 end
În acest caz, soluţia obţinută prin tehnica greedy nu este ı̂ntotdeauna optimă. De exemplu, pentru datele de mai
sus, algoritmul greedy oferă soluţia optimă: se selectează ı̂n ı̂ntregime obiectele 3, 4 şi 1, câştigul total fiind egal
cu 27.
Considerăm datele:
n = 3, M = 7
masa: 5, 3, 4
castigul: 6, 3, 4
Strategia greedy pentru această instanţă a problemei oferă soluţia: se ı̂ncarcă obiectul 1, având cel mai mare câştig
unitar, iar profitul total este egal cu 6. Soluţia returnată de algoritm nu este optimă. Optim ar fi să se aleagă
obiectele 2 şi 3, care ar da un câştig total egal cu 7, mai mare decât cel furnizat de algoritm.
Totuşi, soluţiile obţinute folosind tehnica greedy pentru această problemă sunt sub–optimale (aproximează suficient
de bine soluţiile optime, iar pentru instanţe de dimensiuni mari, pot fi considerate acceptabile).
Exemplul 2.18. Un vânzător a aşezat ı̂n mod eronat n produse ı̂n n + 1 cutii (n produse au fost aşezate ı̂n n
cutii, iar o cutie a rămas goală), ı̂ncurcând cutiile şi produsele. Să se determine numărul minim de mutări pe care
trebuie să le facă vânzătorul pentru ca produsele să ajungă ı̂n cutiile lor corecte.
Produsele sunt identificate prin numere de la 1 la n.
În cele ce urmează vom da câteva exemple de instanţe ale problemei: n reprezintă numărul produselor, configuraţia
iniţială (CI) şi cea finală (CF) conţin n + 1 numere distincte de la 0 la n, reprezentând dispunerea iniţială, respectiv
cea dorită (finală), a produselor ı̂n cutiile numerotate de la 1 la n + 1. Numărul 0 ı̂n ambele configuraţii are aceeaşi
semnificaţie – cutie goală.
n = 4
CI: 3 4 1 0 2
CF: 4 0 2 1 3
Conform acestor date de intrare, sunt 4 produse dispuse ı̂n 5 cutii: cutia 1 conţine produsul 3, dar ar trebui să
conţină produsul 4, cutia 2 conţine produsul 4, dar ar trebui să fie goală, cutia 3 conţine produsul 1, dar ar trebui
să conţină produsul 2, cutia 4 este goală, dar ar trebui să conţină produsul 1, iar cutia 5 conţine produsul 2, dar
ar trebui să conţină produsul 3.
Alte exemple de instanţe ale problemei:
n = 4
CI: 2 0 1 4 3
CF: 3 0 2 1 4
sau
n = 6
CI: 2 5 0 6 1 4 3
CF: 3 6 5 0 2 1 4
Fiecare produs i, i ∈ {1, 2, . . . , n} trebuie mutat la locul său (dacă nu este deja acolo). Există aşadar două cazuri
posibile:
1. cutia ı̂n care trebuie aşezat produsul i este goală, caz ı̂n care este necesară o singură mutare;
2. cutia ı̂n care trebuie aşezat produsul i nu este goală, fiind ocupată de produsul j. În acest caz sunt necesare
2 mutari: mai ı̂ntâi se eliberează cutia, prin mutarea produsului j ı̂n cutia goală şi apoi se mută produsul i
ı̂n cutia sa corectă.
În ceea ce priveşte numărul de mutări, prima situaţie este de preferat şi atunci, o vom identifica ı̂n rezolvarea
problemei ori de câte ori este posibil.
Vom păstra configuraţia iniţială şi cea finală a produselor ı̂n vectorii s şi f. Indicele cutiei goale atât ı̂n configuraţia
iniţială, cât şi ı̂n cea finală se modifică după fiecare mutare. Variabilele sgol şi fgol memorează aceşti indici. Vom

100
Algoritmi şi complexitate Note de curs

folosi algoritmul de căutare secvenţială pentru identificarea indicelui cutiei ce conţine produsul pe care dorim să-l
plasăm ı̂n cutia corectă. Situaţia 1 se ı̂ntâlneşte când indicele cutiei goale din configuraţia iniţială este diferit de
indicele cutiei goale din configuraţia finală.
1 function cauta ( integer v [] , integer n , integer p )
2 begin
3 integer i
4 for i = 0 to n - 1 do
5 if p == v [ i ] then
6 return i
7 end if
8 end for
9 return -1
10 end
11

12 function cutiiGreedy ( integer s [] , integer f [] , integer n )


13 begin
14 integer schimb , mutari , i , sgol1
15 integer sgol = cauta (s , n + 1 , 0)
16 integer fgol = cauta (f , n + 1 , 0)
17 mutari = 0
18 i = 0
19 sgol1 = sgol
20 repeat
21 schimb = 0 // inca nu am schimbat nimic
22 while sgol1 != fgol do // rezolvam situatia 1
23 // se cauta produsul ce urmeaza a fi plasat in cutia corecta
24 sgol1 = cauta (s , n + 1 , f [ sgol ])
25 // mutam produsul corect in cutia goala
26 s [ sgol ] = f [ sgol ]
27 // se elibereaza o noua cutie
28 s [ sgol1 ] = 0
29 sgol = sgol1
30 mutari = mutari + 1 // s - a facut o mutare
31 schimb = 1 // s - a realizat o schimbare
32 end while
33 // se cauta o cutie nearanjata
34 while s [ i ] == f [ i ] && i < n + 1 do
35 i = i + 1
36 end while
37 if i <= n then // se rezova situatia 2
38 sgol1 = cauta (s , n + 1 , f [ i ])
39 s [ sgol ] = s [ i ] // se goleste cutia nearanjata
40 s [ i ] = f [ i ] // se aduce produsul corect
41 s [ sgol1 ] = 0 // se goleste o noua cutie
42 sgol = sgol1
43 i = i + 1
44 mutari = mutari + 2 // s - au facut 2 mutari
45 schimb = 1 // s - a realizat o schimbare
46 end if
47 until schimb == 0
48 write mutari
49 end
Pentru primul exemplu, şirul mutărilor este:
produsul 1 se mută din cutia 3 ^
ın cutia 4

101
Algoritmi şi complexitate Note de curs

produsul 2 se mută din cutia 5 ı


^n cutia 3
produsul 3 se mută din cutia 1 ^
ın cutia 5
produsul 4 se mută din cutia 2 ^
ın cutia 1
ajungându-se la configuraţia dorită ı̂n 4 mutări.

2.5 Tehnica programării dinamice


2.5.1 Descrierea metodei
Dacă ı̂n cazul tehnicii backtracking, de exemplu, se pot identifica foarte simplu problemele rezolvabile folosind
această metodă, nu acelaşi lucru se poate spune şi despre metoda programării dinamice. Această tehnică de proiec-
tare a algoritmilor presupune rezolvarea unei probleme prin descompunerea acesteia ı̂n subprobleme de dimensiuni
mai mici. Spre deosebire de metoda divide et impera, unde subproblemele rezultate trebuie să fie independente, ı̂n
cadrul acestei metode, subproblemele care apar ı̂n descompunere nu sunt independente. Acestea se suprapun, iar
soluţia unei subprobleme se utilizează ı̂n construirea soluţiilor altor subprobleme. În cazul ı̂n care subproblemele
se suprapun şi s-ar folosi o abordare directă a problemei, s-ar efectua calcule redundante, rezolvându-se fiecare
subproblemă ca şi când aceasta nu ar mai fi fost ı̂ntâlnită până atunci. Folosind metoda programării dinamice
ı̂nsă, se rezolvă fiecare dintre subprobleme o singură dată şi se memorează soluţia acesteia ı̂ntr-o structură de date,
evitând astfel rezolvarea redundantă a aceleiaşi subprobleme.
În denumirea metodei, cuvântul programare nu se referă la scrierea de cod ı̂ntr-un anumit limbaj informatic, ci la
programarea matematică, care constă ı̂n optimizarea unei funcţii obiectiv prin alegerea de valori dintr-o mulţime
anume, iar cuvântul dinamic se referă la maniera ı̂n care sunt construite structurile ı̂n care se stochează soluţiile
subproblemelor.
Metoda programării dinamice se foloseşte ı̂n special pentru rezolvarea problemelor de optimizare. Deşi o astfel
de problemă poate avea mai multe soluţii optime, metoda furnizează doar una singură. Pentru a genera toate
soluţiile optime, putem să o combinăm cu tehnica backtracking. Deoarece metoda programării dinamice găseşte o
soluţie fără a genera toate soluţiile posibile, putem găsi o asemănare cu metoda greedy, ambele metode exploatând
proprietatea de substructură optimă a problemei (orice soluţie optimă a problemei iniţiale conţine o soluţie optimă a
unei subprobleme, deci soluţia optimă a problemei se obţine prin combinarea soluţiilor optime ale subproblemelor).
Rezolvarea unei probleme folosind metoda programării dinamice presupune parcurgerea următoarelor etape:
1. se identifică subproblemele ı̂n care poate fi ı̂mpărţită problema originală; se stabileşte modul ı̂n care soluţia
problemei depinde de soluţiile subproblemelor (ı̂n această etapă se verifică proprietatea de substructură op-
timă);
2. se găsesc relaţii de recurenţă care leagă soluţiile subproblemelor ı̂ntre ele şi de soluţia problemei globale;
3. se dezvoltă relaţia de recurenţă şi se reţin rezultatele parţiale ı̂ntr-o structură de date (tablou unidimensional,
bidimensional etc.), dacă acest lucru este necesar;
4. se reconstituie soluţia pornind de la datele deja memorate.
Ne amintim că există două abordări ı̂n dezvoltarea relaţiilor de recurenţă:
ˆ descendentă, ı̂n care valoarea de calculat se exprimă prin valori anterioare, ce trebuie la rândul lor calculate.
Această abordare se implementează, de regulă, recursiv şi, de cele mai multe ori, este ineficientă; folosind
această abordare se rezolvă doar subproblemele ce contribuie la soluţia problemei, ı̂nsă o subproblemă este
rezolvată de mai multe ori, de câte ori aceasta apare;
ˆ ascendentă, se porneşte de la cazul elementar şi se generează noi valori pe baza celor deja calculate, conform
formulei de recurenţă; folosind această abordare se rezolvă toate subproblemele, indiferent dacă contribuie
sau nu la soluţia problemei, ı̂nsă o subproblemă este rezolvată o singură dată.
Dezvoltarea relaţiilor de recurenţă folosind abordarea descendentă poate deveni eficientă prin folosirea tehnicii me-
moizării. Tehnica presupune memorarea rezultatelor returnate de o funcţie ı̂n vederea reutilizării acestora. Com-
binată cu programarea dinamică, tehnica memoizării presupune iniţializarea structurii utilizate pentru păstrarea
soluţiilor subproblemelor cu o valoare virtuală, diferită de orice valoare posibilă ce ar putea rezulta din dezvoltarea

102
Algoritmi şi complexitate Note de curs

relaţiilor de recurenţă. Relaţiile de recurenţă sunt abordate descendent, dar soluţiile subroblemelor sunt memorate
ı̂ntr-o structură şi utilizate ı̂n construirea soluţiilor altor subprobleme, atunci când este necesar. Astfel se vor
rezolva doar subproblemele care contribuie la soluţia problemei şi doar o singură dată.

2.5.2 Aplicaţii
Exemplul 2.19. (Şirul lui Fibonacci) Revenim la problema determinării termenului de rang n ∈ N al şirului lui
Fibonacci, definit prin: (
n, n = 1 sau n = 0
Fn = (2.4)
Fn−1 + Fn−2 , n ≥ 2.
Reamintim că elaborarea unui algoritm recursiv care să descrie direct recurenţa de mai sus conduce la calcule
redundate, iar complexitatea asimptotică a unui astfel de algoritm este exponenţială (vezi Exemplul 1.31).
Într-o primă abordare, vom descrie relaţia de recurenţă ascendent. Termenii şirului ı̂i vom păstra ı̂ntr-un tablou
unidimensional F [0..n], de dimensiune n + 1 (declarat global).
1 integer F [0.. n ]
2

3 function Fibo ( integer n )


4 begin
5 integer i
6 F [0] = 0
7 F [1] = 1
8 for i = 2 to n do
9 F [ i ] = F [ i - 1] + F [ i - 2]
10 end for
11 return F [ n ]
12 end
13

14 algorithm Fibonacci
15 begin
16 integer n
17 read n
18 write Fibo ( n )
19 end
Complexitatea – timp a algoritmului este de ordinul Θ(n), iar spaţiul suplimentar necesar este de acelaşi ordin.
O altă abordare este să descriem relaţia de recurenţă descendent, folosind tehnica memoizării. Abordarea este una
similară descrierii descendente a formulei recursive, doar că soluţiile subproblemelor sunt păstrate ı̂ntr-o structură
de tip tablou unidimensional, pe măsură ce sunt calculate. Vom folosi un vector cu toate elementele iniţializate
cu −1 (această valoare este “virtuală”, imposibilă pentru Fn , ∀n ∈ N). Atunci când vom calcula soluţia unei
subprobleme, mai ı̂ntâi o vom căuta ı̂n tablou. Dacă aceasta este deja calculată o vom returna, dacă nu, atunci va
fi calculată şi plasată ı̂n vector, ı̂n scopul reutilizării viitoare.
1 integer F [0.. n ]
2

3 function FiboMem ( integer n )


4 {
5 if F [ n ] == -1 then // daca F [ n ] nu a fost calculat inca
6 if n == 0 || n == 1 then
7 F[n] = n
8 else
9 F [ n ] = FiboMem ( n - 1) + FiboMem ( n - 2)
10 end if
11 end if
12 return F [ n ]

103
Algoritmi şi complexitate Note de curs

13 }
14

15 algorithm Fibonacci
16 begin
17 integer i , n
18 read n
19 for i = 0 to n do
20 F [ i ] = -1
21 end for
22 write FiboMem ( n )
23 end
Obţinem o complexitate liniară a algoritmului, atât timp, cât şi apaţiu. Privind arborele de apeluri al algoritmului
recursiv FiboMem(6), observăm că subproblema FiboMem(4) este rezolvată o singură dată, rezultatul fiind plasat
ı̂n F [4] şi reutilizat. La fel, subproblemele FiboMem(3), FiboMem(2) etc.
FiboMem(6)

FiboMem(5) FiboMem(4)

F [4]

FiboMem(4) FiboMem(3)

F [3]
FiboMem(3) FiboMem(2)

F [2]
FiboMem(2) FiboMem(1)

F [1]
FiboMem(1) FiboMem(0)

F [1] F [0]

Un nou algoritm, de complexitate logaritmică de data aceasta, pleacă de la următoarea reprezentare ı̂n formă
matriceală:  n  
1 1 Fn+1 Fn
= , n ≥ 1. (2.5)
1 0 Fn Fn−1
Afirmaţia (2.5) poate fi demonstrată prin inducţie. Pentru n = 1, afirmaţia devine
   
1 1 F2 F1
= ,
1 0 F1 F0

ceea ce e adevărat, conform definiţiei (2.4). Presupunem afirmaţia adevărată pentru n = k şi demonstrăm că este
adevărată pentru n = k + 1:
 k+1  k         
1 1 1 1 1 1 Fk+1 Fk 1 1 Fk+1 + Fk Fk+1 Fk+2 Fk+1
= = = = .
1 0 1 0 1 0 Fk Fk−1 1 0 Fk + Fk−1 Fk Fk+1 Fk

Aşadar, afirmaţia (2.5) este adevărată, (∀) n ≥ 1.


Ţinem cont că, pentru n ≥ 1,
2n  n+n 
Fn2 + Fn+1
2
    
1 1 1 1 Fn+1 Fn Fn+1 Fn Fn−1 Fn + Fn Fn+1
= = = 2 .
1 0 1 0 Fn Fn−1 Fn Fn−1 Fn−1 Fn + Fn Fn+1 Fn−1 + Fn2

104
Algoritmi şi complexitate Note de curs

În acelaşi timp, folosind (2.5), obţinem


 2n  
1 1 F2n+1 F2n
= .
1 0 F2n F2n−1

Din ultimele două relaţii, obţinem că


2
F2n−1 = Fn−1 + Fn2
F2n = (Fn−1 + Fn+1 )Fn
= (Fn−1 + (Fn + Fn−1 ))Fn
= (2Fn−1 + Fn )Fn .

Astfel, am obţinut o nouă relaţie de recurenţă pentru termenul de rang n al şirului lui Fibonacci:

n,
 n = 0 sau n = 1
Fn = (2Fn/2−1 + Fn/2 )Fn/2 , n ≥ 2, n par
F 2
 2
n ≥ 2, n impar
(n−1)/2 + F(n+1)/2 ,

Descriem recurenţa ı̂ntr-un algoritm recursiv, folosind tehnica memoizării.


1 integer F [0.. n ]
2

3 function FiboMem2 ( integer n )


4 begin
5 if F [ n ] == -1 then
6 if n == 0 || n == 1 then
7 F[n] = n
8 else
9 integer k , F1 , F2
10 if n % 2 == 0 then
11 k = n / 2
12 F2 = FiboMem2 ( k )
13 F [ n ] = (2 * FiboMem2 ( k - 1) + F2 ) * F2
14 else
15 k = ( n + 1) / 2
16 F1 = FiboMem2 ( k - 1)
17 F2 = FiboMem2 ( k )
18 F [ n ] = F1 * F1 + F2 * F2
19 end if
20 end if
21 end if
22 return F [ n ]
23 end
24

25 algorithm Fibonacci
26 begin
27 integer i , n
28 read n
29 for i = 0 to n do
30 F [ i ] = -1
31 end for
32 write FiboMem2 ( n )
33 end

105
Algoritmi şi complexitate Note de curs

Exemplul 2.20. (Calculul coeficienţilor binomiali C(n, k), n, k ∈ N, 0 ≤ k ≤ n) Pentru rezolvarea problemei,
amintim formula de recurenţă:
(
1, k = 0 sau k = n,
C(n, k) =
C(n − 1, k − 1) + C(n − 1, k), 1 ≤ k ≤ n − 1.

Problema poate fi rezolvată implementând direct recurenţa, algoritmul rezultat având o complexitate exponenţială:
1 function binCoef1 ( integer n , integer k )
2 begin
3 if k == 0 || k == n then
4 return 1
5 end if
6 return binCoef1 ( n - 1 , k - 1) + binCoef1 ( n - 1 , k )
7 end
Varianta aceasta nu este recomandată, fiind una redundantă, aceeaşi valoare fiind calculată de mai multe ori
(complexitate exponenţială). De exemplu, pentru a calcula C(5, 2), se calculează C(3, 1) de 2 ori, C(2, 1) de 3 ori
etc., aşa cum se poate observa din arborele de apeluri corespunzător.
C(5,2)

C(4,1) C(4,2)

C(3,0) C(3,1)
C(3,1) C(3,2)
C(2,0) C(2,1)
C(2,0) C(2,1) C(2,1) C(2,2)
C(1,0) C(1,1)
C(1,0) C(1,1) C(1,0) C(1,1)

În cele ce urmează vom aborda relaţia de recurenţă ascendent, valorile coeficienţilor binomiali calculându-se progre-
siv. Aceste valori vor fi salvate ı̂ntr-o matrice C (declarată global) cu (n + 1) linii şi (k + 1) coloane. Complexitatea
– timp a algoritmului este de ordinul Θ(nk), iar spaţiul suplimentar necesar este de acelaşi ordin.
1 integer C [0.. n ][0.. k ]
2

3 function binCoef2 ( integer n , integer k )


4 begin
5 integer i , j
6 for i = 0 to n do
7 // se construieste triunghiul lui Pascal
8 for j = 0 to i do // ar fi suficient ca j sa ia valori de la 0 la min (i , k )
9 if j == 0 || j == i then
10 C [ i ][ j ] = 1
11 else
12 C [ i ][ j ] = C [ i - 1][ j - 1] + C [ i - 1][ j ]
13 end if
14 end for
15 end for
16 return C [ n ][ k ]
17 end

106
Algoritmi şi complexitate Note de curs

Dacă trebuie calculat doar C(n, k), ı̂n scopul optimizării spaţiului de memorie alocat, se poate folosi ca spaţiu de
stocare un tablou unidimensional (declarat global) de dimensiune k + 1.
1 integer C [0.. k ]
2

3 function min ( integer a , integer b )


4 begin
5 if a < b then
6 return a
7 else
8 return b
9 end if
10 end
11

12 function binCoef3 ( integer n , integer k )


13 begin
14 integer i , j
15 C [0] = 1 // C (n ,0)
16 for i = 1 to n do
17 /* Se calculeaza urmatoarea linie din triunghiul lui Pascal , folosind - o pe
precedenta */
18 for j = min (i , k ) downto 1 do
19 C [ j ] = C [ j ] + C [ j - 1]
20 end for
21 end for
22 return C [ k ]
23 end
De exemplu, dacă vrem să calculăm C(5, 2) (n = 5, k = 2), toate cele k + 1 = 3 elemente ale vectorului auxiliar
0
C sunt iniţializate cu 0: C[0] = C[1] = C[2] = 0. Apoi are loc atribuirea C[0] = 1, ı̂ntrucât Cm = 1, cu
m = 0, 1, 2, 3, 4, 5.
Pentru i = 1: C[1] = C[1] + C[0] = 0 + 1 = 1, deci C(1, 1) = 1
Pentru i = 2: C[2] = C[2] + C[1] = 0 + 1 = 1, deci C(2, 2) = 1
C[1] = C[1] + C[0] = 1 + 1 = 2, deci C(2, 1) = 2
Pentru i = 3: C[2] = C[2] + C[1] = 1 + 2 = 3, deci C(3, 2) = 3
C[1] = C[1] + C[0] = 2 + 1 = 3, deci C(3, 1) = 3
Pentru i = 4: C[2] = C[2] + C[1] = 3 + 3 = 6, deci C(4, 2) = 6
C[1] = C[1] + C[0] = 3 + 1 = 4, deci C(4, 1) = 4
Pentru i = 5: C[2] = C[2] + C[1] = 6 + 4 = 10, deci C(5, 2) = 10
C[1] = C[1] + C[0] = 4 + 1 = 5, deci C(5, 1) = 5.
Deci C[2] = 10 va fi valoarea returnată de funcţie.
Folosind tehnica memoizării combinată cu programarea dinamică, abordarea este una similară descrierii descendente
a formulei recursive, doar că soluţiile subproblemelor sunt păstrate ı̂ntr-o structură de tip matrice. Vom folosi o
matrice iniţializată cu 0. Atunci când vom calcula soluţia unei subprobleme, mai ı̂ntâi o vom căuta ı̂n matrice.
Dacă aceasta este deja calculată o vom returna, dacă nu, atunci va fi calculată şi plasată ı̂n matrice, ı̂n scopul
reutilizării viitoare.
1 integer C [0.. n ][0.. k ]
2

3 function binCoef4 ( integer n , integer k )


4 begin
5 if C [ n ][ k ] == 0 then // daca C [ n ][ k ] nu a fost calculat inca
6 if k == 0 || k == n then
7 C [ n ][ k ] = 1
8 else

107
Algoritmi şi complexitate Note de curs

9 C [ n ][ k ] = binCoef4 ( n - 1 , k - 1) + binCoef4 ( n - 1 , k )
10 end if
11 end if
12 return C [ n ][ k ]
13 end

Exemplul 2.21. (Plata restului cu un număr minim de bancnote) Un vânzător trebuie să dea rest unui client
o sumă de bani S, având la dispoziţie bancnote de valori b1 , b2 , ..., bn , ı̂n număr nelimitat. Ştiind că vânzătorul
intenţionează să folosească un număr cât mai mic de bancnote şi că are la dispoziţie ı̂ntotdeauna bancnote de valoare
1, să se afişeze modalitatea optimă de plată a restului. Presupunem că S şi b1 , b2 , ..., bn sunt numere naturale.
Proprietatea de substructură optimă. Notăm cu P (S) problema plăţii sumei S cu un număr minim de bancnote
de valori b1 , . . . , bn . Considerăm s o soluţie optimă a problemei P (S). Împărţim soluţia ı̂n două părţi, sl şi sr , cu
următoarea semnificaţie: sl reprezintă modalitatea de a plăti suma j, iar sr , suma S − j:

b1 b3 b5 . . . b1 b2 b1 b4 . . . b2
| {z }| {z }
suma j suma S−j

sl şi sr reprezintă soluţii optime pentru subproblemele corespunzătoare, P (j) şi P (S − j). Altfel, dacă se presupune
prin reducere la absurd că soluţia sl nu este optimă pentru problema P (j), atunci ar exista o soluţie mai buna, s0l .
Înlocuind pe sl cu s0l ı̂n soluţia optimă s, s-ar obţine un număr mai mic de bancnote pentru plata sumei S, ceea ce
este o contradicţie. Acelaşi raţionament se utilizeazz̆ pentru sr .
Vom folosi tabloul unidimensional C cu S + 1 componente pentru a salva soluţiile subproblemelor: C[j] reprezintă
numărul minim de bancnote de tipul b1 , b2 , ..., bn folosite pentru plata sumei j. Numărul minim de bancnote
necesar plăţii sumei S (soluţia optimă a problemei iniţiale) va fi calculat ı̂n C[S]. Dacă pentru plata optimă a
sumei j se foloseşte o bancnotă de tipul bi , atunci numărul de bancnote utilizate creşte cu o unitate şi se reduce
corespunzător suma de plată. Astfel vom avea

C[j] = 1 + C[j − bi ].

Pentru fiecare sumă rămasă de plată j, j = 0, S, alegem acea bancnotă bi din cele n tipuri de bancnote, care
satisface restricţia bi ≤ j şi care minimizează 1 + C[j − bi ]. Evident, dacă suma de plată este 0, atunci soluţia
optimă a problemei este 0 (C[0] = 0).
Obţinem astfel formula recursivă de calcul:

0, dacă j = 0,
C[j] =
minim(1 + C[j − bi ]), dacă j ≥ 1.
i : bi ≤j

Pe măsură ce se completează soluţia optimă, tipul bancnotei folosite este păstrat ı̂ntr-un tablou suplimentar s.
1 integer n , S , C [0.. S ] , b [0.. n -1] , s [0.. S ] , M [0.. S ]
2

3 function plataS () // Programare dinamica -- abordare ascendenta


4 begin
5 integer i , j , min , bm
6 C [0] = 0 // tabloul C are S +1 elemente
7 for j = 1 to S do
8 min = INT_MAX
9 for i = 0 to n - 1 do
10 if b [ i ] <= j then
11 if 1 + C [ j - b [ i ]] < min then
12 min = 1 + C [ j - b [ i ]]
13 bm = b [ i ]
14 end if
15 end if
16 end for

108
Algoritmi şi complexitate Note de curs

17 C [ j ] = min
18 s [ j ] = bm
19 end for
20 return C [ S ]
21 end
22

23 /* afisarea sirului de bancnote folosite pentru plata sumei S */


24 function constructSol ( integer j )
25 begin
26 if j > 0 then
27 constructSol ( j - s [ j ])
28 write s [ j ]
29 end if
30 end
31

32 // tehnica memoizarii & Programare dinamica -- abordare descendenta


33 function init () // initializarea elementelor tabloului M cu o valoare virtuala
34 begin
35 for i = 0 to S do
36 M [ i ] = -1
37 end for
38 end
39

40 function plataSmem ( integer j )


41 begin
42 // valorile intermediare necesare obtinerii solutiei se stocheaza
43 integer i
44 if M [ j ] == -1 then
45 if j == 0 then
46 M[j] = 0
47 else
48 M [ j ] = INT_MAX
49 for i = 0 to n - 1 do
50 M [ j - b [ i ]] = plataSmem ( j - b [ i ])
51 if b [ i ] <= j && 1 + M [ j - b [ i ]] < M [ j ] then
52 M [ j ] = 1 + M [ j - b [ i ]]
53 end if
54 end for
55 end if
56 end if
57 return M [ j ]
58 end
59

60 algorithm plataRest
61 begin
62 read n , S , b
63 write plataS ()
64 constructSol ( S )
65 init () ;
66 write plataSmem ( S )
67 end
Complexitatea algoritmului este O(nS).

109
Algoritmi şi complexitate Note de curs

Dacă vrem să plătim suma S = 129, cu un număr minim de bancnote de tipul b1 = 12, b2 = 5, b3 = 1 şi
b4 = 25, algoritmul greedy corespunzător nu ne furnizează soluţia optimă. În schimb, algoritmul proiectat folosind
programarea dinamică construieşte soluţia optimă a problemei.
n = 4, S = 129
valori bancnote: 12 5 1 25
Numarul minim de bancnote: 7
Bancnotele selectate: 25 25 25 25 5 12 12
Exemplul 2.22. (Problema rucsacului, cazul discret) Considerăm problema rucsacului ı̂n varianta ı̂n care nu este
permisă fracţionarea obiectelor. Avem la dispoziţie n obiecte, fiecare cu o anumită masă mi şi un anumit câştig
ci , i = 1, n, care s-ar obţine prin transportul obiectului i ı̂n ı̂ntregime. Se pune problema ce obiecte să selectăm
pentru a le transporta cu un rucsac ı̂n care se poate ı̂ncărca o cantitate maximă M , astfel ı̂ncât să obţinem un
câştig maxim. Presupunem că M şi m1 , . . . , mn sunt numere naturale.
Am văzut că, ı̂n acest caz, ı̂n care nu este permisă fracţionarea obiectelor, metoda greedy nu furnizează ı̂ntotdeauna
soluţia optimă.
Proprietatea de substructură optimă. Notăm cu P (n, M ) problema selectării unor obiecte identificate prin numerele
{1, 2, . . . , n} pentru a umple optim un rucsac ı̂n care ı̂ncape maxim o cantitate M . De asemenea, notăm cu P (i, j)
problema selectării unor obiecte din mulţimea {1, . . . , i} pentru a umple optim un rucsac ı̂n care ı̂ncape maxim o
cantitate j. Observăm că pentru i < n, j < M , P (i, j) este o subproblemă a lui P (n, M ). Fie s(i, j) o soluţie
optimă a problemei P (i, j). Dacă
ˆ obiectul i este selectat, atunci se ajunge la subproblema P (i − 1, j − mi ), iar dacă s(i, j) este optimă pentru
problema P (i, j), atunci s(i − 1, j − mi ) va fi soluţie optimă pentru P (i − 1, j − mi ).
ˆ obiectul i nu este selectat, atunci se ajunge la subproblema P (i − 1, j), iar dacă s(i, j) este optimă pentru
problema P (i, j) atunci s(i − 1, j) va fi soluţie optimă pentru P (i − 1, j).
Aşadar, problema rucsacului are proprietatea de substructură optimă.
Presupunem că măcar un obiect are masa mai mică decât M . Vom folosi tabloul bidimensional D, cu n + 1
linii şi M + 1 coloane, ı̂n care vom păstra soluţiile subproblemelor: D[i][j] este cel mai bun câştig obţinut pentru
primele i obiecte, având masa ı̂nsumată maxim j. Soluţia se construieşte astfel: dacă D[n][M ] este câştigul maxim
obţinut prin selectarea unei submulţimi de obiecte din cele n disponibile, care nu depăşesc masa totală care poate
fi transportată ı̂n rucsac, M , atunci relaţia de recurenţă este:

D[n][M ] = maxim(D[n − 1][M ], D[n − 1][M − mn ] + cn ).

Astfel, câştigul maxim se obţine fie fără a adăuga obiectul n, rămânând la câştigul obţinut pentru n − 1 obiecte, fie
prin adăugarea obiectului n, caz ı̂n care se adaugă la câştigul obţinut pentru n − 1 obiecte şi greutate M − mn , cel
rezultat prin transportul obiectului n. Ideea algoritmului este deci următoarea: la fiecare pas, la soluţia curentă
ori nu adăugăm deloc obiectul i şi rămânem la câştigul obţinut pentru i − 1 obiecte, ori adăugăm obiectul i, caz ı̂n
care adăugăm câştigul rezultat prin transportul lui, la cel obţinut pentru primele i − 1 obiecte şi greutate maximă
j − mi . Obţinem formula de recurenţă:

0,
 dacă i = 0 sau j = 0,
D[i][j] = D[i − 1][j], dacă i > 0 şi j < mi ,

maxim(D[i − 1][j], D[i − 1][j − mi ] + ci ), dacă i > 0 şi j ≥ mi .

1 integer n , M , D [0.. n ][0.. M ] , m [1.. n ] , c [1.. n ] , sol [1.. n ] , S [0.. n ][0.. M ]


2

3 function max ( integer a , integer b )


4 begin
5 if a > b then
6 return a
7 else
8 return b
9 end if

110
Algoritmi şi complexitate Note de curs

10 end
11

12 function rucsac1 () // Programare dinamica -- abordare ascendenta


13 begin
14 integer i , j
15 for i = 0 to n do // tabloul D are n +1 linii si M +1 coloane
16 for j = 0 to M do
17 if i == 0 || j == 0 then
18 D [ i ][ j ] = 0
19 else
20 if j < m [ i ] then
21 D [ i ][ j ] = D [i -1][ j ]
22 else
23 D [ i ][ j ] = max ( D [i -1][ j ] , D [i -1][ j - m [ i ]] + c [ i ])
24 end if
25 end if
26 end for
27 end for
28 return D [ n ][ M ]
29 end
30

31 function constructSol ()
32 begin
33 integer i , j
34 i = n
35 j = M
36 while D [ i ][ j ] > 0 do
37 if D [ i ][ j ] == D [i -1][ j ] then
38 i = i - 1
39 else
40 sol [ i ] = 1
41 j = j - m[i]
42 i = i - 1
43 end if
44 end while
45 end
46

47 // tehnica memoizarii & Programare dinamica -- abordare descendenta


48 function init () // initializarea matricei S cu o valoare virtuala
49 begin
50 for i = 0 to n do
51 for j = 0 to M do
52 S [ i ][ j ] = -1
53 end for
54 end for
55 end
56

57 function rucsac2 ( integer i , integer j )


58 begin
59 // valorile intermediare necesare obtinerii solutiei se stocheaza
60 if S [ i ][ j ] == -1 then
61 if i == 0 || j == 0 then
62 S [ i ][ j ] = 0
63 else
64 if j < m [ i ]
65 S [ i ][ j ] = rucsac2 ( i - 1 , j )

111
Algoritmi şi complexitate Note de curs

66 else
67 S [ i ][ j ] = max ( rucsac2 (i -1 , j ) , rucsac2 (i -1 ,j - m [ i ]) + c [ i ])
68 end if
69 end if
70 end for
71 return S [ i ][ j ]
72 end
73

74 algorithm R u c s a c _ c a z u l _ d i s c r e t
75 begin
76 integer i
77 read n , M , m , c
78 write rucsac1 ()
79 constructSol ()
80 for i = 1 to n do
81 if sol [ i ] != 0
82 write i
83 end if
84 end for
85 init ()
86 write rucsac2 (n , M )
87 end
Pentru datele de intrare: n = 3, M = 7, m1 = 5, m2 = 3, m3 = 4, c1 = 6, c2 = 3, c3 = 4, metoda greedy nu
oferă soluţia optimă. Algoritmul implementat prin tehnica programării dinamice ne conduce la soluţia optimă a
problemei.

n = 3, M = 7
masa: 5 3 4
castigul: 6 3 4
1. Varianta ascendenta:
Castigul total: 7
Obiectele selectate: 2 3
2. Tehnica memoizarii:
Castigul total: 7
Exemplul 2.23. (Cel mai lung subşir ordonat crescător (CMLSC) al unui şir oarecare) Se consideră un şir v
oarecare de n elemente numere ı̂ntregi, v0 , v1 , . . . , vn−1 . Se cere să se determine CMLSC al şirului.
Se poate demonstra că problema are proprietatea de substructură optimă. Pentru a calcula lungimea CMLSC
vom calcula mai ı̂ntâi lungimile celor mai lungi subşiruri crescătoare care ı̂ncep cu fiecare element al vectorului.
Vom memora ı̂n L[k] lungimea CMLSC care ı̂ncepe de la poziţia k şi până la sfârşitul şirului iniţial. Tabloul
unidimensional L are n elemente. Pentru ultimul element, L[n − 1] = 1. Calculăm apoi pe rând L[n − 2], . . . , L[1],
L[0]. Lungimea CMLSC va fi dată de cea mai mare valoare a vectorului L.
Ne propunem să calculăm L[k]. Pentru aceasta, trebuie mai ı̂ntâi să calculăm lungimea CMLSC din dreapta lui k,
subşir al cărui prim element este mai mare sau egal cu elementul de pe poziţia k. Dar această lungime o putem
determina ı̂n acelaşi mod. Obţinem următoarea formulă de recurenţă:

1, k = n − 1,
L[k] = 1 + maxim L[i], k = 0, 1, . . . , n − 2.

i: k<i<n, v[k]≤v[i]

Pentru a afişa şi elementele din CMLSC, folosim un vector suplimentar, next, ı̂n care reţinem pe poziţia k indicele
primului element al subşirului folosit pentru calcularea lui L[k].
1 function CMLSC ( integer v [] , integer n )
2 begin
3 integer Lmax , start , i , k

112
Algoritmi şi complexitate Note de curs

4 integer L [0.. n -1] , next [0.. n -1]


5 L [n -1] = 1
6 Lmax = 1
7 start = n - 1
8 next [n -1] = n - 1
9 for k = n - 2 downto 0 do
10 L[k] = 1
11 next [ k ] = k
12 for i = k + 1 to n - 1 do
13 if v [ k ] <= v [ i ] && L [ k ] <= L [ i ] then
14 L[k] = L[i] + 1
15 next [ k ] = i
16 end if
17 end for
18 if L [ k ] > Lmax then
19 Lmax = L [ k ]
20 start = k // pozitia de inceput a subsirului
21 end if
22 end for
23 write Lmax
24 write v [ start ]
25 for i = 1 to Lmax - 1 do
26 start = next [ start ]
27 write v [ start ]
28 end for
29 end
Pentru n = 15 şi v[ ] = {34, 1, 3, 5, 45, 23, 0, −5, 39, 40, 234, −7, −10, 56, 45}, obţinem Lmax = 7 şi CMLSC:
{1, 3, 5, 23, 39, 40, 234}.

2.6 Algoritmi aleatori


În cazul unui algoritm determinist, plecând de la acelaşi set de date de intrare, fiecare execuţie a programului care
implementează algoritmul conduce la aceleaşi rezultate, ı̂n timp finit.
Un algoritm a cărui comportare depinde nu numai de datele de intrare, ci şi de valorile produse de un generator
de numere aleatoare se numeşte algoritm aleator. Algoritmii aleatori pot fi utilizaţi pentru a impune o anumită
repartiţie a datelor de intrare, evitându-se astfel intrările ce conduc la o performanţă slabă a algoritmului. În cele
ce urmează, vom presupune că dispunem de un generator de numere aleatoare. De altfel, majoritatea mediilor
de programare dispun de un generator de numere pseudoaleatoare, adică dispun de un algoritm determinist care
generează numere ce “par” a fi aleatoare. Astfel, presupunem că avem la dispoziţie o procedură pentru generarea
de numere (pseudo)aleatoare numită random. Printr-un apel de tipul random(a,b), se generează la ı̂ntâmplare un
număr real x astfel ı̂ncât a ≤ x < b. Distribuţia lui x este uniformă pe intervalul [a, b), adică oricare număr din
1
acest interval este generat cu aceeaşi probabilitate, egală cu . Presupunem că, prin apelul repetat al rutinei,
b−a
se generează numere ı̂n mod independent de valorile generate anterior. Pentru a genera numere ı̂ntregi aleatoare,
vom extinde notaţia la random(i..j), unde i şi j sunt numere ı̂ntregi, i ≤ j. Prin apelul rutinei se generează un
număr ı̂ntreg k, ales la ı̂ntâmplare, uniform şi independent, astfel ı̂ncât i ≤ k ≤ j.
Chiar dacă am avea la dispoziţie doar o procedură de generare aleatoare de numere reale uniform distribuite
din [0, 1), putem genera la ı̂ntâmplare numere reale uniform distribuite ı̂n [a, b), printr-o prelucrare de tipul
(b-a)*random(0,1)+a. De asemenea, poate fi descrisă o rutină corespunzătoare de generare aleatoare de nu-
mere ı̂ntregi. Un exemplu interesant este algoritmul moneda, descris mai jos, prin care se simulează experienţa
aleatoare a aruncării (pe o masă) a unei monede ideale. Ambele faţe, atât cea cu stema, cât şi cea cu valoarea, au
aceeaşi probabilitate de apariţie, 1/2.
1 function uniform_cont ( real a , real b ) // random (a , b )

113
Algoritmi şi complexitate Note de curs

2 begin
3 return ( b - a ) * random (0 ,1) + a
4 end
5

6 function uniform_discret ( integer i , integer j ) // random ( i .. j )


7 begin
8 return [ uniform_cont (i , j + 1) ]
9 end
10

11 function moneda ()
12 begin
13 if uniform_discret (0 ,1) == 0 then
14 return ’s ’ // stema
15 else
16 return ’v ’ // valoarea
17 end if
18 end
Algoritmul constă ı̂n generarea aleatoare a unui număr din mulţimea {0, 1}, ambele numere având aceeaşi şansă
de a fi alese. Dacă 0 este numărul generat, atunci spunem că am obţinut faţa cu stema, iar dacă a fost generat
numărul 1, spunem că am obţinut faţa cu valoarea.
Algoritmii aleatori se caracterizează prin faptul că, la fiecare rulare, au un comportament diferit: timpul de execuţie
poate să difere de la o execuţie la alta şi chiar rezultatele obţinute.
Un algoritm aleator ale cărui rezultate sunt aleatoare se numeşte algoritm Monte Carlo. Astfel, ieşirile nu sunt
neapărat exacte sau corecte, iar probabilitatea de eroare poate fi redusă ı̂n mod semnificativ prin rulări repetate
independente. Probabilitatea de eroare poate fi redusă sub pragul de eroare al calculatorului, mai ales dacă acesta
trebuie să execute un algoritm ı̂ntr-un timp semnificativ mai mare pentru a oferi rezultatul determinist (datorită
timpului mare de execuţie, se acumulează erorile de calcul). În mod paradoxal, un algoritm aleator poate oferi un
rezultat nu doar ı̂ntr-un timp mai bun, ci poate fi şi mai de “ı̂ncredere” decât un rezultat obţinut printr-un algoritm
determinist. Mai mult decât atât, sunt probleme pentru care nu este cunoscut niciun algoritm, fie el determinist
sau nu, care să ofere rezultatul exact ı̂ntr-un timp rezonabil, chiar ignorând erorile de calcul. În schimb, dacă este
permisă o mică probabilitate de eroare, există algoritmi aleatori care ar putea rezolva aceste probleme ı̂ntr-un timp
mai bun (de exemplu, problema prin care se testează dacă un număr mare este prim).
Un algoritm aleator care returnează ı̂ntotdeauna rezultatul corect şi doar timpul de execuţie este o variabilă
aleatoare se numeşte algoritm Las Vegas.
În cele ce urmează, vom prezenta câţiva algoritmi aleatori de simulare numerică (de aproximare).
Exemplul 2.24. (Integrarea Monte Carlo) Considerăm o funcţie f : R −→ R+ , continuă pe [a, b]. Ne propunem
să evaluăm integrala
Zb
I = f (x)dx.
a

I reprezintă aria suprafeţei S mărginită de graficul funcţiei y = f (x), axa Ox şi dreptele x = a, x = b.

114
Algoritmi şi complexitate Note de curs

Metoda poate fi aplicată doar pentru f ≥ 0. Dacă f ia şi valori negative, dar este mărginită inferior, atunci putem
utiliza o translaţie, astfel ı̂ncât să avem de integrat o funcţie nenegativă. De regulă, pentru a evalua numeric acest
tip de integrală, algoritmii aleatori nu reprezintă prima alegere, ı̂nsă pot fi utili ı̂n cazul ı̂n care integrala este dificil
sau imposibil de evaluat altfel.

Metoda 1. Încadrăm graficul funcţiei f ı̂ntr-un dreptunghi

D = [a, b] × [0, d]

cu d > sup f . Evaluăm integrala folosindu-ne de calculul probabilităţii evenimentului A, ca un un punct ales la
[a,b]
ı̂ntâmplare din interiorul dreptunghiului D, să se afle sub graficul funcţiei f (x). Realizăm următoarea experienţă
aleatoare: alegem ı̂n mod uniform un punct din interiorul dreptunghiului D şi verificăm dacă acesta se află sub
graficul lui f (x). Repetăm experienţa de N ori, ı̂n mod independent, unde N este un număr suficient de mare.
Numărăm de câte ori punctul generat este sub grafic, adică determinăm frecvenţa absolută de realizare a eveni-
mentului A, notată cu fN (A). Pentru un număr mare de experimente, probabilitatea ca un punct generat aleator
ı̂n interiorul dreptunghiului să se afle sub graficul funcţiei va fi aproximată prin limita şirului frecvenţelor relative,
adică
fN (A)
P (A) = lim .
N →∞ N
Pe de altă parte, probabilitatea teoretică a lui A este raportul dintre măsura mulţimii cazurilor favorabile şi măsura
mulţimii cazurilor egal posibile:
I
P (A) = ,
aria(D)
obţinând aproximarea
fN (A)
I ≈ aria(D) .
N
Pentru a obţine o precizie bună, N trebuie să fie foarte mare, aşadar această metodă nu este foarte eficientă.
1. Să evaluăm integrala
Z5
2
I= e−x dx.
−2

Generăm N puncte aleatoare ı̂n interiorul dreptunghiului [−2, 5] × [0, 1] (aria = 7) şi testăm care dintre acestea se
2
găsesc sub graficul funcţiei f (x) = e−x , x ∈ [−2, 5].
1 function Integrala1 ( integer N )
2 begin
3 integer i , fN
4 real x , y
5 fN = 0
6 for i = 1 to N do
7 x = random ( -2 ,5)
8 y = random (0 ,1)
9 if y < exp ( - x * x ) then
10 fN = fN + 1
11 end if
12 end for
13 return 7.0 * fN / N
14 end

Metoda 2. Considerăm un dreptunghi [a, b] × [0, I/(b − a)] plasat ca ı̂n figura de mai jos.

115
Algoritmi şi complexitate Note de curs

Aria dreptunghiului este tot I. Cum atât dreptunghiul, cât şi suprafaţa S, au aceeaşi arie şi aceeaşi lăţime,
ı̂nseamnă că au şi aceeaşi ı̂nălţime medie. Aşadar, ı̂nălţimea medie a suprafeţei S este I/(b − a). Generăm N
numere aleatoare Xi , i = 1, . . . , N , uniform distribuite ı̂n [a, b] şi estimăm ı̂nălţimea medie prin
N
f (X1 ) + · · · + f (Xn ) 1 X
= f (Xi ),
N N i=1

şi atunci, valoarea integralei se aproximează prin


N
b−aX
I≈ f (Xi ).
N i=1

2. Estimăm valoarea integralei de la exerciţiul 1.


1 function Integrala2 ( integer N )
2 begin
3 integer i
4 real sum , x
5 sum = 0
6 for i = 1 to N do
7 x = random ( -2 ,5)
8 sum = sum + exp ( - x * x )
9 end for
10 return 7.0 / N * sum
11 end

Această metodă poate fi generalizată pentru calculul aproximativ al integralelor de tipul


Z
f (x)dx, cu Ω ⊂ Rn .

În general, un algoritm determinist de aproximare a unei integrale definite necesită mai puţine iteraţii pentru a
obţine o precizie comparabilă cu cea obţinută folosind un algoritm aleator. Există ı̂nsă anumite funcţii pentru care
algoritmii determinişti oferă rezultate eronate.
Metoda Monte Carlo de integrare numerică este cu adevărat utilă pentru evaluarea integralelor multiple. Pentru a
obţine o anumită precizie, algoritmii determinişti de aproximare a integralelor multiple necesită un număr de puncte
de discretizare ce creşte exponenţial odată cu ordinul integralei de evaluat. Pentru algoritmii aleatori prezentaţi,
ordinul integralei afectează prea puţin precizia aproximării. În practică, această metodă de aproximare se utilizează
pentru evaluarea integraleleor de ordin 4 şi mai mare, pentru care nu există decât algoritmi determinişti foarte
complicaţi.
3. Ne propunem să evaluăm numeric integrala dublă
Z1 Z1 p
4 − x2 − y 2 dx dy.
0 −1

116
Algoritmi şi complexitate Note de curs

Vom genera aleator N puncte (Xi , Yi ) uniform distribuite ı̂n dreptunghiul D = [−1, 1] × [0, 1]. Integrala o vom
aproxima prin
N
aria(D) X
I≈ f (Xi , Yi ).
N i=1

1 function Integrala3 ( integer N )


2 begin
3 integer i
4 real x , y , sum
5 sum = 0
6 for i = 1 to N do
7 x = random ( -1 ,1)
8 y = random (0 ,1)
9 sum = sum + sqrt (4 - x * x - y * y )
10 end for
11 return 2.0 / N * sum
12 end

4. Să aproximăm integrala triplă


ZZZ p p
z 2 x2 + y 2 + z 2 dx dy dz, cu Ω = {(x, y, z) ∈ R3 ; 0 ≤ z ≤ 4 − x2 − y 2 , 0 ≤ x ≤ y}.

Observăm că 0 ≤ x, y, z ≤ 2, astfel că vom genera aleator N (de exemplu, N = 106 ) puncte (Xi , Yi , Zi ) uniform
distribuite ı̂n cubul [0, 2] × [0, 2] × [0, 2]. Verificăm apoi dacă acestea se găsesc ı̂n domeniul Ω. Vom aproxima
integrala prin
N
vol(cub) X
I≈ f (Xi , Yi , Zi ),
N i=1

pentru (Xi , Yi , Zi ) ∈ Ω.
1 function Integrala4 ( integer N )
2 begin
3 integer i
4 real x , y , z , sum
5 sum = 0
6 for i = 1 to N do
7 x = random (0 ,2)
8 y = random (0 ,2)
9 z = random (0 ,2)
10 if x * x + y * y <= 4 && x <= y && z <= sqrt (4 - x * x -y * y ) then
11 sum = sum + z * z * sqrt ( x * x + y * y + z * z )
12 end if
13 end for
14 return 8.0 / N * sum
15 end

Exemplul 2.25. (Aproximarea numărului π folosind jocul de darts) Să presupunem că un jucător aruncă o săgeată
ascuţită, spre o tablă pătrată din lemn, de latură 1, ı̂n interiorul căreia se află desenat un cerc circumscris pătratului.
Dacă săgeata se ı̂nfinge ı̂n disc, atunci jucătorul câştigă un punct, ı̂n caz contrar, nu câştigă nimic. Repetăm jocul
de N de ori şi numărăm câte puncte a acumulat jucătorul. Notăm numărul de puncte acumulate cu fN . Să
presupunem că orice punct de pe tablă are aceeaşi şansă de a fi ţintit şi că de fiecare dată când jucătorul aruncă
săgeata, ea se ı̂nfinge ı̂n tablă.
Pe baza jocului de mai sus se poate aproxima valoarea numărului π. În acest scop, vom descrie un algoritm de
simulare a jocului. Notăm cu A evenimentul ca săgeata să se ı̂nfingă ı̂n interiorul discului. În cazul ı̂n care numărul

117
Algoritmi şi complexitate Note de curs

de aruncări, N , este foarte mare, atunci probabilitatea evenimentului A, P (A), este bine aproximată de limita
şirului frecvenţelor relative, adică
fN (A)
P (A) = lim .
N →∞ N
Pe de altă parte, probabilitatea teoretică este
aria(disc) π
P (A) = = ,
aria(patrat) 4
deoarece raza cercului circumscris pătratului este 1/2. Aşadar, putem aproxima numărul π prin
fN (A)
π≈4 ,
N
pentru N suficient de mare.
1 function Darts ( integer N )
2 begin
3 integer i , fN
4 real x , y , P
5 fN = 0
6 for i = 1 to N do
7 x = random (0 ,1)
8 y = random (0 ,1)
9 if ( x - 0.5) *( x - 0.5) + ( y - 0.5) *( y - 0.5) <= 0.25 then
10 fN = fN + 1
11 end if
12 end for
13 P = ( real ) fN / N
14 return 4 * P // aproximarea numarului pi
15 end

Prezentăm ı̂n cele ce urmează doi algoritmi aleatori, primul de tip de tip Monte Carlo, iar al doilea de tip Las
Vegas. Spre deosebire de algoritmii Las Vegas, algoritmii Monte Carlo nu oferă ı̂ntotdeauna rezultatul corect. În
acest caz, timpul de execuţie este determinist, dar rezultatele pot fi incorecte cu o anumită probabilitate (de obicei,
mică). Probabilitatea de eroare scade o dată cu creşterea timpului de execuţie. Se poate spune că rezultatul oferit
ı̂ntr-un timp acceptabil este aproape sigur corect.
Exemplul 2.26. (Testul de primalitate pentru √ un număr natural n) Rezolvarea√deterministă clasică a problemei,
prin care se caută divizorii numărului până la n, are ordinul de complexitate O( n), ı̂n ipoteza că toate operaţiile
se execută ı̂n timp constant. Pentru numere mari, ı̂nsă, acest lucru nu mai este adevărat, iar estimarea timpului
de execuţie al algoritmului se realizează ı̂n funcţie de numărul de biţi pe care este reprezentat n. Dacă n ocupă k
biţi, atunci algoritmul are complexitatea O(2k/2 ). Nu se cunoaşte nici un algoritm, fie el determinist sau nu, care
să rezolve exact această problemă pentru numere mari, ı̂ntr-un timp rezonabil. Problemă este una importantă ı̂n
criptografie.
Enunţăm o teoremă importantă din teoria numerelor, numită mica teoremă a lui Fermat.
Teorema 2.2. Dacă n este un număr prim, atunci oricare ar fi numărul natural x astfel ı̂ncât 1 ≤ x ≤ n − 1,
xn−1 mod n = 1.
Această teoremă ne conduce către un prim algoritm aleator pentru testarea primalităţii. Mai ı̂ntâi propunem un
algoritm eficient pentru a calcula xp mod n, pornind de la observaţia:


 1, dacă p = 0

x mod n, dacă p=1
xp mod n = p/2 p/2


 ((x mod n) · (x mod n)) mod n, dacă p este par
p−1
(x · x ) mod n, dacă p este impar

118
Algoritmi şi complexitate Note de curs

1 function putere ( integer x , integer p , integer n )


2 begin
3 integer rest
4 rest = 1
5 while p > 0 do
6 if p % 2 == 0 then
7 x = (x * x) % n
8 p = p / 2
9 else
10 rest = ( x * rest ) % n
11 p = p - 1
12 end if
13 end while
14 return rest
15 end
16

17 function primF ( integer n )


18 begin
19 integer x
20 if n == 2 then
21 return true
22 end if
23 if n <= 1 || n % 2 == 0 then
24 return false
25 end if
26 x = random (1.. n - 1)
27 if putere (x , n - 1 , n ) == 1 then
28 return true
29 else
30 return false
31 end if
32 end
Dacă algoritmul returnează false, atunci cu siguranţă n nu este un număr prim. Dacă se returnează true, ı̂nsă,
nu putem trage concluzia că n este prim cu siguranţă, deoarece există şi numere compuse ce verifică ecuaţia din
teorema lui Fermat (de exemplu, numărul compus 15, 414 mod 15 = 1). Deoarece nu putem estima pentru un
număr compus n câte numere x sunt, cu 3 ≤ x ≤ n − 1, pentru care nu se verifică ecuaţia, nu poate fi calculată
limita de calcul, adică de câte ori să repetăm experienţa pentru a obţine o anumită probabilitate de eroare. De
altfel, există numere ce satisfac ecuaţia pentru orice x ≤ n − 1, astfel ı̂ncât (n, x) = 1 (de exemplu, n = 561).
Ne propunem să remediem acest neajuns printr-o modificare a testului Fermat: presupunem că n este un număr
impar mai mare decât 4, iar numerele naturale s şi t sunt astfel ı̂ncât n − 1 = 2s t. Deoarece n − 1 este par, ı̂nseamnă
că s > 0. Notăm cu M (n) mulţimea tuturor numerelor naturale x, cu 2 ≤ x ≤ n − 2 pentru care, ori xt mod n = 1,
i
ori există un număr natural i, 0 ≤ i < s astfel ı̂ncât x2 t mod n = n − 1. Vom descrie un algoritm care, pentru un
număr impar n şi 2 ≤ x ≤ n − 2, returnează true doar dacă x ∈ M (n).
1 function testM ( integer x , integer n )
2 begin
3 integer i , s , t , y
4 s = 0
5 t = n - 1
6 repeat
7 s = s + 1
8 t = t / 2
9 until t % 2 == 1
10 y = putere (x , t , n )
11 if y == 1 || y == n - 1 then

119
Algoritmi şi complexitate Note de curs

12 return true
13 end if
14 for i = 1 to s - 1 do
15 y = (y * y) % n
16 if y == n - 1 then
17 return true
18 end if
19 end for
20 return false
21 end
S-a demonstrat că x ∈ M (n) pentru toţi 2 ≤ x ≤ n − 2, dacă n este prim.
Teorema 2.3. Fie un număr impar n, n > 4. Atunci
1. dacă n este prim, atunci M (n) = {x; 2 ≤ x ≤ n − 2};
2. dacă n este compus, atunci card(M (n)) ≤ (n − 9)/4.
În consecinţă, algoritmul testM(x, n) returnează true dacă n este un număr prim, mai mare decât 4 şi 2 ≤ x ≤
n − 2, ı̂n timp ce returnează false cu o probabilitate mai bună decât 3/4, dacă n este un număr impar compus,
mai mare decât 4 şi x este ales la ı̂ntâmplare din mulţimea {2, . . . , n − 2}.
Algoritmul Miller-Rabin pentru testarea primalităţii are la bază ultimele rezultate enunţate şi va fi apelat pentru
n > 4, impar.
1 function primMR ( integer n )
2 begin
3 integer x
4 x = random (2.. n - 2)
5 return testM (x , n )
6 end
Pentru a reduce probabilitatea de eroare, repetăm experienţa de m ori, independent.
1 function repeatMR ( integer n , integer m )
2 begin
3 integer i
4 for i = 1 to m do
5 if primMR ( n ) == false then
6 return false
7 end if
8 end for
9 return true
10 end
Algoritmul returnează rezultatul corect pentru n > 4, număr prim. Atunci când n > 4 este un număr compus
impar, fiecare apel al algoritmului primMR are probabilitatea de cel mult 1/4 de a returna true eronat. Aşadar,
1
dacă n trece testul pentru m valori ale lui x, probabilitatea ca n să fie de fapt compus este cel mult m , şi deci
4
4m − 1
probabilitatea ca n să fie prim este cel puţin .
4m
Ne punem ı̂ntrebarea de cât timp este nevoie pentru a decide asupra primalităţii unui număr n cu o probabilitate
1 1
de eroare mărginită de o valoare ε. Algoritmul primMR trebuie repetat de m ori, cu m ≤ ε, adică 22m ≥ şi
4 ε
1 1 1 1
m ≥ log . Deci m va fi cel mai mic număr natural care este mai mare sau egal cu log . Deoarece algoritmul
2 ε 2 ε
testM are o complexitate O(log n), rezultă că algoritmul repeatMR are complexitatea O(log2 n), iar ı̂n funcţie de
numărul de biţi necesari pentru reprezentarea lui n, O(k 2 ).
Următorul algoritm este de tip Las Vegas: rezultatul furnizat este sigur corect, dar timpul de execuţie este aleator.

120
Algoritmi şi complexitate Note de curs

Exemplul 2.27. (Algoritmul aleator quicksort) Reamintim că algoritmul de sortare rapidă este proiectat folosind
tehnica divizării şi se bazează pe partiţionarea şirului dat. Astfel, având tabloul a, partiţionarea presupune o
rearanjare a elementelor sale astfel ı̂ncât toate elementele aflate la stânga unui element numit pivot, sunt mai mici
sau egale cu acesta, iar toate elementele aflate la dreapta pivotului sunt mai mari sau egale cu acesta. Apoi se
aplică aceeaşi tehnică recursiv pentru fiecare dintre subtablourile obţinute. Astfel, partiţionarea este prelucrarea
cea mai importantă a algoritmului, presupunând identificarea pivotului. Presupunem că lucrăm pe subtabloul
a[stang..drept]. În loc să considerăm pivotul ca fiind primul element al vectorului, a[stang], aşa cum am procedat
ı̂n cazul algoritmului determinist, ı̂l vom alege aleator. Astfel, la fiecare pas al algoritmului, vom interschimba
elementul a[stang] cu un element ales la ı̂ntâmplare din tabloul a[stang..drept]. Alegerea aleatoare a pivotului
conduce la reducerea numărului de situaţii ı̂n care se ajunge la partiţionare dezechilibrată (cazul cel mai defavorabil
al agoritmului determinist).
1 function r a n d o m i z e d _ p a r t i t i o n ( integer a [] , integer stang , integer drept )
2 begin
3 integer i , j , pivot , aux , ind
4 ind = random ( stang .. drept )
5 aux = a [ stang ]
6 a [ stang ] = a [ ind ]
7 a [ ind ] = aux
8 i = stang
9 j = drept + 1
10 pivot = a [ stang ]
11 while true do
12 repeat
13 i = i + 1
14 until i > drept || a [ i ] >= pivot
15 repeat
16 j = j - 1
17 until j < stang || a [ j ] <= pivot
18 if i < j then
19 aux = a [ i ]
20 a[i] = a[j]
21 a [ j ] = aux
22 else
23 aux = a [ stang ]
24 a [ stang ] = a [ j ]
25 a [ j ] = aux
26 return j
27 end if
28 end while
29 end
30

31 function r a n d o m i z e d _ q u i c k s o r t ( integer a [] , integer stang , integer drept )


32 begin
33 integer s
34 if stang < drept then
35 s = r an d o m i z e d _ p a r t i t i o n (a , stang , drept )
36 r a n d o mi z e d _ q u i c k s o r t (a , stang , s - 1)
37 r a n d o mi z e d _ q u i c k s o r t (a , s + 1 , drept )
38 end if
39 end
Algoritmul este unul de tip Las Vegas, deci ne oferă sigur soluţia corectă, dar timpul de execuţie este o variabilă
aleatoare. S-a demonstrat că valoarea aşteptată a timpului de execuţie (media variabilei aleatoare) este Θ(n log n)
(ı̂n ipoteza că elementele tabloului sunt distincte). Algoritmul aleator de sortare rapidă poate fi ı̂mbunătăţit printr-o
alegere mai atentă a pivotului prin metoda “medianei celor 3 valori”, ı̂n care se alege elementul mijlociu ca mărime,
dintre trei elemente alese la ı̂ntâmplare din vector.

121
Algoritmi şi complexitate Note de curs

2.7 Clase de probleme


O preocupare importantă a specialiştilor ı̂n domeniul informaticii, ı̂n general, şi ı̂n teoria complexităţii computaţio-
nale, ı̂n special, este să stabilească dacă o problemă dată poate fi sau nu rezolvată printr-un algoritm ı̂n timp
polinomial. Să presupunem că problema de rezolvat are dimensiunea n.
Definiţia 2.1. Spunem că un algoritm rezolvă problema ı̂n timp polinomial dacă, ı̂n cazul cel mai defavorabil,
timpul de execuţie al algoritmului este de ordinul O(p(n)), cu p(n) un polinom de grad finit. Problemele care se
pot rezolva ı̂n timp polinomial se numesc tractabile, iar cele care nu pot fi rezolvate ı̂n timp polinomial se numesc
netractabile.
Datorită utilizării notaţiei asimptotice O, problemele rezolvabile ı̂n timp logaritmic, de exemplu, sunt rezolvabile şi
ı̂n timp polinomial, de asemenea. Problemele netractabile nu pot fi rezolvate ı̂ntr-un timp rezonabil, decât pentru
instanţe de dimensiuni mici. Desigur că este o diferenţă mare de eficienţă şi ı̂ntre timpii polinomiali, de exemplu
ı̂ntre un algoritm de complexitate liniară şi unul cu timpul de execuţie de ordinul O(n3 ) ce rezolvă o aceeaşi
problemă. De altfel, există prea puţini algoritmi utili ı̂n practică care să aibă o complexitate polinomială cu un
grad al polinomului mai mare decât trei.
Vom prezenta ı̂n cele ce urmează doar câteva notaţii, definiţii şi noţiuni din teoria complexităţii. Pentru detalii,
poate fi consultată bibliografia.

2.7.1 Clasele de probleme P şi NP


Multe dintre problemele pe care le-am abordat pe parcursul acestui curs au putut fi rezolvate ı̂n timp polinomial:
algoritmul lui Euclid, algoritmii de căutare sau sortare etc. Dintr-o perspectivă informală, clasa P conţine toate
problemele rezolvabile ı̂n timp polinomial. O definiţie formală este că P conţine toate problemele de decizie ce pot
fi rezolvate ı̂n timp polinomial. O problemă de decizie este o problemă care poate răspunde doar cu da/nu.
Definiţia 2.2. P este clasa problemelor de decizie ce pot fi rezolvate prin algoritmi determinişti ı̂n timp polinomial.
Restrângerea lui P la problemele de decizie este motivată prin faptul că multe dintre problemele ce nu sunt formulate
natural ca probleme de dicizie, pot fi formalizate astfel. De exemplu, problema colorării unui graf cu un număr
minim de culori astfel ı̂ncât oricare două noduri adiacente să fie colorate diferit, poate fi formalizată ı̂ntr-o problemă
de decizie astfel: există o colorare a unui graf cu nu mai mult de m culori, m = 1, 2, . . . , astfel ı̂ncât oricare două
noduri adiacente să fie colorate diferit? Cel mai mic m pentru care problema de decizie are soluţie reprezintă soluţia
problemei de optimizare.
Nu toate problemele de decizie pot fi rezolvate ı̂n timp polinomial. De altfel, sunt probleme de decizie ce nu pot
rezolvabile printr-un algoritm, numite probleme nedecidabile. Problemele de decizie pentru care există algoritmi
de rezolvare se numesc decidabile. Un exemplu celebru de problemă nedecidabilă a fost ilustrat de Alan Turing ı̂n
1936, problema opririi : considerându-se un program arbitrar şi o intrare pentru acesta, nu există un algoritm prin
care să se poată decide dacă programul se ı̂ncheie ı̂ntr-un număr finit de paşi sau rulează la infinit.
Problemele pentru care nu se cunosc algoritmi de rezolvare a acestora ı̂n timp polinomial (probleme de decizie sau
nu) au ı̂n comun faptul că dimensiunea spaţiului din care trebuie aleasă soluţia creşte exponenţial (sau mai rău)
odată cu dimensiunea intrării.
O altă trăsătură comună a unei mari majorităţi a problemelor de decizie este faptul că, deşi rezolvarea unor astfel de
probleme poate fi dificilă din punct de vedere computaţional, verificarea dacă o soluţie propusă rezolvă problema,
se poate face ı̂n timp polinomial. Aceasta este motivaţia care a condus către algoritmii nedeterminişti.
Definiţia 2.3. Un algoritm nedeterminist este o procedură ce primeşte ca intrare o instanţă I a unei probleme de
decizie şi se realizează ı̂n două etape:
ˆ etapa nedeterministă (“ghicitul”): se generează o structură arbitrară S ce poate fi considerată o soluţie can-
didat pentru instanţa dată I (aceasta poate fi, de asemenea, departe de soluţie);
ˆ etapa deterministă (“verificarea”): printr-un algoritm determinist ce are ca intrări instanţa I şi structura
S, se verifică dacă S este o soluţie pentru instanţa I, adică dacă sunt verificate condiţiile de rezolvare a
problemei (dacă S nu este soluţie a instanţei I, algoritmul ori returnează nu, ori ı̂i este permis să ruleze la
infinit).

122
Algoritmi şi complexitate Note de curs

Spunem că un algoritm nedeterminist rezolvă o problemă de decizie dacă şi numai dacă pentru fiecare instanţă
“da” a problemei, ı̂ntoarce “da ”pentru o anumită execuţie, adică un algoritm nedeterminist trebuie să fie capabil
de a “ghici” soluţia corectă măcar o dată. Spunem că un algoritm nedeterminist este polinomial dacă eficienţa-timp
a etapei de verificare este una polinomială.

Definiţia 2.4. NP este clasa problemelor de decizie ce pot fi rezolvate prin algoritmi nedeterminişti ı̂n timp poli-
nomial.
Majoritatea problemelor de decizie sunt ı̂n NP. De asemenea, are loc incluziunea

P ⊆ NP,

deci orice problemă din P este şi ı̂n NP. O preocupare a specialiştilor ı̂n domeniul informaticii teoretice este să
răspundă la ı̂ntrebarea dacă incluziunea este strictă sau nu. Există destule motive să se creadă că acest lucru
este adevărat, dar nu s-a putut demonstra până acum. Nu s-a putut identifica o metodă prin care algoritmii
nedeterminişti să fie transformaţi ı̂n algoritmi determinişti ı̂n timp polinomial. De exemplu, să considerăm problema
sumei elementelor submulţimilor unei mulţimi: dată fiind o mulţime de numere ı̂ntregi, există o submulţime nevidă
a sa ale cărei elemente au suma k? Nu există niciun algoritm cunoscut care să identifice răspunsul ı̂n timp polinomial
(există unul exponenţial), dar pentru o instanţă a problemei şi o anumită submulţime generată, cerinţa este uşor
de verificat. Aşadar, această problemă este ı̂n NP, dar nu neapărat ı̂n P.

2.7.2 Clasele de probleme NP – dificile şi NP – complete


Definiţia 2.5. Pentru orice problemă din NP, există polinoamele p(n) şi q(n) astfel ı̂ncât aceasta poate fi rezolvată
printr-un algoritm determinist ı̂n timpul O(p(n)2q(n) ).
Definiţia 2.6. Spunem că o problemă de decizie P poate fi transformată ı̂n problema de decizie Q ı̂n timp polinomial
sau că problema P este reductibilă polinomial la Q, dacă există o funcţie t care transformă instanţele lui P ı̂n
instanţele lui Q astfel ı̂ncât:
1) t are complexitate polinomială de calcul;
2) pentru orice instanţă I a lui P , I şi t(I) au aceeaşi valoare de adevăr (ambele “da” sau ambele “nu”).
Definiţia 2.7. 1) O problemă de decizie P este NP – dificilă dacă Q se reduce polinomial la P , pentru orice
Q ∈ NP.
2) O problemă P este NP – completă dacă P ∈ NP şi P este NP – dificilă.
Prima problemă pentru care s-a demonstrat că este NP – completă este problema satisfiabilităţii (Cook, 1971).
Această problemă se referă la lucrul cu expresii booleene: dată o expresie din calculul propoziţional ı̂n formă
normală conjugată, există o atribuire pentru variabile pentru care expresia este adevărată? Alte exemple de
probleme NP – complete: submulţime de sumă dată, problema rucsacului (varianta discretă), problema comisului
voiajor, problema m-colorării nodurilor unui graf, cel mai lung drum ı̂ntr-un graf etc.

2.8 Algoritmi aproximativi şi euristici


După ce pentru o problemă, ı̂n special de optimizare, s-a demonstrat că este NP – completă, o variantă de abordare ar
fi utilizarea unui algoritm de aproximare sau a unei euristici. Un algoritm de aproximare oferă o soluţie suboptimală
ı̂n timp polinomial şi poate fi preferat algoritmilor ce oferă soluţia exactă ı̂n timp exponenţial, ı̂n special pentru
probleme de dimensiune mare. Mulţi dintre algoritmii de aproximare se bazează pe euristici. O euristică este
o tehnică concepută pentru a rezolva o problemă atunci când metodele clasice sunt ineficiente. Astfel, aceasta
este o tehnică ce derivă mai degrabă din experienţă şi mai puţin din afirmaţii demonstrate matematic. Există
euristici constructive, cum e cazul euristicii greedy, ı̂n care se porneşte de la mulţimea vidă şi soluţia se construieşte
progresiv, prin alegerea la fiecare pas a candidatului cu cel mai bun potenţial la acel moment, fără ı̂nsă a privi deloc
spre viitor. Există şi varianta euristicii ı̂mbunătăţite, ı̂n care se porneşte cu o soluţie şi se ı̂ncearcă ı̂mbunătăţirea
acesteia, de regulă prin modificări aduse soluţiei curente.

123
Algoritmi şi complexitate Note de curs

Dacă lucrăm cu algoritmi aproximativi, ne interesează cât de precis este rezultatul oferit de aceştia. Presupunem că
s∗ este soluţia optimă exactă a unei probleme de minimizare a unei funcţii obiectiv f , iar sa este soluţia aproximativă
oferită de un algoritm. Putem cuantifica precizia soluţiei aproximative, calculând eroarea relativă:

f (sa ) − f (s∗ )
errrel (sa ) =
f (s∗ )

sau folosind rata de precizie


f (sa )
r(sa ) = .
f (s∗ )
Dacă problema este una de maximizare, atunci rata de precizie a soluţiei aproximative se calculează de regulă prin

f (s∗ )
r(sa ) =
f (sa )

pentru a fi mai mare sau egală cu 1, la fel ca ı̂n cazul problemelor de minimizare. Cu cât r(sa ) este mai apropiată
de 1, cu atât aproximarea este mai bună. Deoarece, de obicei, această rată nu poate fi calculată efectiv ı̂ntrucât nu
ştim f (s∗ ), putem spera să obţinem o margine superioară cât mai bună pentru rata de precizie.
Definiţia 2.8. Spunem că un algoritm de aproximare, ce rezolvă o problemă ı̂n timp polinomial, are rata de precizie
mărginită de ε ≥ 1 dacă r(sa ) ≤ ε, pentru toate instanţele problemei.
Cea mai mică valoare ε astfel ı̂ncât r(sa ) ≤ ε, pentru toate instanţele problemei, se numeşte rata de performanţă
a algoritmului şi se notează cu RA . Desigur, ne dorim algoritmi aproximativi cu rata de performanţă cât mai
apropiată de 1.
Exemplul 2.28. (Problema colorării unei hărţi cu un număr minim de culori ) Se consideră o hartă plană cu
n regiuni, introdusă sub forma unei matrice a, ı̂n care elementul a[i][j] = 1, dacă regiunile i şi j sunt vecine şi
a[i][j] = 0, dacă regiunile i şi j nu sunt vecine. Ss̆ se identifice o modalitate de colorare a hărţii cu cât mai puţine
culori, astfel ı̂ncât oricare două regiuni vecine să fie colorate diferit.
Am văzut că pentru a se obţine soluţia optimă exactă a acestei probleme putem folosi tehnica backtracking. Timpul
de excuţie este ı̂nsă unul exponenţial. Un algoritm bazat pe euristica greedy oferă o soluţie acceptabilă ı̂n timp
polinomial, dar aceasta nu este neapărat optimă. Identificăm culorile prin numerele 0, 1, . . . . Vom colora prima
regiune folosind culoarea 0 şi apoi, succesiv, toate celelalte regiuni, folosind pentru fiecare dintre acestea, cea mai
mică culoare posibilă. Algoritmul poate fi descris astfel:
1 algorithm Greedy coloring
2 begin
3 Pas 1: se coloreaza prima regiune folosind culoarea 0
4 Pas 2: se executa urmatoarea secventa pentru celelalte n - 1 regiuni :
5 - pentru regiunea curenta se alege cea mai mica culoare posibila ,
diferita de a regiunilor vecine deja colorate
6 - daca toate culorile folosite anterior apar intre regiunile vecine
regiunii curente , se aloca o noua culoare
7 end
Pentru acest algoritm, RA = ∞.
Exemplul 2.29. (Problema comisului voiajor ) Se consideră o listă de n oraşe, cunoscându-se distanţele ı̂ntre
fiecare două oraşe. Care este cel mai scurt traseu pe care să-l parcurgă comisul voiajor, pentru a vizita fiecare oraş
o singură dată şi pentru a se ı̂ntoarce ı̂n oraşul de plecare?
Există mai mulţi algoritmi de aproximare pentru această problemă. Vom presupune că are loc o relaţie de tip
“inegalitatea triunghiului”:
a[i][j] + a[j][k] ≥ a[i][k],
unde tabloul bidimensional a reţine distanţele dintre oraşe (matricea a este una simetrică şi se numeşte matricea
costurilor). Vom propune ı̂n continuare un algoritm bazat pe euristica greedy: la fiecare pas, se continuă cu cel
mai apropiat vecin, care nu a fost vizitat ı̂ncă. Astfel, algoritmul este

124
Algoritmi şi complexitate Note de curs

1 algorithm Nearest - neighbor


2 begin
3 Pas 1: se alege in mod arbitrar un oras de pornire
4 Pas 2: se repeta urmatoarea operatie pana cand toate orasele sunt vizitate :
5 se continua cu cel mai apropiat vecin al ultimului oras adaugat la
6 traseu , vecin ce nu a fost vizitat inca
7 Pas 3: se revine in orasul de pornire
8 end
Pentru acest algoritm, RA = ∞, dar rata de precizie este de ordinul Θ(log n).

125
Bibliografie

[1] R. Andonie, I. Gârbacea, Algoritmi Fundamentali. O Perspectivă C++, Ed. Libris, Cluj-Napoca, 1995.

[2] G. Brassard, P. Bratley, Fundamental of Algorithmics, Prentice-Hall, 1996.


[3] T.H. Cormen, C.E. Leiserson, R.L. Rivest. Introduction to Algorithms (3rd ed.), MIT Press, 2009.
[4] T.H. Cormen, C.E. Leiserson, R.L. Rivest, Introducere ı̂n Algoritmi, Computer Libris Agora, Cluj-Napoca,
2000 (traducere).

[5] C.A. Giumale, Introducere ı̂n Analiza Algoritmilor. Teorie şi aplicaţie, Ed. Polirom, 2004.
[6] D. Knuth. Arta Programării Calculatoarelor (vol. 1, 2, 3), Ed. Teora, 1999 (traducere).
[7] A. Levitin, Introduction to the design and analysis of algorithm (3rd ed.), Pearson Ed., 2012.

[8] D. Lucanu, M. Craus, Proiectarea Algoritmilor, Ed. Polirom, 2008


[9] S. Skiena, The Algorithm Design Manual (2nd ed.), Springer–Verlag, Londra, 2008.
[10] D. Zaharie, Introducere ı̂n Proiectarea şi Analiza Algoritmilor, Ed. Eubeea, 2008.

126

S-ar putea să vă placă și