Documente Academic
Documente Profesional
Documente Cultură
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
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.
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.
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):
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
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
START
R: a, b
no yes
b != 0
r = a % b
W: a
a = b
STOP
b = r
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 >
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
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
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
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.
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
13
Algoritmi şi complexitate Note de curs
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
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
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
19
Algoritmi şi complexitate Note de curs
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])}.
20
Algoritmi şi complexitate Note de curs
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ă.
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
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
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
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.
Î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
Î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.
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
Î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
(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 → ∞:
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
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
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
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).
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
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
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)).
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
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:
(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
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
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
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.
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
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,
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
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).
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 ).
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}.
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
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.
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 ).
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}.
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:
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,
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
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.
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.
50
Algoritmi şi complexitate Note de curs
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.
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.
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.
53
Algoritmi şi complexitate Note de curs
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 + ...
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
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).
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
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
58
Capitolul 2
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
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
61
Algoritmi şi complexitate Note de curs
Î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 .
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
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
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.
67
Algoritmi şi complexitate Note de curs
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.
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
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 )),
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.
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
73
Algoritmi şi complexitate Note de curs
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,
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
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.
(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
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).
(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
82
Algoritmi şi complexitate Note de curs
45
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
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
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
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
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
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
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
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
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
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
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
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.
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).
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
10 // sau
11
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
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
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;
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
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
101
Algoritmi şi complexitate Note de curs
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
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
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
104
Algoritmi şi complexitate Note de curs
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 ,
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
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
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
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
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:
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 .
110
Algoritmi şi complexitate Note de curs
10 end
11
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
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
113
Algoritmi şi complexitate Note de curs
2 begin
3 return ( b - a ) * random (0 ,1) + a
4 end
5
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.
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
Î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
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
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
121
Algoritmi şi complexitate Note de curs
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.
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∗ )
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
125
Bibliografie
[1] R. Andonie, I. Gârbacea, Algoritmi Fundamentali. O Perspectivă C++, Ed. Libris, Cluj-Napoca, 1995.
[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.
126