Sunteți pe pagina 1din 206

Cuprins

1 Introducere ı̂n algoritmi 3


1.1 Limbajul pseudocod . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2 Exemple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.3 Elemente de analiza algoritmilor . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.3.1 Metoda substituţiei . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
1.3.2 Schimbarea de variabilă . . . . . . . . . . . . . . . . . . . . . . . . . . 21
1.3.3 Metoda iterativă . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
1.3.4 Teorema master . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.4 Exerciţii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

2 Grafuri. Grafuri neorientate 28


2.1 Noţiuni de bază . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.2 Operaţii pe grafuri . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.3 Moduri de reprezentare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
2.4 Parcurgerea grafurilor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
2.4.1 Parcurgerea ı̂n lăţime (BFS-Breadth First Search) . . . . . . . . . . . . 40
2.4.2 Parcurgerea D (D - Depth) . . . . . . . . . . . . . . . . . . . . . . . . 44
2.4.3 Parcurgerea ı̂n adâncime (DFS-Depth First Search) . . . . . . . . . . . 45
2.5 Componente conexe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
2.6 Muchie critică . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
2.7 Exerciţii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55

3 Grafuri euleriene şi hamiltoniene 58


3.1 Grafuri Euleriene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
3.1.1 Algoritm pentru determinarea unui ciclu eulerian . . . . . . . . . . . . 59
3.1.2 Algoritmul lui Rosenstiehl . . . . . . . . . . . . . . . . . . . . . . . . . 62
3.1.3 Algoritmul lui Fleury . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
3.2 Grafuri Hamiltoniene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
3.2.1 Problema comis–voiajorului . . . . . . . . . . . . . . . . . . . . . . . . 70

4 Arbori. Arbori binari 80


4.1 Arbori binari . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
4.1.1 Moduri de reprezentare . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
4.1.2 Metode de parcurgere . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
4.2 Arbori binari de căutare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
4.3 Exerciţii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98

1
5 Arbori oarecare 102
5.1 Moduri de reprezentare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
5.2 Metode de parcurgere . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
5.3 Arbori de acoperire de cost minim . . . . . . . . . . . . . . . . . . . . . . . . . 110
5.3.1 Algoritmul lui Boruvka . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
5.3.2 Algoritmul lui Prim . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
5.3.3 Structuri de date pentru mulţimi disjuncte . . . . . . . . . . . . . . . . 116
5.3.4 Algoritmul lui Kruskal . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
5.4 Exerciţii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124

6 Grafuri orientate 127


6.1 Noţiuni de bază . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
6.2 Parcurgerea grafurilor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
6.3 Sortarea topologică . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
6.4 Componente tare conexe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
6.4.1 Algoritmul lui Kosaraju . . . . . . . . . . . . . . . . . . . . . . . . . . 137
6.4.2 Algoritmul lui Tarjan . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
6.4.3 Algoritmul lui Gabow . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
6.5 Exerciţii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145

7 Heap-uri 149
7.1 Heap-uri binare (Min-heapuri sau Max -heapuri) . . . . . . . . . . . . . . . . . 149
7.1.1 Inserarea unui element . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
7.1.2 Ştergerea elementului minim . . . . . . . . . . . . . . . . . . . . . . . . 152
7.1.3 Crearea unui heap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
7.2 Ordonare prin metoda HeapSort . . . . . . . . . . . . . . . . . . . . . . . . . . 155
7.3 Aplicatie - Coadă cu prioritate . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
7.4 Exerciţii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161

8 Distanţe ı̂n grafuri 165


8.1 Drumul minim de la un vârf la celelalte vârfuri . . . . . . . . . . . . . . . . . . 166
8.1.1 Algoritmul lui Moore . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
8.1.2 Algoritmul lui Dijkstra . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
8.2 Drumuri minime ı̂ntre toate perechile de vârfuri . . . . . . . . . . . . . . . . . 175
8.2.1 Algoritmul lui Roy-Floyd-Warshall . . . . . . . . . . . . . . . . . . . . 175
8.3 Exerciţii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182

A Metoda Backtracking 186

2
Capitolul 1

Introducere ı̂n algoritmi

Definiţia 1.1 Algoritmul constituie o reprezentare finită a unei metode de calcul ce per-
mite rezolvarea unei anumite probleme. Se poate spune că un algoritm reprezintă o secvenţă
finită de operaţii, ordonată şi complet definită, care, pornind de la datele de intrare, produce
rezultate.

Termenul de algoritm ı̂i este atribuit matematicianului persan Abu Ja‘far Mohammed
ibn Musa al-Khowarizmi (sec. VIII-IX), care a scris o carte de matematică cunoscută ı̂n tra-
ducere latină sub titlul de ”Algorithmi de numero indorum”, iar apoi ca ”Liber algorithmi ”,
unde termenul de ”algorithm” provine de la ”al-Khowarizmi ”, ceea ce literal ı̂nseamnă ”din
orasul Khowarizm”. Matematicienii din Evul Mediu ı̂nţelegeau prin algoritm o regulă (sau
o mulţime de reguli) pe baza căreia se efectuau calcule aritmetice: de exemplu ı̂n secolul al
XVI-lea, algoritmii se foloseau la ı̂nmulţiri sau ı̂njumătăţiri de numere.
Fiecare propoziţie ce face parte din descrierea unui algoritm este, de fapt, o comandă ce
trebuie executată de cineva, acesta putând fi o persoană sau o maşină de calcul. De altfel,
un algoritm poate fi descris cu ajutorul oricărui limbaj, de la limbajul natural şi până la
limbajul de asamblare al unui calculator. Denumim limbaj algoritmic un limbaj al cărui scop
este acela de a descrie algoritmi.
Algoritmul specifică succesiuni posibile de transformări ale datelor. Un tip de date poate
fi caracterizat printr-o mulţime de valori ce reprezintă domeniul tipului de date şi o mulţime
de operaţii definite peste acest domeniu. Tipurile de date pot fi organizate ı̂n următoarele
categorii:

1. tipuri de date elementare (de exemplu tipul ı̂ntreg, tipul real ) - valorile sunt unităţi
atomice de informaţie;

2. tipuri de date structurate (de exemplu tipul tablou, tipul ı̂nregistrare) - valorile sunt
structuri relativ simple rezultate ı̂n urma combinaţiei unor valori elementare;

3. tipuri de date structurate de nivel ı̂nalt (de exemplu stiva) - se pot descrie independent
de limbaj iar valorile au o structură mai complexă.

Un algoritm trebuie să posede următoarele trăsături caracteristice:

1. claritate - la fiecare pas trebuie să specifice operaţia pe care urmează să o efectueze
algoritmul asupra datelor de intrare;

2. corectitudine - rezultatele trebuie să fie corecte;

3
3. generalitate - algoritmul trebuie să ofere soluţia nu numai pentru o singură problemă
ci pentru o ı̂ntreagă clasă de probleme;
4. finitudine - algoritmul trebuie să se termine ı̂ntr-un timp finit;

5. eficienţă - un algoritm poate fi utilizat numai ı̂n situaţia ı̂n care resursele de calcul
necesare acestuia sunt ı̂n cantităţi rezonabile, şi nu depăşesc cu mult posibilităţile cal-
culatoarelor la un moment dat.
Un program reprezintă implementarea unui algoritm ı̂ntr-un limbaj de programare.
Studiul algoritmilor cuprinde mai multe aspecte:
• elaborare - activitatea de concepere a unui algoritm are şi un caracter creativ, din
această cauză nefiind posibilă deprinderea numai pe cale mecanică. Pentru a facilita
obţinerea unei soluţii la o problemă concretă se recomandă folosirea tehnicilor generale
de elaborare a algoritmilor la care se adaugă ı̂n mod hotărâtor intuiţia programatorului;

• exprimare - implementarea unui algoritm intr-un limbaj de programare se poate face


utilizând mai multe stiluri: programare structurată, programare orientată pe obiecte
etc.;

• validare - verificarea corectitudinii algoritmului prin metode formale;


• analiză - stabilirea unor criterii pentru evaluarea eficienţei unui algoritm pentru a-i
putea compara şi clasifica.
Un model de reprezentare al memoriei unei maşini de calcul este acela al unei structuri
liniare compusă din celule, fiecare celulă fiind identificată printr-o adresă şi putând păstra o
valoare corespunzătoare unui anumit tip de dată. Accesul la celule este facilitat de variabile.
O variabilă se caracterizează prin:
• un identificator - un nume ce referă variabila;

• o adresă - desemnează o locaţie de memorie;


• un tip de date - descrie tipul valorilor memorate ı̂n celula de memorie asociată.

1.1 Limbajul pseudocod


Limbajul natural nu permite o descriere suficient de riguroasă a algoritmilor, de aceea, pentru
reprezentarea acestora se folosesc alte modalităţi de descriere precum:
• scheme logice;

• limbajul pseudocod.
În continuare vom prezenta pricipalele construcţii din cadrul limbajului pseudocod.

Intrări/ieşiri
Citirea datelor de intrare se poate realiza prin intermediul enunţului Input:
1: Input {lista variabile}
Afişarea rezultatelor este reprezentată cu ajutorul instrucţiunii Output:
1: Output {lista de valori}

4
Instrucţiunea de atribuire
Este instrucţiunea cel mai des utilizată ı̂ntr-un algoritm şi realizează ı̂ncărcarea unei variabile
(locaţii de memorie) cu o anumită valoare. Are următoarea sintaxă:
1: < variabila >←< expresie >
unde < expresie > este o expresie aritmetică sau logică.
Se evaluează expresia < expresie > iar rezultatul se atribuie variabilei < variabila >,
memorându-se ı̂n locaţia de memorie asociată. Această variabilă trebuie să fie de acelaşi tip
de dată cu expresia sau un tip de dată care să includă şi tipul expresiei.
O expresie este constituită din operanzi şi operatori. Operanzii pot fi variabile şi valori
constante, iar operatorii pot fi:
• operatori aritmetici - + (adunare), − (scădere), ∗ (ı̂nmulţire), / (ı̂mpărţire), ˆ (ridicare
la putere), div (câtul ı̂mpărţirii ı̂ntregi), mod (restul ı̂mpărţirii ı̂ntregi);

• operatori relaţionali - = (egal), 6= (diferit), < (strict mai mic), ≤ (mai mic sau egal),
> (strict mai mare), ≥ (mai mare sau egal);

• operatori logici - OR sau ∨ (disjuncţie), AND sau ∧ (conjuncţie), NOT sau ¬ (negaţie).
Cea mai simplă expresie este formată dintr-o variabilă sau o constantă (operand). Ex-
presiile mai complicate se obţin din operaţii efectuate ı̂ntre variabile şi constante. La scrierea
expresiilor trebuie să se ţină cont de faptul că, ı̂n momentul evaluării lor, ı̂n primul rând
se vor evalua expresiile din paranteze, iar operaţiile se execută ı̂n ordinea determinată de
priorităţile lor.

Enunţuri de ramificare

1: if <expresie-booleana> then
2: <instructiune1> 1: if (a mod 2 = 0) then
[ 2: Output {’Numarul este par’ }
3: else 3: else
4: <instructiune2> 4: Output {’Numarul este impar’ }
] 5: end if
5: end if
Enunţul if . . . then . . . else evaluează mai ı̂ntâi expresia booleană pentru a determina
unul din cele două drumuri pe care le poate lua execuţia algoritmului. Ea poate include
opţional o clauză else.
Dacă <expresie-booleana> se evaluează la valoarea de adevăr true, atunci se execută
<instructiune1> şi se continuă cu următoarea instrucţiune după if. Dacă <expresie-booleana>
are valoarea de adevăr false, atunci se execută <instructiune2>. <instructiune1> şi
<instructiune2> sunt instrucţiuni compuse ce pot să conţină, la rândul lor, o altă instrucţiune
if.

Exemplul 1.1 Un algoritm simplu este cel ce rezolvă ecuaţia de gradul I, ax+b = 0, a, b ∈ R
(algoritmul 1). Acesta se bazează pe rezolvarea matematică a ecuaţiei de gradul I:
1. dacă a = 0, atunci ecuaţia devine 0 · x + b = 0. Pentru cazul ı̂n care b 6= 0 avem
0 · x + b = 0, egalitate ce nu poate fi satisfăcută pentru nicio valoare a lui x ∈ R sau
C. Spunem, ı̂n acest caz, că ecuaţia este incompatibilă. Dacă b = 0, avem 0 · x + 0 =
0, relaţia fiind adevărată pentru orice valoare a lui x ∈ R. Spunem că ecuaţia este
compatibil nedeterminată.

5
2. dacă a 6= 0, ecuaţia are o singură soluţie, x1 = − ab ∈ R.

Algoritm 1 Algoritm pentru rezolvarea ecuaţiei de gradul I


1: Input {a, b}
2: if (a = 0) then
3: if (b = 0) then
4: Output { ’Ecuatie compatibil nedeterminata’ }
5: else
6: Output { ’Ecuatie incompatibila’ }
7: end if
8: else
9: x ← − ab
10: Output { ’Solutia este:’, x }
11: end if

Având acest algoritm drept model să se realizeze un algoritm pentru rezolvarea ecuaţiei
generale de gradul al II-lea, ax2 + bx + c = 0, unde a, b, c ∈ R.

Enunţuri repetitive
Enunţurile repetitive permit descrierea unor prelucrări ce trebuie efectuate ı̂n mod repetat,
ı̂n funcţie de poziţia condiţiei de continuare existând două variante de structuri repetitive:
structura repetitivă condiţionată anterior şi structura repetitivă condiţionată posterior.
Structura repetitivă condiţionată anterior while (sau instrucţiune de ciclare cu test
iniţial) are următoarea sintaxă:
1: sum ← 0
2: i←1
1: while <expresie-booleana> do
3: while (i ≤ n) do
2: <instructiune>
4: sum ← sum + i
3: end while
5: i ← i+1
6: end while
Cât timp <expresie-booleana> este adevarată, se execută <instructiune>; dacă <expresie-
booleana> este falsă chiar la prima evaluare, atunci <instructiune> nu ajunge să fie re-
alizată niciodată. Acest comportament este opus celui corespunzător structurii repetitive
condiţionată posterior repeat, unde <instructiune> este executată cel puţin o dată. (Dacă
o expresie are valoarea de adevăr true spunem atunci că expresia este adevărată; dacă o
expresie are valoarea de adevăr false spunem atunci că expresia nu este adevărată - este
falsă).

Exemplul 1.2 Vom realiza un algoritm pentru calculul câtului şi restului ı̂mpărţirii a două
numere ı̂ntregi, prin scăderi succesive (a se vedea algoritmul 2).
Mai ı̂ntâi, se iniţializează câtul cu valoarea zero (linia 3) şi restul cu valoarea deı̂mpărţitului
(linia 4). Apoi, atâta timp cât restul este mai mare decât valoarea ı̂mpărţitorului (liniile 5 -
8), vom incrementa câtul cu o unitate, şi decrementa restul cu valoarea ı̂mpărţitorului. La
final, sunt afişate valorile câtului şi restului.

Un caz particular de structură repetitivă condiţionată anterior este for, utilizată ı̂n cazul
ı̂n care o instrucţiune sau un grup de instrucţiuni trebuie să se repete de 0 sau mai multe
ori - numărul de repetiţii fiind cunoscut ı̂nainte de ı̂nceperea sa. Enunţurile repetitive bazate

6
Algoritm 2 Algoritm pentru calculul ı̂mpărţirii ı̂ntregi
1: Input {a, b}
2: if ((a > 0) ∧ (b > 0)) then
3: cat ← 0
4: rest ← a
5: while (rest ≥ b) do
6: rest ← rest − b
7: cat ← cat + 1
8: end while
9: Output {cat, rest}
10: else
11: Output {’Numerele trebuie sa fie strict pozitive!’}
12: end if

pe instrucţiunile while şi repeat sunt mult mai potrivite ı̂n cazul ı̂n care condiţia de ter-
minare trebuie reevaluată ı̂n timpul ciclării (atunci când numărul de repetiţii nu este cunoscut
apriori).
Instrucţiunea for are următoarea sintaxă:
1: for <Var> ← <Expresie1>, <Expresie2>, 1: sum ← 0
Step < p > do 2: for i ← 1, n do
2: <instructiune> 3: sum ← sum + i
3: end for 4: end for
Comportamentul enunţului repetitiv for se descrie cel mai bine prin comparaţie cu
enunţul repetitiv while. Astfel secvenţa următoare de limbaj pseudocod
1: for v ← e1 , e2 ST EP p do
2: < instructiune >
3: end for
este echivalentă cu secvenţa ce conţine enunţul repetitiv while:
1: v ← e1
2: while (v ≤ e2 ) do
3: <instructiune>
4: v ←v+p
5: end while
e1 reprezintă valoarea iniţială, e2 limita finală, iar p pasul de lucru: la fiecare pas, valoarea
variabilei v este incrementată cu valoarea variabilei p, valoarea ce poate fi atât pozitivă cât
şi negativă. Dacă p ≥ 0 atunci v va lua valori ı̂n ordine crescătoare, iar dacă p < 0 atunci v
va primi valori ı̂n ordine descrescătoare.
În cazul ı̂n care pasul de incrementare este 1, atunci el se poate omite din enunţul repetitiv
for, variabila contor fiind incrementată automat la fiecare repetiţie cu valoarea 1. Dacă pasul
de incrementare p are valoarea 1 atunci avem următoarele situaţii posibile pentru construcţia
for:

• Expresie1 > Expresie2: <instructiune> nu se execută niciodată;

• Expresie1 = Expresie2: <instructiune> se execută exact o dată;

• Expresie1 < Expresie2: <instructiune> se execută de Expresie2 − Expresie1 + 1 ori.

Exemplul 1.3 Un caz destul de frecvent ı̂ntâlnit ı̂n practică este cel ı̂n care se cere deter-
minarea elementului de valoare maximă, respectiv minimă dintr-un şir de elemente. Acest

7
şir de elemente poate fi păstrat ı̂ntr-o structură de date elementară, cunoscută sub numele
de vector. Un vector este un caz particular de matrice având o singură linie şi n coloane.
Prezentăm ı̂n continuare algoritmul pentru determinarea elementului de valoare minimă
ce aparţine unui şir de numere naturale (a se vedea algoritmul 3).

Algoritm 3 Algoritm pentru calculul elementului de valoare minimă dintr-un şir


1: Input {N, x1 , x2 , . . . , xN }
2: min ← x1
3: for i ← 2, N do
4: if (min > xi ) then
5: min ← xi
6: end if
7: end for
8: Output { min }

În acest algoritm se observă utilizarea enunţului repetitiv for, ı̂ntâlnit, ı̂n general, ı̂n
situaţiile ı̂n care se cunoaşte apriori numărul de paşi pe care trebuie să-l realizeze instrucţiunea
repetitivă.
Mai ı̂ntâi se iniţializează variabila min cu valoarea elementului x1 , presupunând că ele-
mentul de valoare minimă este primul element din cadrul vectorului X (linia 2). În continuare
vom compara valoarea variabilei min cu valoarea fiecărui element din şir (liniile 3–7): dacă
vom găsi un element a cărui valoare este mai mică decât cea a minimului calculat până la
momentul curent (linia 4), vom reţine noua valoare ı̂n variabila min (linia 5).

Structura repetitivă condiţionată posterior repeat . . . until prezintă următoarea sin-


taxă:
1: sum ← 0
2: i←1
1: repeat
3: repeat
2: <instructiune>
4: sum ← sum + i
3: until <expresie-booleana>
5: i ← i+1
6: until (i > n)

Atâta timp cât <expresie-booleana> este falsă, se execută <instructiune>. Se observă


faptul că instrucţiunile dintre repeat şi until se vor executa cel puţin o dată.

Exemplul 1.4 Algoritmul 4, prezentat ı̂n continuare, calculează suma primelor n numere
naturale, S = 1 + 2 + 3 + . . . + n.
Notăm cu Sn suma primelor n numere naturale (Sn = 1 + 2 + 3 + . . . + n). Aceasta se
poate calcula ı̂ntr-o manieră incrementală astfel:

S1 = 1
S2 = 1 + 2 = S1 + 2
S3 = (1 + 2) + 3 = S2 + 3
S4 = (1 + 2 + 3) + 4 = S3 + 4
...
Sn = (1 + 2 + . . . + n − 1) + n = Sn−1 + n

8
Algoritm 4 Algoritm pentru calculul sumei primelor n numere naturale (prima variantă)
1: Input {N }
2: S←0
3: i←1
4: repeat
5: S ←S+i
6: i←i+1
7: until (i > N )
8: Output {S}

Observaţia 1.5 1. Algoritmul 4 utilizează enunţul repetitiv cu test final, repeat . . .


until.

2. Enunţurile S ← 0 şi i ← 1 au rolul de a atribui valori iniţiale variabilelor S şi i.


Întotdeauna variabilele trebuie iniţializate corect din punctul de vedere al algoritmului
de calcul. Variabila ce reţine rezultatul unui proces de ı̂nsumări succesive se va iniţializa
ı̂ntotdeauna cu valoarea 0, valoare ce nu va influenţa rezultatul final. O variabilă ce
păstrează rezultatul unei operaţii (sumă sau produs) se va iniţializa cu elementul neutru
faţă de operaţia respectivă.

3. Atribuirile i = i + 1 şi S = S + i (liniile 5 şi 6) sunt lipsite de sens din punct de


vedere algebric. Însă, atunci când ne referim la un sistem de calcul electronic, aceste
operaţii se referă la valoarea curentă şi la cea anterioară a unei variabile. De aseme-
nea, trebuie să se facă o distincţie clară ı̂ntre operaţia de atribuire şi cea de egalitate:
operaţia de atribuire (’←’) face ca valoarea curentă a variabilei din stânga operatorului
de atribuire să fie iniţializată cu valoarea expresiei din dreapta acestuia (ı̂n expresia
’k ← i + 2’ variabila k primeşte valoarea rezultată ı̂n urma evaluării expresiei ’i + 2’),
pe când operaţia de egalitate verifică dacă valoarea elementului din stânga operatoru-
lui ’=’ (sau a expresiei aflate ı̂n partea stângă a operatorului) este egală cu valoarea
expresiei din dreapta acestuia (de exemplu rezultatul evaluării expresiei k = i + 2) are
valoarea de adevăr true dacă valoarea variabilei ’k’ este egală cu valoarea rezultată ı̂n
urma evaluării expresiei ’i + 2’ şi are valoarea de adevăr false ı̂n caz contrar).
Prin urmare, ı̂n cazul expresiei i ← i + 1, valoarea curentă a variabilei i devine valoarea
anterioară a aceleiaşi variabile incrementată cu o unitate.

Suma primelor n numere naturale se constituie ı̂n suma termenilor unei progresii arit-
metice ce se poate calcula direct cu formula sumei, astfel 1 + 2 + . . . + n = n(n+1)
2
. Având
ı̂n vedere această identitate, algoritmul de calcul a sumei primelor n numere naturale se
simplifică foarte mult (a se vedea algoritmul 5).

Algoritm 5 Algoritm pentru calculul sumei primelor n numere naturale (a doua variantă)
1: Input {N }
2: S ← n · (n + 1)/2
3: Output {S}

9
Proceduri şi funcţii
Procedurile sunt subrutine ale căror instrucţiuni se execută ori de câte ori acestea sunt apelate
prin numele lor.
Apelarea procedurilor se face ı̂n unitatea de program apelantă prin numele procedurii,
primit ı̂n momentul definirii, urmat de lista parametrilor actuali. Această listă trebuie să
corespundă ca număr şi tip cu lista parametrilor formali, ı̂n situaţia ı̂n care există o listă de
parametri formali ı̂n antetul subrutinei. Definiţia unei proceduri are următoarea sintaxă:
1: procedure <nume>(<lista parametri formali>)
2: <instructiune>
3: end procedure
lista parametri formali (opţională) simbolizează o listă de identificatori (parametri)
ce permite transferul datelor ı̂ntre subrutina apelantă şi subrutina apelată. Aceşti parametri
se specifică prin nume (identificator) urmat, eventual, de tipul de dată al parametrului. Mai
mulţi parametri de acelaşi tip pot fi grupaţi, folosindu-se drept separator virgula.
De fapt, lista parametrilor formali poate fi descrisă mai detaliat astfel:
1: procedure <nume>(<PFI; PFO>)
2: end procedure
unde
PFI = lista parametrilor formali de intrare.
PFO = lista parametrilor formali de ieşire.
Enunţul de apel al unei proceduri are următoarea sintaxă:
1: CALL <nume>(PAI; PAO)
PAI - lista parametrilor actuali de intrare, ce corespund parametrilor formali de intrare.
Corespondenţa se face de la stânga la dreapta şi trebuie să fie de acelaşi tip cu parametrii
formali.
PAO - lista parametrilor actuali de ieşire.

Exemplul 1.6 Să se realizeze un algoritm care determină cel mai mare divizor comun a
două numere naturale.
Reamintim câteva rezultate teoretice ce vor fi folositoare pentru ı̂nţelegerea procesului de
calcul al algoritmului ce va fi prezentat.

Teorema 1.7 (Teorema ı̂mpărţirii cu rest) Pentru două numere ı̂ntregi a şi b, cu b 6=
0, ∃ două numere ı̂ntregi q şi r, unice, astfel ı̂ncât:

a = b · q + r, 0 ≤ r < |b|

unde a se numeşte deı̂mpărţitul, b ı̂mpărţitorul, q câtul iar r restul.

Definiţia 1.2 Cel mai mare divizor comun (notat cmmdc) a două numere naturale a şi b
este un număr natural d cu proprietăţile:

1. d|a, d|b (d este un divizor comun al lui a şi b);

2. ∀d′ ∈ N astfel ı̂ncât d′ |a, d′ |b avem d′ |d (oricare alt divizor comun al lui a şi b, ı̂l divide
şi pe d).

Observaţia 1.8 1. cmmdc(a, 0) = a.

2. Dacă cmmdc(a, b) = 1 se spune că numerele a şi b sunt prime ı̂ntre ele.

10
3. Între cel mai mic multiplu comun şi cel mai mare divizor comun există următoarea
relaţie:
a·b
cmmmc(a, b) = . (1.1)
cmmdc(a, b)
Metoda de calcul pentru a obţine cel mai mare divizor comun a două numere a şi b prin
ı̂mpărţiri succesive, cunoscută sub numele de algoritmul lui Euclid, se poate descrie astfel:
Se ı̂mparte a la b şi se reţine restul r. Dacă r este nul atunci cel mai mare divizor comun
este b. În caz contrar, se ı̂mparte b la r şi se păstrează noul rest. Calculele se continuă,
folosindu-se ca deı̂mpărţit vechiul ı̂mpărţitor, iar, ca ı̂mpărţitor, ultimul rest obţinut, până
când restul obţinut devine nul.
Operaţiile anterioare pot fi transpuse prin intermediul următoarelor formule matematice:
a = bq1 + r1 0 ≤ r1 < b
b = r1 q2 + r2 0 ≤ r 2 < r1
r1 = r2 q3 + r3 0 ≤ r 3 < r2
...
rk = rk+1 qk+2 + rk+2 0 ≤ rk+2 < rk+1
...
rn−2 = rn−1 qn + rn 0 ≤ rn < rn−1
rn−1 = rn qn+1 + rn+1 0 ≤ rn+1 < rn
Aceste ı̂mpărţiri nu se constituie ı̂ntr-un proces infinit, deoarece secvenţa de numere natu-
rale r1 , r2 , . . . , rn+1 , . . . este strict descrescătoare (r1 > r2 > . . . > rn > . . .) şi mărginită
inferior de 0. Rezultă că ∃p ∈ N astfel ı̂ncât rp 6= 0, şi rp+1 = 0.

Algoritm 6 Algoritmul lui Euclid pentru calculul cmmdc a două numere


1: procedure cmmdc(x, y; b)
2: a←x
3: b←y
4: if (b = 0) then
5: b←a
6: else
7: r ← a mod b
8: while (r 6= 0) do ⊲ Procesul se ı̂ncheie atunci când r ia valoarea 0
9: a←b
10: b←r
11: r ← a mod b
12: end while
13: end if
14: end procedure

Dacă analizăm ı̂ntregul proces de calcul, se observă faptul că deı̂mpărţitul este ı̂mpărţitorul
de la etapa anterioară, iar ı̂mpărţitorul este restul de la etapa anterioară. De asemenea, tre-
buie menţionat că ı̂n acest proces de calcul, câtul nu participă ı̂n mod activ.
Algoritmul 6 este un foarte bun exemplu de utilizare a instrucţiunii de ciclare cu test
iniţial, while. Din analiza algoritmului reiese faptul că, mai ı̂ntâi, se efectuează o evaluare a
restului r (calculul său, liniile 7 şi 11), după care se testează condiţia egalităţii acestuia cu
valoarea 0 (linia 8).

11
Funcţiile au aceeaşi sintaxă ca şi procedurile:
1: function <nume>(<parametri>)
2: <instructiune>
3: end function
Funcţiile pot fi apelate prin numele lor, ca termen al unei expresii.

1.2 Exemple
Exemplul 1.9 Să se verifice printr-un algoritm dacă un număr natural este prim sau nu.

Definiţia 1.3 Un număr natural k ≥ 2 se numeşte prim dacă singurii săi divizori naturali
sunt 1 şi k.

Pe baza acestei definiţii se poate concluziona că un număr natural k este prim dacă nu are
niciun divizor propriu ı̂n intervalul [2, k − 1].

Algoritm 7 Algoritm pentru verificarea dacă un număr este prim


1: Input {N }
2: if (n < 2) then
3: Output {’Numarul nu este prim.’}
4: else
5: i←2
6: prim ← true
7: while ((i ≤ n − 1) ∧ (prim = true)) do
8: if (n mod i = 0) then
9: prim ← f alse
10: else
11: i ← i+1
12: end if
13: end while
14: if (prim = true) then
15: Output {’Numarul este prim.’}
16: else
17: Output {’Numarul nu este prim.’}
18: end if
19: end if

Dacă n este număr prim, corpul enunţului de ciclare while se va executa de n − 2 ori (a
se vedea algoritmul 7).
Condiţia i ≤ n − 1 poate fi ı̂mbunătăţită cu i ≤ n/2, deoarece ı̂ntre jumătatea numărului
şi n nu mai există niciun alt divizor, pentru orice valoare a lui n.
Dacă 2 este un divizor al numărului n atunci şi n/2 este un divizor al lui n. Dacă 2 nu
este divizor al lui n atunci nici n/2 nu este divizor al lui n. Un enunţ asemănător este valabil

12
şi pentru numerele 3, 4, 5, . . .. Astfel se formează două şiruri:
corespunde n
2 →
2
corespunde n
3 →
3
corespunde n
4 →
4
.. ..
. .
corespunde n
k →
|{z} k
|{z}
Sir1 Sir2

Se observă faptul că primul şir (Sir1 ) este compus din elemente cu valori consecutive,
iar al doilea şir (Sir2 ) nu respectă această proprietate. Numerele dintr-o pereche (p, np ) sunt
legate ı̂ntre ele astfel: ı̂n momentul ı̂n care am verificat faptul că numărul p nu este divizor
al lui n, implicit am dedus că nici n/p nu este un divizor al lui n. Prin urmare nu mai este
nevoie să verificăm şi valoarea n/p. Primul şir este strict crescător iar cel de-al doilea este
strict descrescător. Elementele din primul
√ şir devin mai mari decât cele din al doilea atunci
când k ≅ n/k√ ⇒ k 2 ≅ n ⇒ k ≅ n. Astfel rezultă condiţia pentru limita superioară a
ciclării: i ≤ ⌊ n⌋ (a se vedea algoritmul 8).

Algoritm 8 Algoritm pentru verificarea dacă un număr este prim (variantă optimizată)
1: Input {N }
2: if (n < 2) then
3: Output {’Numarul nu este prim.’}
4: else
5: i←2
6: prim ← true

7: while ((i ≤ ⌊ n⌋) ∧ (prim = true)) do
8: if (n mod i = 0) then
9: prim ← f alse
10: else
11: i ← i+1
12: end if
13: end while
14: if (prim = true) then
15: Output {’Numarul este prim.’}
16: else
17: Output {’Numarul nu este prim.’}
18: end if
19: end if

Exemplul 1.10 Se spune că un vector este simetric dacă primul element este egal cu ultimul,
al doilea cu penultimul etc. Algoritmul următor verifică dacă un vector de numere ı̂ntregi este
simetric.
Vom utiliza două variabile de indici, ce pornesc, una din stânga, şi cealaltă din dreapta.
Atâta timp cât variabila din stânga este mai mică decât variabila din dreapta iar elementele
din vector corespunzătoare variabilelor de indiciere sunt egale, se incrementează variabila din
stânga şi se decrementează variabila din dreapta. După părăsirea instrucţiunii de ciclare, se

13
face o verificare pentru determinarea condiţiei care nu a mai fost ı̂ndeplinită şi a condus la
ieşirea din buclă. Dacă i ≥ j atunci condiţia (i < j) nu a mai fost ı̂ndeplinită. Acest lucru
conduce la concluzia că cealaltă condiţie, (xi = xj ) a fost ı̂ndeplinită tot timpul (∀i < j, i, j =
1, n). Deci şirul este simetric ı̂n această situaţie.

Algoritm 9 Algoritm pentru verificarea simetriei unui vector


1: Input {N, x1 , x2 , ..., xN }
2: i ← 1, j ← N
3: while ((i < j) ∧ (xi = xj )) do
4: i←i+1
5: j ← j −1
6: end while
7: if (i ≥ j) then
8: Output {’Vectorul este simetric.’}
9: else
10: Output {’Vectorul nu este simetric.’}
11: end if

Exemplul 1.11 Se dă un număr format din n cifre. Vom prezenta un algoritm ı̂n care se
determină cel mai mare număr obţinut prin eliminarea a k cifre (k < n) dintre cele n (vezi
algoritmul 10).
Compararea a două numere naturale ce prezintă acelaşi număr de cifre se face ı̂ncepând
de la stânga spre dreapta (de la cifrele mai semnificative către cele mai puţin semnificative).
Fie A = a1 a2 . . . an numărul nostru.Vom nota prin m numărul de cifre ce vor rămâne din
numărul iniţial după eliminarea celor k cifre (m = n−k). Problema se reduce la determinarea
celui mai mare număr format cu m cifre dintre cele n cifre iniţiale.
La ı̂nceput numărul conţine n cifre, păstrate ı̂n ordine, ı̂n vectorul A: a1 , a2 , . . . , an .
Soluţia se construieşte ı̂n mod iterativ, la pasul i alegându–se o valoare corespunzătoare: se
determină prima cifră de valoare maximă şi poziţia acesteia ı̂n cadrul subsecvenţei de limite
l şi n − m + i, al , . . . , an−m+i .

• În instrucţiunea for (linia 4) pentru fiecare poziţie liberă din numărul rezultat, se va
alege cifra maximă dintre cele disponibile;

• linia 5 - se trece la poziţia următoare: l reprezintă indexul din şirul A al cifrei aleasă
la pasul anterior (i − 1) şi conţine poziţia pe care a fost găsită cifra maximă. Cifra
maximă pentru pasul curent va fi căutată ı̂n intervalul l . . . n − m + i, unde n − m + i
reprezintă limita superioară a indexului până la care se poate alege un element la pasul
curent;
a1 . . . a ...a . . . an
| l {zn−m+i}
subsecvenţa pentru care se determină cifra maximă

• liniile 6, 7 - se iniţializează variabila de ciclare j cu l şi valoarea maximă cu 0; Deoarece


ai ≥ 0, ∀i = 1, n, se poate iniţializa variabila max (unde se va păstra valoarea maximă)
cu 0;

• linia 8 - se caută cifra maximă până la limita din dreapta n − m + i (dacă se merge mai
la dreapta dincolo de această limită nu se mai pot alege restul de m − i cifre);
• linia 10 - se actualizează valoarea maximă;

14
Algoritm 10 Algoritm pentru determinarea numărului maxim obţinut după eliminarea a k
cifre
1: Input {n, a1 , . . . , an , k}
2: m← n−k
3: l←0
4: for i ← 1, m do
5: l ←l+1
6: j←l
7: max ← 0
8: while (j ≤ n − m + i) do
9: if (max < aj ) then
10: max ← aj
11: l←j
12: end if
13: j ←j+1
14: end while
15: Output {max}
16: end for

• linia 11 - se actualizează poziţia valorii maxime.

Exemplul 1.12 Vom prezenta doi algoritmi ce determină toate tripletele de numere naturale
(a, b, c) ce verifică relaţia a2 + b2 = c2 unde c ≤ 100.
Prima variantă de lucru prezintă generarea tuturor tripletelor (i, j, k) unde 2 ≤ i < j <
k ≤ n. Dintre acestea se vor alege doar acelea ce verifică relaţia k · k = i · i + j · j. Dacă vom
considera doar instrucţiunea de ciclare 4, atunci condiţia 5 se va verifica de n − (j + 1) + 1 =
n − j ori. Deoarece j = i + 1, n − 1, vom obţine faptul că instrucţiunea 5 se va executa de:
n−i−1
X (n − i) · (n − i − 1)
(n − i − 1) + (n − i − 2) + . . . + (n − n + 1) = j=
j=1
2

ori.
Din i = 2, n − 2 rezultă că

n−2 n−i−1 n−2 n−2


X X X (n − i) · (n − i − 1) 1X 2
j = = (i + (1 − 2n)i + n2 − n)
i=2 j=1 i=2
2 2 i=2
n−2 n−2
1X 2 X
= [ i + (1 − 2n) i + (n2 − n) · (n − 3)]
2 i=2 i=2

15
1: Input {n} 1: Input {n}
2: for i ← 2, n − 2 do 2: for i ← 2, n − 1 do
3: for j ← i + 1, n − 1 do 3: for j ← i + 1, n do
4: for k ← j + 1, n do 4: s ← i·i+j ·j

5: if (k · k = i · i + j · j) then 5: k ← ⌊ s⌋
6: Output {i, j, k} 6: if ((k · k = s) ∧ (k ≤ n)) then
7: end if 7: Output {i, j, k}
8: end for 8: end if
9: end for 9: end for
10: end for 10: end for
11: return 11: return

Pentru cea de-a doua variantă, cea din dreapta, se generează perechile de numere (i, j) cu
proprietatea că 2 ≤ i < j ≤ n, se calculează
√ 2 s ← i · i + j · j şi se verifică dacă numărul
obţinut este pătrat perfect (linia 6: (⌊ s⌋) = s). Condiţia de la linia 6 se va executa de
n − (i + 1) + 1 = n − i ori.
Deoarece linia 2 se va executa de n − 1 − 2 + 1 = n − 2 ori, valoarea totală reprezentând
numărul de verificări (linia 6) va fi:
n−1 n−1 n−1
X X X n(n − 1)
(n − i) = (n − i) − (n − 1) = i − (n − 1) = −n+1
i=2 i=1 i=1
2

1.3 Elemente de analiza algoritmilor


Gradul de dificultate al unei probleme P poate fi pus in evidenţă prin timpul de execuţie al
algoritmului corespunzător şi/sau prin spaţiul de memorie necesar. Timpul de execuţie ı̂n
cazul cel mai defavorabil ne dă durata maximă de execuţie a unui algoritm. Timpul mediu
de execuţie se obţine prin ı̂nsumarea timpilor de execuţie pentru toate mulţimile de date de
intrare urmată de raportarea la numărul acestora.

Definiţia 1.4 Timpul de execuţie ı̂n cazul cel mai defavorabil al unui algoritm A este o
funcţie TA : N −→ N unde TA (n) reprezintă numărul maxim de instrucţiuni executate de
către A ı̂n cazul unor date de intrare de dimensiune n.

Definiţia 1.5 Timpul mediu de execuţie al unui algoritm A este o funcţie TAmed : N −→ N
unde TAmed (n) reprezintă numărul mediu de instrucţiuni executate de către A ı̂n cazul unor
date de intrare de dimensiune n.

Fiind dată o problemă P , o funcţie T (n) se spune că este o margine superioară dacă există
un algoritm A ce rezolvă problema P iar timpul de execuţie ı̂n cazul cel mai defavorabil al
algoritmului A este cel mult T (n).
Fiind dată o problemă P , o funcţie T (n) se spune că este o margine pentru cazul mediu
dacă există un algoritm A ce rezolvă problema P iar timpul mediu de execuţie al algoritmului
A este cel mult T (n).
Fiind dată o problemă P , o funcţie T (n) se spune că este o margine inferioară dacă orice
algoritm A ce rezolvă problema P va avea cel puţin un timp T (n) pentru unele date de intrare
de dimensiune n, atunci când n tinde la infinit (n → ∞).

Definiţia 1.6 Fiind dată o funcţie g : N −→ R vom nota cu O(g(n)) mulţimea: O(g(n)) =
{f (n)| ∃c > 0, n0 ≥ 0 a.ı̂. 0 ≤ f (n) ≤ c · g(n), ∀n ≥ n0 }.

16
Vom spune despre f că nu creşte ı̂n mod sigur mai repede decât funcţia g. Pentru a indica
faptul că o funcţie f (n) este un membru al mulţimii O(g(n)), vom scrie f (n) = O(g(n)), ı̂n
loc de f (n) ∈ O(g(n)).

Observaţia 1.13 Vom prezenta câteva proprietăţi ale lui O(·):

• O(f (n) + g(n)) = O(max(f (n), g(n))).


De exemplu pentru funcţia f (n) = 7 · n5 − n2 + 3 · log n aplicând regula valorii maxime,
vom avea: O(f (n)) = O(max(7 · n5 , n2 , 3 · log n)) adică O(f (n)) = O(n5 ).

• O(loga n) = O(logb n);

• f (n) = O(f (n)) (reflexivitate);

• f (n) = O(g(n)) şi g(n) = O(h(n)) atunci f (n) = O(h(n)) (tranzitivitate).

Definiţia 1.7 Fiind dată o funcţie g : N −→ R vom nota cu Θ(g(n)) mulţimea: Θ(g(n)) =
{f (n)| ∃c1 , c2 > 0, ∃n0 ≥ 0 a.ı̂. 0 ≤ c1 · g(n) ≤ f (n) ≤ c2 · g(n), ∀n ≥ n0 }

Spunem că g(n) este o margine asimptotic tare pentru f (n).

Definiţia 1.8 Fiind dată o funcţie g : N −→ R vom nota cu Ω(g(n)) mulţimea: Ω(g(n)) =
{f (n)| ∃c > 0, n0 ≥ 0 a.ı̂. 0 ≤ c · g(n) ≤ f (n), ∀n ≥ n0 }

La fel cum O furnizează o delimitare asimptotică superioară pentru o funcţie, Ω furnizează


o delimitare asimptotică inferioară pentru aceasta.

Teorema 1.14 Pentru orice două funcţii f (n) şi g(n), avem f (n) = Θ(g(n)) ⇐⇒ f (n) =
O(g(n)) şi f (n) = Ω(g(n)).

Definiţia 1.9 Fiind dată o funcţie g : N −→ R vom nota cu o(g(n)) mulţimea: o(g(n)) =
{f (n)| ∀c > 0, ∃n0 > 0 a.ı̂. 0 ≤ f (n) < c · g(n), ∀n ≥ n0 }
f (n)
Observaţia 1.15 f (n) = o(g(n)) ⇐⇒ limn→∞ g(n)
= 0.

Definiţia 1.10 f (n) ∈ ω(g(n)) ⇐⇒ g(n) ∈ o(f (n))


ω(g(n)) este o mulţime ce se defineşte astfel:
ω(g(n)) = {f (n)| ∀c > 0, ∃n0 > 0 a.ı̂. 0 ≤ c · g(n) < f (n), ∀n ≥ n0 }

Observaţia 1.16 1. f (n) = Θ(g(n)), g(n) = Θ(h(n)) =⇒ f (n) = Θ(h(n)) (tranzitivi-


tate);

2. f (n) = Θ(f (n)) (reflexivitate);

3. f (n) = Θ(g(n)) ⇐⇒ g(n) = Θ(f (n)) (simetrie).

Definiţia 1.11 O funcţie f are o creştere exponenţială dacă ∃c > 1 a.i. f (x) = Ω(cx ) şi
∃d a.ı̂. f (x) = O(dx ). O funcţie f este polinomială de gradul d dacă f (n) = Θ(nd ) şi

f (n) = O(nd ), ∀d′ ≥ d.

17
Teorema 1.17 
c ⇒ g(n) ∈ Θ(f (n)), c > 0
g(n) 
lim = 0 ⇒ g(n) ∈ o(f (n)) (1.2)
n→∞ f (n) 

∞ ⇒ f (n) ∈ o(g(n))

Exemplul 1.18 • pentru x ∈ R∗+ avem xn ∈ o(n!).


Deoarece
xn xn xn xn 1
< 4 4 ≤ 4 n/2
= 2n
= ( )n (1.3)
n! x
| .{z
. . x} (x ) x x
⌈n/2⌉ori

rezultă că
xn
lim =0 (1.4)
n→∞ n!

• log n ∈ o(n).
1
log x (log x)′ x ln 2
lim = lim = lim =0
x→∞ x x→∞ (x)′ x→∞ 1

• pentru a, b > 1, a, b ∈ R∗ avem loga n ∈ Θ(logb n).

Calcularea cu exactitate a timpului de execuţie al unui program oarecare se poate dovedi


o activitate dificilă. De aceea, ı̂n practică se utilizează estimări ı̂ncercându-se eliminarea
constantelor şi simplificarea formulelor ce intervin ı̂n cadrul evaluării.
Dacă două părţi ale unui program, P1 şi P2 , au timpii de execuţie corespunzători T1 (n) şi
T2 (n), unde T1 (n) = O(f (n)) şi T2 (n) = O(g(n)), atunci programul P1 ⊕ P2 va avea timpul
de execuţie T1 (n) + T2 (n) = O(max(f (n), g(n))) (regula sumei ), unde ⊕ reprezintă operaţia
de concatenare.
Prin aplicarea acestei reguli, rezultă faptul că, timpul necesar execuţiei unui număr finit
de operaţii este, neluând ı̂n considerare constantele, caracterizat ı̂n principal de către timpul
de execuţie al operaţiei cele mai costisitoare.
Cea de-a doua regulă, regula produsului, spune că, fiind date două funţii f (n) şi g(n) astfel
ı̂ncât T1 (n) = O(f (n)) şi T2 (n) = O(g(n)), atunci avem T1 (n) · T2 (n) = O(f (n) · g(n)).
Pe baza regulilor produsului şi sumei putem face următoarele reduceri:

O(1) ⊆ O(log n) ⊆ O(n) ⊆ O(n log n) ⊆ O(n2 ) ⊆ O(n3) ⊆ O(2n )

Prezentăm ı̂n continuare câteva reguli generale pentru evaluarea complexităţii unui algoritm:

• timpul de execuţie al unei instrucţiuni de atribuire, citire sau afişare a unei variabile
este O(1).

• timpul de execuţie al unei secvenţe de instrucţiuni este proporţional cu timpul instrucţiunii


care durează cel mai mult.

• timpul de execuţie al unei instrucţiuni de decizie (if) este timpul executării instrucţiu-
nilor de pe ramura aleasă plus timpul necesar evaluării condiţiei.

• timpul de execuţie pentru o instrucţiune de ciclare este suma, după numărul de paşi
pe care ı̂i realizează instrucţiunea de ciclare, dintre timpul necesar executării corpului
instrucţiunii plus timpul necesar evaluării condiţiei.
1: for i ← 1, n do

18
2: A(i)
3: end for

Dacă instrucţiunii compuse A(i) ı̂i corespunde un timp de execuţie constant t, ce nu


depinde de i, atunci timpul corespunzător ı̂ntregii instrucţiuni de ciclare for anterioare
este: n n
X X
t=t 1 = t · n = O(n) (1.5)
i=1 i=1

În cazul general, timpul de execuţie al instrucţiunii compuse A(i) depinde


Pn de pasul i,
notat cu ti . Astfel timpul total corespunzător instrucţiunii for este i=1 ti .
• Instrucţiunile de ciclare al căror număr de paşi depinde de ı̂ndeplinirea unei condiţii
(while) sau de neı̂ndeplinirea acesteia (repeat . . . until), sunt mult mai dificil de
analizat deoarece nu există o metodă generală de a afla cu exactitate numărul de repetări
al corpului instrucţiunii. De exemplu, pentru fragmentul următor
1: i ← l
2: while (ai < x) do
3: ... ⊲ calcule ce pot modifica, direct sau indirect, valoarea lui ai
4: i←i+1
5: end while

nu se poate preciza de câte ori se va ajunge la realizarea instrucţiunii de incrementare


din interiorul instrucţiunii while. În marea majoritate a cazurilor se utilizează elemente
de teoria probabilităţilor ı̂n vederea obţinerii unei estimări a numărului de repetări al
corpului instrucţiunii.

Exemplul 1.19 De exemplu


1: for i ← 1, n do
2: instructiune1
3: end for

are timpul de execuţie O(n).


1: for i ← 1, n do
2: for j ← 1, m do
3: instructiune2
4: end for
5: end for

Timpul de execuţie pentru secvenţa de cod ce conţine două instrucţiuni de ciclare imbricate
este O(n · m).

Exemplul 1.20 Să considerăm un alt exemplu: se dă un şir PiA de n numere reale, şi se
1
doreşte calcularea elementelor unui şir B astfel ı̂ncât bi = i · j=1 aj , pentru i = 1, n.
1: for i ← 1, n do
2: s←0
3: for j ← 1, i do
4: s ← s + aj
5: end for
6: bi ← si
7: end for
8: return

19
Dacă notăm cu o constantă cx timpul necesar pentru efectuarea unei operaţii atomice,
vom obţine: costul efectuării liniei 1 este c1 · n, al liniei 2 este c2 · n, al liniei 3 este c3 · i, al
liniei 4 este c4 · i, al liniei 6 este c5 · n iar al liniei 8 este c6 .
n
X n
X n
X n
X
T (n) = c1 n + c2 n + c3 i + c4 i + c5 n + c6 = c1 n + c2 n + c3 i + c4 i + c5 n + c6
i=1 i=1 i=1 i=1
n(n + 1)
= (c3 + c4 ) + n(c1 + c2 + c5 ) + c6
2
1 1
= (c3 + c4 )n2 + (c3 + c4)n + n(c1 + c2 + c5 ) + c6
2 2
1 1
= (c3 + c4 )n2 + [ (c3 + c4 ) + c1 + c2 + c5 ]n + c6 = n2 · p + n · q + c6
2 2
De obicei nu se efectuează o analiză atât de detaliată a algoritmului, dar se ı̂ncearcă o eval-
uare a blocurilor principale, cum ar fi instrucţiunile de ciclare, atribuindu-le direct valori
corespunzătoare complexităţii timp, atunci când este posibil:

T (n) = O(n2 ) + O(n) + O(1) = O(n2 ) (1.6)

Vom modifica algoritmul anterior astfel ı̂ncât să reducem numărul de calcule efectuate:
1: s←0
2: for i ← 1, n do
3: s ← s + ai
4: bi ← si
5: end for
6: return
Pentru această variantă de lucru, complexitatea timp este următoarea:

T (n) = O(1)(linia 1) + O(n)(liniile 2–4) + O(1)(linia 6) = O(n) (1.7)

În urma analizei de complexitate, putem concluziona faptul că cea de-a doua variantă a
algoritmului rulează ı̂ntr-un timp liniar.

1.3.1 Metoda substituţiei


Metoda substituţiei presupune mai ı̂ntâi ghicirea (estimarea) formei soluţiei pentru relaţia
de recurenţă şi apoi demonstrarea prin inducţie matematică a corectitudinii soluţiei alese.
Metoda poate fi aplicată ı̂n cazul unor ecuaţii pentru care forma soluţiei poate fi estimată.
Să considerăm următoarea relaţie de recurenţă:
(
1 , dacă n = 1
T (n) = n
(1.8)
2T ( 2 ) + n , ı̂n rest

Vom presupune că soluţia acestei relaţii de recurenţă este T (n) = O(n log n) (notăm
log2 n = log n). Folosind metoda inducţiei matematice, vom ı̂ncerca să demonstrăm inegali-
tatea:
T (n) ≤ cn log n. (1.9)
Presupunem, mai ı̂ntâi, că inecuaţia (1.9) are loc pentru ∀k < n, inclusiv pentru n2 , adică
T ( n2 ) ≤ c · n2 · log ( n2 ).

20
Vom demonstra că inecuaţia (1.9) este ı̂ndeplinită şi pentru n: T (n) = 2T ( n2 ) + n.
n n n
T (n) ≤ 2(c log ) + n = cn log + n
2 2 2
= cn log n − cn log 2 + n = cn log n − cn + n (1.10)
≤ cn log n
Metoda inducţiei matematice presupune să arătăm că soluţia respectă şi cazurile particulare
(T (1) = 1). Pentru n = 1 vom avea T (1) = c · 1 · log 1 = 0 ceea ce contrazice relaţia T (1) = 1.
Această situaţie poate fi rezolvată dacă considerăm că T (n) = cn log n, ∀n ≥ n0 (n0 este
o constantă).
Din T (2) = 4 şi T (3) = 5 vom alege valoarea parametrului c astfel ı̂ncât să fie ı̂ndeplinite
inegalităţile T (2) ≤ 2c log 2 şi T (3) ≤ 3c log 3. Se observă că orice valoare a lui c ≥ 2 satisface
inegalităţile anterioare.

1.3.2 Schimbarea de variabilă


Această metodă presupune realizarea unei schimbări de variabilă ı̂n vederea simplificării
formulei sau pentru regăsirea unei formule
√ deja cunoscută.
Să considerăm ecuaţia T (n) = 2T ( n) + log n. Dacă ı̂nlocuim pe n cu 2m obţinem:
m m
T (2m ) = 2T (2 2 ) + log 2m = 2T (2 2 ) + m (1.11)

Notăm cu S(m) = T (2m ) şi ı̂nlocuind ı̂n (1.11), obţinem o nouă relaţie de recurenţă:
m
S(m) = 2S( )+m (1.12)
2
Se observă că această relaţie are o formă similară cu cea din formula (1.8).
Soluţia relaţiei (1.12) este:

S(m) = O(m log m) ⇒ T (n) = T (2m ) = S(m) = O(m log m) = O(log n log log n) (1.13)

În concluzie avem T (n) = O(log n log log n).

1.3.3 Metoda iterativă


Metoda iterativă este mult mai eficientă deoarece nu presupune ’ghicirea’ soluţiei, variantă ce
conduce deseori la rezultate greşite şi timp pierdut. De exemplu, să considerăm următoarea
formulă de recurenţă: (
1 , dacă n = 1
T (n) = (1.14)
T ( n2 ) + 1 , ı̂n rest
Pentru rezolvarea acestei recurenţe vom substitui succesiv pe n cu n2 :
n
T (n) = T ( ) + 1
2
n
= T( ) + 2
4
n (1.15)
= T( ) + 3
8
...
n
= T( k) + k
2
21
Prin urmare atunci când 1 = 2nk ⇒ k = log n, vom avea
T (n) = 1 + k = 1 + log n ⇒ T (n) = O(log n).
Să considerăm o altă formulă de recurenţă:
(
1 , dacă n = 1
T (n) = n
(1.16)
2T ( 2 ) + n , ı̂n rest

n
Înlocuind succesiv pe n cu 2
vom obţine următoarea serie de identităţi:
n
T (n) = 2T ( ) + n
2
n n n
= 2(2T ( ) + ) + n = 4T ( ) + 2n
2 2 4
n n n (1.17)
= 4(2T ( ) + ) + 2n = 8T ( ) + 3n
8 4 8
...
n
= 2k T ( k ) + kn
2
n
Deoarece substituţia se opreşte atunci când 2k
= 1, adică pentru k = log n, vom avea:

T (n) = 2k + kn = n + n log n = O(n log n) (1.18)

1.3.4 Teorema master


Metoda master pune la dispoziţie o variantă de rezolvare a unor recurenţe având forma
următoare
n
T (n) = aT ( ) + f (n) (1.19)
b
unde a ≥ 1, b > 1 sunt constante, iar f (n) este o funcţie asimptotic pozitivă. Formula de
recurenţă (1.19) descrie timpul de execuţie al unui algoritm ce presupune descompunerea unei
probleme de dimensiune n ı̂n a subprobleme, fiecare având dimensiunea datelor de intrare nb .
f (n) reprezintă costul asociat ı̂mpărţirii datelor de intrare cât şi costul combinării rezultatelor
celor a subprobleme.

Teorema 1.21 (Teorema master) [30] Fie a ≥ 1 şi b > 1 două constante, f (n) o funcţie
asimptotic pozitivă, şi T (n) definită de relaţia de recurenţa T (n) = aT ( nb ) + f (n), unde nb va
fi ⌊ nb ⌋ sau ⌈ nb ⌉.
Atunci T (n) este mărginită asimptotic după cum urmează:

1. dacă f (n) = O(nlogb a−ǫ ) pentru o constantă ǫ > 0, atunci T (n) = Θ(nlogb a );

2. dacă f (n) = Θ(nlogb a logk n), atunci T (n) = Θ(nlogb a logk+1 n) (de obicei k = 0);

3. dacă f (n) = Ω(nlogb a+ǫ ) pentru o constantă ǫ > 0, şi dacă af ( nb ) ≤ cf (n) pentru o
constantă c < 1 şi oricare număr n suficient de mare, atunci T (n) = Θ(f (n)).

Corolarul 1.22 Pentru o funcţie f de tip polinomial unde f (n) = cnk avem:

1. dacă a > bk atunci T (n) = O(nlogb a );

2. dacă a = bk atunci T (n) = O(nk log n);

22
3. dacă a < bk atunci T (n) = O(nk ).

Exemplul 1.23 • Fie T (n) = 2T ( n2 )+n. Avem a = 2, b = 2, k = 1, f (n) = nk . Aplicând


corolarul pentru cazul a = bk , soluţia este T (n) = O(nk log n) = O(n log n).
• Fie T (n) = 9T ( n3 ) + n. Avem a = 9, b = 3, k = 1. Aplicând corolarul pentru cazul
a > bk , se obţine soluţia T (n) = O(nlog3 9 ) = O(n2 ).
• Fie T (n) = 4T ( n2 ) + n3 . Avem a = 4, b = 2, k = 3, f (n) = n3 . Aplicând corolarul
pentru cazul a < bk , se obţine soluţia T (n) = O(n3 ).
• Fie T (n) = 2n T ( n2 ) + nn . Nu se poate aplica Teorema master deoarece a = 2n nu este
constant.
• Fie T (n) = 2T ( n2 ) + n log n. Atunci avem a = 2, b = 2, k = 1, f (n) = Θ(nlog2 2 logk n),
şi aplicând Teorema master, cazul al doilea, obţinem: T (n) = Θ(n log2 n).
• Fie T (n) = 12 T ( n2 ) + n1 . Nu se poate aplica Teorema master deoarece a = 12 < 1.
√ √
• Fie T (n) = 2T ( n2 ) + log n. Aplicând Teorema master pentru a = 2, b = 2, f (n) =
1 √
O(n 2 −ǫ ) obţinem: T (n) = Θ( n).
• Fie T (n) = 3T ( n4 ) + n log n. Aplicând Teorema master pentru a = 3, b = 4, f (n) =
Ω(nlog4 3+ǫ ) obţinem: T (n) = Θ(n log n).
• Fie T (n) = 16T ( n4 ) − n log n. Nu se poate aplica Teorema master deoarece f (n) =
−n log n nu este o funcţie asimptotic pozitivă.

Exemplul 1.24 Analiza acţiunilor


Deschiderea unei acţiuni la o anumită dată calendaristică se calculează drept numărul
maxim de zile consecutive (până la acea dată) ı̂n care preţul acţiunii a fost mai mic sau egal
cu preţul din ziua respectivă. Fie pk preţul unei acţiuni ı̂n ziua k iar dk deschiderea calculată
ı̂n aceeaşi zi.
În continuare se prezintă un exemplu de calcul al deschiderii unei acţiuni pentru un număr
de 7 zile:

p0 p1 p2 p3 p4 p5 p6
9 6 3 4 2 5 7
d0 d1 d2 d3 d4 d5 d6
1 1 1 2 1 4 6

Algoritm 11 Algoritm de calcul al deschiderii unei acţiuni (prima variantă)


1: procedure ComputeSpan1(p, n; d)
2: for k ← 0, n − 1 do
3: j←1
4: while (j < k) ∧ (pk−j ≤ pk ) do
5: j ←j+1
6: end while
7: dk ← j
8: end for
9: return
10: end procedure

23
Algoritmul 11 calculează deschiderea unei acţiuni pentru un interval mai lung de timp pe
baza evoluţiei preţurilor acesteia. Timpul de execuţie al acestui algoritm este O(n2 ).
Vom prezenta un alt mod de calcul al deschiderii unei acţiuni folosind o stivă (a se vedea
algoritmul 12).
Stiva este o structură de date de tip container deoarece ea depozitează elemente de un
anumit tip. Pentru a putea să operăm asupra unei colecţii de elemente păstrate ı̂ntr-o stivă,
se definesc următoarele operaţii, ı̂n afară de operaţiile fundamentale push şi pop:

• push(x: object) - adaugă obiectul x ı̂n vârful stivei.

• pop(): object - elimină şi ı̂ntoarce obiectul din vârful stivei; dacă stiva este goală
avem o situaţie ce generează o excepţie.

• peek(): object - ı̂ntoarce valoarea obiectului din vârful stivei fără a-l extrage.

• size(): integer - ı̂ntoarce numărul de obiecte din stivă.

• isEmpty(): boolean - ı̂ntoarce true dacă stiva nu conţine nici un obiect.

• isFull(): boolean - ı̂ntoarce true dacă stiva este plină.

• init(capacitate:integer) - iniţializează stiva.

Algoritm 12 Algoritm de calcul al deschiderii unei acţiuni (a doua variantă)


1: procedure ComputeSpan2(p, n; d)
2: call init(S)
3: for k ← 0, n − 1 do
4: ind ← 1
5: while (isEmpty(S) = f alse) ∧ (ind = 1) do
6: if (pk ≥ ppeek(S)) then
7: call pop(S)
8: else
9: ind ← 0
10: end if
11: end while
12: if (ind = 1) then
13: h ← −1
14: else
15: h ← peek(S)
16: end if
17: dk ← k − h
18: call push(S, k)
19: end for
20: return
21: end procedure

Timpul de execuţie al noului algoritm este O(n).

24
1.4 Exerciţii
1. (a) Să se determine dacă un număr natural este simetric sau nu.
(b) Să se determine toate cifrele distincte dintr-un număr natural.
(c) Să se determine reprezentarea ı̂n baza 2 a unui număr natural.
(d) Să se determine forma corespunzătoare ı̂n baza 10 a unui număr reprezentat ı̂n
baza 2.
(e) Să se elimine dintr–un număr natural toate cifrele de forma 3k + 1 şi să se afişeze
numărul rezultat.
(f) Pentru un număr natural dat să se construiască din cifrele acestuia cel mai mare
număr prin amplasarea mai ı̂ntâi a cifrelor impare şi apoi a cifrelor pare.

2. (a) Să se determine toţi divizorii unui număr natural.


(b) Să se verifice dacă două numere naturale sunt prime ı̂ntre ele.
(c) Să se determine toţi divizorii comuni a două numere naturale.
(d) Să se calculeze toate numerele prime mai mici decât o valoare specificată.
(e) Să se descompună ı̂n factori primi un număr natural.
(f) Să se determine cel mai mare divizor comun a două numere naturale folosindu–se
descompunerea ı̂n factori primi.

3. Să se determine toate numerele naturale n (1 ≤ n ≤ 106 ) cu proprietatea că atât n cât
şi oglinditul său sunt numere prime.

4. Pentru o secvenţă de numere naturale de lungime n, să se realizeze un algoritm care să
determine cea mai lungă subsecvenţă de elemente consecutive având aceeaşi valoare.

5. Să se realizeze un algoritm care să determine primele n numere prime, pentru un număr
natural n dat.

6. Să se realizeze un algoritm pentru calcularea tuturor soluţiilor ecuaţiei:

3x + y + 4xz = 100, x, y, z ∈ N. (1.20)

Indicaţie Pentru o valoare z ≥ 25 şi x 6= 0 (x > 0) avem:

3x + y + 4xz ≥ 3x + 4xz ≥ 3 + 4z > 100.

Astfel pentru z obţinem că 0 ≤ z ≤ 24.


Ecuaţia 1.20 se poate scrie astfel:
100 − y
3x + y + 4xz = 100 ⇔ x(3 + 4z) = 100 − y ⇔ x = (1.21)
3 + 4z
100−y
 100−y   100−y 
Dacă x 6= 0 şi y ≥ 0 ⇒ x = 3+4z
≥ 3+4z
⇒ x ∈ [1, 3+4z
].
Astfel, pentru fiecare pereche de numere (z, x) vom calcula pe y astfel: y = 100 − 3x −
4xz.
De asemenea, ı̂n 1.21 dacă luăm x = 0 obţinem mulţimea de soluţii (0, 100, z), ∀z ∈ N.

25
Algoritm 13 Algoritm pentru descompunerea ı̂n factori primi a unui număr natural
1: Input {n}
2: j←2
3: while (j ≤ n) do
4: if (n mod j = 0) then
5: k←0
6: while (n mod j = 0) do
7: k ←k+1
8: n ← n div j
9: end while
10: Output {j k }
11: end if
12: j ← j +1
13: end while

7. Să se evalueze complexitatea algoritmului 13, ce realizează descompunerea ı̂n factori


primi a unui număr natural.

8. Pentru fiecare dintre următoarele relaţii de recurenţă calculaţi complexitatea timp:

(a) T (n) = 2T ( n2 ) + n
log n
;
(b) T (n) = 16T ( n4 ) + n!;

(c) T (n) = 3T ( n3 ) + n;
(d) T (n) = 3T ( n3 ) + n2 .

9. Să se calculeze timpul mediu de lucru T (n) al algoritmului 14.

Algoritm 14 Algoritm Calcul5


1: for i ← 1, n do
2: j←n
3: while (j ≥ 1) do
4: ...
5: j ← ⌊ 2j ⌋
6: end while
7: end for

10. Să se calculeze timpul mediu de lucru T (n) al algoritmului 15.

Algoritm 15 Algoritm Calcul6


1: i ← n
2: while (i ≥ 1) do
3: j←i
4: while (j ≤ n) do
5: ...
6: j ←j·2
7: end while
8: i ← ⌊ 2i ⌋
9: end while

26
11. Să se rezolve următoarele ecuaţii recurente:

• T (n) = 2 · T ( n2 ) + n lg n, n = 2k ;
• T (n) = 3 · T ( n2 ) + c · n, n = 2k > 1.

12. Să se rezolve ecuaţia recurentă T (n) = n · T 2 ( n2 ), n = 2k , T (1) = 6.

13. Să se arate că ex = 1 + x + Ω(x2 ) unde x → ∞.

14. Să se determine primele n elemente ale şirurilor ak şi bk date prin următoarele relaţii
de recurenţă:
5ak + 3 ak + 3
ak+1 = , bk = , k ≥ 0, a0 = 1. (1.22)
ak + 3 ak + 1
15. Să se verifice dacă următoarele afirmaţii sunt adevărate:

(a) n2 ∈ O(n3 );
(b) n3 ∈ O(n2 );
(c) 2n+1 ∈ O(2n );
(d) (n + 1)! ∈ O(n!);
(e) ∀f : N −→ R∗ , f ∈ O(n) ⇒ f 2 ∈ O(n2 );
(f) ∀f : N −→ R∗ , f ∈ O(n) ⇒ 2f ∈ O(2n ).

27
Capitolul 2

Grafuri. Grafuri neorientate

2.1 Noţiuni de bază


Fie V o mulţime finită şi nevidă având n elemente (V = {x1 , x2 , . . . , xn }). Fie E o mulţime
finită astfel ı̂ncât E ⊆ V × V (unde V × V este produsul cartezian al mulţimii V cu ea ı̂nsăşi)
şi E = {(x, y)|x, y ∈ V } (E este mulţimea perechilor (x, y) cu proprietatea că x şi y aparţin
mulţimii V ).

Definiţia 2.1 Se numeşte graf o pereche ordonată G = (V, E).


Elementele xi ∈ V se numesc noduri sau vârfuri. Elementele mulţimii E sunt arce
sau muchii. O muchie (xk , xl ) ∈ E se mai notează şi cu [xk , xl ].

|V | se numeşte ordinul grafului G şi reprezintă numărul vârfurilor acestuia, iar |E| se
numeşte dimensiunea grafului G şi reprezintă numărul muchiilor/ arcelor grafului G.
Graful Φ = (∅, ∅) se numeşte graful vid.
Dacă ı̂ntr-o pereche [xk , xl ] nu ţinem cont de ordine atunci graful este neorientat iar
perechea reprezintă o muchie ([xk , xl ] = [xl , xk ]). Dacă se introduce un sens fiecărei muchii
atunci aceasta devine arc iar graful se numeşte orientat ([xk , xl ] 6= [xl , xk ]). Muchiile ce au
aceleaşi vârfuri se spune că sunt paralele. O muchie de forma [u, u] se numeşte buclă.
Un graf G se numeşte simplu dacă oricare două vârfuri ale sale sunt extremităţi pentru
cel mult o muchie. Un graf este finit dacă mulţimile V şi E sunt finite.
În continuare vom considera un graf neorientat, simplu şi finit. Dacă [x, y] ∈ E vom
spune că x şi y sunt adiacente, iar muchia [x, y] este incidentă cu vârfurile x şi y. x şi y se

Fig. 2.1: a) Un exemplu de graf neorientat cu 6 vârfuri b) Graful complet K5

28
mai numesc capetele muchiei. Două muchii sunt adiacente dacă au un vârf comun. Un graf
este trivial dacă are un singur vârf. Dacă E = ∅ atunci graful G = (V, E) se numeşte graf
nul.

Observaţia 2.1 Un graf neorientat, simplu, finit poate fi utilizat drept model de reprezentare
al unei relaţii simetrice peste o mulţime.

Definiţia 2.2 Se numeşte graf complet de ordinul n, şi se notează cu Kn , un graf cu pro-
prietatea că oricare două vârfuri distincte ale sale sunt adiacente (∀x ∈ V, y ∈ V, x 6= y ⇒
[x, y] ∈ E).

Exemplul 2.2 Să considerăm grafurile din figura 2.1.

a) G = (V, E), V = {1, 2, 3, 4, 5, 6}, E = {[1, 2], [1, 5], [2, 3], [2, 6], [3, 4], [4, 5], [5, 6]} (vezi
figura 2.1 a));

b) K5 este graful complet cu 5 vârfuri. Vârfurile 3 şi 5 sunt adiacente (vezi figura 2.1 b)).

Fig. 2.2: a) Un exemplu de graf neorientat cu 5 vârfuri. b) Subgraf al grafului din figura 2.1 b). c) Graf
parţial al grafului din figura 2.1 b).

Definiţia 2.3 Un graf parţial al unui graf dat G = (V, E) este un graf G1 = (V, E1 ) unde
E1 ⊆ E.

Definiţia 2.4 Un subgraf al unui graf G = (V, E) este un graf H = (V1 , E1 ) unde V1 ⊂ V
iar muchiile din E1 sunt toate muchiile din E care au ambele extremităţi ı̂n mulţimea V1
(E1 = E|V1 ×V1 = {[x, y]|[x, y] ∈ E, x, y ∈ V1 }).

Se spune că graful H este indus sau generat de submulţimea de vârfuri V1 şi se notează
H = G|V1 sau H = [V1 ]G .
Iată alte câteva notaţii des ı̂ntâlnite:

- G − V1 = subgraful ce se obţine din G prin eliminarea submulţimii de vârfuri V1 (V1 ⊆


V );

- G − x = subgraful G − {x};

- < E1 >G = graful parţial al lui G generat de E1 ;

29
- G − E1 =< E \ E1 >G ;

- G − e = G − {e}, graful parţial obţinut prin eliminarea unei muchii e.

Exemplul 2.3 Graful parţial din figura 2.2 c) se obţine din graful 2.1 b) prin ştergerea
muchiilor [2, 4], [2, 5], [3, 5], [4, 5]. Subgraful din figura 2.2 b) este indus de mulţimea V1 =
{1, 3, 4, 5} din graful complet K5 (H = K5 |V1 ).

Definiţia 2.5 Gradul unui vârf este egal cu numărul muchiilor incidente cu vârful x şi se
notează cu d(x) (d(x) = |{[x, u]|[x, u] ∈ E, u ∈ V }|). Un vârf cu gradul 0 (d(x) = 0) se
numeşte vârf izolat.

Notăm δ(G) = min{dG (u)|u ∈ G} şi ∆(G) = max{dG (u)|u ∈ G}.

Fig. 2.3: Graful lui Petersen

Exemplul 2.4 Graful lui Petersen din figura 2.3 este un exemplu de graf trivalent sau cubic
(toate vârfurile grafului au acelaşi grad, 3).

Propoziţia 2.5 Pentru un graf G = (V, E), V = {x1 , x2 , . . . , xn }, |E| = m, avem următoarea
relaţie:
Xn
d(xk ) = 2m. (2.1)
k=1

Astfel ı̂n orice graf G există un număr par de vârfuri al căror grad este un număr impar.

Definiţia 2.6 Se numeşte secvenţă grafică un şir de numere naturale d1 , d2 , . . . , dn cu


proprietatea că ele reprezintă gradele vârfurilor unui graf neorientat.

Corolarul 2.6 Pentru ca o secvenţă de numere naturale d1 , d2 , . . . , dn să fie secvenţă grafică,
este necesar ca:
1. ∀k = 1, n, dk ≤ n − 1;
Pn
2. k=1 dk este un număr par.

Definiţia 2.7 Un lanţ L = [v0 , v1 , . . . , vm ] este o succesiune de vârfuri cu proprietatea că


oricare două vârfuri vecine sunt adiacente ([vi , vi+1 ] ∈ E, ∀i = 0, m − 1). Vârfurile v0 şi vm
se numesc extremităţile lanţului, iar m reprezintă lungimea lanţului.

30
Dacă vârfurile v0 , v1 , . . . , vm sunt distincte două câte două, lanţul se numeşte elementar
(vi 6= vj , ∀i, j = 0, m).

Definiţia 2.8 Un lanţ L pentru care v0 = vm se numeşte ciclu.

Definiţia 2.9 Se numeşte ciclu hamiltonian un ciclu elementar ce trece prin toate vârfurile
grafului. Un graf ce admite un ciclu hamiltonian se numeşte graf Hamilton sau graf
hamiltonian.

Definiţia 2.10 Un lanţ L ce conţine fiecare muchie exact o singură dată se numeşte lanţ
eulerian. Dacă v0 = vm şi lanţul este eulerian atunci ciclul se numeşte ciclu eulerian.
Un graf ce conţine un ciclu eulerian se numeşte graf eulerian.

Exemplul 2.7 [1, 2, 3, 1, 4] este un exemplu de lanţ ı̂n graful din figura 2.2 c). Vârfurile 1
şi 4 sunt extremităţile lanţului. Lanţul nu este elementar deoarece vârful 1 se ı̂ntâlneşte de
două ori, ı̂n schimb [1, 2, 3, 4, 1] este un ciclu elementar.
[3, 4, 5, 1, 2, 3] este un ciclu hamiltonian ı̂n graful din figura 2.2 a). [4, 5, 1, 2, 4, 3] este un
lanţ eulerian, precum şi [2, 4, 3, 2, 1, 5, 4]. În acest graf nu există nici un ciclu eulerian.

Fig. 2.4: Componente conexe

Definiţia 2.11 Un graf se numeşte conex dacă pentru orice pereche de vârfuri x şi y există
un lanţ de la x la y (∀x, y ∈ V, ∃x y).
Se numeşte componentă conexă un subgraf conex maximal, adică un subgraf conex ı̂n
care nici un vârf din subgraf nu este adiacent cu unul din afara lui prin intermediul unei
muchii aparţinând grafului iniţial.

Definiţia 2.12 O muchie e ∈ E se numeşte muchie critică dacă prin eliminarea acesteia
o componentă conexă se ı̂mparte ı̂n două sau mai multe componente conexe.

Exemplul 2.8 Graful din figura 2.4 are două componente conexe: {1, 2, 3, 4, 5, 6} şi {7, 8, 9}.
Muchia [2, 5] este muchie critică.

Definiţia 2.13 Un graf planar este un graf ce poate fi reprezentat ı̂n plan astfel ı̂ncât
muchiile sale să nu se intersecteze două câte două.

Definiţia 2.14 Un graf G = (V, E) se numeşte graf bipartit dacă există o partiţie a
mulţimii nodurilor {V1 , V2 } (V = V1 ∪V2 , V1 ∩V2 = ∅, V1 , V2 6= ∅) astfel ı̂ncât orice muchie din
E va avea o extremitate ı̂n mulţimea V1 şi cealaltă extremitate ı̂n mulţimea V2 (∀[x, y] ∈ E
avem x ∈ V1 şi y ∈ V2 ).

31
Propoziţia 2.9 Un graf este bipartit dacă şi numai dacă nu conţine cicluri de lungime
impară.

Definiţia 2.15 Un graf bipartit este complet dacă ∀x ∈ V1 , ∀y ∈ V2 , ∃[x, y] ∈ E (G =


(V, E), V = V1 ∪ V2 , V1 ∩ V2 = ∅). Dacă |V1 | = p, |V2 | = q atunci graful bipartit complet se
notează Kp,q .

Fig. 2.5: Un exemplu de graf bipartit şi graf bipartit complet.

Exemplul 2.10 În figura 2.5 a) este ilustrat un graf bipartit (V1 = {1, 2, 3}, V2 = {4, 5}),
iar ı̂n cazul b) avem un graf bipartit complet, K2,3 .

Definiţia 2.16 Se numeşte izomorfism de la graful G la graful G′ o funcţie bijectivă ϕ :


V (G) → V (G′ ) astfel ı̂ncât [u, v] ∈ E(G) ⇔ ϕ([u, v]) ∈ E(G′ ).
Dacă există un izomorfism de la graful G la graful G′ atunci se spune că G este izomorf
cu G′ şi notăm acest lucru astfel: G ≅ G′ .

Definiţia 2.17 Un graf etichetat este un graf ı̂n care fiecare muchie şi vârf poate avea
asociată o etichetă.

Vom prezenta ı̂n continuare câteva operaţii dintre cele mai ı̂ntâlnite ce se pot efectua
asupra unui graf oarecare:

• fiind dat un nod se cere lista vecinilor săi. Această operaţie este cea mai utilizată pentru
o serie ı̂ntreagă de algoritmi pe grafuri;

• fiind dată o pereche de noduri {u, v} se cere să se determine dacă aceasta constituie o
muchie sau un arc al grafului;

• adăugarea sau ştergerea unui nod sau a unei muchii/arc;

• translatarea dintr–un mod de reprezentare ı̂n altul. De foarte multe ori modul de
reprezentare sub forma căruia au fost introduse datele, diferă de modul de reprezentare
optim recomandat pentru un anumit algoritm. În această situaţie este indicată trans-
formarea primului mod de reprezentare ı̂n modul de reprezentare optim;

• fiind dată o muchie sau un nod se cere o informaţie asociată acestui element: de exemplu
lungimea muchiei sau distanţa de la sursă la nodul specificat.

32
2.2 Operaţii pe grafuri
1. Complementarul grafului G = (V, E) se defineşte astfel: este graful Gc = (V c , E c ), unde
V c = V şi E c = {[x, y]|x, y ∈ V, [x, y] ∈
/ E}. Altfel spus E c = (V × V ) \ E.

Fig. 2.6: Un exemplu de graf complementar altui graf.

În figura 2.6 b) este reprezentat graful complementar Gc al grafului G = (V, E), din
figura 2.6 a) (V = {1, 2, 3, 4, 5}, E = {[1, 2], [1, 5], [2, 3], [3, 4], [4, 5]}). Conform definiţiei
Gc = (V c , E c ) unde V c = V şi E c = {[1, 3], [1, 4], [2, 4], [2, 5], [3, 5]}.

2. Graful obţinut din G = (V, E) prin inserţia unui vârf v ∈ / V pe o muchie [x, y] ∈ E
este graful Gi = (V i , E i ) unde V i = V ∪ {v}, E i = (E \ {[x, y]}) ∪ {[x, v], [v, y]}.
Considerând graful din figura 2.6 a), prin inserţia vârfului 6 pe muchia [1, 5] se obţine
graful din figura 2.7 a).

3. Graful obţinut din G = (V, E) prin contracţia unei muchii u = [x, y] la un vârf t este
graful Gct = (V ct , E ct ), unde V ct = (V \ {x, y}) ∪ {t}.

Fig. 2.7: a) Graful obţinut prin inserarea vârfului 6 ı̂n graful din figura 2.6 a). b) Graful obţinut prin
contracţia muchiei [3, 4] ı̂n graful din figura 2.6 a).

În figura 2.7 b) este reprezentat graful obţinut prin contracţia muchiei [3, 4] din graful
G = (V, E) (vezi figura 2.6 a)).

4. Se numeşte graf reprezentativ al muchiilor unui graf G = (V, E), graful GR = (VR , ER )
ı̂n care |VR | = |E| şi ER = {[e, f ]|e, f ∈ E adiacente} (vezi figura 2.8).

5. Definim graful total al unui graf G = (V, E) ca fiind graful GT = (VT , ET ) unde VT =
V ∪ E şi ET = {[u, v]|u, v ∈ V ∪ E iar u şi v sunt adiacente sau incidente ı̂n G}.

33
Fig. 2.8: Figura b) prezintă graful reprezentativ al muchiilor grafului din figura a).

Fig. 2.9: a) Un exemplu de graf neorientat. b) Graful total al grafului din figura a).

Graful total GT = (VT , ET ), unde VT = {1, 2, 3, 4, v1, v2 , v3 , v4 }, şi E = {[1, 2], [1, 4],
[2, 3], [3, 4], [v1, v2 ], [v1 , v4 ], [v2 , v3 ], [v3 , v4 ], [v1 , 1], [v1 , 2], [v2 , 2], [v2 , 3], [v3 , 3], [v3 , 4], [v4 , 4], [v4 , 1]},
corespunde grafului G = (V, E), V = {1, 2, 3, 4}, E = {[1, 2], [1, 4], [2, 3], [3, 4]} (vezi
figura 2.9).

6. Reuniunea şi intersecţia a două grafuri se definesc astfel:

• dacă V1 = V2 atunci: G1 ∪ G2 = (V1 , E1 ∪ E2 ), G1 ∩ G2 = (V1 , E1 ∩ E2 );


• dacă V1 ∩ V2 6= ∅ atunci: G1 ∪ G2 = (V1 ∪ V2 , E1 ∪ E2 ), G1 ∩ G2 = (V1 ∩ V2 , E1 ∩ E2 );
• dacă V1 ∩ V2 = ∅ atunci G1 ∪ G2 se numeşte reuniune disjunctă a grafurilor G1 şi
G2 , iar G1 ∩ G2 este graful vid.

7. Definim suma grafurilor G1 şi G2 ca fiind graful complementar al reuniunii comple-


mentarelor celor două grafuri: G1 ⊕ G2 = (Gc1 ∪ Gc2 )c .
Pentru grafurile complete Kp şi Kq avem: Kp ⊕ Kq = (Kpc ∪ Kqc )c = Kp+q . Un alt
exemplu de sumă a două grafuri poate fi urmărit ı̂n figura 2.10.

8. Se numeşte produsul cartezian al grafurilor G1 = (V1 , E1 ) şi G2 = (V2 , E2 ), graful


G1 × G2 = (V × , E × ), unde V × = V1 × V2 şi E × = {[(u1 , u2), (v1 , v2 )]|u1 , v1 ∈ V1 , u2 , v2 ∈
V2 ; u1 = v1 şi [u2 , v2 ] ∈ E2 sau u2 = v2 şi [u1 , v1 ] ∈ E1 }.

34
Fig. 2.10: a) Două grafuri neorientate, G1 şi G2 . b) Grafurile complementare Gc1 şi Gc2 . c) Reuniunea
grafurilor Gc1 şi Gc2 . d) Graful complementar al grafului reuniune (Gc1 ∪ Gc2 )c .

Fig. 2.11: Graful produs cartezian a două grafuri

Fie G1 = (V1 , E1 ) unde V1 = {u1 , v1 } şi E1 = {[u1 , v1 ]}, şi G2 = (V2 , E2 ), unde V2 =
{u2, v2 , w2 } şi E2 = {[u2 , v2 ], [v2 , w2]}, două grafuri neorientate. Atunci graful produs
cartezian al grafurilor G1 şi G2 este G1 × G2 = (V × , E × ) unde:
V × = {(u1 , u2 ), (u1, v2 ), (u1, w2 ), (v1 , u2 ), (v1 , v2 ), (v1 , w2 )}, şi
E × = {[(u1 , u2), (v1 , u2)], [(u1 , u2 ), (u1, v2 )], [(u1 , v2 ), (v1 , v2 )], [(u1 , v2 ), (u1, w2 )],
[(u1 , w2), (v1 , w2 )], [(v1 , u2 ), (v1 , v2 )], [(v1 , v2 ), (v1 , w2 )]} (vezi figura 2.11).

9. Se numeşte rădăcina pătrată a grafului G, un graf neorientat H cu proprietatea că


H 2 = G (H 2 = H × H).

10. Operaţia de compunere a două grafuri neorientate G1 = (V1 , E1 ) şi G2 = (V2 , E2 ),


notată cu G1 [G2 ], se realizează astfel: vârful (x1 , y1 ) este adiacent cu vârful (x2 , y2) ı̂n
graful rezultat dacă vârful x1 este adiacent cu vârful x2 ı̂n graful G1 sau (x1 = x2 şi
vârful y1 este adiacent cu vârful y2 ı̂n graful G2 ) (vezi figura 2.12).

35
Fig. 2.12: Graful rezultat ı̂n urma compunerii grafurilor G1 şi G2 din figura 2.11

2.3 Moduri de reprezentare

Fig. 2.13: Un exemplu de graf neorientat

Pentru o mulţime de noduri V vom presupune un mod de ordonare a nodurilor grafului


(primul nod, al doilea nod, etc.), deoarece reprezentarea unui graf depinde de această or-
donare. Astfel, numerotăm ı̂n mod arbitrar vârfurile grafului cu 1, 2, . . . , |V |.
1. Matricea de adiacenţă - este o matrice pătratică A ∈ Mn×n (N), n = |V |, unde ai,j = 1
dacă există o muchie ı̂ntre nodurile numerotate i şi j (ı̂ntre xi şi xj ), sau ai,j = 0 dacă
nu există
( o muchie ı̂ntre aceste noduri.
1 , dacă [xi , xj ] ∈ E
ai,j =
0 , dacă [xi , xj ] ∈
/ E.

Observaţia 2.11 Pentru un graf neorientat această matrice este simetrică (ai,j = aj,i).

Exemplul 2.12 Matricea de adiacenţă A corespunzătoare grafului din figura 2.13 este:
 
0 1 1 1 0 0 0 0
1 0 0 0 1 0 0 0
 
1 0 0 0 1 0 0 0
 
1 0 0 0 0 1 0 0
A=
0

 1 1 0 0 0 1 0

0 0 0 1 0 0 1 1
 
0 0 0 0 1 1 0 0
0 0 0 0 0 1 0 0

36
Fig. 2.14: Un exemplu de graf neorientat ponderat

Matricea de adiacenţă are un număr de n2 elemente, iar elementele neredundante sunt


ı̂n număr de n(n−1)
2
. Prin urmare complexitatea-spaţiu a matricii de adiacenţă este
2
O(n ).
2. Matricea costurilor - reprezintă o variantă a matricei de adiacenţă ce se obţine ı̂n mod
natural luându-se ı̂n considerare graful ponderat: fiecărei muchii i se ataşează un cost
d (d ∈ R), iar valorile matricii C (C ∈ Mn×n (R)) se definesc astfel:

0
 , dacă xi = xj
ci,j = +∞ , dacă [xi , xj ] ∈
/E (2.2)


d>0 , dacă [xi , xj ] ∈ E
sau 
0
 , dacă xi = xj
ci,j = −∞ , dacă [xi , xj ] ∈
/E (2.3)


d>0 , dacă [xi , xj ] ∈ E.

Exemplul 2.13 Matricea costurilor corespunzătoare grafului din figura 2.14 are următoarele
valori:
 
0 35 7 20 ∞ ∞ ∞ ∞
35
 0 ∞ ∞ 50 ∞ ∞ ∞
7
 ∞ 0 ∞ 22 ∞ ∞ ∞
20 ∞ ∞ 0 ∞ 15 ∞ ∞
C=
∞

 50 22 ∞ 0 ∞ 44 ∞
∞ ∞ ∞ 15 ∞ 0 10 27
 
∞ ∞ ∞ ∞ 44 10 0 ∞
∞ ∞ ∞ ∞ ∞ 27 ∞ 0

Observaţia 2.14 Notaţia (2.2) este folosită ı̂n aplicaţii ı̂n care se operează cu drumuri
de lungime minimă iar notaţia (2.3) este folosită ı̂n aplicaţii ı̂n care se operează cu
drumuri de lungime maximă ı̂ntre două noduri.

Matricea costurilor va ocupa un spaţiu de memorie mai mare decât cel ocupat de
matricea de adiacenţă: pentru reprezentarea unui element al matricii de adiacenţă este
suficient un bit, pe când pentru reprezentarea unui element al matricii costurilor se
foloseşte un byte, un ı̂ntreg (lung) sau un număr real, ı̂n funcţie de valoarea maximă pe
care o poate lua d.

37
Observaţia 2.15 Matricea costurilor pentru un graf neorientat este simetrică.

Prin urmare pentru reprezentarea unei matrici de adiacenţă de dimensiuni n × n sunt


necesari n · ⌈ n8 ⌉ octeţi, pe când pentru reprezentarea unei matrici de costuri cu elemente
numere ı̂ntregi sunt necesari 2 · n2 octeţi, dacă presupunem că un număr ı̂ntreg fără
semn este păstrat pe 2 octeţi.

3. Liste de adiacenţă - pentru fiecare nod se reţine lista tuturor nodurilor sale adiacente.
Mai exact, unui nod xk se ataşează lista tuturor vecinilor săi.
Această reprezentare se pretează mai bine pentru grafuri cu un număr mare de noduri şi
un număr mic de muchii (graf rar ). În cazul unui graf neorientat, numărul elementelor
din listele de adiacenţă este 2 · |E|, deoarece o muchie [u, v] va fi prezentă de două ori,
atât ı̂n lista de adiacenţă a nodului u cât şi ı̂n lista de adiacenţă a nodului v.

Exemplul 2.16 Listele de adiacenţă pentru graful din figura 2.13 sunt:
1: (2, 3, 4) 5: (2, 3, 7)
2: (1, 5) 6: (4, 7, 8)
3: (1, 5) 7: (5, 6)
4: (1, 6) 8: (6)

Aceste liste de adiacenţă (sau liste de vecini) pot fi reprezentate, ca structuri de date,
prin intermediul tablourilor sau prin intermediul listelor simplu sau dublu ı̂nlănţuite.
Pentru reprezentarea ce utilizează tablouri, se vor defini două tablouri notate Cap şi
List (Cap ∈ M1×n (N), List ∈ M2×2m (N), n = |V |, m = |E|) unde:

0 , dacă nodul respectiv nu are vecini

Capk = j , unde j indică numărul coloanei din matricea List unde se află memorat


primul vecin al vârfului xk .

List1,j = indicele unui vârf ce se află ı̂n lista de vecini a vârfului xk .




 0 , dacă vârful respectiv este ultimul (această valoare are aceeaşi

 semnificaţie ca şi valoarea NIL sau NULL utilizată ı̂n lucrul cu pointeri)
List2,j =


 i , unde i reprezintă numărul coloanei unde se află memorat următorul

nod (vecin) din lista de vecini a nodului xk .

Exemplul 2.17 Pentru exemplul din figura 2.13 avem următoarea configuraţie pentru
cei doi vectori consideraţi, Cap şi List:
Nod 1 2 3 4 5 6 7 8
Cap 1 4 6 8 10 13 16 18
 
2 3 4 1 5 1 5 1 6 2 3 7 4 7 8 5 6 6
List =
2 3 0 5 0 7 0 9 0 11 12 0 14 15 0 17 0 0

De exemplu, ı̂n limbajul C putem defini următoarele structuri de date, având rolul de a
ne ajuta la menţinerea ı̂n memorie a listelor de adiacenţă ale unui graf oarecare folosind
liste liniare simplu ı̂nlănţuite (a se vedea figura 2.15):

38
1 2 3 4 NULL

2 1 5 NULL
3 1 5 NULL
4 1 6 NULL
5 2 3 7 NULL
6 4 7 8 NULL
7 5 6 NULL
8 6 NULL

Fig. 2.15: Liste de adiacenţă

typedef str u ct nod {


i nt nodeIndex ;
str u ct nod ∗ next ;
}NOD;

#define MAXN 100


NOD∗ v e c i n i [MAXN] ;

NOD* vecini[MAXN] este un array (tablou) de pointeri; un element al tabloului vecini[k]


păstrează capul listei de vecini ai nodului de indice k.
struct nod *next reprezintă un pointer către următoarea ı̂nregistrare de tip NOD,
ı̂nregistrare ce conţine informaţii referitoare la următorul vecin.
nodeIndex păstrează indicele (eticheta) unui nod.
Cantitatea de memorie necesară pentru păstrarea listelor de adiacenţă se poate aproxi-
ma astfel:

• reprezentarea folosind matricile Cap şi List - putem presupune că n, m ≤ 65535
adică sunt suficienţi 2 octeţi pentru reprezentarea acestor numere ı̂n memorie
(216 − 1 = 65535). În această situaţie, cantitatea de memorie necesară pentru
matricea Cap este 2n octeţi, iar pentru matricea List este 8m octeţi. Astfel
complexitatea-spaţiu este O(n + m).
• reprezentarea folosind pointeri - presupunem că folosim 2 octeţi pentru informaţia
utilă şi 2 octeţi pentru informaţia de legătură. Numărul de octeţi folosiţi pentru
reprezentarea unui pointer ı̂n limbajul C poate depinde de modelul de memorie
ales (de exemplu small, huge etc.) sau de dimensiunea instrucţiunilor (pe 16, 32
sau 64 de biţi).
Să presupunem că se utilizează 2n octeţi pentru vectorul ce conţine adresele primu-
lui element (capul) al fiecărei liste de vecini, şi 8m octeţi pentru ı̂nregistrările
listelor de vecini.
Din această cantitate de memorie, 4m reprezintă informaţie utilă (indicii vecinilor),
iar 2n + 4m informaţie auxiliară (pointeri).

4. Lista de muchii - ı̂ntr-o structură de date se păstrează lista tuturor muchiilor grafului,
practic, pentru fiecare muchie fiind memorate valorile indicilor nodurilor-extremităţi
ale acesteia. Aceasta constituie cea mai simplă modalitate de reprezentare a unui graf.

39
Operaţia de adăugare a unui nod sau a unei muchii se realizează ı̂ntr-un timp constant,
alte operaţii fiind mai costisitoare: de exemplu, determinarea listei de vecini a unui nod
necesită un timp Ω(m).

Exemplul 2.18 Pentru păstrarea ı̂n memorie se poate folosi o matrice M cu două linii
şi |E| coloane (M ∈ M2×|E|(N)), unde:
M1,k - indicele vârfului ce reprezintă prima extremitate a muchiei k;
M2,k - indicele vârfului ce reprezintă cea de-a doua extremitate a muchiei k.
 
1 1 1 2 3 4 5 6 6
M=
2 3 4 5 5 6 7 7 8

Complexitatea timp pentru diverse operaţii efectuate asupra fiecăruia dintre structurile
de date utilizate de modurile de reprezentare amintite se poate sintetiza astfel:

Matricea de adiacenţă Liste de vecini Lista de muchii


Gradul unui nod xi O(n) O(d(xi )) O(m)
(xi , xj ) ∈ E? O(1) O(d(xi )) O(m)
Următorul vecin al lui xi O(n) O(d(xi )) O(m)

2.4 Parcurgerea grafurilor


Pentru un graf dat este important să se stabilească o modalitate sistematică de vizitare
a tuturor vârfurilor grafului, parcurgere ce se realizează ı̂n scopul prelucrării informaţiei
conţinute de către acestea.

2.4.1 Parcurgerea ı̂n lăţime (BFS-Breadth First Search)


Metoda de parcurgere ı̂n lăţime vizitează nodurile grafului ı̂n felul următor (a se vedea algo-
ritmul 16):

• se vizitează mai ı̂ntâi vârful de pornire (să ı̂l notăm k);

• urmează, ı̂n ordine, toţi vecinii ı̂ncă nevizitaţi ai nodului k;

• se continuă cu vecinii ı̂ncă nevizitaţi ai acestora, ş.a.m.d.

Pentru graful considerat ı̂n figura 2.13 ordinea de parcurgere este următoarea: 1, 2, 3, 4,
5, 6, 7, 8. Dacă considerăm muchiile folosite ı̂n timpul parcurgerilor (muchiile prin intermediul
cărora s-a ı̂naintat ı̂n graf) se obţine un arbore/pădure de parcurgere/vizitare/acoperire. În
figura 2.16 este reprezentat arborele de acoperire ı̂n lăţime rezultat ı̂n urma parcurgerii gra-
fului din exemplu.
Algoritmul de parcurgere utilizează o structură de date de tip coadă ı̂n care vor fi memo-
rate nodurile vizitate, dar care nu au fost ı̂ncă prelucrate (nu au fost cercetaţi vecinii lor).
Reamintim că numărul de noduri din mulţimea V este n. Vectorul vizitat păstrează situaţia
vizitării nodurilor
( grafului G, astfel:
1 , dacă nodul k a fost vizitat
vizitatk =
0 , dacă nodul k nu a fost vizitat.
Implementarea ı̂n limbajul C a algoritmului 16 este următoarea:

40
Fig. 2.16: Arbore de acoperire ı̂n lăţime

Algoritm 16 Algoritm de vizitare ı̂n lăţime


1: procedure BFS(k, n, V ecin)

k
 - nodul de la care ı̂ncepe vizitarea
Input: n - numărul de noduri din graf


V ecin - matricea de adiacenţă a grafului
2: for i ← 1, n do
3: vizitati ← 0
4: end for
5: vizitatk ← 1 ⊲ marcarea nodului curent ca fiind vizitat
6: Q⇐k ⊲ inserarea nodului curent k ı̂n coadă
7: call V izitare(k) ⊲ vizitarea nodului curent
8: while (Q 6= ∅) do
9: Q⇒k ⊲ extragere nod curent din coadă
10: for i ← 1, n do
11: if ((vizitati = 0) ∧ (vecink,i = 1)) then
12: vizitati ← 1 ⊲ marcarea nodului i ca fiind vizitat
13: Q⇐i ⊲ inserarea nodului i ı̂n coadă
14: call V izitare(i) ⊲ vizitarea nodului i
15: end if
16: end for
17: end while
18: end procedure

Listing 2.1: parcurgerebf.c


#include <s t d i o . h>
#include <mem. h>

#define MAXN 100


#define TRUE 1
#define FALSE 0

i nt n ; // Numarul de n o d u r i d i n g r a f
char v e c i n [MAXN] [MAXN] ; // M a t r i c e a de a d i a c e n t a
i nt coada [MAXN] ; // S t r u c t u r a de da te c e p a s t r e a z a e l e m e n t e l e c o z i i
i nt f i r s t ; // I n d i c e l e p r i m u l u i element d i n coada
i nt l a s t ; // I n d i c e l e u l t i m u l u i element d i n coada

41
i nt v i d a ; // P a s t r e a z a s t a r e a c o z i i

/∗ ∗
∗ Initializarea cozii circulare .
∗/
void i n i t Q u e u e ( void ) {
v i d a = TRUE;
f i r s t = 0;
l a s t = MAXN;
}

/∗ ∗
∗ I n t o a r c e pentr u p o z i t i a k , ur ma to a r ea p o z i t i e d i n coada .
∗/
i nt next ( i nt k ) {
return ( k + 1 ) % MAXN;
}

/∗ ∗
∗ I n s e r e a z a e l e m e n t u l a c a r u i v a l o a r e e s t e p a s t r a t a i n v a r i a b i l a v i n coada .
∗/
void enQueue ( i nt v ) {
l a s t = next ( l a s t ) ;
coada [ l a s t ] = v ;
i f ( vida )
v i d a = FALSE ;
}

/∗ ∗
∗ E xtr a g e un element d i n coada .
∗/
i nt deQueue ( void ) {
i nt v = coada [ f i r s t ] ;
f i r s t = next ( f i r s t ) ;
i f ( f i r s t == next ( l a s t ) )
v i d a = TRUE;
return v ;
}

/∗ ∗
∗ P a r cur g e i n l a t i m e g r a f u l po r nind de l a no dul de s t a r t k .
∗/
void b f s ( i nt k ) {
i nt i ;
char v i z i t a t [MAXN] ;

memset ( v i z i t a t , 0 , s i z e o f ( v i z i t a t ) ) ;
vizitat [ k] = 1;
enQueue ( k ) ;
p r i n t f ( ”%d ” , k ) ;
while ( ! v i d a ) {
k = deQueue ( ) ;
for ( i = 0 ; i < n ; i ++)
i f ( ( v i z i t a t [ i ] == 0 ) && ( v e c i n [ k ] [ i ] == 1 ) ) {
vizitat [ i ] = 1;
enQueue ( i ) ;
p r i n t f ( ”%d ” , i ) ;
}
}

42
}

/∗ ∗
∗ Se c i t e s c numarul de n o d u r i precum s i m a t r i c e a de a d i a c e n t a .
∗/
void r e a d I n p u t ( void ) {
i nt i , j ;

p r i n t f ( ”n = ” ) ; s c a n f ( ”%d” ,&n ) ;
for ( i = 0 ; i < n − 1 ; i ++)
for ( j = i + 1 ; j < n ; j ++) {
p r i n t f ( ” a[%d,%d ] = ” , i , j ) ;
s c a n f ( ”%d” , &v e c i n [ i ] [ j ] ) ;
vecin [ j ] [ i ] = vecin [ i ] [ j ] ;
}
}

void main ( void ) {


readInput ( ) ;
initQueue ( ) ;
bfs (0 );
}

• În exemplul anterior, funcţia de citire readInput() este una foarte simplă: se citeşte
mai ı̂ntâi numărul de noduri al grafului, iar apoi, se citesc elementele matricei de
adiacenţă a grafului. Variabilele n şi vecin sunt declarate drept variabile globale.
Deoarece matricea vecin este simetrică, pentru a reduce numărul de citiri, se vor
solicita numai valorile aflate deasupra diagonalei principale:
void r e a d I n p u t ( void ) {
i nt i , j ;

p r i n t f ( ”n = ” ) ; s c a n f ( ”%d” ,&n ) ;
for ( i = 0 ; i < n − 1 ; i ++)
for ( j = i + 1 ; j < n ; j ++) {
p r i n t f ( ” a[%d,%d ] = ” , i , j ) ;
s c a n f ( ”%d” , &v e c i n [ i ] [ j ] ) ;
vecin [ j ] [ i ] = vecin [ i ] [ j ] ;
}
}

• Funcţia memset() este o funcţie de bibliotecă a cărei declaraţie poate fi găsită ı̂n header-
ele mem.h şi string.h la Borland C sau memory.h şi string.h la Visual C++ 2010 1.
Funcţia respectivă prezintă următoarea semnătură2
void *memset(void *dest, int c, size t count);
şi are rolul de a iniţializa cu valoarea c primii count octeţi din zona de memorie ce
ı̂ncepe de la adresa identificată de pointerul dest.

• vida este o variabilă globală ce ia valoarea true atunci când coada nu conţine nici un
element, sau valoarea false ı̂n caz contrar.
1
http://msdn.microsoft.com/en-us/library/1fdeehz6.aspx
2
http://en.wikibooks.org/wiki/C_Programming/Strings#The_memset_function

43
• Structura de date abstractă de tip coadă este implementată static sub forma unei cozi
circulare.

• Vizitarea ı̂ncepe de la nodul 0 (bfs(0)).

• Subrutina vizitare(), ı̂n principiu, afişează valoarea etichetei nodului trimis ca argu-
ment, ı̂nsă poate efectua şi alte prelucrări necesare asupra nodului curent, ı̂n funcţie de
cerinţele specifice ale algoritmului.

Algoritmul BFS este utilizat de către metoda Branch-and-Bound ca metodă de explorare


a spaţiului soluţiilor, cât şi ı̂n cadrul problemelor de determinare a distanţei minime de la
un vârf la toate celelalte vârfuri, ı̂n cazul ı̂n care lungimea unui drum dintre două noduri se
consideră ca fiind egală cu numărul de noduri intermediare ale acestuia.

2.4.2 Parcurgerea D (D - Depth)


Metoda de parcurgere D este asemănătoare cu metoda de parcurgere ı̂n lăţime (BFS):
• la ı̂nceput se vizitează vârful de pornire (notat cu k);

• urmează, ı̂n ordine, toţi vecinii ı̂ncă nevizitaţi ai nodului de pornire;

• la pasul următor se consideră ultimul nod vizitat v şi se ı̂ncearcă vizitarea vecinilor ı̂ncă
nevizitaţi ai acestuia. Dacă nodul v nu mai are vecini nevizitaţi, se continuă procesul
de parcurgere cu nodul ce a fost vizitat exact ı̂naintea nodului v, etc.
Metoda de parcurgere D este o combinaţie ı̂ntre metoda de parcurgere ı̂n lăţime (a se
vedea secţiunea 2.4.1) şi metoda de parcurgere ı̂n adâncime (a se vedea secţiunea 2.4.3).
Pentru un nod, se vizitează toţi vecinii ı̂ncă nevizitaţi ai acestuia, ı̂nsă spre deosebire de
metoda de parcurgere ı̂n lăţime unde ordinea ı̂n care au fost vizitate nodurile se păstrează şi
la parcurgere, la metoda de parcurgere D se prelucrează mereu ultimul nod vizitat.
Pentru aceasta, ı̂n algoritmul BFS se ı̂nlocuieşte structura de date de tip coadă, cu o
structură de date de tip stivă şi se obţine algoritmul D (a se vedea algoritmul 17).

Fig. 2.17: Arbore de acoperire pentru parcurgerea D

În figura 2.17 poate fi urmărit arborele de acoperire pentru parcurgerea D a grafului G
prezentat ı̂n figura 2.13.

44
Algoritm 17 Algoritm de vizitare D
1: procedure D(k, n, V ecin)
2: for i ← 1, n do
3: vizitati ← 0
4: end for
5: vizitatk ← 1 ⊲ marcarea nodului curent ca fiind vizitat
6: S⇐k ⊲ inserarea nodului curent k ı̂n stivă
7: call V izitare(k) ⊲ vizitarea nodului curent
8: while (S 6= ∅) do
9: S⇒k ⊲ extragerea nodului curent din stivă
10: for j ← 1, n do
11: if ((vizitatj = 0) ∧ (vecink,j = 1)) then ⊲ se cauta un vecin nevizitat
12: vizitatj ← 1 ⊲ marcarea nodului j ca fiind vizitat
13: S⇐j ⊲ inserarea nodului j in stivă
14: call V izitare(j) ⊲ vizitarea nodului j
15: end if
16: end for
17: end while
18: end procedure

2.4.3 Parcurgerea ı̂n adâncime (DFS-Depth First Search)


În cadrul acestei metode se va merge ı̂n adâncime ori de câte ori este posibil: prelucrarea
unui vârf constă ı̂n prelucrarea primului dintre vecinii săi ı̂ncă nevizitaţi.

• se vizitează vârful de pornire (notat cu k);

• urmează, primul vecin ı̂ncă nevizitat al acestuia;

• se caută primul vecin, ı̂ncă nevizitat, al primului vecin nevizitat al nodului de start,
ş.a.m.d.;

• se merge ı̂n adâncime până când se ajunge la un vârf ce nu are vecini, sau pentru care
toţi vecinii săi au fost vizitaţi. În acest caz, se revine ı̂n nodul său părinte (nodul din
care a fost vizitat nodul curent), şi se continuă algoritmul, cu următorul vecin ı̂ncă
nevizitat al nodului curent.

Pentru graful considerat (vezi figura 2.13), ı̂n urma parcurgerii ı̂n adâncime, vom obţine
nodurile ı̂n ordinea următoare: 1, 2, 5, 3, 7, 6, 4, 8 (a se vedea figura 2.18). Dacă ne propunem
să vizităm exact o singură dată toate nodurile unui graf, aplicând algoritmul DFS de câte ori
este nevoie, şi selectăm numai muchiile utilizate ı̂n timpul explorării, rezultă o pădure de
arbori. Fiecare dintre aceşti arbori constituie un arbore de acoperire ı̂n adâncime.
În urma parcurgerii ı̂n adâncime, muchiile unui graf pot fi clasificate ı̂n următoarele ca-
tegorii:

1. muchie a arborelui de acoperire - muchia [u, v] este o muchie a arborelui de acoperire


dacă dfs(u) apelează direct dfs(v) sau invers;

2. muchie de ı̂ntoarcere - muchia [u, v] este o muchie de ı̂ntoarcere dacă dfs(u) apelează
indirect dfs(v) (∃x ∈ V a.ı̂. df s(u) df s(x) df s(v)) sau invers, dfs(v) apelează
indirect dfs(u).

45
Fig. 2.18: Arbore de acoperire ı̂n adâncime

Exemplul 2.19 Pentru graful din figura 2.13 avem:

• muchiile [1, 2], [2, 5], [3, 5], [4, 6], [5, 7], [6, 7], [6, 8] sunt muchii ale arborelui de acoperire;

• muchiile [1, 3], [1, 4] sunt muchii de ı̂ntoarcere;

Algoritm 18 Algoritm de vizitare ı̂n adâncime (varianta recursivă)


1: procedure DFS(k, n, V ecin)

k
 - nodul curent ce se vizitează
Input: n - numărul de noduri din graf


V ecin - matricea de adiacenţă a grafului
2: vizitatk ← 1 ⊲ marcarea nodului curent ca fiind vizitat
3: call V izitare(k) ⊲ vizitarea nodului curent
4: for i ← 1, n do
5: if ((vizitati = 0) ∧ (vecink,i = 1)) then
6: call DF S(i, n, vecin) ⊲ apelul recursiv al subrutinei DF S pentru nodul i
7: end if
8: end for
9: end procedure

Implementarea algoritmului se poate realiza fie ı̂n variantă recursivă (a se vedea algoritmul
18), fie ı̂n variantă nerecursivă (a se vedea algoritmul 19). În mod asemănător ca şi la
algoritmul de parcurgere ı̂n lăţime, vectorul vizitat gestionează situaţia vizitării nodurilor
grafului G:(
1 , dacă nodul k a fost vizitat
vizitatk =
0 , ı̂n caz contrar.
Subrutina DFS (algoritmul 18) ı̂ncepe cu marcarea nodului curent ca fiind vizitat (vizitatk ←
1) (linia 2) şi apelul procedurii Vizitare() (call Vizitare(k)) (linia 3). Se caută apoi
primul vecin nevizitat i al vârfului curent k (nod ce verifică condiţia (vizitati = 0)∧(vecink,i =
1)) şi se reia secvenţa de operaţii pentru nodul i cu această proprietate, printr-un apel recursiv
(call DFS(i)) (linia 6).

46
Enunţul repetitiv for i ← 1, n (linia 4) poate fi optimizat astfel ı̂ncât să nu se mai
verifice toate vârfurile grafului, ci numai nodurile adiacente cu nodul curent: ı̂n această
situaţie reprezentarea cu liste de adiacenţă este optimă din punct de vedere al numărului de
operaţii efectuate, fiind preferată reprezentării prin matricea de adiacenţă.

Algoritm 19 Algoritm de vizitare ı̂n adâncime (varianta nerecursivă)


1: procedure DFS Nerecursiv(k, n, V ecin)

k
 - nodul de la care se porneşte vizitarea
Input: n - numărul de noduri din graf


V ecin - matricea de adiacenţă a grafului
2: for i ← 1, n do
3: vizitati ← 0
4: end for
5: vizitatk ← 1 ⊲ marcarea nodului curent ca fiind vizitat
6: call V izitare(k) ⊲ vizitarea nodului curent
7: S⇐k ⊲ inserarea nodului curent k ı̂n stivă
8: f ound ← f alse
9: while (S 6= ∅) do ⊲ cât timp stiva nu este vidă
10: if (¬f ound) then
11: S⇒k ⊲ extragerea nodului curent din stivă
12: end if
13: i←1
14: f ound ← f alse
15: while ((i ≤ n) ∧ (f ound = f alse)) do
16: if ((vizitati = 0) ∧ (vecink,i = 1)) then
17: vizitati ← 1 ⊲ marcarea nodului i ca fiind vizitat
18: S⇐k ⊲ inserarea nodului curent k ı̂n stivă
19: call V izitare(i) ⊲ vizitarea nodului i
20: k←i
21: f ound ← true
22: else
23: i←i+1
24: end if
25: end while
26: end while
27: end procedure

Algoritmul 19 utilizează o structură de date de tip stivă S pentru a păstra tot timpul nodul
grafului din care s-a ajuns la nodul curent (tatăl nodului curent din arborele de acoperire ı̂n
adâncime). Secvenţa de instrucţiuni 19.13-19.25 nu este optimă deoarece, de fiecare dată când
se revine la un nod părinte, se verifică ı̂ntreaga mulţime de noduri V pentru a identifica un
vecin nevizitat al nodului curent. În vederea reducerii numărului de verificări, se poate utiliza
reprezentarea prin liste de adiacenţă, şi se salvează pe stivă, pe lângă valoarea nodului curent
k, valoarea vecinului nevizitat găsit al acestuia (să ı̂l notăm cu u), astfel ı̂ncât, la revenire,
să se continue căutarea următorului vecin nevizitat al nodului curent k, pornind de la acest
nod u. Dacă nu mai există nici un vecin nevizitat al vârfului k (linia 10), atunci se revine la
părintele nodului curent, păstrat pe stivă. Algoritmul se opreşte ı̂n cazul ı̂n care stiva este
vidă (atunci când nodul curent este rădăcina arborelui de vizitare ı̂n adâncime).
Am ales ı̂n vederea implementării, varianta recursivă pentru uşurinţa de programare şi
eleganţa ei. Prezentăm ı̂n continuare această implementare a algoritmului 18 ı̂n limbajul C:

47
Listing 2.2: dfsrec.c
#include <s t d i o . h>
#include <mem. h>

#define MAXN 100

char v e c i n [MAXN] [MAXN] ; // M a t r i c e a de a d i a c e n t a


char v i z i t a t [MAXN] ; // Vecto r c e p a s t r e a z a s t a r e a unui nod : v i z i t a t sau nu
i nt n ; // Numarul de v a r f u r i d i n g r a f
i nt no di ; // Nodul d i n c a r e s e p o r n e s t e v i z i t a r e a

/∗ ∗
∗ Se c i t e s c numarul de n o d u r i precum s i m a t r i c e a de a d i a c e n t a .
∗/
void r e a d I n p u t ( void ) {
i nt i , j ;

p r i n t f ( ”n = ” ) ; s c a n f ( ”%d” , &n ) ;
for ( i = 0 ; i < n − 1 ; i ++)
for ( j = i + 1 ; j < n ; j ++) {
p r i n t f ( ” a[%d,%d ] = ” , i , j ) ;
s c a n f ( ”%d” , &v e c i n [ i ] [ j ] ) ;
vecin [ j ] [ i ] = vecin [ i ] [ j ] ;
}
p r i n t f ( ” Nodul i n i t i a l = ” ) ; s c a n f ( ”%d” , &no di ) ;
memset ( v i z i t a t , 0 , s i z e o f ( v i z i t a t ) ) ;
}

/∗ ∗
∗ P a r c u r g e r e a i n adancime po r nind d i n no dul k .
∗/
void d f s ( i nt k ) {
i nt i ;

vizitat [ k] = 1;
p r i n t f ( ”%d ” , k ) ;
for ( i = 0 ; i < n ; i ++)
i f ( v i z i t a t [ i ] == 0 && v e c i n [ k ] [ i ] == 1 )
dfs ( i ) ;
}

void main ( void ) {


readInput ( ) ;
d f s ( no di ) ;
}

Complexitatea algoritmilor de parcurgere


Complexitatea algoritmilor 18 şi 19 prezentaţi este O(n2 ) deoarece se utilizează matricea de
adiacenţă drept modalitate de reprezentare a grafului. Dacă se utilizează reprezentarea prin
liste de vecini, atunci complexitatea algoritmilor devine O(n + m), unde m = |E|.

Matricea de adiacenţă Liste de vecini Lista de muchii


BFS O(n2 ) O(n + m) O(n + m2 )
D O(n2 ) O(n + m) O(n + m2 )
DFS O(n2 ) O(n + m) O(n + m2 )

48
2.5 Componente conexe
Definim pe mulţimea vârfurilor V a unui graf neorientat G o relaţie ρ ı̂n felul următor: xρy
dacă x = y sau există ı̂n G un lanţ de la x la y.
Se demonstrează foarte uşor că relaţia ρ este o relaţie de echivalenţă (exerciţiu). Se
cunoaşte faptul că o relaţie de echivalenţă determină pe mulţimea pe care este definită o
partiţie. Componentele conexe vor fi elementele acestei partiţii, formate din vârfurile ce sunt
echivalente ı̂ntre ele (conform relaţiei de echivalenţă).
Cu alte cuvinte, există o partiţie a mulţimii vârfurilor V
m
[
V = Vi , Vi ∩ Vj = ∅, ∀i, j = 1, m, i 6= j (2.4)
i=1

şi o partiţie a mulţimii muchiilor E


m
[
E= Ei , Ei ∩ Ej = ∅, ∀i, j = 1, m, i 6= j (2.5)
i=1

astfel ı̂ncât fiecare graf Gi = (Vi , Ei ), ∀i = 1, m, este conex (Ei = E|Vi ×Vi ).

Algoritm 20 Algoritm pentru determinarea componentelor conexe

( ComponenteConexe(n, V ecin)
1: procedure
n - numărul de noduri din graf
Input:
V ecin - matricea de adiacenţă a grafului
2: for i ← 1, n do
3: vizitati ← 0
4: end for
5: cmp conex nr ← 0
6: for i ← 1, n do
7: if (vizitati = 0) then
8: cmp conex nr ← cmp conex nr + 1
9: call DF S(i, n, V ecin) ⊲ determinarea componentei conexe ce conţine nodul i
10: end if
11: end for
12: end procedure

Pentru determinarea componentelor conexe vom utiliza metoda de vizitare ı̂n adâncime a
unui graf. Paşii algoritmului descrişi ı̂n limbaj natural sunt următorii (algoritmul 20 prezintă
descrierea formalizată ı̂n pseudo-cod):

Pas 1. La ı̂nceput, toate nodurile sunt marcate ca nevizitate.

Pas 2. Se caută un nod ı̂ncă nevizitat.

Pas 3. Începând cu acesta se parcurg toate nodurile accesibile şi nevizitate, având grijă să
marcăm vizitarea acestora. Toate aceste noduri formează o componentă conexă.

Pas 4. Dacă mai există noduri nevizitate, se reia procesul de calcul de la pasul 2, altfel procesul
de calcul se ı̂ncheie.

49
2.6 Muchie critică
Reamintim faptul că ı̂ntr-un graf neorientat G = (V, E), o muchie critică este acea muchie
care prin eliminarea ei conduce la creşterea numărului de componente conexe ale grafului.
Se cere să se determine toate muchiile critice ale unui graf dat. În continuare sunt prezen-
tate două variante de rezolvare.

Soluţia I
Pentru simplitate, vom trata situaţia ı̂n care graful considerat este conex (dacă graful nu este
conex se determină componentele conexe şi numărul acestora şi se aplică algoritmul pentru
fiecare componentă conexă ı̂n parte). Se elimină, pe rând, fiecare muchie a grafului şi apoi
se verifică dacă graful rezultat mai este conex (a se vedea algoritmul 21).

Algoritm 21 Algoritm de determinare a muchiei critice (prima variantă)


1: procedure MuchieCriticaI(n, m, V ecin)

n
 - numărul de noduri din graf
Input: m - numărul de muchii din graf


V ecin - matricea de adiacenţă
2: for k ← 1, m do
3: elimină muchia k
4: if (Conex(n, V ecin) = f alse) then
5: Output {”Muchia k este critica”}
6: end if
7: adaugă muchia k ⊲ reface graful iniţial
8: end for
9: end procedure
10: function Conex(n, V ecin)
11: for i ← 1, n do
12: vizitati ← 0
13: end for
14: call DF S(1, n, V ecin) ⊲ vizitarea ı̂n adâncime a grafului
15: for i ← 1, n do
16: if (vizitati = 0) then
17: return f alse
18: end if
19: end for
20: return true
21: end function

Subrutina Conex() verifică dacă graful identificat prin matricea de adiacenţă Vecin este
conex (este compus dintr-o singură componentă conexă). Subrutina ı̂ntoarce valoarea true
ı̂n cazul ı̂n care graful considerat este conex şi false, ı̂n caz contrar. Pentru aceasta, la
ı̂nceput, se marchează toate nodurile ca fiind nevizitate, şi se ı̂ncearcă parcurgerea nodurilor
grafului, prin intermediul unui apel al subrutinei de vizitare, DFS(1, n, Vecin).
Implementarea algoritmului 21 ı̂n limbajul C este următoarea:
Listing 2.3: muchiecriticav1.c
#include <s t d i o . h>
#include <mem. h>

50
#define MAXN 100
#define TRUE 1
#define FALSE 0

char v e c i n [MAXN] [MAXN] ; // M a t r i c e a de a d i a c e n t a


char v i z i t a t [MAXN] ; // Vecto r c e p a s t r e a z a s t a r e a unui nod : v i z i t a t sau nu
i nt n ; // Numarul de v a r f u r i d i n g r a f

/∗ ∗
∗ Se c i t e s c numarul de n o d u r i precum s i m a t r i c e a de a d i a c e n t a .
∗/
void r e a d I n p u t ( void ) {
i nt i , j ;

p r i n t f ( ”n = ” ) ; s c a n f ( ”%d” , &n ) ;
for ( i = 0 ; i < n − 1 ; i ++)
for ( j = i + 1 ; j < n ; j ++) {
p r i n t f ( ” a[%d,%d ] = ” , i , j ) ;
s c a n f ( ”%d” , &v e c i n [ i ] [ j ] ) ;
vecin [ j ] [ i ] = vecin [ i ] [ j ] ;
}
}

/∗ ∗
∗ P a r c u r g e r e a i n adancime a g r a f u l u i po r nind d i n no dul de s t a r t k .
∗/
void d f s ( i nt k ) {
i nt i ;

v i z i t a t [ k ] = TRUE;
p r i n t f ( ”%d ” , k ) ;
for ( i = 0 ; i < n ; i ++)
i f ( ( v i z i t a t [ i ] == FALSE) && ( v e c i n [ k ] [ i ] == 1 ) )
dfs ( i ) ;
}

/∗ ∗
∗ F u n c t i a v e r i f i c a daca g r a f u l e s t e conex .
∗/
i nt conex ( void ) {
i nt i ;

memset ( v i z i t a t , 0 , s i z e o f ( v i z i t a t ) ) ;
dfs (0 );
for ( i = 0 ; i < n ; i ++)
i f ( v i z i t a t [ i ] == FALSE) {
return FALSE ;
}
return TRUE;
}

void main ( void ) {


i nt i , j ;

readInput ( ) ;
for ( i = 1 ; i < n ; i ++)
for ( j = 0 ; j < i ; j ++)
i f ( v e c i n [ i ] [ j ] == 1 ) {

51
vecin [ i ] [ j ] = 0;
vecin [ j ] [ i ] = 0;
i f ( conex ( ) == FALSE) {
p r i n t f ( ” Muchia (%d,%d ) e s t e c r i t i c a ! \n” , i , j ) ;
}
vecin [ i ] [ j ] = 1;
vecin [ j ] [ i ] = 1;
}
}

Observaţia 2.20 Am presupus că numerotarea nodurilor se face de la 0 şi că există un nod
cu eticheta 0.

Complexitatea-timp a acestui algoritm, presupunând că avem o singură componentă


conexă, este O(m(n + m)): se consideră fiecare muchie (O(m)), se elimină din graf, şi se
verifică dacă graful rămas este conex (O(n + m)).

Soluţia a II-a
Cea de-a doua variantă de abordare pentru identificarea unei soluţii foloseşte următoarea
proprietate:
Observaţia 2.21 O muchie nu este critică dacă ea face parte din cel puţin un ciclu elemen-
tar al grafului respectiv.
În urma vizitării DFS, toate muchiile unui graf se ı̂mpart ı̂n muchii ale arborelui de
acoperire şi muchii de ı̂ntoarcere.
Vom numerota toate nodurile grafului ı̂n preordine (numerotarea se realizează efectiv ı̂n
momentul ı̂n care un nod este marcat ca fiind vizitat), toate valorile fiind păstrate ı̂ntr-un
vector prenum. Fie lowu valoarea unui nod u, calculată după formula următoare:

prenumu

lowu = min prenumx , dacă [u, x] este muchie de ı̂ntoarcere (2.6)


lowy , ∀y descendent direct al lui u.

Dacă prenumu ≥ lowv , ∀v descendent direct al lui u, atunci ı̂nseamnă că nodul v sau un
descendent al lui v prezintă o muchie de ı̂ntoarcere la u sau la un strămoş al acestuia. Astfel
muchia [u, v] aparţine unui ciclu elementar, şi, prin urmare, nu este muchie critică (a se vedea
algoritmul 22). Prin negaţie, dacă există cel puţin un vârf v, descendent direct al lui u, cu
proprietatea că prenumu < lowv atunci [u, v] este muchie critică. După fiecare apel recursiv
al subrutinei DFS (linia 16) se verifică gradul de adevăr al expresiei (prenumk < lowi ) (linia
18).
Variabila counter este globală pentru cele două subrutine, MuchieCriticaII() şi
DFS critic(). La numerotarea ı̂n preordine se folosesc atribuirile: counter ← counter + 1,
şi prenumk ← counter. Când un nod i este vecin cu nodul curent k, şi nu a fost ı̂ncă vizitat,
valoarea lui lowk se calculează astfel: lowk ← min {lowk , lowi}. În situaţia ı̂n care pentru un
nod i, vecin cu nodul curent k, deja vizitat, avem o muchie de ı̂ntoarcere (nodului k i s-a
atribuit deja un număr ı̂n preordine), atunci valoarea lowk se calculează după formula

lowk ← min {lowk , prenumi}.

Implementarea ı̂n limbajul C a algoritmului 22 este următoarea:

52
Algoritm 22 Algoritm de determinare a muchiei critice (a doua variantă)

( MuchieCriticaII(n, V ecin)
1: procedure
n - numărul de noduri din graf
Input:
V ecin - matricea de adiacenţă
2: for i ← 1, n do
3: vizitati ← 0
4: end for
5: counter ← 0
6: call DF S critic(1, n, V ecin) ⊲ vizitarea ı̂n adâncime a grafului
7: end procedure
8: procedure DFS critic(k, n, V ecin)
9: vizitatk ← 1
10: counter ← counter + 1
11: prenumk ← counter, lowk ← counter
12: for i ← 1, n do
13: if (vecink,i = 1) then
14: if (vizitati = 0) then
15: tatai ← k
16: call DF S critic(i, n, V ecin)
17: lowk ← M in(lowk , lowi )
18: if (prenumk < lowi ) then
19: Output {’Muchia (k,i) este critica’}
20: end if
21: else
22: if (tatak 6= i) then
23: lowk ← M in(lowk , prenumi )
24: end if
25: end if
26: end if
27: end for
28: end procedure

Listing 2.4: muchiecriticav2.c


#include <s t d i o . h>
#include <mem. h>

#define MAXN 100

char v e c i n [MAXN] [MAXN] ; // M a t r i c e a de a d i a c e n t a


char v i z i t a t [MAXN] ; // Vecto r c e p a s t r e a z a s t a r e a unui nod : v i z i t a t sau nu
i nt n ; // Numarul de n o d u r i d i n g r a f
i nt t a t a [MAXN] ; // P a s t r e a z a t a t a l f i e c a r u i nod i n a r b o r e l e de a c o p e r i r e
// i n a dincime g e n e r a t de metoda DFS .
i nt prenum [MAXN] ; // prenum [ k ] − numer o ta r ea i n p r e o r d i n e
i nt low [MAXN] ;
i nt c o u n t e r ; // c o n t o r g l o b a l c e numara momentul cand e s t e v i z i t a t un nod

void r e a d I n p u t ( void ) {
i nt i , j ;

p r i n t f ( ”n = ” ) ; s c a n f ( ”%d” , &n ) ;
for ( i = 0 ; i < n − 1 ; i ++)

53
for ( j = i + 1 ; j < n ; j ++) {
p r i n t f ( ” a[%d,%d ] = ” , i , j ) ;
s c a n f ( ”%d” , &v e c i n [ i ] [ j ] ) ;
vecin [ j ] [ i ] = vecin [ i ] [ j ] ;
}
}

i nt min ( i nt x , i nt y ) {
return ( x < y ) ? x : y ;
}

void d f s ( i nt k ) {
i nt i ;

vizitat [ k] = 1;
c o u n t e r++;
prenum [ k ] = c o u n t e r ; low [ k ] = c o u n t e r ;
for ( i = 0 ; i < n ; i ++)
i f ( v e c i n [ k ] [ i ] == 1 )
i f ( v i z i t a t [ i ] == 0 ) {
tata [ i ] = k ;
dfs ( i ) ;
low [ k ] = min ( low [ k ] , low [ i ] ) ;
i f ( prenum [ k ] < low [ i ] )
p r i n t f ( ”%d −> %d \n” , k , i ) ;
}
else
i f ( t a t a [ k ] != i )
low [ k ] = min ( low [ k ] , prenum [ i ] ) ;
/∗ p r i n t f ( ” prenum[%d ] = %d low[%d ] = %d\n ” , k , prenum [ k ] , k , low [ k ] ) ; ∗/
}

void c r i t i c ( void ) {
memset ( v i z i t a t , 0 , s i z e o f ( v i z i t a t ) ) ;
counter = 0;
dfs (0 );
}

void main ( void ) {


p r i n t f ( ” \n” ) ;
readInput ( ) ;
critic ();
}

Exemplul 2.22 Pentru graful din figura 2.13, valorile prenum şi low sunt cele din figura
2.19 (valoarea din partea dreaptă a unui vârf reprezintă valoarea sa la numerotarea ı̂n preor-
dine, prenum, iar numărul din stânga reprezintă valoarea calculată low). Condiţia prenumu <
lowv , unde [u, v] este o muchie a grafului, este ı̂ndeplinită doar pentru u = 6 şi v = 8. Prin
urmare [6, 8] este muchie critică ı̂n graful considerat.

Deoarece acest algoritm este constituit din algoritmul modificat de vizitare ı̂n adâncime
a unui graf, complexitatea-timp este O(n + m) atunci când graful este reprezentat prin liste
de adiacenţă şi O(n2 ) ı̂n situaţia ı̂n care graful este reprezentat prin matricea de adiacenţă.

54
Fig. 2.19: Algoritmul 22 aplicat pentru graful din figura 2.13

2.7 Exerciţii
1. Un graf neorientat cu n noduri, G = (V, E) se numeşte graf scorpion dacă posedă trei
noduri speciale:

(a) acul - dG (u) = 1 şi este legat de coadă;

(b) coada - dG (u) = 2 şi este legată de ac şi corp;

(c) corpul - dG (u) = n − 2 fiind legat de toate nodurile din graf cu excepţia acului.

Nu există alte restricţii cu privire la nodurile grafului.


Să se realizeze un algoritm care determină dacă un graf este graf scorpion folosind O(n)
ı̂ntrebări de forma ”Există o muchie ı̂ntre nodurile u şi v? ”.

2. Se consideră n bazine dispuse circular şi lipite unul de altul ı̂n ordinea numerelor de
ordine. Se dă lista celor m perechi de bazine ce trebuie unite prin canale. Un număr
de bazine poate să apară ı̂n mai multe perechi. Canalele pot fi realizate ı̂n interiorul
sau ı̂n exteriorul cercului, cu condiţia ca ele să nu se intersecteze.
Dacă problema are soluţie, vor fi afişate două liste: cea a perechilor de bazine unite
prin canale interioare şi cea a perechilor de bazine unite prin canale exterioare. Dacă
problema nu are soluţie, atunci se va raporta acest lucru.
În cazul ı̂n care problema are soluţie să se determine toate posibilităţile de unire prin
canale exterioare sau interioare.
(ONI, 1993)

3. Să se parcurgă ı̂n lăţime şi ı̂n adâncime graful din figura 2.20, să se construiască arborii
de parcurgere şi să se pună ı̂n evidenţă muchiile de ı̂ntoarcere.

4. Compania TLC (Telephone Line Company) a stabilit o nouă reţea de cablu telefonic.
Ea leagă mai multe locaţii numerotate cu numere ı̂ntregi de la 1 la N. Nu există două
locaţii numerotate cu acelaşi număr. Liniile sunt bidirecţionale şi conectează două
locaţii; fiecare locaţie are un post telefonic. Din orice locaţie poate fi apelată oricare

55
Fig. 2.20: Un exemplu de graf neorientat cu 8 vârfuri

altă locaţie, prin legătura directă sau conexiuni intermediare. Uneori reţeaua cade ı̂n
unele locaţii şi conexiunea aferentă nu mai este posibilă. Tehnicienii de la TLC au
realizat că, ı̂n acest caz, nu numai că locaţia respectivă nu mai poate fi apelată, dar
ea ı̂ntrerupe legătura şi ı̂ntre alte locaţii pentru care asigură conexiunea. În această
situaţie spunem că locaţia (unde a apărut căderea) este critică.
Se cere să se elaboreze un algoritm care să determine numărul tuturor acestor puncte
critice.
(ACM Europa Centrală, 1996)

5. Fiind dat un graf G = (V, E), să se verifice dacă este aciclic.

6. Fiind dat un graf G = (V, E), să se verifice dacă este bipartit.

7. Într-un oraş, intersecţiile sunt numerotate de la 1 la n (se consideră intersecţie nu


numai locul unde se intersectează mai multe străzi ci şi punctele terminale, capetele
de străzi). Edilii oraşului doresc să numeroteze şi străzile oraşului, dar ı̂ntr-un mod
care să ţină seama de numerotarea intersecţiilor, şi anume: două străzi diferite vor
avea numere diferite şi ı̂n fiecare intersecţie trebuie să sosească o stradă care să aibă
numărul intersecţiei.
Se cere să se realizeze un algoritm care să determine o astfel de numerotare, dacă există.
Datele de intrare constau dintr-un număr n reprezentând numărul de puncte de inter-
secţie; fiecare punct este identificat prin numărul cu care este numerotat, ı̂ntre 1 şi n,
urmat de lista punctelor cu care acesta comunică direct printr-o stradă. Prin stradă se
ı̂nţelege o porţiune de drum aflată ı̂ntre două puncte de intersecţie, neglijând faptul că
ı̂n practică străzile cuprind mai multe intersecţii.

8. Se consideră o tablă de şah de dimensiune n × n (n ≤ 100) pe care sunt dispuse


obstacole. Se cere să se determine numărul minim de mutări necesare unui nebun
pentru a se deplasa, respectând regulile jocului de şah şi ocolind obstacolele, dintr-o
poziţie iniţială dată ı̂ntr-o poziţie finală dată.
Se consideră că obstacolele nu coincid cu poziţia iniţială şi nici cu cea finală a nebunului.
(Concurs studenţesc, 1993)

9. Pe o tarla agricolă sunt mai multe parcele ce trebuie cosite. Parcelele de diferite culturi
(lucernă, trifoi, iarbă, furaje) vor fi cosite de muncitori diferiţi ce lucrează numai la

56
un anumit fel de cultură (de exemplu un muncitor este specializat numai pe cositul
lucernei).
Terenul agricol se reprezintă printr-o matrice n × m (n linii şi m coloane). Fiecare
element al matricii corespunde unei mulţimi de un anumit tip. Două elemente ale
matricii sunt vecine dacă au o latură comună. O parcelă este o mulţime maximală de
elemente astfel ı̂ncât un muncitor se poate deplasa ı̂ntre oricare două elemente de-a
lungul a două elemente vecine.
Se cere să se determine numărul de muncitori care pot să cosească o cultură dată.

57
Capitolul 3

Grafuri euleriene şi hamiltoniene

3.1 Grafuri Euleriene


În anul 1736, matematicianul Leonhard Euler publica o lucrare asupra problemei podurilor
din Königsberg1 (a se vedea figura 3.1) ı̂n care se prezintă un studiu teoretic asupra lanţurilor
şi ciclurilor Euleriene.

Fig. 3.1: Podurile din Königsberg

Să considerăm un graf neorientat G = (V, E).

Definiţia 3.1 Un lanţ L ce conţine fiecare muchie a unui graf G o singură dată se numeşte
lanţ Euler sau lanţ eulerian. Dacă extremităţile lanţului sunt identice şi lanţul este
eulerian atunci ciclul se numeşte ciclu Euler sau ciclu eulerian. Un graf ce conţine un
ciclu eulerian se numeşte graf eulerian.

O problemă mai cunoscută legată de noţiunile de ciclu şi lanţ eulerian este aceea de a
desena cu o singură linie neı̂ntreruptă o anumită figură (a se vedea figura 3.2).

Teorema 3.1 (Euler) Un graf G conex (|V | ≥ 3) este eulerian dacă şi numai dacă gradul
fiecărui vârf este par.
1
http://en.wikipedia.org/wiki/Seven Bridges of Königsberg

58
Fig. 3.2: Două figuri geometrice ce pot fi desenate folosind o singură linie

Demonstraţie: ”⇒” Fie G un graf conex eulerian ⇒ ∃ un ciclu eulerian C. Acesta


trece o singură dată prin toate muchiile grafului. Gradul fiecărui vârf u este par, deoarece
pentru fiecare muchie incidentă cu u prin intermediul căreia se ajunge ı̂n vârful respectiv,
există o altă muchie prin intermediul căreia se părăseşte vârful u.
”⇐” Presupunem că ∀u ∈ V , dG (u) este par. Fie L un lanţ de lungime maximă ı̂n graful
G şi fie u şi v extremităţile lanţului.
Presupunem că u 6= v. Deoarece dG (v) este par, ∃[v, w] ∈ E astfel ı̂ncât [v, w] ∈ / L.
Lanţul L ∪ {[v, w]} va avea lungimea mai mare decât lanţul iniţial L, contradicţie. Prin
urmare u = v. Rezultă că L = C este un ciclu de lungime maximă.
Presupunem că ciclul C nu este ciclu eulerian. Atunci ∃x, y ∈ V, [x, y] ∈ E, [x, y] ∈ / C şi
x ∈ C (există o muchie a grafului G ce nu aparţine ciclului C cu proprietatea că cel puţin o
extremitate aparţine ciclului).
Fie L1 = [x, . . . , u, v, . . . , x] un lanţ ı̂n graful G ce conţine exact muchiile ciclului C.
Atunci L2 = [x, . . . , u, v, . . . , x, y] este un lanţ ı̂n G ce are lungimea mai mare decât cea a
lanţului L, contradicţie. Rezultă atunci că ciclul C este eulerian ⇒ G este un graf eulerian. 

Teorema 3.2 (Euler) Un graf G conex are un lanţ eulerian dacă şi numai dacă există exact
două vârfuri ı̂n G al căror grad să fie impar.

Demonstraţie: ”⇒” Se demonstrează la fel ca la teorema 3.1.


”⇐” Fie G = (V, E) un graf conex ce are exact două vârfuri de grad impar, u şi v. Să
consideră un nod w ∈ / V ı̂mpreună cu muchiile [u, w] şi [v, w]. Fie G′ = (V ′ , E ′ ), V ′ = V ∪{w},
E ′ = E ∪ {[u, w], [v, w]}, graful obţinut din G prin adăugarea nodului w şi a muchiilor [u, w],
[v, w]. Atunci dG′ (u) este par, ∀u ∈ V ′ ⇒ există un ciclu eulerian C ı̂n graful G′ . Dacă
eliminăm muchiile [u, w] şi [v, w] obţinem un lanţ eulerian ı̂n graful G. 

Corolarul 3.3 Un graf G conex (|V | ≥ 3) este eulerian dacă şi numai dacă mulţimea muchi-
ilor sale poate fi descompusă ı̂n cicluri disjuncte.

Teorema 3.4 Pentru un graf conex cu 2k vârfuri de grad impar, k ≥ 1, există k lanţuri ı̂n
G ale căror mulţimi de muchii formează o partiţie a mulţimii E, şi din care cel mult unul
are lungimea impară.

3.1.1 Algoritm pentru determinarea unui ciclu eulerian


Pornind de la Teorema 3.1 construim un ciclu eulerian. Se consideră drept punct de plecare
un vârf oarecare al grafului. La fiecare pas se alege pentru vârful curent u, un vârf adiacent

59
Fig. 3.3: Exemplu de graf eulerian

v, diferit de vârful din care s–a ajuns ı̂n u. Parcurgerea se continuă până ı̂n momentul ı̂n
care ajungem ı̂ntr–un vârf ce a fost deja vizitat şi se ı̂nchide ciclul C. Dacă ciclul C nu
este eulerian, atunci se elimină din graful G toate muchiile ciclului C, rezultând mai multe
componente conexe G11 , G12 , . . . , G1k .
Pentru fiecare astfel de componentă G1i , ce are cel puţin două vârfuri, aplicăm recursiv
algoritmul. Conform teoremei 3.1, fiecare componentă G1i este euleriană. Fie Ci1 ciclul
eulerian corespunzător componentei G1i .
Ciclul C are cel puţin câte un vârf comun cu fiecare ciclu Ci1 . Fie acest vârf ui . Definim
un ciclu C0 astfel: pentru fiecare i, i = 1, k, adăugăm la ciclul C, ı̂n vârful ui, ciclul Ci1 .
Ciclul C0 conţine toate muchiile grafului G, prin urmare este eulerian.
Funcţia IsEulerian() verifică dacă un graf este eulerian conform teoremei 3.1 (a se vedea
algoritmul 23).

Algoritm 23 Algoritm de verificare dacă un graf conex este eulerian


1: function IsEulerian(G = (V, E))
2: if (|V | < 3) then
3: return f alse
4: end if
5: for i ← 1, n do
6: s←0
7: for j ← 1, n do
8: s ← s + vecini,j
9: end for
10: if (s mod 2 = 1) then
11: return false
12: end if
13: end for
14: return true
15: end function

După cum se poate vedea din funcţia IsEulerian() numai componentele conexe ce au
ordinul mai mare sau egal cu 3 vor fi luate ı̂n considerare.

60
În algoritmul 24 este prezentată funcţia EulerRec() pentru determinarea unui ciclu eu-
lerian.
Procedura FindCycle(G) (a se vedea algoritmul 24) construieşte un ciclu: se alege un
vârf u0 şi se caută prima muchie [u0 , u1 ]. În vârful u1 se caută o muchie [u1 , u2 ] astfel ı̂ncât
vârful u2 să fie distinct de vârful din care s-a ajuns ı̂n u1 (u2 6= u0 ) şi să nu mai fi fost vizitat.
Dacă u2 a fost vizitat atunci funcţia se termină. Se continuă procedeul de căutare până când
se ajunge la un vârf ce a fost vizitat anterior şi se returnează ciclul cuprins ı̂ntre cele două
apariţii ale aceluiaşi vârf.
Procedura F indComponents(G′ ; k, G11 , G12 , . . . , G1k ) determină componentele conexe ale
grafului G′ = G − E(C), obţinut prin eliminarea din graful G a muchiilor ciclului C, unde
G11 , G12 , . . . , G1k sunt cele k componente conexe returnate de aceasta.
Procedura MergeCycles(C, C11 , . . . , Ck1 ; C0 ) construieşte un ciclul C0 obţinut prin adăugarea
la ciclul C, succesiv, a ciclurilor C11 , . . . , Ck1 .

Algoritm 24 Algoritm de determinare a unui ciclu eulerian ı̂ntr–un graf conex


1: function EulerRec(G = (V, E))
2: if (IsEulerian(G) = f alse) then
3: return ∅
4: end if
5: call F indCycle(G; C)
6: if (E(G) \ E(C) = ∅) then
7: return C
8: end if
9: G′ ← G − E(C)
10: call F indComponents(G′ ; k, G11 , G12 , . . . , G1k )
11: for i ← 1, k do
12: Ci1 ← EulerRec(G1i )
13: end for
14: call M ergeCycles(C, C11 , . . . , Ck1 ; C0 )
15: return C0
16: end function

Fig. 3.4: Grafurile parţiale obţinute ı̂n urma primelor două etape ale algoritmului 24

Exemplul 3.5 Să aplicăm algoritmul 24 pentru graful din figura 3.3. Presupunem că primul
element (elementul de start) este vârful u0 = 1. Procedura F indCycle construieşte lanţul

61
Fig. 3.5: Grafurile parţiale obţinute ı̂n urma etapelor 3 şi 4 ale algoritmului 24

L = [1, 2, 3, 4, 5, 3]. Se observă prezenţa ciclului C1 = [3, 4, 5, 3]. În urma eliminării acestuia
rămâne graful din figura 3.4 a) care este un graf conex.
Continuăm procesul de construire a unui ciclu eulerian, prin apelul procedurii F indCycle
pentru noul graf. Aceasta returnează ciclul C2 = [1, 2, 3, 9, 1]. Prin eliminarea muchiilor
acestuia, E(C2 ), se obţine graful din figura 3.4 b). În continuare se construieşte lanţul
L = [2, 8, 7, 5, 6, 4, 10, 2], ce conţine ciclul C3 = [2, 8, 7, 5, 6, 4, 10, 2]. Graful obţinut prin
eliminarea muchiilor ciclului este cel din figura 3.5 a). Apelul procedurii F indCycle conduce
la determinarea lanţului L = [6, 7, 9, 8, 10, 6] şi a ciclului C4 = [6, 7, 9, 8, 10, 6].
Ciclul eulerian se formează prin reuniunea ciclurilor intermediare determinate: C =
(((C4 ∪ C3 ) ∪ C2 ) ∪ C1 ) adică C = [1, 2, 8, 7, 5, 6, 7, 9, 8, 10, 6, 4, 10, 2, 3, 4, 5, 3, 9, 1] (C4 ∪ C3 =
[2, 8, 7, 5, 6, 7, 9, 8, 10, 6, 4, 10, 2]).

3.1.2 Algoritmul lui Rosenstiehl


Algoritmul lui Rosenstiehl [51] construieşte un lanţ L care la final va deveni un ciclu eulerian,
folosind pentru aceasta o stivă drept structură de date auxiliară.
Se porneşte de la un nod oarecare. Se ı̂naintează atâta timp cât este posibil: pentru
nodul curent u se caută o muchie incidentă cu el, şi care să nu mai fi fost parcursă la unul
din paşii anteriori. Dacă există o astfel de muchie (e = [u, v]), atunci se salvează pe stivă
nodul u, iar nodul v devine nodul curent. În momentul ı̂n care nodul curent u nu mai are
muchii nevizitate, se adaugă lanţului L, şi se extrage de pe stivă nodul anterior (din care s-a
ajuns ı̂n u) (a se vedea algoritmul 25). Vectorul vizit are drept scop să păstreze situaţia
muchiilor: (
1 , dacă e ∈ E a fost parcursă
vizite =
0 , dacă e ∈ E nu a fost ı̂ncă parcursă.

Exemplul 3.6 Să aplicăm algoritmul 25 pentru graful din figura 3.3. Să presupunem că
vârful u de la care porneşte algoritmul este vârful 1. La sfârşitul primului pas al enunţului
repetitiv while (liniile 6 – 14) avem valorile:

S = [1, 2, 3, 4, 5, 3, 9], L = [1], u = 1.

La pasul următor, se extrage valoarea 9 de pe stivă şi astfel u devine 9. La sfârşitul acestuia

62
Algoritm 25 Algoritm lui Rosenstiehl de determinare a unui ciclu eulerian ı̂ntr–un graf
conex
1: function Rosenstiehl(u, G)
2: for e ∈ E do
3: vizite ← 0
4: end for
5: S⇐u ⊲ Se inserează pe stivă nodul u
6: while (S 6= ∅) do
7: S⇒u ⊲ Se extrage din stivă nodul curent
8: while (∃e = [u, v] ∈ E) ∧ (vizite = 0)) do
9: vizite ← 1 ⊲ Se marchează muchia e ca fiind utilizată
10: S⇐u ⊲ Se salvează pe stivă nodul curent u
11: u←v ⊲ Nodul curent devine nodul v
12: end while
13: L⇐u ⊲ Se adaugă la lista L nodul curent
14: end while
15: return L
16: end function

avem:
S = [1, 2, 3, 4, 5, 3, 9, 7, 5, 6, 4, 10, 2, 8, 7, 6, 10, 8], L = [1, 9], u = 9.
În continuare, deoarece nu mai există muchii nevizitate, se vor extrage elementele aflate pe
stiva S câte unul la fiecare pas, formând secvenţa (8, 10, 6, 7, 8, 2, 10, 4, 6, 5, 7, 9, 3, 5, 4, 3, 2, 1),
şi se vor introduce ı̂n lista L:

P as 20 : S = [], L = [1, 9, 8, 10, 6, 7, 8, 2, 10, 4, 6, 5, 7, 9, 3, 5, 4, 3, 2, 1], u = 1.

Ciclul eulerian obţinut se află ı̂n lista L.

În continuare se prezintă implementarea ı̂n limbajul C++ a algoritmului lui Rosenstiehl :
Listing 3.1: rosenstiehl.cpp
#include <v e c t o r >
#include <s t a c k >
#include <l i s t >
#include <i o s t r e a m>
#include <f s t r e a m>
#include <iomanip>
#include <c o n i o . h>

u s i n g namespace s t d ;

#define INPUT FILE ” g r a f 1 . t x t ”

typedef v e c t o r <int> L i n i e ;
typedef v e c t o r <L i n i e > M a t r i c e ;

i nt r e a d I n p u t ( M a t r i c e& ma) {
i nt n , i , j , v a l u e ;
i f s t r e a m f i n ( INPUT FILE ) ;

f i n >> n ;
ma = M a t r i c e ( n , L i n i e ( n , 0 ) ) ;

63
for ( i = 0 ; i < n ; i ++)
for ( j = 0 ; j < n ; j ++) {
f i n >> ma [ i ] [ j ] ;
}

fin . close ();


return n ;
}

void p r i n t ( l i s t <int>& l ) {
l i s t <int > : : i t e r a t o r i t ;

co ut << ” C i c l u e u l e r i a n : [ ” ;
for ( i t = l . b e g i n ( ) ; i t != l . end ( ) ; i t ++)
co ut << ∗ i t << ” , ” ;
co ut << ” ] \ n” ;
}

void r o s e n s t i e h l ( i nt n , M a t r i c e& ma , i nt u , l i s t <int>& l ) {


s t a c k <int> s ;
i nt v ;

s . push ( u ) ;
while ( ! s . empty ( ) ) {
u = s . top ( ) ;
s . pop ( ) ;
v = 0;
while ( v < n ) {
i f (ma [ u ] [ v ] == 1 ) {
ma [ u ] [ v ] = 0 ;
ma [ v ] [ u ] = 0 ;
s . push ( u ) ;
u = v;
v = 0;
}
else
v++;
}
l . push ba ck ( u ) ;
}
}

void main ( void ) {


M a t r i c e ma ;
l i s t <int> l = l i s t <int > ( ) ;
i nt n = r e a d I n p u t (ma ) ;

r o s e n s t i e h l ( n , ma , 0 , l ) ;
print ( l ) ;
}

Datele de intrare vor fi preluate dintr-un fişier. Conţinutul fişierului de intrare graf1.txt
corespunzător grafului din figura 3.6 este următorul:
6
0 1 1 1 1 0
1 0 1 1 1 0
1 1 0 1 0 1

64
1 1 1 0 0 1
1 1 0 0 0 0
0 0 1 1 0 0

Pe prima linie avem numărul de vârfuri al grafului (cardinalul mulţimii V ), şi ı̂ncepând
cu cea de-a doua linie avem valorile matricei de adiacenţă, separate prin spaţii, câte un rând
al acesteia pe fiecare pe linie.

Fig. 3.6: Un alt exemplu de graf eulerian

Pentru a reduce timpul de implementare, am utilizat câteva structuri de date deja exis-
tente ı̂n limbajul C++, mai exact structuri de date implementate cu ajutorul template-urilor
din cadrul librăriei Standard Template Library - STL2 3 :

• stiva - stack 4. Am folosit o stivă de numere ı̂ntregi: stack<int> s;.

• lista - list 5 . Pentru a păstra elementele ciclului eulerian am folosit o listă liniară,
informaţia utilă din cadrul nodurilor fiind constituită din valori ı̂ntregi: list<int> l
= list<int>();.

• vector - vector 6 . Matricea de adiacenţă am implementat-o sub forma unui vector de


vectori cu elemente numere ı̂ntregi:
typedef v e c t o r <int> L i n i e ;
typedef v e c t o r <L i n i e > M a t r i c e ;
...
M a t r i c e ma ;
ma = M a t r i c e ( n , L i n i e ( n , 0 ) ) ;

După cum se poate observa din programul anterior, pentru reprezentarea internă a
grafului s-a folosit matricea de adiacenţă.

• Parcurgerea elementelor listei se realizează cu ajutorul unui iterator - iterator 7 . Declararea


unui obiect de tip list<int>::iterator se poate face astfel: list<int>::iterator
it;.
2
http://en.wikipedia.org/wiki/Standard_Template_Library,
3
http://www.sgi.com/tech/stl/
4
http://www.sgi.com/tech/stl/stack.html
5
http://www.sgi.com/tech/stl/List.html
6
http://www.sgi.com/tech/stl/Vector.html
7
http://www.sgi.com/tech/stl/Iterators.html

65
void p r i n t ( l i s t <int>& l ) {
l i s t <int > : : i t e r a t o r i t ;

co ut << ” C i c l u e u l e r i a n : [ ” ;
for ( i t = l . b e g i n ( ) ; i t != l . end ( ) ; i t ++)
co ut << ∗ i t << ” , ” ;
co ut << ” ] \ n” ;
}

Deoarece muchiile sunt parcurse o singură dată ı̂n cadrul acestui algoritm, şi pentru a
nu mai păstra o structură auxiliară care să marcheze faptul că o muchie a fost vizitată, vom
marca parcurgerea unei muchii prin ştergerea acesteia din matricea de adiacenţă astfel:
ma [ u ] [ v ] = 0 ;
ma [ v ] [ u ] = 0 ;

Ciclul eulerian obţinut pentru graful din figura 3.6 este: L = [1, 5, 2, 4, 6, 3, 4,
1, 3, 2, 1].

3.1.3 Algoritmul lui Fleury


Un alt algoritm pentru determinarea unui ciclu eulerian este algoritmul lui Fleury.

Fig. 3.7: Exemplu de graf eulerian pentru algoritmul lui Fleury

Se porneşte cu un vârf oarecare al grafului G (G = (V, E), |V | = n, |E| = m). Ideea constă
ı̂n a construi un lanţ prin alegerea la fiecare pas a unei muchii nealeasă la paşii anteriori, şi
care, de preferat, să nu fie muchie critică ı̂n graful parţial obţinut prin eliminarea muchiilor
deja alese.
Să considerăm că, la un moment dat, avem construit un lanţ Lk = [v0 , [v0 , v1 ], v1 , . . . ,
[vk−1 , vk ], vk ]. Căutăm o muchie ek+1 = [vk , vk+1 ] astfel ı̂ncât ek+1 ∈ / Lk şi care nu este muchie
critică ı̂n graful parţial Gk = G − {e1 , e2 , . . . , ek } (graful obţinut prin eliminarea tuturor
muchiilor lanţului Lk ). Dacă nu există decât muchii critice ı̂n graful parţial Gk incidente cu
vk atunci alegem una dintre ele.
Dacă există o muchie ek+1 cu aceste proprietăţi atunci construim lanţul Lk+1 = [Lk , ek+1, vk+1 ]
(Lk+1 = [v0 , [v0 , v1 ], v1 , . . . , [vk−1 , vk ], vk , [vk , vk+1 ], vk+1 ]). În momentul ı̂n care E(Lk ) = E(G)
algoritmul lui Fleury se opreşte (vezi algoritmul 26).

66
Algoritm 26 Algoritm lui Fleury de determinare a unui ciclu eulerian ı̂ntr–un graf conex
1: function Fleury(u0 , G = (V, E))
2: k←0
3: L0 ← [u0 ]
4: while (k ≤ m) do
5: caut ek+1 = [vk , vk+1 ] a.i. ek+1 ∈
/ Lk şi ek+1 , de preferat, nu este muchie critică ı̂n graful
Gk = G − {e1 , e2 , . . . , ek }
6: Lk+1 ← [Lk , ek+1 , vk+1 ]
7: k ←k+1
8: end while
9: return Lk
10: end function

Exemplul 3.7 Să aplicăm algoritmul 26 pentru graful din figura 3.7. La ı̂nceput L0 = [u1 ].
Apoi se intră ı̂n ciclul while (liniile 4 – 8): tabelul următor indică muchia ce a fost aleasă
la fiecare pas k, precum şi configuraţia lanţului Lk .

Pasul k Muchia aleasă Lk


1 [u1 u4 ] L1 = [u1 , [u1 u4 ], u4 ]
2 [u4 u10 ] L2 = [u1 , [u1 u4 ], u4 , [u4 u10 ], u10 ]
3 [u10 u3 ] L3 = [u1 , [u1 u4 ], u4 , [u4 u10 ], u10 , [u10 u3 ], u3 ]
4 [u3 u2 ] L4 = [. . . , [u10 u3 ], u3 , [u3 , u2 ], u2 ]
5 [u2 u6 ] L5 = [. . . , u2 , [u2 u6 ], u6 ]
6 [u6 u1 ] L6 = [. . . , u6 , [u6 u1 ], u1 ]
7 [u1 u5 ] L7 = [. . . , u1 , [u1 u5 ], u5 ]
8 [u5 u6 ] L8 = [. . . , u5 , [u5 u6 ], u6 ]
9 [u6 u7 ] L9 = [. . . , u6 , [u6 u7 ], u7 ]
10 [u7 u2 ] L10 = [. . . , u7 , [u7 u2 ], u2 ]
11 [u2 u8 ] L11 = [. . . , u2 , [u2 u8 ], u8 ]
12 [u8 u3 ] L12 = [. . . , u8 , [u8 u3 ], u3 ]
13 [u3 u9 ] L13 = [. . . , u3 , [u3 u9 ], u9 ]
14 [u9 u8 ] L14 = [. . . , u9 , [u9 u8 ], u8 ]
15 [u8 u7 ] L15 = [. . . , u8 , [u8 u7 ], u7 ]
16 [u7 u13 ] L16 = [. . . , u7 , [u7 u13 ], u13 ]
17 [u13 u5 ] L17 = [. . . , u13 , [u13 u5 ], u5 ]
18 [u5 u12 ] L18 = [. . . , u5 , [u5 u12 ], u12 ]
19 [u12 u4 ] L19 = [. . . , u12 , [u12 u4 ], u4 ]
Pasul k Muchia aleasă Lk
20 [u4 u11 ] L20 = [. . . , u4 , [u4 u11 ], u11 ]
21 [u11 u10 ] L21 = [. . . , u11 , [u11 u10 ], u10 ]
22 [u10 u9 ] L22 = [. . . , u10 , [u10 u9 ], u9 ]
23 [u9 u13 ] L23 = [. . . , u9 , [u9 u13 ], u13 ]
24 [u13 u11 ] L24 = [. . . , u13 , [u13 u11 ], u11 ]
25 [u11 u12 ] L25 = [. . . , u11 , [u11 u12 ], u12 ]
26 [u12 u1 ] L26 = [. . . , u12 , [u12 u1 ], u1 ]
La final, lanţul are următoarea componenţă: L2 = [u1 , u4 , u10 , u3, u2 , u6 , u1, u5 , u6 , u7, u2 ,
u8 , u3, u9 , u8 , u7, u13 , u5 , u12 , u4 , u11, u10 , u9 , u13 , u11 , u12 , u1 ] (s-au omis din această enumerare
muchiile grafului).

67
3.2 Grafuri Hamiltoniene
Definiţia 3.2 Se numeşte lanţ Hamilton sau lanţ hamiltonian un lanţ L ce trece o
singură dată prin toate vârfurile unui graf.

Definiţia 3.3 Se numeşte ciclu hamiltonian un ciclu elementar ce trece prin toate vârfurile
grafului. Un graf ce admite un ciclu hamiltonian se numeşte graf hamiltonian.

Problema determinării dacă un graf oarecare G este hamiltonian este o problemă dificilă,
atenţia cercetătorilor ı̂ndreptându–se către enunţarea unor condiţii suficiente de existenţă a
unui ciclu hamiltonian.

Lema 3.8 Fie G = (V, E) un graf neorientat şi fie u şi v două vârfuri neadiacente ale grafului
(u, v ∈ V , u 6= v, [u, v] ∈
/ E), astfel ı̂ncât dG (u) + dG (v) ≥ n. Atunci G este hamiltonian ⇔
G + [u, v] este hamiltonian.

Demonstraţie: ” ⇒ ” Dacă G este hamiltonian, atunci cu atât mai mult, graful G+[u, v]
este hamiltonian.
” ⇐ ” Presupunem că G + [u, v] este un graf hamiltonian. Atunci există un ciclu hamil-
tonian ı̂n graful G + [u, v] pe care ı̂l notăm cu C.

1. dacă [u, v] ∈
/ C atunci C este un ciclu hamiltonian şi ı̂n graful G ⇒ G este un graf
hamiltonian.

2. dacă [u, v] ∈ C atunci C = [u, v, x3 , x4 , . . . , xn−1 , xn , u]. Pentru fiecare muchie [u, xk ] ∈
E putem avea următoarele situaţii:

(a) [v, xk+1] ∈ E. Atunci ciclul C1 = [u, xk , xk−1 , . . . , x3 , v, xk+1, . . . , xn , u] este hamil-
tonian ı̂n graful G ⇒ graful G este hamiltonian.

(b) [v, xk+1] ∈


/ E. Notăm dG+[u,v] (u) = k. Atunci dG+[u,v] (v) ≤ n − k − 1. şi De aici,
rezultă că dG (u) + dG (v) < dG+[u,v] (u) + dG+[u,v] (v) ≤ n − k + k − 1 = n − 1 < n.
Contradicţie cu dG (u) + dG (v) ≥ n, ∀u, v ∈ V, u 6= v, [u, v] ∈ / E.

Prin urmare lema este demonstrată. 

Teorema 3.9 (Dirac, 1952) Un graf G = (V, E) (|V | ≥ 3) este hamiltonian dacă ∀u ∈ V
avem dG (u) ≥ n2 (orice vârf al grafului are gradul mai mare decât jumătate din numărul de
vârfuri din graf ).

Teorema 3.10 (Ore, 1961) Un graf G = (V, E) (|V | ≥ 3) este hamiltonian dacă ∀u, v ∈ V
avem dG (u) + dG (v) ≥ n unde u 6= v, [u, v] ∈ / E (pentru oricare două vârfuri distincte,
neadiacente, ale grafului suma gradelor lor este mai mare decât numărul de vârfuri din graf ).

Teorema 3.11 (Chvatal, 1972) Fie un graf G = (V, E) (|V | ≥ 3) şi d1 , d2 , . . . , dn o


secvenţă grafică. Dacă este satisfăcută relaţia
n
∀k a.i. dk ≤ k ≤ ⇒ dn−k ≥ n − k
2
atunci graful este hamiltonian.

68
Definiţia 3.4 Pentru un graf G construim un şir de grafuri G = G1 , G2 , . . . astfel: graful
Gk+1 se obţine din graful Gk prin adăugarea muchiei [uk , vk ], unde vârfurile uk , vk ∈ V nu
sunt adiacente ı̂n Gk şi dG (uk ) + dG (vk ) ≥ n. Procesul se ı̂ncheie ı̂n momentul ı̂n care nu
mai există două vârfuri neadiacente distincte astfel ı̂ncât dG (up ) + dG (vp ) ≥ n. Graful Gp se
numeşte ı̂nchiderea lui G şi se notează cu cl(G).
Lema 3.12 Orice graf prezintă o singură ı̂nchidere.
Corolarul 3.13 Un graf G = (V, E) (|V | ≥ 3) este hamiltonian dacă cl(G) ≅ Kn (dacă
ı̂nchiderea lui G este izomorfă cu graful complet de ordinul n).
Definim δ(G) = min{dG (u)|u ∈ V } şi ∆(G) = max{dG (u)|u ∈ V }.
Corolarul 3.14 Un graf G = (V, E) (|V | ≥ 3) este hamiltonian dacă δ(G) ≥ n2 .
Definiţia 3.5 O mulţime de vârfuri A a unui graf G se spune că este independentă dacă
oricare două elemente distincte din A sunt independente. Numărul de independenţă al lui G,
notat cu β(G), reprezintă numărul maxim de vârfuri dintr–o mulţime independentă.
β(G) = 1 dacă şi numai dacă graful G este complet.
Definiţia 3.6 Pentru un graf G se numeşte conectivitatea lui G, şi notăm cu κ(G),
numărul minim de vârfuri ale unei tăieturi. O tăietură este o submulţime U a lui V astfel
ı̂ncât graful G − U să fie neconex.
Teorema 3.15 Un graf G = (V, E) având ordinul n ≥ 3, este hamiltonian dacă κ(G) ≥
β(G).
Observaţia 3.16 Pentru un graf bipartit G = (V1 , V2 , E) condiţia necesară pentru a fi hamil-
tonian este |V1 | = |V2 |.
Problema comis–voiajorului include problema determinării existenţei unui ciclu hamilto-
nian ı̂ntr–un graf.
Căutarea optimului printre toate variantele de cicluri nu este o soluţie fezabilă deoarece
pentru un graf G numărul de cicluri poate fi foarte mare. De exemplu pentru graful complet
Kn avem (n−1)!
2
cicluri hamiltoniene distincte.
Exemplul 3.17 Graful din figura 2.20 nu este hamiltonian (nu admite un ciclu hamilto-
nian).
• Graful din figura 3.6 are mai multe cicluri hamiltoniene: de exemplu ciclurile C1 = [1, 5,
2, 3, 6, 4, 1] şi C2 = [1, 3, 6, 4, 2, 5, 1].

Nod 1 2 3 4 5 6
dG 4 4 4 4 2 2
Teorema lui Dirac (1952) nu se poate aplica deoarece nu este ı̂ndeplinită condiţia ”orice vârf
al grafului are gradul mai mare decât jumătate din numărul de vârfuri din graf”:
n 6
dG (5) = 2 < = = 3.
2 2
De asemenea, pentru teorema lui Ore (1961) nu este ı̂ndeplinită condiţia ”pentru oricare două
vârfuri dinstincte, neadiacente, ale grafului suma gradelor lor este mai mare decât numărul
de vârfuri din graf”: fie vârfurile 5 şi 6, neadiacente, pentru care avem
dG (5) + dG (6) = 2 + 2 = 4 < 6.

69
• Pentru graful din figura 3.8 avem:

Nod 1 2 3 4 5 6 7 8
dG 3 4 3 4 4 5 3 4

– condiţia teoremei lui Dirac (1952), nu este ı̂ndeplinită: dG (1) = 3 < 4 = n2 ;


– condiţia teoremei lui Ore (1961), nu este ı̂ndeplinită: dG (2) + dG (7) = 4 + 3 = 7 < 8;
– observăm că nici condiţiile teoremei lui Chvatal (1972) nu sunt ı̂ndeplinite: fie d1 , d2 , . . . , dn
o secvenţă grafică.
Nod 1 3 7 2 4 5 8 6
dG 3 3 3 4 4 4 4 5

Relaţia următoare nu este satisfăcută:


n
∀k a.i. dk ≤ k ≤ ⇒ dn−k ≥ n − k.
2
3.2.1 Problema comis–voiajorului
Un comis-voiajor trebuie să viziteze n oraşe etichetate cu numerele de la 1 la n. Pentru a
simplifica problema, acesta va pleca ı̂ntotdeauna din oraşul numărul 1 şi se va ı̂ntoarce tot ı̂n
1, trecând prin fiecare oraş o singură dată (cu excepţia oraşului 1 care va fi vizitat de două
ori). Cunoscând distanţele ı̂ntre oraşe, să se determine o parcurgere (ciclu hamiltonian) de
cost minim.
Pentru a reprezenta drumurile directe dintre oraşe (existenţa unei legături directe ı̂ntre
acestea) utilizăm matricea costurilor A = (aij ), i, j = 1, n:

0
 , dacă xi = xj
ai,j = +∞ /E.
, dacă [xi , xj ] ∈ (3.1)


d > 0 , dacă [xi , xj ] ∈ E
Vectorul soluţiilor rezultat va fi X = (x1 , x2 , . . . , xn+1 ) ∈ V × . . . × V unde x1 = xn+1 = 1:
la pasul i, i = 2, n + 1, se vizitează oraşul xi ∈ V . Primul oraş, oraşul din care se pleacă,
este fixat la 1 (vezi algoritmul 27).
La pasul k (2 ≤ k ≤ n + 1) se ı̂ncearcă vizitarea oraşului xk + 1 dacă nu a fost vizitat
la un pas anterior, şi dacă costul parţial al lanţului este mai mic decât costul celui mai bun
ciclu hamiltonian obţinut până ı̂n acel moment ı̂n graf:
1 ≤ xk < n şi vizitatxk +1 = 0 şi cost + axk−1 ,xk +1 ≤ costoptim (3.2)
(pentru k = n + 1 trebuie verificată şi condiţia xk = 1).
La ı̂nceput costul celui mai scurt ciclu hamiltonian poate fi determinat cu un algoritm
de tip Greedy sau i se poate atribui valoarea +∞ (o valoare suficient de mare, de exemplu
n · max{ai,j |∀i, j = 1, n, i 6= j, ai,j 6= ∞}).
Dacă condiţiile anterioare sunt ı̂ndeplinite, atunci xk ← xk + 1. Se marchează oraşul ales
la pasul k ca fiind vizitat (vizitatk ← 1), se actualizează costul drumului până ı̂n momentul
curent (cost ← cost + axk−1 ,xk ), şi se trece la pasul k + 1.
În caz contrar, nu există nici o alegere convenabilă pentru oraşul de pe poziţia k şi va
trebui să ne ı̂ntoarcem pentru o altă alegere pentru oraşul vizitat la pasul k−1. Pentru aceasta
se marchează oraşul ales la pasul anterior ca nefiind vizitat (vizitatxk ← 0), şi se scade din
costul drumului până ı̂n momentul curent, costul muchiei [xk−1 , xk ] (cost ← cost − axk−1 ,xk ).

70
Fig. 3.8: Exemplu de graf pentru problema comis-voiajorului

Exemplul 3.18 Matricea costurilor corespunzătoare grafului din figura 3.8 are următoarele
valori:
 
0 14 6 ∞ 5 ∞ ∞ ∞
14 0 ∞ 12 16 20 ∞ ∞
 
6 ∞ 0 ∞ ∞ 12 ∞ 12
 
∞ 12 ∞ 0 21 ∞ 24 10
A=  5 16 ∞ 21 0

 16 ∞ ∞
∞ 20 12 ∞ 16 0 14 6
 
∞ ∞ ∞ 24 ∞ 14 0 10
∞ ∞ 12 10 ∞ 6 10 0
Graful respectiv are mai multe cicluri hamiltoniene:

• C1 = [1, 2, 4, 5, 6, 7, 8, 3, 1].
• C2 = [1, 2, 5, 4, 8, 7, 6, 3, 1].
• C3 = [1, 5, 6, 2, 4, 7, 8, 3, 1].
• C4 = [1, 3, 6, 7, 8, 4, 2, 5, 1].

Fig. 3.9: Rezultatul rulării programului pentru determinarea ciclului hamiltonian optim

Programul corespunzător scris ı̂n limbajul C este următorul:

71
Algoritm 27 Algoritm pentru problema comis-voiajorului
(
A - matricea de costuri ce conţine distanţele dintre oraşe
Input:
n - numărul de oraşe
1: function CanContinue(A, X, k, cost)
2: if (vizitatxk = 1) ∨ (cost + axk−1 ,xk > costoptim ) ∨ ((k 6= n + 1) ∧ (xk = x1 ))) then
3: return f alse
4: else
5: return true
6: end if
7: end function

8: procedure Evaluare Solutie(X, cost)


9: X optim ← X
10: costoptim ← cost
11: end procedure

12: procedure ComisVoiajorBacktracking(A, n)


13: x1 ← 1, cost ← 0, costoptim ← ∞
14: k ← 2, xk ← 1
15: while (k > 1) do
16: gasit ← f alse
17: while ((xk + 1 ≤ n) ∧ (gasit = f alse)) do
18: xk ← xk + 1
19: gasit ← CanContinue(A, X, k, cost)
20: end while
21: if (gasit = true) then
22: if (k = n + 1) then
23: call Evaluare Solutie(X, cost + axk−1 ,xk )
24: else
25: vizitatxk ← 1
26: cost ← cost + axk−1 ,xk
27: k ←k+1
28: xk ← 0
29: end if
30: else
31: k ←k−1
32: vizitatxk ← 0
33: if (k > 1) then
34: cost ← cost − axk−1 ,xk
35: end if
36: end if
37: end while
38: Output {’Solutia optima: ’, xoptim
1 , . . . , xoptim
n+1 , cost
optim }

39: end procedure

Listing 3.2: hamilton.c


#include <s t d i o . h>
#include <memory . h>

#define NMAX 100 // numarul maxim de n o d u r i d i n g r a f

72
#define LIMIT 100000 // lung imea maxima a u n e i muchii e c h i v a l e n t a cu i n f i n i t

typedef i nt M a t r i c e [NMAX] [NMAX] ;


typedef i nt Vecto r [NMAX] ;

long c o s t o p t i m ; // c o s t u l c i r c u i t u l u i h a m i l t o n i a n optim
Vecto r x o ptim ; // e l e m e n t e l e c i r c u i t u l u i h a m i l t o n i a n optim

/∗ ∗
∗ F u n c t i a c i t e s t e v a l o r i l e d a t e l o r de i n t r a r e .
∗/
i nt r e a d I n p u t ( M a t r i c e a ) {
i nt n ;
i nt i , j , max = 0 ;

s c a n f ( ”%d” , &n ) ;

for ( i = 1 ; i <= n ; i ++)


for ( j = 1 ; j <= n ; j ++) {
s c a n f ( ”%d” , &a [ i ] [ j ] ) ;
i f (max < a [ i ] [ j ] )
max = a [ i ] [ j ] ;
i f ( a [ i ] [ j ] < 0)
a [ i ] [ j ] = LIMIT ;
}

c o s t o p t i m = n ∗ max ;
return n ;
}

/∗ ∗
∗ F u n c t i a v e r i f i c a daca a + b > c .
∗/
i nt mai mare ( long a , long b , long c ) {
if (a + b > c)
return 1 ;
else
return 0 ;
}

/∗ ∗
∗ Functia a f i s e a z a s o l u t i a c a l c u l a t a a problemei .
∗/
void A f i s a r e S o l u t i e ( i nt n ) {
i nt i ;

p r i n t f ( ” C o s t u l c i c l u l u i optim : %l d \n” , c o s t o p t i m ) ;
p r i n t f ( ” Ciclu hamiltonian : [ ” ) ;
for ( i = 1 ; i < n+1; i ++)
p r i n t f ( ”%d , ” , x o ptim [ i ] ) ;
p r i n t f ( ”%d ] \ n” , x o ptim [ n + 1 ] ) ;
}

/∗ ∗
∗ F u n c t i a de c o n t i n u a r e : a i c i s e v e r i f i c a daca e l e m e n t u l de pe p o z i t i a k
∗ nu s e mai a f l a i n s i r u l A.
∗/
i nt CanContinue ( i nt n , M a t r i c e a , i nt k , long c o s t , Vecto r v i z i t a t , Vecto r x ) {
i f ( ( v i z i t a t [ x [ k ] ] == 1 ) | | ( mai mare ( c o s t , a [ x [ k − 1 ] ] [ x [ k ] ] , c o s t o p t i m ) )

73
| | ( ( k != n+1) && ( x [ k ] == 1 ) ) )
return 0 ;
else
return 1 ;
}

/∗ ∗
∗ F u n c t i a s a l v e a z a s o l u t i a optima ( c i r c u i t u l h a m i l t o n i a n de c o s t minim )
∗ d e t e r m i n a t a / g a s i t a pana i n momentul c u r e n t .
∗/
void E v a l u a r e S o l u t i e ( i nt n , long c o s t , Vecto r x ) {
i nt i ;

cost optim = cost ;


for ( i = 1 ; i <= n+1; i ++)
x o ptim [ i ] = x [ i ] ;
A f i s a r e S o l u t i e (n ) ;
}

/∗ ∗
∗ Metoda b a c k t r a c k i n g implementa ta .
∗/
void C o m i s V o i a j o r B a c k t r a c k i n g ( i nt n , M a t r i c e a ) {
Vecto r x ;
Vecto r v i z i t a t ;
i nt k , g a s i t ;
long c o s t ;

memset ( v i z i t a t , 0 , s i z e o f ( v i z i t a t ) ) ;
x [1] = 1;
cost = 0;
k = 2; x[ k] = 1;
while ( k > 1 ) {
gasit = 0;
while ( ( x [ k ] + 1 <= n ) && ( g a s i t == 0 ) ) {
x[k] = x [k] + 1;
g a s i t = CanContinue ( n , a , k , c o s t , v i z i t a t , x ) ;
}
i f ( g a s i t == 1 ) {
i f ( k == n+1)
EvaluareSolutie (n , cost + a [ x [ k −1]][ x [ k ] ] , x ) ;
else {
vizitat [ x[k ] ] = 1;
cost = cost + a [ x [ k −1]][ x [ k ] ] ;
k = k + 1;
x[ k] = 0;
}
}
else {
k = k − 1;
vizitat [x[ k ] ] = 0;
i f (k > 1)
cost = cost − a [ x [ k −1]][ x [ k ] ] ;
}
}
}

void main ( void ) {


Matrice a ;

74
i nt n = r e a d I n p u t ( a ) ;

ComisVoiajorBacktracking (n , a ) ;
}

Conţinutul fişierului graf1.txt este:


8
0 14 6 -1 5 -1 -1 -1
14 0 -1 12 16 20 -1 -1
6 -1 0 -1 -1 12 -1 12
-1 12 -1 0 21 -1 24 10
5 16 -1 21 0 16 -1 -1
-1 20 12 -1 16 0 14 6
-1 -1 -1 24 -1 14 0 10
-1 -1 12 10 -1 6 10 0

Corespunzător valorii ∞ am utilizat ı̂n fişierul de test valoarea negativă −1. În timpul
citirii datelor aceasta a fost ı̂nlocuită cu o constantă suficient de mare:
...
s c a n f ( ”%d” , &a [ i ] [ j ] ) ;
i f (max < a [ i ] [ j ] )
max = a [ i ] [ j ] ;
i f ( a [ i ] [ j ] < 0)
a [ i ] [ j ] = LIMIT ;

În timpul citirii se determină valoarea maximă, max{ai,j |∀i, j = 1, n, i 6= j, ai,j 6= ∞}, pentru
a se iniţializa mai apoi costul optim cu valoarea n · max.
Funcţia ı̂n care se verifică condiţiile de continuare este următoarea:
i nt CanContinue ( i nt n , M a t r i c e a , i nt k , long c o s t , Vecto r v i z i t a t , Vecto r x ) {
i f ( ( v i z i t a t [ x [ k ] ] == 1 ) | | ( mai mare ( c o s t , a [ x [ k − 1 ] ] [ x [ k ] ] , c o s t o p t i m ) )
| | ( ( k != n+1) && ( x [ k ] == 1 ) ) )
return 0 ;
else
return 1 ;
}

Folosim o funcţie (mai mare()) cu scopul de a realiza verificarea inegalităţii cost +


axk−1 ,xk +1 > costoptim :
i nt mai mare ( long a , long b , long c ) {
if (a + b > c)
return 1 ;
else
return 0 ;
}

şi a preveni eventualele depăşiri (overflow )8 ı̂n timpul calculelor.


Nu am afişat numai soluţia optimă, ci am ales să afişăm toate ciclurile hamiltoniene care
ı̂mbunătăţeau soluţia anterioară:
void E v a l u a r e S o l u t i e ( i nt n , long c o s t , Vecto r x ) {
i nt i ;

cost optim = cost ;


8
http://en.wikipedia.org/wiki/Arithmetic_overflow

75
for ( i = 1 ; i <= n+1; i ++)
x o ptim [ i ] = x [ i ] ;
A f i s a r e S o l u t i e (n ) ;
}

Partea principală a programului, motorul, este constituită din funcţia:


void C o m i s V o i a j o r B a c k t r a c k i n g ( i nt n , M a t r i c e a ) {
Vecto r x ;
Vecto r v i z i t a t ;
i nt k , g a s i t ;
long c o s t ;

memset ( v i z i t a t , 0 , s i z e o f ( v i z i t a t ) ) ;
x [1] = 1;
cost = 0;
k = 2; x[ k] = 1;
while ( k > 1 ) {
gasit = 0;
while ( ( x [ k ] + 1 <= n ) && ( g a s i t == 0 ) ) {
x[k] = x [k] + 1;
g a s i t = CanContinue ( n , a , k , c o s t , v i z i t a t , x ) ;
}
i f ( g a s i t == 1 ) {
i f ( k == n+1)
EvaluareSolutie (n , cost + a [ x [ k −1]][ x [ k ] ] , x ) ;
else {
vizitat [ x[k ] ] = 1;
cost = cost + a [ x [ k −1]][ x [ k ] ] ;
k = k + 1;
x[ k] = 0;
}
}
else {
k = k − 1;
vizitat [x[ k ] ] = 0;
i f (k > 1)
cost = cost − a [ x [ k −1]][ x [ k ] ] ;
}
}
}

Pentru n = 8, mulţimea Ai va fi alcătuită din elementele {1, 2, . . . , 8}. La ı̂nceput, x1 va


primi valoarea 1, iar pentru k = 2, x2 va primi tot valoarea 1:
x1 x2 x3 x4 x5 x6 x7 x8 x9
1 1
Pentru k = 2, se verifică condiţiile de continuare pentru x2 = x2 + 1 = 1 + 1 = 2, şi
deoarece acestea sunt respectate, se trece la pasul următor (oraşul următor) (la iniţializare
avem k = k + 1 = 3, x3 = 0):
x1 x2 x3 x4 x5 x6 x7 x8 x9
1 2 0
Se verifică mai multe valori (1, 2 şi 3) pentru x3 , pentru care condiţiile de continuare nu
sunt ı̂ndeplinite. Pentru x3 = 4 condiţiile de continuare sunt ı̂ndeplinite, şi se trece la pasul
următor, k = k + 1 = 4:
x1 x2 x3 x4 x5 x6 x7 x8 x9
1 2 4 0

76
Pentru k = 4, prima valoare ce verifică condiţiile de continuare este 5:
x1 x2 x3 x4 x5 x6 x7 x8 x9
1 2 4 5 0
În mod asemănător se aleg valorile pentru paşii k = 5, 6, 7, 8:
x1 x2 x3 x4 x5 x6 x7 x8 x9
1 2 4 5 6 3 8 7 0
La pasul k = 9, nici una dintre valorile 1, 2, . . . , 8 nu verifică condiţiile de continuare (fiind
la ultimul oraş, acesta ar trebui să fie 1, adică oraşul de unde s-a pornit). Astfel, se trece la
pasul anterior:
k = k - 1;
vizitat[x[k]] = 0;
if (k > 1)
cost = cost - a[x[k-1]][x[k]];

adică la pasul k = k − 1 = 9 − 1 = 8.
Valoarea 8, următoarea valoare din domeniul de valori neverificată şi singura ce mai
rămăsese de atribuit pentru x8 , nu verifică condiţiile de continuare, şi deoarece nu mai sunt
valori de testat la pasul k = 8, se trece la nivelul anterior: k = k − 1 = 8 − 1 = 7.
La pasul k = 7, deoarece nu au mai rămas valori netestate din mulţimea A7 = {1, 2, . . . , 8},
se trece la pasul k = 6:
x1 x2 x3 x4 x5 x6 x7 x8 x9
1 2 4 5 6 3
Aici continuăm cu verificarea valorilor 4, 5, 6 şi 7. Pentru x6 = 7 sunt verificate condiţiile
de continuare şi se poate trece la pasul următor, k = 7:
x1 x2 x3 x4 x5 x6 x7 x8 x9
1 2 4 5 6 7 0
Doar pentru valoarea 8 sunt ı̂ndeplinite condiţiile de continuare:
x1 x2 x3 x4 x5 x6 x7 x8 x9
1 2 4 5 6 7 8 0
La pasul k = 8, xk = 3 ı̂ndeplineşte conţiile de continuare şi se poate trece la nivelul
următor, k = 9:
x1 x2 x3 x4 x5 x6 x7 x8 x9
1 2 4 5 6 7 8 3 0
Aici soluţia optimă găsită este următoarea:
x1 x2 x3 x4 x5 x6 x7 x8 x9
1 2 4 5 6 7 8 3 1
având un cost al ciclului hamiltonian de 105.
Se păstrează soluţia optimă identificată până ı̂n acest moment, se afişează, şi se continuă
efectuarea calculelor metodei până la epuizarea spaţiului soluţiilor posibile.

77
Fig. 3.10: Pasii efectuati de algoritm pentru determinarea ciclului hamiltonian asociat grafului din figura 3.8

78
Fig. 3.11: Pasii efectuati de algoritm pentru determinarea ciclului hamiltonian asociat grafului din figura 3.8
(continuare)

79
Capitolul 4

Arbori. Arbori binari

Definiţia 4.1 Fiind dată o mulţime M de elemente denumite noduri, vom numi arbore o
submulţime finită de noduri astfel ı̂ncât:

1. există un nod cu destinaţie specială, numit rădăcina arborelui;


2. celelalte noduri sunt repartizate ı̂n n mulţimi distincte, disjuncte două câte două, A1 , A2 , . . . , An ,
fiecare mulţime Ai constituind la rândul ei un arbore.

Această definiţie este una recursivă, construcţia unui arbore depinzând de alţi arbori.
Descendenţii direcţi ai rădăcinii arborelui sunt rădăcinile subarborilor A1 , A2 , . . . , An , n ≥ 1.
Orice nod al unui arbore cu rădăcină constituie rădăcina unui subarbore compus din nodul
respectiv şi toţi descendenţii săi.
Se observă că un arbore impune o structură de organizare ierarhică asupra elementelor
unei mulţimi.

Definiţia 4.2 Se numeşte arbore liber (”free tree”) un graf G = (V, E) neorientat, conex
şi aciclic.

Teorema 4.1 (Proprietăţile arborilor liberi) Fie G = (V, E) un graf neorientat. Următoarele
afirmaţii sunt echivalente:

1. G este un arbore liber.


2. Oricare două vârfuri din G sunt conectate printr-un drum elementar unic.
3. G este conex, dar, dacă eliminăm o muchie oarecare din E, graful obţinut nu mai este conex.
4. G este conex, şi |E| = |V | − 1.
5. G este aciclic, şi |E| = |V | − 1.
6. G este aciclic, dar dacă adăugăm o muchie oarecare ı̂n E, graful obţinut conţine un ciclu.

Dacă se alege un vârf sau nod drept rădăcina arborelui, atunci un arbore liber devine
arbore cu rădăcină. În general vom folosi termenul de arbore ı̂n loc de termenul corect arbore
cu rădăcină.
Un vârf terminal (frunză) este un vârf fără descendenţi. Vârfurile care nu sunt terminale
sunt neterminale (sau noduri interioare). De obicei, se consideră că există o ordonare a
descendenţilor aceluiaşi părinte.

80
1

2 3

4 5 6 7

8 9 10 11 12 13 14 15

Fig. 4.1: Exemplu de arbore binar plin cu 24 − 1 vârfuri

Definiţia 4.3 Adâncimea unui vârf este dată de lungimea drumului de la rădăcină la acel
vârf. Înălţimea unui vârf se determină ca fiind lungimea celui mai lung drum dintre acel vârf
şi un vârf terminal. Înălţimea rădăcinii determină ı̂nălţimea arborelui. Nivelul unui vârf se
calculează ca diferenţa dintre ı̂nălţimea arborelui şi adâncimea acestui vârf.

Toate vârfurile unui arbore ce au aceeaşi adâncime se spune că sunt pe acelaşi nivel. Un
arbore oarecare cu rădăcină este n-ar dacă fiecare vârf are până la n descendenţi direcţi.
O mulţime de arbori disjuncţi formează o pădure.
Din punct de vedere grafic, un arbore se poate reprezenta descriind nodurile cu ajutorul
unor cercuri sau pătrate ı̂n care se află informaţia aferentă, iar relaţia de descendenţă prin
legăturile ce unesc nodurile cu fii lor.

4.1 Arbori binari


Definiţia 4.4 Un arbore binar este un arbore oarecare cu rădăcină, fiecare vârf al său
având cel mult doi descendenţi, indicându-se cine este descendentul stâng al rădăcinii şi cine
este cel drept.

Trebuie menţionat că un arbore binar nu este un caz particular de arbore oarecare,
deoarece ı̂n cazul unui arbore oarecare este suficient să spunem că că un vârf are un singur
decendent spre deosebire de cazul arborelui binar când trebuie precizat dacă descendentul
este drept sau stâng. Arborele binar ce nu conţine nici un nod se numeşte arbore vid sau
arbore nul.

Definiţia 4.5 Se numeşte arbore binar plin un arbore binar cu 2k − 1 vârfuri aşezate pe
k nivele astfel ı̂ncât pe fiecare nivel i avem 2i−1 vârfuri.

Se observă că fiecare vârf al arborelui are doi descendenţi, descendentul stâng şi descen-
dentul drept, cu excepţia nodurilor terminale.
În figura 4.1 este prezentat un arbore binar plin ı̂n care vârfurile sunt aşezate pe 4 niveluri
(k = 4). De regulă vârfurile unui arbore binar plin se numerotează ı̂n ordinea nivelurilor, iar
ı̂n cadrul unui nivel, de la stânga la dreapta.

Definiţia 4.6 Se numeşte arbore binar complet cu n vârfuri un arbore binar plin având
k nivele, unde 2k−1 ≤ n < 2k , din care se elimină vârfurile numerotate n+ 1, n+ 2, . . . , 2k −1.

81
1

2 3

4 5 6 7

8 9 10 11 12

Fig. 4.2: Exemplu de arbore binar complet cu 12 vârfuri

În figura 4.2 este ilustrat un exemplu de arbore binar complet.


Un arbore binar poate fi transformat ı̂ntr–un arbore binar plin ı̂n modul următor: com-
pletăm arborele cu noi vârfuri astfel ı̂ncât fiecare vârf să aibă doi descendenţi sau nici unul,
renumerotăm vârfurile iar vârfurilor nou introduse li se asociază o valoare ce va indica faptul
că ele sunt fictive.

4.1.1 Moduri de reprezentare


Cele mai cunoscute modalităţi de reprezentare a arborilor binari sunt:

1. expresii cu paranteze;

2. forma standard ;

3. reprezentarea tip tată.

2 8

3 5 9

4 6 7

Fig. 4.3: Exemplu de arbore binar

1. expresii cu paranteze– expresia ı̂ncepe cu rădăcina şi după fiecare vârf k urmează expre-
siile subarborilor ce au ca rădăcini descendenţii vârfului respectiv, separate prin virgulă
şi incluse ı̂ntre paranteze. Dacă un descendent al unui vârf nu există atunci expresia
respectivă este ı̂nlocuită cu 0 sau NULL. Un arbore cu un singur vârf (rădăcina),
etichetat cu a1 , se reprezintă astfel: a1 (0, 0). Un arbore format din rădăcină a1 şi doi
descendenţi, a2 şi a3 , se reprezintă astfel: a1 (a2 (0, 0), a3(0, 0)).
Pentru arborele din figura 4.3 avem:

1(2(3(0, 4(0, 0)), 5(6(0, 0), 7(0, 0))), 8(0, 9(0, 0)))

82
2. forma standard – se indică rădăcina arborelui, iar pentru fiecare vârf k se precizează
descendentul său stâng şi/sau drept.
(
i , dacă nodul i este descendentul stâng al nodului k
Stangk =
0 , dacă nu există descendentul stâng
(
i , dacă nodul i este descendentul drept al nodului k
Dreptk =
0 , dacă nu există descendentul drept.
Pentru arborele din figura 4.3 avem următoarea reprezentare (Rad = 1):
Nod 1 2 3 4 5 6 7 8 9
Stang 2 3 0 0 6 0 0 0 0
Drept 8 5 4 0 7 0 0 9 0

Fig. 4.4: Exemplu de arbore binar cu 8 noduri

După modelul listei liniare dublu ı̂nlăţuită, putem folosi o structură de date asemănătoare
pentru reprezentarea unui arbore binar. Un nod al arborelui are următoarea configuraţie:

typedef struct nod {


TipOarecare data;
struct nod* stang;
struct nod* drept;
}Nod;

Nod ∗ rad; – rad este o variabilă de tip pointer la Nod (variabila păstrează adresa
unei zone de memorie ce conţine date numerice de tip Nod). rad desemnează adresa
rădăcinii arborelui.
În figura 4.5 avem reprezentarea arborelui din figura 4.4.
3. reprezentarea tip tată – fiecărui vârf k i se indică tatăl nodului respectiv.
(
i , dacă nodul i este părintele nodului k
tatak =
0 , nu există

Nod 1 2 3 4 5 6 7 8 9
Tata 0 1 2 3 2 5 5 1 8

Observaţia 4.2 Următoarea numerotare este utilizată pentru anumite tipuri de arbori
binari, şi are drept caracteristică faptul că structura arborelui se poate deduce printr-o
numerotare adecvată a vârfurilor:

83
Fig. 4.5: Exemplu de arbore binar cu 8 noduri

(
⌊ 2i ⌋ , i≥2
T atai =
nu există , i=1
( (
2·i , 2·i≤n 2·i+1 , 2·i+1≤ n
Stangi = Drepti =
nu există , 2·i>n nu există , 2 · i + 1 > n

4.1.2 Metode de parcurgere


Există mai multe moduri de parcurgere a unui arbore binar. Indiferent de metoda de parcurg-
ere aleasă se parcurge mai ı̂ntâi subarborele stâng şi apoi subarborele drept. După momentul
ı̂n care un nod k este vizitat faţă de subarborii săi stâng şi drept, avem:

• parcurgere ı̂n preordine (rădăcină, subarbore stâng, subarbore drept).


În urma parcurgerii ı̂n preordine a arborelui din figura 4.3 rezultă următoarea ordine
pentru noduri: 1, 2, 3, 4, 5, 6, 7, 8, 9.
În figura 4.6 este prezentată parcurgerea ı̂n preordine a unui arbore binar, descrisă ı̂n
mai mulţi paşi, conform descrierii recursive a acestei metode.
Vom da exemplu de o procedură nerecursivă de vizitare ı̂n preordine (vezi algoritmul
28). Se pleacă de la rădăcină şi se merge spre stânga atâta timp cât este posibil (liniile
5–8). Dacă nu se mai poate merge spre stânga se ı̂ncearcă continuarea algorimului
din descendentul drept, dacă este posibil. Dacă nu se poate face un pas spre dreapta
(descendentul drept nu există – linia 10), se va urca ı̂n arbore câte un nivel (liniile
12–22), până când se ajunge ı̂n situaţia de a se veni din descendentul stâng (linia 19)
şi se reia algoritmul trecând ı̂n descendentul drept, dacă există (linia 25). În momentul
ı̂n care s–a ajuns ı̂n nodul rădăcină venind din dreapta, algoritmul se ı̂ncheie. Datorită
faptului că atunci când se urcă ı̂n arbore trebuie să ştim din ce descendent venim, vom
utiliza şi vectorul tata.
Această procedură poate fi utilizată şi pentru celelalte moduri de parcurgere, modificările
necesare referindu–se la momentul când trebuie vizitat nodul curent (prin apelul pro-
cedurii V izit - instrucţiunea call V izit(k)).

84
Fig. 4.6: Parcurgerea ı̂n preordine a unui arbore binar a) arborele iniţial; b) arborele vizitat ı̂n preordine,
primul nivel; c) arborele vizitat ı̂n preordine, al doilea nivel; d) arborele vizitat ı̂n preordine, al treilea nivel.

Implementarea ı̂n limbajul C a algoritmului de parcurgere ı̂n preordine este:

#include <stdio.h>

#define MAX 100

/** stang[k] - descendentul stang al nodului k; 0 daca nu exista */


char stang[MAX];
/** drept[k] - descendentul drept al nodului k */
char drept[MAX];
/** tata[k] - parintele nodului k */
char tata[MAX];
/** numarul de noduri din arbore */
int n;
/** nodul radacina */
int rad;

void vizit(int nod) {


printf("%2d ", nod);
}

void readInput(void) {
int k;

printf("n = "); scanf("%d", &n);


printf("rad = "); scanf("%d", &rad);

for (k = 1; k <= n; k++) {


printf("stang[%d] = ", k); scanf("%d", &stang[k]);
}

85
Algoritm 28 Algoritm de parcurgere ı̂n preordine
1: procedure Preordine(Rad, Stang, Drept, T ata)
2: k ← rad
3: q←0
4: while (q = 0) do
5: while (stangk 6= 0) do
6: call V izit(k) ⊲ prelucrarea informaţiei din nodul vizitat
7: k ← stangk
8: end while
9: call V izit(k)
10: while (q = 0) ∧ (dreptk = 0) do
11: f ound ← 0
12: while (q = 0) ∧ (f ound = 0) do
13: j←k
14: k ← tatak
15: if (k = 0) then
16: q←1
17: else
18: if (j = stangk ) then
19: f ound ← 1
20: end if
21: end if
22: end while
23: end while
24: if (q = 0) then
25: k ← dreptk
26: end if
27: end while
28: return
29: end procedure

for (k = 1; k <= n; k++) {


printf("drept[%d] = ", k); scanf("%d", &drept[k]);
}

for (k = 1; k <= n; k++) {


printf("tata[%d] = ", k); scanf("%d", &tata[k]);
}
}

void preord(int rad) {


int i, j;
int q, found;

i = rad; q = 0;
while (q == 0) {
while (stang[i] != 0) {
vizit(i);
i = stang[i];

86
}
vizit(i);
while ((q == 0) && (drept[i] == 0)) {
found = 0;
while ((q == 0) && (found == 0)) {
j = i;
i = tata[i];
if (i == 0)
q = 1;
else
if (j == stang[i])
found = 1;
}
}
if (q == 0)
i = drept[i];
}
}

void main(void){
readInput();
preord(rad);
}

• parcurgere ı̂n inordine (arbore stâng, rădăcină, arbore drept): 3, 4, 2, 6, 5, 7, 1, 8, 9.


Metoda de parcurgere ı̂n inordine este ilustrată de algoritmul 29 [93], [123]. Vom utiliza
o stivă S unde se vor introduce nodurile din care se coboară spre stânga (liniile 6–9).
Atunci când nu se mai poate merge spre stânga se ı̂ncearcă continuarea algoritmului din
descendentul drept al nodului curent. Dacă acesta nu există, se reface drumul ı̂napoi
spre rădăcină, compus numai din descendenţii stângi (liniile 11–18). Parcurgerea se
termină ı̂n momentul ı̂n care stiva este vidă (vezi algoritmul 29).
Implementarea ı̂n limbajul C a algoritmului 29 este:

#include <stdio.h>

#define MAX 100

/** stang[k] - descendentul stang al nodului k; 0 daca nu exista */


char stang[MAX];
/** drept[k] - descendentul drept al nodului k */
char drept[MAX];
/** Numarul de noduri din arbore */
int n;
/** Nodul radacina */
int rad;

void vizit(int nod) {


printf("%2d ", nod);
}

void readInput(void) {

87
Algoritm 29 Algoritm de parcurgere ı̂n inordine
1: procedure Inordine(Rad, Stang, Drept)
2: k ← rad
3: q←0
4: S←∅ ⊲ iniţializare stivă vidă
5: while (q = 0) do
6: while (stangk 6= 0) do
7: S⇐k ⊲ S.push(k)
8: k ← stangk
9: end while
10: call V izit(k)
11: while (q = 0) ∧ (dreptk = 0) do
12: if (S = ∅) then ⊲ verificare dacă stiva este vidă
13: q←1
14: else
15: S⇒k ⊲ S.pop(k)
16: call V izit(k)
17: end if
18: end while
19: if (q = 0) then
20: k ← dreptk
21: end if
22: end while
23: return
24: end procedure

int k;

printf("n = "); scanf("%d", &n);


printf("rad = "); scanf("%d", &rad);

for (k = 1; k <= n; k++) {


printf("stang[%d] = ", k); scanf("%d", &stang[k]);
}

for (k = 1; k <= n; k++) {


printf("drept[%d] = ", k); scanf("%d", &drept[k]);
}
}

void inord(int rad) {


int i, cap;
int q;
int stack[MAX];

i = rad; q = 0;
cap = -1;

while (q == 0) {
while (stang[i] > 0) {

88
cap++;
stack[cap] = i;
i = stang[i];
}
vizit(i);
while ((q == 0) && (drept[i] == 0)) {
if (cap < 0)
q = 1;
else {
i = stack[cap];
cap--;
vizit(i);
}
}
if (q == 0)
i = drept[i];
}
}

void main(void) {
readInput();
inord(rad);
}

• parcurgere ı̂n postordine (subarbore stâng, subarbore drept, rădăcină): 4, 3, 6, 7, 5, 2, 9, 8, 1


(vezi algoritmul 30).

Algoritm 30 Algoritm de parcurgere ı̂n postordine


1: procedure Postordine(K, Stang, Drept)
2: if (k 6= 0) then
3: call P ostordine(stangk , stang, drept)
4: call P ostordine(dreptk , stang, drept)
5: call V izit(k)
6: end if
7: return
8: end procedure

Modalitatea de vizitare recursivă a arborilor binari este exemplificată la arborii binari de


sortare.
Dacă presupunem realizarea unei acţiuni de vizitare pe exteriorul frontierei arborelui,
deplasarea efectuându-se ı̂n sens trigonometric şi având drept punct de plecare rădăcina
acestuia, vom ajunge să vizităm cel puţin o dată toate nodurile arborelui (vezi figura 4.7).
Ţinând cont de momentul vizitării se poate obţine oricare dintre metodele de parcurgere
(preordine, inordine, postordine): pentru preordine vom marca un nod ı̂n momentul ı̂n care
ı̂l vizităm pentru prima dată, ı̂n cazul metodei de vizitare ı̂n inordine vom marca un nod
ı̂n momentul ı̂n care ı̂l vizităm pentru prima dată dacă este frunză, respectiv a doua oară
dacă este un nod interior, iar la postordine vom marca un nod ı̂n momentul ı̂n care ı̂l vizităm
pentru ultima oară.

89
1

2 8

3 5 9

6 7

Fig. 4.7: Vizitarea unui arbore pe frontieră

Fig. 4.8: Arbore asociat expresiei aritmetice 2 · ((a + b) − (c + b))

Arborele asociat unei expresii aritmetice


Oricărei expresii aritmetice i se poate asocia un arbore binar astfel:

• fiecărui operator ı̂i corespunde un nod neterminal având drept subarbori stâng şi drept
cei doi operanzi corespunzători.

• fiecare nod terminal este etichetat cu o variabilă sau o constantă.

Arborele din figura 4.8 corespunde expresiei matematice: 2 · ((a + b) − (c + b)). În urma
vizitării ı̂n postordine a acestui arbore se obţine forma poloneză postfixată asociată expresiei
anterioare: 2ab + cb + −∗. Pentru a obţine valoarea expresiei respective, arborele poate fi
parcurs ı̂n postordine.

4.2 Arbori binari de căutare


Definiţia 4.7 [93] Se numeşte arbore binar de căutare un arbore binar ce verifică următoarea
proprietate:

90
pentru orice vârf i aparţinând arborelui, avem
(
Infi ≥ Infj , pentru toate vârfurile j ce aparţin descendentului stâng al vârfului i
Infi ≤ Infj , pentru toate vârfurile j ce aparţin descendentului drept al vârfului i
unde Infi este informaţia asociată vârfului i. Această informaţie este de un tip de date peste
care avem o relaţie de ordine (de obicei un tip numeric sau tipul şir de caractere).
Un arbore binar de căutare mai este cunoscut şi sub numele de arbore binar de sortare
deoarece parcurgerea ı̂n inordine a unui arbore binar de căutare ne conduce la obţinerea
elementelor vectorului Inf ı̂n ordine crescătoare. În principal această structură de date este
utilizată pentru regăsirea eficientă a unor informaţii.
Operaţiile de creare a arborelui, ştergere a unui nod, modificare a informaţiei asociate
unui nod sau inserare a unui nod se pot realiza ı̂ntr-un mod optim astfel ı̂ncât să nu se
distrugă proprietatea de arbore de căutare.
Dacă dorim ca arborele să nu conţină chei duplicate, vom modifica definiţia de mai sus
astfel:
pentru orice vârf i aparţinând arborelui, avem
(
Infi < Infj , pentru toate vârfurile j ce aparţin descendentului stâng al vârfului i
Infi > Infj , pentru toate vârfurile j ce aparţin descendentului drept al vârfului i.

În continuare vom utiliza această definiţie pentru descrierea algoritmilor ce urmează.

Crearea unui arbore de căutare


Crearea (construirea) unui arbore de căutare se face aplicând ı̂n mod repetat operaţia de
inserare.

Căutarea unei informatii ı̂ntr-un arbore de căutare


Informaţia asociată unui nod mai poartă numele de cheie.
Să presupunem că dorim să căutăm o cheie x ı̂ntr-un arbore de căutare. Vom compara
mai ı̂ntâi cheia x cu informaţia asociată rădăcinii arborelui (vezi algoritmul 31):
1. dacă cheia x este mai mică decât cheia rădăcinii, atunci se va continua căutarea ı̂n
subarborele stâng;
2. dacă cheia x este mai mare decât cheia rădăcinii, atunci se va continua căutarea ı̂n
subarborele drept;
3. dacă cheia x este egală cu cheia rădăcinii, atunci căutarea se ı̂ncheie cu succes.

Inserarea unui nod ı̂ntr-un arbore de căutare


Aceasta operaţie prezintă următoarele particularităţi (vezi algoritmul 32):
• se verifică prin intermediul operaţiei de căutare dacă cheia x a nodului care se doreşte
să fie inserat mai există ı̂n arbore;
• ı̂n cazul ı̂n care căutarea se ı̂ncheie cu succes, inserarea nu va mai avea loc;
• ı̂n cazul ı̂n care operaţia de căutare se termină fără succes, atunci se va crea un nod
nou ı̂n locul subarborelui vid unde s-a terminat căutarea.
În figura 4.9 este ilustrată inserarea unui nod ı̂ntr–un arbore binar.

91
Algoritm 31 Algoritm de căutare a unei valori ı̂ntr-un arbore binar de căutare
1: function SearchNode(p, Key)
2: if (p 6= N U LL) then
3: if (p.inf o > key) then
4: return SearchN ode(p.lef t, key)
5: else
6: if (p.inf o < key) then
7: return SearchN ode(p.right, key)
8: else
9: return p
10: end if
11: end if
12: else
13: return N U LL
14: end if
15: end function

Algoritm 32 Algoritm de inserare ı̂ntr-un arbore binar de căutare


1: function InsertNode(p, Key)
2: if (p = N U LL) then
3: call CreateN ode(p, key)
4: else
5: if (p.inf o > key) then
6: p.lef t ← InsertN ode(p.lef t, key)
7: else
8: if (p.inf o < key) then
9: p.right ← InsertN ode(p.right, key)
10: end if
11: end if
12: end if
13: return p
14: end function

Ştergerea unui nod dintr-un arbore de căutare


Mai ı̂ntâi se va căuta nodul ce se doreşte să fie eliminat.
Să presupunem că am găsit nodul ce urmează a fi şters. Avem următoarele situaţii (vezi
algoritmul 34):

1. nodul ce urmează să fie şters este un nod terminal - se face ştergerea normal având
grijă să se ı̂nlocuiască legătura din nodul părinte către el cu null;

2. nodul ce urmează să fie şters are un singur descendent - nodul respectiv se şterge iar
părintele va conţine acum noua legătură către descendentul fostului fiu;

3. nodul ce urmează a fi şters (notat A) are doi descendenţi:

• se determină cel mai din stânga nod (notat B) din subarborele drept al nodului
ce trebuie şters (vezi algoritmul 33); acesta va fi şters ı̂n mod fizic, dar la un pas
ulterior;

92
Fig. 4.9: Arbore binar de căutare. Inserarea unui nod.

• toate informaţiile legate de datele conţinute ı̂n nodul B vor fi copiate ı̂n nodul A;

• subarborele drept al nodului B se va lega fie ca descendent drept al nodului A


(dacă nodul B este descendent direct al nodului A), fie ca descendent stâng al
tatălui nodului B;

• se va şterge fizic nodul B.

Exemplul 4.3 Fie un arbore binar, eticheta fiecărui nod fiind un şir de caractere (un cuvânt).
Se cere să se realizeze un program care să creeze un arbore binar de sortare, să-l parcurgă ı̂n
inordine şi să permită inserarea şi ştergerea unui nod specificat.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <conio.h>

typedef struct nod {

93
Fig. 4.10: Arbore binar de căutare. Ştergerea unui nod. a) Nod fără descendenţi b) Nod cu un singur
descendent c) Nod cu doi descendenţi.

94
Algoritm 33 Algoritm pentru determinarea celui mai din stânga nod
1: function LeftmostNode(P arent, Curent)
2: while (curent.lef t 6= N U LL) do
3: parent ← curent
4: curent ← curent.lef t
5: end while
6: if (parent.right = curent) then
7: parent.right ← curent.right
8: else
9: parent.lef t ← curent.right
10: end if
11: return curent
12: end function

char *cuvint;
struct nod *left,*right;
} NOD;

NOD *rad;

NOD* Search(NOD *p, char *sir) {


int ind;

if (p != NULL) {
ind = strcmp(p->cuvint, sir);
if (ind < 0)
return Search(p->right, sir);
else
if (ind > 0)
return Search(p->left, sir);
else
return p;
}
else
return NULL;
}

NOD* CreareNod(char *sir) {


NOD *p;

p = (NOD*)malloc(sizeof(NOD));
p->left = p->right = NULL;
p->cuvint = (char*)malloc(sizeof(char) * (strlen(sir) + 1));
strcpy(p->cuvint, sir);

return p;
}

NOD* Insert(NOD *p, char *sir) {


int ind;

95
Algoritm 34 Algoritm de ştergere a unui nod ı̂ntr-un arbore binar de căutare
1: function DeleteNode(p, Key)
2: if (p 6= N U LL) then
3: if (p.inf o > key) then
4: p.lef t ← DeleteN ode(p.lef t, key)
5: else
6: if (p.inf o < key) then
7: p.right ← DeleteN ode(p.right, key)
8: else
9: if (p.lef t = N U LL) ∨ (p.right = N U LL) then
10: if (p.lef t 6= N U LL) then
11: tmp ← p.lef t
12: else
13: tmp ← p.right
14: end if
15: call DisposeN ode(p)
16: p ← tmp
17: else
18: tmp ← p.right
19: tmp ← Lef tmostN ode(p, tmp)
20: tmp.lef t ← p.lef t
21: tmp.right ← p.right
22: call DisposeN ode(p)
23: p ← tmp
24: end if
25: end if
26: end if
27: end if
28: return p
29: end function

if (p == NULL)
p = CreareNod(sir);
else {
ind = strcmp(p->cuvint, sir);
if (ind < 0)
p->right = Insert(p->right, sir);
else
if (ind > 0)
p->left = Insert(p->left, sir);
}

return p;
}

void VisitInord(NOD *p) {


if (p != NULL) {
VisitInord(p->left);
printf("[%s] ", p->cuvint);

96
VisitInord(p->right);
}
}

NOD* LeftmostNod(NOD* parent, NOD* curent) {


while (curent->left != NULL) {
parent = curent;
curent = curent->left;
}

if (parent->right == curent)
parent->right = curent->right;
else
parent ->left = curent->right;

return curent;
}

NOD* ElibNod(NOD *p) {


free(p->cuvint);
free(p);
return NULL;
}

NOD* DeleteNod(NOD* p, char *sir) {


NOD *tmp;
int ind;

if (p != NULL){
ind = strcmp(p->cuvint, sir);
if (ind < 0)
p->right = DeleteNod(p->right, sir);
else
if (ind > 0)
p->left = DeleteNod(p->left, sir);
else
if (p->left == NULL || p->right == NULL) {
if (p->left != NULL)
tmp = p->left;
else
tmp = p->right;
ElibNod(p);
p = tmp;
}
else{
tmp = p->right;
tmp = LeftmostNod(p, tmp);
tmp->left = p->left;
tmp->right = p->right;
ElibNod(p);
p = tmp;
}

97
}

return p;
}

void main(void) {
char cuvint[100];
char ch;
NOD *p;

rad = NULL;
while (1) {
printf("***********************************\n");
printf("1. Inserare \n");
printf("2. Cautare\n");
printf("3. Stergere\n");
printf("4. Afisare arbore\n");
printf("0. Exit \n");

ch = getch();
if (ch != ’0’ && ch != ’4’) {
printf("Cuvint: "); scanf("%s", cuvint);
}

switch (ch) {
case ’1’: rad = Insert(rad, cuvint); break;
case ’2’: p = Search(rad, cuvint);
if (!p)
printf("Cuvintul %s nu a fost gasit! \n", cuvint);
else
printf("Cuvintul %s exista in arbore! \n", cuvint);
break;
case ’3’: rad = DeleteNod(rad, cuvint); break;
case ’4’: VisitInord(rad); break;
case ’0’: exit(1);
}
}
}

4.3 Exerciţii
1. (a) Să se realizeze o subrutină ce determină cea mai mare valoare păstrată ı̂ntr–un
arbore binar de căutare;
(b) Aceeaşi cerinţă pentru cea mai mică valoare.
2. Să se realizeze o subrutină ce determină toate nodurile unui arbore binar de căutare cu
proprietatea că informaţia k asociată unui nod verifică relaţia a ≤ k ≤ b, unde a şi b
sunt date.
3. (a) Să se realizeze un algoritm care determină numărul arborilor binari distincţi cu
n noduri, unde n este un număr natural dat.

98
Fig. 4.11: Exemplu de arbore binar complet cu 7 vârfuri

(b) Aceeaşi problemă pentru arbori binari de căutare.

4. Să considerăm o secvenţă de intrare compusă din termeni definiţi recursiv astfel:

(a) X este un termen;

(b) dacă A şi B sunt termeni, atunci (A B) este un termen;

(c) orice termen se construieşte cu ajutorul regulilor (i) şi (ii).

Un termen este transformat cu ajutorul următoarei reguli de rescriere: şablonul


(((X A) B) C) unde A, B şi C sunt termeni, este ı̂nlocuit cu ((A C)(B C)).
Reducerea unui termen ı̂nseamnă aplicarea acestei reguli. Un termen se numeşte redus
dacă nu conţine nici un subtermen care să poată fi redus. Un termen poate fi considerat
ca subtermen pentru el ı̂nsuşi.
Să se realizeze un algoritm care determină pentru un termen de intrare, termenul redus
corespunzător. Un termen conţine doar simbolurile ’X’, ’(’ şi ’)’, fără spaţii ı̂ntre ele.
Lungimea unui termen de intrare este 100.

Observaţia 4.4 O expresie va avea maxim 9 nivele de parantezare. Numărul de redu-


ceri ce pot fi efectuate este finit. Liniile de ieşire conţin maxim 80 caractere. Caracterele
posibile din termenii de ieşire sunt tot ’X’, ’(’ şi ’)’.

Intrare Ieşire
(((XX)X)X) ((XX)(XX))
(((XX)(XX)X) ((XX)((XX)X))
(XX) (XX)
(ACM, Bucureşti, 1996)

5. Prin arbore binar complet ı̂nţelegem un arbore binar ı̂n care un nod are fie doi descendenţi,
fie nu are nici unul.
Un arbore binar complet poate fi reprezentat prin codificarea drumurilor de la rădăcină
la fiecare frunză utilizând numai valorile 0 şi 1: atunci când coborâm ı̂n arbore spre
stânga se adaugă valoarea 0 iar atunci când coborâm spre dreapta se adaugă valoarea
1.

99
Pentru arborele din figura 4.11 drumurile de la rădăcină la fiecare frunză se codifică
astfel:
ABC : 00
ABDE : 010
ABDF : 011
AG : 1
Concatenând toate drumurile de la rădăcină către frunze, arborele anterior poate fi
reprezentat prin secvenţa ce poate fi interpretată ca reprezentarea ı̂n baza 2 a unui
număr (000100111).
Fiind date m şi n două numere naturale, se cere să se determine dacă există un arbore
binar complet a cărui reprezentare va conţine exact m cifre 0 şi n cifre 1 (0 < m, n ≤
100).
(Timişoara-pregătire, 1996)

6. Scrieţi o subrutină nerecursivă care numără nodurile frunză ale unui arbore binar.
7. Realizaţi o subrutină care să determine nivelul cu cele mai multe noduri frunze dintr–un
arbore binar.
8. Determinaţi ı̂năţimea unui arbore binar printr–o funcţie nerecursivă.
9. Se consideră un dicţionar ce este implementat sub forma unei structuri de arbore.
Pentru fiecare cuvânt se cunoaşte traducerea acestuia precum şi probabilitatea lui de
apariţie ı̂n texte.
Se cere să se realizeze un algoritm care să conducă la o căutare optimă a cuvintelor
ı̂n dicţionar. Se menţionează faptul că probabilitatea de a căuta un cuvânt care nu se
găseşte ı̂n dicţionar este zero.
Datele de ieşire constau din afişarea arborelui optimal de căutare compus din cuvintele
introduse.
Spre exemplu, un set de date de intrare poate fi:

5
1 abroad in_strainatate
4 baker brutar
2 calf gamba
1 dice zaruri
2 ear ureche

10. O casă de comenzi este interesată de crearea unui program pe calculator care să ţină
evidenţa comenzilor ı̂n ordinea datelor la care au fost onorate, iar ı̂n cadrul fiecărei date
de livrare, ı̂n ordine lexicografică după numele produsului. Să se scrie un program care
foloseşte structuri de date de tip arbore.
Intrarea este formată din mai multe linii. Pe fiecare linie avem câte o comandă dată
sub forma: [numep rodus] [zi] [luna] [an]. Intrarea se termină cu o linie pe care avem
doar ’&’.
Ieşirea este formată prin afişarea comenzilor ı̂n ordinea datei la care au fost onorate,
iar ı̂n cadrul unei date, ı̂n ordine lexicografică ı̂n funcţie de numele comenzii.
Indicaţie: Se va crea un arbore de căutare după data de livrare şi ı̂n fiecare nod va fi
un arbore de căutare după numele comenzii.

100
11. Se consideră o expresie logică formata din n variabile logice reprezentate printr–o sin-
gură literă şi operatorii & (AND), | (OR), ! (NOT) (nu avem paranteze). Expresia este
reprezentată sub forma unui arbore binar.
Se cere să se realizeze un algoritm care pentru o expresie logică dată cercetează existenţa
unei combinaţii de valori logice (true/false), pentru care expresia dată ia valoarea logică
true.
De exemplu pentru expresia a|!b|c&d soluţia este a = f alse, b = f alse, c = f alse,
d = f alse, iar pentru expresia !a&a nu avem soluţie.

12. Se consideră o expresie matematică reprezentată printr-un arbore binar. Operatorii


corespund operaţiilor matematice uzuale +, −, ∗, /, şi ? (pentru ridicare la putere).
Nu avem paranteze şi toţi operatorii sunt operatori binari. Operanzii sunt identificaţi
printr–o singură literă.
Se cere să se realizeze un algoritm ce crează structura de date corespunzătoare unei
expresii date şi să evalueaze expresia pentru o mulţime de valori ale operanzilor.
Pentru expresia a + b ∗ c unde a = 2, b = 3 şi c = −1 avem valoarea −1, iar ı̂n urma
evaluării expresiei a − b/c?a cu a = 2, b = 9 şi c = 3 obţinem valoarea 1.

101
Capitolul 5

Arbori oarecare

Reamintim definiţia unui arbore oarecare:

Definiţia 5.1 Fiind dată o mulţime M de elemente denumite noduri, vom numi arbore o
submulţime finită de noduri astfel ı̂ncât:

1. există un nod cu destinaţie specială, numit rădăcina arborelui;


2. celelalte noduri sunt repartizate ı̂n n mulţimi disjuncte două câte două, A1 , A2 , · · · , An , fiecare
mulţime Ai constituind la rândul ei un arbore.

5.1 Moduri de reprezentare


Pentru reprezentarea arborilor oarecare pot fi utilizate următoarele metode:

• legături fiu-frate: f iuk - este primul descendent al vârfului k, f ratek - următorul descen-
dent al tatălui nodului k, ce urmează după k. Între descendenţii unui vârf se presupune
că definim o relaţie de ordine.
Rădăcina arborelui din figura 5.1 este Rad = 1.

2 3 4

5 6 7 8 9 10

Fig. 5.1: Exemplu de arbore oarecare

Table 5.1: Reprezentarea cu legături fiu–frate a arborelui din figura 5.1.

Nod 1 2 3 4 5 6 7 8 9 10
Fiu 2 0 5 7 0 0 0 0 0 0
Frate 0 3 4 0 6 0 8 9 10 0

102
Dacă identificăm F iu cu Stang şi F rate cu Drept, unui arbore oarecare i se poate
asocia un arbore binar.
Pentru reprezentarea unui arbore oarecare, modelul de reprezentare fiu-frate ı̂n varianta
de alocare statică poate fi uşor extins la o variantă de alocare dinamică, folosind pointeri
pentru legăturile către primul descendent respectiv pentru următorul frate. Astfel un
nod al arborelui poate avea următoarea configuraţie:

typedef struct nod {


TipOarecare data;
struct nod* fiu;
struct nod* frate;
}Nod;

Asemănător cu modelul construit la arbori binari, rad (Nod ∗ rad;) este o variabilă de
tip pointer la Nod (variabila păstrează adresa unei zone de memorie). rad desemnează
adresa rădăcinii arborelui.

rad

1 NULL

2 3 4 NULL

NULL

5 6 NULL 7 8 9 10 NULL

NULL NULL NULL NULL NULL NULL

Fig. 5.2: Exemplu de arbore oarecare cu 10 noduri reprezentat prin legături fiu-frate

În figura 5.2 este reprezentat arborele din figura 5.1 prin legături fiu-frate.

• lista descendenţilor. În cadrul acestui mod de reprezentare fiecare vârf este descris prin
lista descendenţilor săi. Pentru memorare se va utiliza un vector cu n componente:
(
0 , vârful respectiv nu are descendenţi
Ck =
j , j indică adresa (coloana) unde ı̂ncepe lista descendenţilor vârfului k.

Listele de descendenţi se păstrează prin intermediul unei matrice L cu 2 linii şi N − 1


coloane:
L1,k - un descendent al vârfului a cărui listă conţine coloana k a matricei date.
(
0 , dacă descendentul respectiv este ultimul
L2,k =
j , j indică coloana unde se află următorul descendent

Exploatând mai departe această idee, ı̂ntr-un nod al arborelui oarecare se poate păstra o
listă cu adresele descendenţilor săi, lista fiind reprezentată sub forma unui tablou alocat

103
Table 5.2: Reprezentarea arborelui din figura 5.1 folosind liste cu descendenţi.

Nod 1 2 3 4 5 6 7 8 9 10
C 1 0 4 6 0 0 0 0 0 0
 
2 3 4 5 6 7 8 9 10
L=
2 3 0 5 0 7 8 9 0

static sau dinamic. Oricum modelul se recomandă atunci când numărul descendenţilor
unui nod este limitat superior, sau când acesta este cunoscut/stabilit ı̂n momentul ı̂n
care se construieşte arborele. Modificările efectuate asupra arborelui, cum ar fi inserări
de descendenţi noi, atunci când intrările alocate pentru aceşti descendenţi sunt deja
alocate nu poate conduce decât la realocarea spaţiului de memorie cu un cost reflectat
ı̂n complexitatea algoritmului. Astfel dacă numărul de descendenţi este limitat superior
putem defini

#define NMAX 100


typedef struct nod {
TipOarecare data;
struct nod* children[NMAX];
}Nod;

sau

typedef struct nod {


TipOarecare data;
int no_of_children;
struct nod** children;
}Nod;

atunci când numărul maxim de descendenţi nu poate fi cunoscut decât ı̂n momentul
construirii arborelui. Astfel, ı̂n acel moment, se alocă spaţiu pentru un tablou de
pointeri către structura de tip Nod definită (children = malloc(sizeof(Nod*) *
no of children)).

rad

1 3

2 0 3 2 4 4

5 0 6 0 7 0 8 0 9 0 10 0

Fig. 5.3: Exemplu de arbore oarecare cu 10 noduri reprezentat prin liste cu descendenţi

În figura 5.3 este reprezentat arborele din figura 5.1 folosind liste cu descendenţi.

104
• Tata - fiecărui nod k se indică nodul părinte.

Table 5.3: Reprezentarea arborelui din figura 5.1 folosind vectorul tata.

Nod 1 2 3 4 5 6 7 8 9 10
Tata 0 1 1 1 3 3 4 4 4 4

Acest mod de reprezentare are dezavantajul că nu păstrează ı̂n mod explicit ordinea
descendenţilor unui nod. Această ordine poate fi dedusă printr–o numerotare adecvată a
nodurilor. De obicei reprezentarea cu vectorul tata nu este folosită singură ci ı̂mpreună
cu alte moduri de reprezentare, ca o completare.
De exemplu, reprezentarea unui nod propusă la primul punct, poate fi modifcată astfel
ı̂ncât să incorporeze şi informaţii despre nodul părinte:

typedef struct nod {


TipOarecare data;
struct nod* fiu;
struct nod* tata;
struct nod* frate;
}Nod;

rad
NULL

1 NULL

NULL

2 3 4 NULL

NULL

5 6 NULL 7 8 9 10 NULL

NULL NULL NULL NULL NULL NULL

Fig. 5.4: Exemplu de arbore oarecare cu 10 noduri reprezentat prin legături fiu-frate-tata

În figura 5.4, reprezentarea din figura 5.2 este ı̂mbunătăţită prin legătura tata.

5.2 Metode de parcurgere


Pentru parcurgerea unui arbore oarecare se pot folosi metodele generale pentru parcurg-
erea arborelui binar asociat. Arborele binar asociat unui arbore oarecare se obţine ı̂n urma
realizării corespondenţei F iu ⇒ Stang şi F rate ⇒ Drept.
În plus faţă de acestea, avem şi două metode de parcurgere pe care putem să le numim
specifice unui arbore oarecare. Acestea se pot clasifica după momentul ı̂n care se realizează
vizitarea nodului părinte astfel:

105
• A–preordine: se vizitează mai ı̂ntâi rădăcina, şi apoi, ı̂n ordine, subarborii săi [93],
[123]. Metoda este identică cu metoda de parcurgere ı̂n preordine a arborelui binar
ataşat: 1, 2, 3, 5, 6, 4, 7, 8, 9, 10 (vezi algoritmul 35). Parcurgerea ı̂n A–preordine este
exemplificată pe arborele oarecare din figura 5.1.

Algoritm 35 Algoritm de parcurgere ı̂n A-preordine


1: procedure APreordine(Rad, F iu, F rate)
2: k ← rad
3: q←0
4: S←∅ ⊲ iniţializare stivă vidă
5: while (q = 0) do
6: if (k 6= 0) then
7: call V izit(k)
8: S⇐k ⊲ S.push(k)
9: k ← f iuk
10: else
11: if (S = ∅) then ⊲ verificare dacă stiva este vidă
12: q←1
13: else
14: S⇒k ⊲ S.pop(k)
15: k ← f ratek
16: end if
17: end if
18: end while
19: return
20: end procedure

• A–postordine: se vizitează mai ı̂ntâi toţi subarborii ce au drept rădăcini, descendenţii


rădăcinii arborelui şi apoi rădăcina arborelui [93], [123]. În urma parcurgerii ı̂n A–
postordine a arborelui din figura 5.1 se obţine 2, 5, 6, 3, 7, 8, 9, 10, 4, 1.

Exemplul 5.1 [31] În continuare se prezintă implementarea ı̂n limbajul de programare C
a variantelor recursive pentru fiecare dintre cele două metode de vizitare ale unui arbore
oarecare.

#include <stdio.h>
#include <conio.h>
#include <stdlib.h>

/**
* Definitii de tipuri necesare cozii
*/
typedef struct nod {
int id;
float info;
struct nod *down, *next;
}NOD;

typedef NOD* TINFO;

106
/**
* Definitii de tipuri pentru reprezentarea arborelui
*/
typedef struct cnod {
TINFO info; //informatia memorata in nod
struct cnod *next; //adresa urmatorului element
}CNOD;

CNOD *prim = NULL, *ultim = NULL;

NOD* citListaDescendenti(NOD *up);

/**
* Functie ce testeaza daca coada e vida
* @return 1 daca coada e vida
* 0 altfel
*/
int isEmpty(void) {
if (prim == NULL)
return 1;
else
return 0;
}

/**
* Functia adauga un element la sfarsitul unei cozi
* unde n reprezinta elementul ce se adauga.
*/
void add(TINFO n) {
CNOD *curent;

curent = (CNOD *) malloc(sizeof(CNOD));


curent->info = n;
curent->next = NULL;
if (!prim) {
prim = ultim = curent;
} else {
ultim->next = curent;
ultim = curent;
}
}

/**
* Functia extrage un element din coada. Returneaza elementul scos.
*/
TINFO get(void) {
CNOD *curent;
TINFO n;

if (prim == ultim) {
n = prim->info;
free(ultim);

107
prim = ultim = NULL;
} else {
curent = prim;
n = prim->info;
prim = prim->next;
free(curent);
}
return n;
}

/**
* Functia realizeaza crearea unui arbore oarecare. Se introduce initial radacina,
* apoi descendentii ei, dupa care se introduc descendentii nodurilor de pe
* nivelul 1, dupa care se introduc descendentii nodurilor de pe nivelul 2 s.a.m.d.
*/
NOD* creare(void) {
NOD *p, *c;

p = (NOD *)malloc(sizeof(NOD));
p->next = NULL;
printf("Dati id:"); scanf("%d", &p->id);
add(p);
while (!isEmpty()) {
c = get();
c->down = citListaDescendenti(c);
}
return p;
}

/**
* Functia citeste informatia asociata unui nod.
* @return 1 daca citirea s-a facut corect
* 0 altfel
*/
int citInfo(NOD *pn) {
printf("Id:");
pn->next = NULL;
pn->down = NULL;
return scanf("%d",&pn->id) == 1;
}

/**
* Functia realizeaza citirea listei de descendenti ai nodului up si
* returneaza adresa primului descendent din lista.
*/
NOD* citListaDescendenti(NOD *up) {
NOD *prim, *ultim, *p;
NOD n;

printf("\nLista de descendenti pt %d (CTRL+Z) daca nu are\n", up->id);


prim = ultim = NULL;
while (citInfo(&n)) {

108
p = (NOD *)malloc(sizeof(NOD));
*p = n;
if (prim == NULL)
prim = ultim = p;
else {
ultim->next = p;
ultim = p;
}
add(p);
}
return prim;
}

/**
* Parcurgerea in A-preordine a arborelui cu radacina p.
*/
void aPreordine(NOD *p) {
NOD *desc;

printf("%d ", p->id);


desc = p->down;
while (desc != NULL) {
aPreordine(desc);
desc = desc->next;
}
}

/**
* Parcurgerea in A-postordine a arborelui cu radacina p.
*/
void aPostordine(NOD *p) {
NOD *desc;

desc = p->down;
while (desc != NULL) {
aPostordine(desc);
desc = desc->next;
}
printf("%d ",p->id);
}

void main(void) {
NOD *rad;

rad = creare();
printf("\n Parcurgerea in A-Preordine este:\n");
aPreordine(rad);
printf("\n Parcurgerea in A-Postordine este:\n");
aPostordine(rad);
}

109
5.3 Arbori de acoperire de cost minim
Fie G = (V, E) un graf neorientat şi fie c : E −→ R o funcţie de cost ce asociează o valoare
reală fiecărei muchii. Notăm cu TG mulţimea arborilor parţiali ai grafului G (un arbore
parţial este un graf parţial conex şi fără cicluri al grafului iniţial).
Cerinţa problemei este aceea de a determina un arbore T ′ ∈ TG având costul cel mai mic
dintre toţi arborii ce aparţin mulţimii TG :

c(T ′ ) = min{c(T )|T ∈ TG }


P
unde costul unui arbore T se defineşte ca c(T ) = e∈ET c(e). Arborele T ′ ce posedă această
proprietate se numeşte arbore de acoperire de cost minim (eng. minimum spanning tree -
MST ). Se mai ı̂ntâlneşte şi sub denumirea de arbore parţial de cost minim sau arbore parţial
minim.
Altfel spus, se cere să se determine un graf parţial conex G1 = (V, E1 ) (E1 ⊆ E) cu
proprietatea că suma costurilor tuturor muchiilor este minimă, graful parţial de cost minim
fiind chiar un arbore.
Determinarea arborelui de acoperire de cost minim are multe aplicaţii practice: de exem-
plu, dacă se dau n oraşe precum şi costul legăturilor ı̂ntre acestea, se cere să se determine o
conectare a tuturor oraşelor astfel ı̂ncât oricare două oraşe să fie conectate direct sau indirect
iar costul conectării să fie minim.

Lema 5.2 (Proprietatea tăieturii) Fie S o submulţime de noduri a lui V şi e muchia ce are
costul minim dintre toate muchiile ce au o singură extremitate ı̂n S. Atunci arborele parţial
de cost minim va conţine muchia e.

Demonstraţie: Fie T ′ un arbore parţial de cost minim. Presupunem prin reducere la


absurd că e ∈/ ET ′ . Dacă adăugăm muchia e la T ′ se obţine un ciclu C. În acest ciclu există
o altă muchie f ce are exact o extremitate ı̂n mulţimea S.
Înlocuind muchia f cu muchia e ı̂n arborele T ′ , obţinem un alt arbore de acoperire T ′′ =

T ∪ {e} \ {f }.
Deoarece c(e) < c(f ) vom avea că c(T ′′ ) = c(T ′ ) + c(e) − c(f ) ⇒ c(T ′′ ) < c(T ′ ) adică am
| {z }
<0
obţinut un arbore de acoperire T ′′ ce are costul mai mic decât T ′ , contradicţie cu faptul că
T ′ este un arbore de acoperire de cost minim. 

Definiţia 5.2 Se numeşte tăietură a grafului G o partiţie de două submulţimi a mulţimii


nodurilor V , notată astfel: < S, T > (unde S ∪ T = V şi S ∩ T = ∅).

Lema 5.3 (Proprietatea ciclului) Fie C un ciclu şi f muchia ce are costul maxim dintre
toate muchiile ce aparţin lui C. Atunci arborele parţial de cost minim T ′ nu conţine muchia
f.

Demonstraţie: Fie T ′ un arbore parţial de cost minim. Presupunem prin reducere la


absurd că f ∈ ET ′ . Dacă ştergem muchia f din arborele T ′ , se obţine o tăietură < S, V \ S >.
Avem că f ∈ C şi o extremitate a lui f aparţine lui S. Există atunci o altă muchie e cu
proprietatea că e ∈ C şi e are o extremitate ce aparţine lui S.
Înlocuind muchia f cu muchia e ı̂n arborele T ′ , obţinem un alt arbore de acoperire T ′′ =
T ′ ∪ {e} \ {f }.
Deoarece c(e) < c(f ) ⇒ c(T ′′ ) < c(T ′ ) adică am obţinut un arbore de acoperire T ′′ ce are
costul mai mic decât T ′ , contradicţie. 

110
Majoritatea algoritmilor ce calculează arborele de acoperire de cost minim prezintă aceeaşi
tehnică generală de calcul. La ı̂nceput se porneşte cu o pădure de arbori, T 0 = {T10 , T20, . . . , Tn0 }
unde Ti0 = {xi }, i = 1, n este un arbore format dintr–un singur nod. La pasul k vom avea
mulţimea T k compusă din n − k arbori: T k = {T1k , T2k , . . . , Tn−k
k
}.
k ′ ′
La fiecare pas, se alege un arbore Ti şi o muchie (u , v ) având costul minim dintre toate
muchiile (u, v) cu proprietatea că o extremitate aparţine mulţimii de noduri a arborelui Tik
(u′ ∈ Tik ) şi cealaltă extremitate aparţine mulţimii V \ VTik (v ′ ∈ V \ VTik ).
Prin urmare, mulţimea T k+1 se obţine din mulţimea T k prin reuniunea arborilor Tik şi
Tj (i 6= j), unde u′ ∈ Tik şi v ′ ∈ Tjk (T k+1 = T k \ {Tik , Tjk } ∪ {Tik ∪ Tjk }).
k

În final, la pasul n − 1, mulţimea T n−1 = {T1n−1 } va fi compusă dintr–un singur element,
acesta fiind arborele de acoperire de cost minim.
Algoritmii cei mai cunoscuţi pentru determinarea arborilor de acoperire de cost minim
sunt:
1. Algoritmul lui Boruvka (vezi algoritmul 36)

Algoritm 36 Algoritmul lui Boruvka (varianta schematica)


1: procedure Boruvka1(G, C, n; L)
2: iniţializează pădurea de arbori P compusă din n arbori, fiecare arbore fiind compus dintr–un
singur nod
3: L←∅
4: while (|P| > 1) do
5: for T ∈ P do
6: alege e muchia de cost minim de la T la G \ T
7: L ⇐ e ⊲ adaugă muchia e la lista de muchii alese ce vor forma arborele de acoperire
de cost minim
8: end for
9: adaugă toate muchiile selectate ı̂n cadrul f or-ului anterior la P
10: end while
11: end procedure

2. Algoritmul lui Prim (vezi algoritmul 37)

Algoritm 37 Algoritmul lui Prim (varianta schematica)


1: procedure Prim1(n, C, u; L)
2: S ← {u}, L ← ∅
3: for i ← 1, n do
4: di ← cu,i
5: end for
6: for i ← 1, n − 1 do
7: k ← min {dk |k ∈ V \ S}
8: L ⇐ (tatak , k)
9: S ← S ∪ {k}
10: for each j ∈ V \ S do
11: dj ← min {dj , ck,j }
12: end for
13: end for
14: end procedure

111
3. Algoritmul lui Kruskal (vezi algoritmul 38)

Algoritm 38 Algoritmul lui Kruskal (varianta schematica)


1: procedure Kruskal1(G, C, n; L)
2: ordonează muchiile ı̂n ordine crescătoare după cost
3: L←∅
4: for each u ∈ V do
5: crează o mulţime compusă din {u}
6: end for
7: count ← 0
8: while count < n − 1 do
9: alege muchia (u, v)
10: if (u şi v sunt ı̂n mulţimi diferite) then
11: L ⇐ (u, v)
12: reuneşte mulţimile ce conţin pe u şi v
13: count ← count + 1
14: end if
15: end while
16: end procedure

Algoritmul lui Prim implementat simplu are o complexitate O(n2 )[30] şi atinge o com-
plexitate de O(m log n) dacă se folosesc heap–uri Fibonacci [54], sau pairing heaps [115].
În tabelul 5.4 sunt prezentaţi mai mulţi algoritmi dezvoltaţi de–a lungul timpului pentru
determinarea arborelui de acoperire minimal şi complexităţile lor. Karger, Klein şi Tarjan [79]
pornind de la algoritmul lui Boruvka au realizat un algoritm randomizat pentru determinarea
arborelui de acoperire minimal, având o complexitate liniară, iar Chazelle [26] a dezvoltat
un algoritm având complexitatea O(nα(m, n)) (α(m, n) este inversa funcţiei lui Ackerman).
Pe de altă parte, Pettie şi Ramachandran [105] au propus un algoritm demonstrat ca fiind
optimal, având complexitatea cuprinsă ı̂ntre O(n + m) şi O(nα(m, n)).

Table 5.4: Algoritmi pentru determinarea arborelui de acoperire minim

Anul Complexitate Autori


1975 E log log V Yao
1976 E log log V Cheriton-Tarjan
1984 E log∗ V, E + V log V Friedman-Tarjan
1986 E log log ∗ V Gabow-Galil-Spencer-Tarjan
1997 Eα(V ) log α(V ) Chazelle
2000 Eα(V ) Chazelle [26]
2002 optimal Pettie-Ramachandran [105]

Fie G(V, E) un graf neorientat unde V = {1, 2, ..., n} este mulţimea nodurilor şi E este
mulţimea muchiilor (E ⊆ V × V ). Pentru reprezentarea grafului se utilizează matricea
costurilor C: 
0
 , dacă i = j
ci,j = ∞ , dacă (i, j) ∈/E


d > 0 , dacă (i, j) ∈ E

112
5.3.1 Algoritmul lui Boruvka
Algoritmul lui Boruvka [77] a fost descoperit de către matematicianul ceh Otakar Boruvka
ı̂n 1926 [21], şi redescoperit apoi de către alţi cercetători. Dintre aceştia, Sollin (1961) este
cel care a mai dat numele algoritmului, acesta fiind cunoscut ı̂n literatura de specialitate şi
sub numele de algoritmul lui Sollin. Pentru ca acest algoritm să poată fi aplicat, trebuie ca
muchiile grafului să aibă costuri distincte (vezi algoritmul 39).

Algoritm 39 Algoritmul lui Boruvka


1: procedure Boruvka2(G, C, n; L)
2: for i ← 1, n do
3: Vi ← {i}
4: end for
5: L ← ∅, M ← {V1 , . . . , Vn }
6: while (|T | < n − 1) do
7: for U ∈ M do
8: fie (u, v) muchia pentru care se obţine valoarea minimă min{c(u′ , v ′ )|(u′ , v ′ ) ∈ E, u′ ∈
U, v ′ ∈
/ V \ U}
9: determină componenta U ′ ce conţine pe v
10: L ⇐ (u, v)
11: end for
12: for U ∈ M do
13: reuneşte mulţimile ce conţin pe u şi v, U şi U ′
14: end for
15: end while
16: end procedure

Exemplul 5.4 Să considerăm graful din figura 5.5:

G = (V, E), V = {1, 2, 3, 4, 5, 6, 7, 8}

Aplicând algoritmul lui Boruvka, la pasul ı̂ntâi vor fi selectate muchiile (1, 2), (3, 6), (4, 5),
(4, 7) şi (7, 8). În urma operaţiilor de reuniune a componentelor conexe pe baza muchiilor
selectate, vor mai rămâne trei componente conexe ı̂n mulţimea M.
La pasul al doilea sunt alese muchiile (2, 5) şi (1, 3) ce conduc, ı̂n urma operaţiilor de
reuniune, la o singură componentă conexă.

5.3.2 Algoritmul lui Prim


Algoritmul a fost descoperit mai ı̂ntâi de V. Jarnik (1930) [76], şi apoi independent de Prim
(1957) [106] şi Djikstra (1959) [38].
Se porneşte cu o mulţime S formată dintr-un singur nod (S = {v0 }). La fiecare pas se
alege muchia de cost minim ce are numai o extremitate ı̂n mulţimea S. Procesul se ı̂ncheie
după n − 1 paşi, rezultând un graf parţial aciclic. Din teorema 4.1 rezultă faptul că acest
graf parţial aciclic cu n − 1 muchii este un arbore (de acoperire).
Conform Proprietăţii tăieturii, toate muchiile alese aparţin arborelui parţial de cost minim
de unde rezultă că acest arbore de acoperire este un arbore parţial minim.
Vom utiliza trei vectori de dimensiune n, unde n reprezintă numărul de vârfuri al grafului
(vezi algoritmul 40):

113
Fig. 5.5: Exemplu de graf ponderat - aplicaţie algoritmul lui Boruvka

• vizitat - vector caracteristic


(
1 , dacă nodul k ∈ S
vizitatk =
0 , dacă nodul k ∈ V \ S

• d - pentru un nod k ∈ / S, dk va conţine distanţa minimă de la k la un nod j ∈ S. La


ı̂nceput, dj = cv0 ,j . Pentru un nod k ales la un moment dat, dj (j ∈ S) se modifică
numai dacă ck,j < dj astfel dj = ck,j .

• tata - conţine pentru fiecare nod k ∈


/ S nodul j ∈ S astfel ı̂ncât ck,j = min{ck,i|i ∈ S}.
La ı̂nceput, (
0 , dacă nodul k = v0
tatak =
v0 , dacă nodul k 6= v0

În momentul ı̂n care se modifică dj , se va modifica şi valoarea lui tataj = k.

Fig. 5.6: Exemplu de graf ponderat - aplicaţie algoritmul lui Prim

114
Algoritm 40 Algoritmul Prim (varianta detaliata)
1: function DistantaMinima(n, vizitat, d)
2: min ← ∞
3: for j ← 1, n do
4: if (vizitatj 6= 1) ∧ (dj < min) then
5: min ← dj
6: j0 ← j
7: end if
8: end for
9: if (min = ∞) then
10: return −1
11: else
12: return j0
13: end if
14: end function

15: procedure Prim2(n, C, v0 ; d, tata)


16: vizitatv0 ← 1
17: tatav0 ← 0
18: for i ← 1, n do
19: if (i 6= v0 ) then
20: vizitati ← 0
21: di ← cv0 ,i
22: tatai ← v0
23: end if
24: end for
25: for i ← 1, n − 1 do
26: k ← DistantaM inima(n, vizitat, d)
27: if (k < 0) then
28: Output ’Graful nu este conex!’
29: return
30: end if
31: vizitatk ← 1 ⊲ (k, tatak ) este o muchie ce aparţine arborelui parţial minim
32: for j ← 1, n do
33: if (vizitatj =6 1) ∧ (dj > ck,j ) then
34: tataj ← k
35: dj ← ck,j
36: end if
37: end for
38: end for
39: end procedure

Exemplul 5.5 Fie graful din figura 5.6:


G = (V, E), V = {1, 2, 3, 4, 5, 6, 7, 8}
Vom lua nodul iniţial v0 = 1. La ı̂nceput, după etapa de iniţializare avem:
1 2 3 4 5 6 7 8
d 14 6 ∞ 5 ∞ ∞ ∞
tata 0 1 1 1 1 1 1 1
vizitat 1 0 0 0 0 0 0 0

115
După primul pas al ciclului, selectăm muchia (1, 5).
1 2 3 4 5 6 7 8
d 14 6 21 5 16 ∞ ∞
tata 0 1 1 5 1 5 1 1
vizitat 1 0 0 0 1 0 0 0
La pasul al doilea se alege nodul 3 şi muchia (1, 3):
1 2 3 4 5 6 7 8
d 14 6 21 5 12 ∞ 12
tata 0 1 1 5 1 3 1 3
vizitat 1 0 1 0 1 0 0 0
La pasul al treilea avem două noduri ale căror distanţe la noduri din mulţimea S sunt
egale: 6 şi 8. Alegem primul nod - 6 şi muchia (3, 6):
1 2 3 4 5 6 7 8
d 14 6 21 5 12 14 6
tata 0 1 1 5 1 3 6 6
vizitat 1 0 1 0 1 1 0 0
Al patrulea nod ales este 8:
1 2 3 4 5 6 7 8
d 14 6 10 5 12 10 6
tata 0 1 1 8 1 3 8 6
vizitat 1 0 1 0 1 1 0 1
La pasul cinci, nodul aflat la distanţă minimă este 7:
1 2 3 4 5 6 7 8
d 14 6 10 5 12 10 6
tata 0 1 1 8 1 3 8 6
vizitat 1 0 1 0 1 1 1 1
La pasul şase este ales nodul 4:
1 2 3 4 5 6 7 8
d 12 6 10 5 12 10 6
tata 0 4 1 8 1 3 8 6
vizitat 1 0 1 1 1 1 1 1
La final, ultimul nod ales este 2 ı̂mpreună cu muchia (4, 2).

Trebuie să remarcăm faptul că algoritmul lui Prim (vezi algoritmul 40) este aproape
identic cu algoritmul lui Dijkstra (vezi algoritmul 62).
După cum am subliniat, implementarea optimă se realizează folosind nişte structuri de
date avansate - heap–uri Fibonacci sau pairing heaps (vezi algoritmul 41).

5.3.3 Structuri de date pentru mulţimi disjuncte


O partiţie a unei mulţimi A este o secvenţă finită de mulţimi (submulţimi) A1 , . . . , Am
disjuncte
Sm două câte două, cu proprietatea că reuniunea acestora este chiar mulţimea A
(A = i=1 Ai şi Ai ∩ Aj = ∅, ∀i, j = 1, m, i 6= j).
Există mai multe probleme ai căror algoritmi de rezolvare depind de următoarele operaţii
efectuate asupra elementelor partiţiei: verificarea dacă două elemente fac parte din aceeaşi
submulţime precum şi operaţia de reuniune a două submulţimi.

116
Algoritm 41 Algoritmul lui Prim folosind structuri de date avansate
1: procedure Prim3(n, C, u)
2: for fiecare v ∈ V do
3: dv ← ∞
4: end for
5: Q←∅ ⊲ iniţializează coada cu prioritate Q cu mulţimea vidă
6: for fiecare v ∈ V do
7: Q⇐v
8: end for
9: S←∅ ⊲ iniţializează mulţimea S cu mulţimea vidă
10: while Q 6= ∅ do
11: u ← deleteM in(Q) ⊲ extrage nodul de prioritate minimă din Q
12: S ← S ∪ {u}
13: for fiecare muchie e = (u, v) ∈ E do
14: if (v ∈
/ S) ∧ (c(e) < dv ) then
15: actualizeaza prioritatea lui v: dv ← c(e)
16: end if
17: end for
18: end while
19: end procedure

O structură de date pentru mulţimi disjuncte memorează o colecţie de mulţimi disjuncte


dinamice. Fiecare mulţime este identificată printr–un reprezentant [30]. Operaţiile de bază
ale acestei structuri de date sunt [2]:

• init(x, B) - procedura crează o mulţime B formată dintr–un singur element x;

• f ind(x) - ı̂ntoarce reprezentantul mulţimii căreia ı̂i aparţine x;

• merge(A, B) - reuneşte mulţimile distincte A şi B (x ∈ A, y ∈ B) ı̂ntr–o nouă mulţime


ce conţine elementele celor două mulţimi.

O astfel de structură de date pentru mulţimi disjuncte se mai numeşte şi structură de date
union–find (eng. union–find data structure).

Reprezentarea folosind vectori


Vom folosi un vector C de dimensiune n, unde n reprezintă numărul de elemente, iar ck = u
indică faptul că elementul k aparţine mulţimii u.
Init constă din iniţializarea lui cx cu identificatorul mulţimii, u. F ind ı̂ntoarce valoarea
lui cx (valoarea identificatorului mulţimii). În funcţia Merge se caută toate elementele ce
fac parte din mulţimea de identificator cy şi se trec ı̂n mulţimea al cărui identificator este cx
(vezi algoritmul 42).

Exemplul 5.6 Pentru o putea opera cu modul de reprezentare ales, cel cu vectori, trebuie
ca elementele mulţimii să ia valori naturale ı̂n intervalul [1,. . . ,n]. Dacă acest lucru nu
este posibil atunci folosim un vector auxiliar A ce păstrează valorile elementelor, ı̂n cadrul
reprezentării utilizându–se indicele acestora. Să presupunem că avem mulţimea de elemente
A = {a, b, e, x, y, z, u, v} şi partiţia {a, x, y}, {b, z, v}, {e, u}. Atunci conform modului de
reprezentare descris avem:

117
Algoritm 42 Algoritmi pentru operaţiile init, find, merge (varianta ı̂ntâi)
1: procedure Init(x, u)
2: cx ← u
3: end procedure
4: function Find(x)
5: return cx
6: end function
7: function Merge(x, y)
8: setx ← cx
9: sety ← cy
10: for k ← 1, n do
11: if (ck = sety) then
12: ck ← setx
13: end if
14: end for
15: return setx
16: end function

1 2 3 4 5 6 7 8
A ’a’ ’b’ ’e’ ’x’ ’y’ ’z’ ’u’ ’v’
C 1 2 3 1 1 2 2 3
a2 =′ b′ are semnificaţia următoare: elementul de pe poziţia 2 are valoarea ′ b′ . c2 = 2 -
elementul de pe poziţia 2 face parte din mulţimea de identificator 2. Sau a4 =′ x′ - elementul
de pe poziţia 4 are valoarea ′ x′ , şi c4 = 1 - elementul de pe poziţia 4 face parte din mulţimea
de identificator 1.

Reprezentarea folosind liste ı̂nlănţuite


În cadrul acestei metode fiecare mulţime este reprezentată printr–o listă simplu ı̂nlănţuită,
elementul reprezentant fiind elementul aflat ı̂n capul listei. Un nod al listei va avea un câmp
ce păstrează informaţia cu privire la un element al unei mulţimi, şi un câmp ce conţine
legătura către următorul nod al listei.
Vom folosi un vector de adrese (List), ce conţine adresa primului element din fiecare listă
(Listi ).
Procedura Init alocă un nod şi iniţializează câmpurile acestuia. Adresa nodului alocat
este păstrată ı̂n Listk (vezi algoritmul 43).
Funcţia F ind caută un element printre elementele păstrate de fiecare listă ı̂n parte. Se
parcurge vectorul List şi se obţine reprezentantul fiecărei submulţimi păstrate. Se parcurge
lista (căutare liniară) şi se caută un element cu valoarea x. Dacă elementul este găsit atunci
se ı̂ntoarce capul listei ı̂n care a fost găsit. Dacă până la sfârşit nu a fost identificat elementul
x, atunci funcţia returnează valoarea NULL.
Funcţia Merge reuneşte două liste: practic se adaugă o listă la sfârşitul celeilalte. Pentru
a realiza această operaţie pe lângă adresa primului element avem nevoie de adresa ultimului
element. Acest lucru se poate obţine indirect prin parcurgerea listei de la primul la ultimul
element, fie direct dacă ı̂n momentul creării păstrăm pentru fiecare listă ı̂ncă o variabilă ce
conţine adresa ultimului element (last).

118
Algoritm 43 Algoritmi pentru operaţiile init, find, merge (varianta a II-a)
1: procedure Init(x, k)
2: Listk ← new node ⊲ alocă spaţiu pentru un nod al listei
3: Listk .data ← x
4: Listk .next ← N U LL
5: end procedure
6: function Find(x)
7: for i ← 1, m do
8: p ← Listi , reprezentant ← p
9: while (p 6= N U LL) ∧ (p.data 6= x) do
10: p ← p.next
11: end while
12: if (p 6= N U LL) then
13: return reprezentant
14: end if
15: end for
16: return N U LL
17: end function
18: function Merge(x, y)
19: capx ← F ind(x)
20: capy ← F ind(y)
21: curent ← capx
22: while (curent.next 6= N U LL) do
23: curent ← curent.next
24: end while
25: curent.next ← capy
26: return capx
27: end function

Reprezentarea folosind o pădure de arbori


În cadrul acestei modalităţi de reprezentare fiecare mulţime este păstrată sub forma unui
arbore. Pentru fiecare nod păstrăm informaţia despre părintele său: tatak reprezintă indicele
nodului părinte al nodului k.
Se poate observa foarte uşor (vezi algoritmul 44) faptul că arborele obţinut ı̂n urma unor
operaţii repetate Merge poate deveni degenerat (o listă liniară). Pentru a putea să evităm o
astfel de situaţie, reuniunea se poate realiza ţinând cont de una din următoarele caracteristici:

1. numărul de elemente din fiecare arbore - rădăcina arborelui ce are mai puţine elemente
(notăm arborele cu TB ) va deveni descendentul direct al rădăcinii arborelui ce posedă
mai multe elemente (notat cu TA ). Astfel toate nodurile din arborele TA vor rămâne cu
aceeaşi adâncime, pe când nodurile din arborele TB vor avea adâncimea incrementată
cu 1. De asemenea, arborele rezultat ı̂n urma reuniunii va avea cel puţin de două ori
mai multe noduri decât arborele TB .

119
Algoritm 44 Algoritmi pentru operaţiile init, find, merge (varianta a III-a)
1: procedure Init(x)
2: tatax ← x
3: end procedure
4: function Find(x)
5: while (x 6= tatax ) do
6: x ← tatax
7: end while
8: return x
9: end function
10: function Merge(x, y)
11: radx ← F ind(x)
12: rady ← F ind(y)
13: tatarady ← radx
14: return radx
15: end function

1: function Merge(x, y)
2: radx ← F ind(x)
3: rady ← F ind(y)
1: procedure Init(x)
4: size ← |tataradx | + |tatarady |
2: tatax ← −1
5: if (|tataradx | > |tatarady |) then
3: end procedure
6: tatarady ← radx
4: function Find(x)
7: tataradx ← −size
5: while (tatax > 0) do
8: return radx
6: x ← tatax
9: else
7: end while
10: tataradx ← rady
8: return x
11: tatarady ← −size
9: end function
12: return rady
13: end if
14: end function
Prin această euristică simplă se urmăreşte o echilibrare a arborelui rezultat pentru a se
evita cazurile degenerate. Complexitatea timp a procedurii F ind este O(log n).

2. ı̂nălţimea fiecărui arbore - rădăcina arborelui ce are ı̂nălţimea mai mică (notăm arborele
cu TB ) va deveni descendentul direct al rădăcinii arborelui cu ı̂nălţimea mai mare (notat
cu TA ). Dacă ı̂nălţimile lui TA şi TB sunt egale şi TA devine subarbore al lui TB atunci
ı̂nălţimea lui TB creşte cu o unitate.
Înălţimea fiecărui subarbore de rădăcină u se păstrează ı̂ntr–o variabilă nouă hu . Pentru
a economisi cantitatea de memorie utilizată, se poate păstra ı̂nălţimea unui arbore ı̂n
vectorul tata: tatax = −valoarea ı̂nălţimii arborelui, atunci când x reprezintă nodul
rădăcină al unui arbore.

120
1: function Merge(x, y)
2: radx ← F ind(x)
1: procedure Init(x) 3: rady ← F ind(y)
2: tatax ← x 4: if (hradx > hrady ) then
3: hx ← 0 5: tatarady ← radx
4: end procedure 6: return radx
5: function Find(x) 7: else
6: while (tatax 6= x) do 8: tataradx ← rady
7: x ← tatax 9: if (hradx = hrady ) then
8: end while 10: hrady ← hrady + 1
9: return x 11: end if
10: end function 12: return rady
13: end if
14: end function
Cea de–a doua tehnică utilizată pentru a reduce complexitatea timp a operaţiilor F ind şi
Merge este reprezentată de comprimarea drumului. Aceasta constă ı̂n a apropia fiecare nod
de rădăcina arborelui căruia ı̂i aparţine. Astfel ı̂n timpul operaţiei F ind(x) se determină mai
ı̂ntâi rădăcina arborelui căruia ı̂i aparţine (rad) şi apoi se mai parcurge o dată drumul de la
nodul x la rădăcină, astfel tatay ← rad, ∀y ∈ lanţul de la x la rad.
1: function Find(x)
2: y←x
3: while (tatay 6= y) do
4: y ← tatay
5: end while
6: rad ← y
7: y←x
8: while (tatay =6 y) do
9: y ← tatay
10: tatax ← rad
11: x←y
12: end while
13: return rad
14: end function

5.3.4 Algoritmul lui Kruskal


Algoritmul lui Kruskal [88] este o ilustrare foarte bună a metodei generale Greedy (vezi
algoritmul 45). La ı̂nceput se pleacă cu o pădure de arbori, fiecare arbore fiind alcătuit
dintr–un singur nod. La fiecare pas se alege o muchie: dacă cele două extremităţi (noduri)
fac parte din acelaşi arbore atunci nu se va adăuga muchia curentă la arborele parţial respectiv
deoarece ar conduce la un ciclu, ceea ce ar strica proprietatea de graf aciclic. Altfel, dacă cele
două noduri fac parte din arbori parţiali distincţi, se adaugă muchia curentă la mulţimea de
muchii alese anterior, iar cei doi arbori parţiali devin unul singur. La fiecare pas, numărul
de arbori parţiali scade cu 1. După n − 1 alegeri pădurea iniţială compusă din n arbori s–a
transformat ı̂ntr–un singur arbore.
Pentru a păstra muchiile grafului vom utiliza o matrice A cu m coloane şi 3 linii: primele
două linii conţin extremităţile muchiilor, iar linia a 3-a costul muchiei respective. Pentru a se
aplica o strategie Greedy, datele de intrare – muchiile – se vor ordona crescător ı̂n funcţie de
costul asociat. Deoarece graful parţial respectiv este un arbore cu n noduri el va avea n − 1
muchii.

121
O verificare eficientă cu privire la apartenenţa la acelaşi arbore a celor două extremităţi
ale unei muchii se poate face dacă vom utiliza o structură de date pentru mulţimi disjuncte.
Ca implementare, am ales reprezentarea cu pădure de arbori. Vom folosi un vector tata
care, pentru fiecare nod, va păstra tatăl acestuia ı̂n arborele parţial căruia ı̂i aparţine.
Când sunt preluate ca date de intrare, muchiile vor fi prezentate ı̂n ordinea crescătoare a
extremităţilor lor, de forma i j cost, cu i < j.

Algoritm 45 Algoritmul lui Kruskal


1: procedure Kruskal2(n, m, A; L, cost)
2: for i ← 1, n do
3: call Init(i)
4: end for
5: cost ← 0
6: j ← 1, L ← ∅
7: for i ← 1, n − 1 do
8: q←0
9: while (q = 0) do
10: r1 ← F ind(A1,j )
11: r2 ← F ind(A2,j )
12: if (r1 6= r2 ) then
13: cost ← cost + A3,j
14: call M erge(r1 , r2 )
15: L ← L ∪ {(A1,j , A2,j )}
16: q←1
17: end if
18: j ←j+1
19: end while
20: end for
21: end procedure

Exemplul 5.7 Să considerăm graful din figura 5.6. Muchiile grafului şi costurile lor sunt
(am ales să reprezentăm matricea A sub forma unei matrice cu m coloane şi 3 linii din motive
de spaţiu):
A 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
linia 1 1 1 1 2 2 2 3 3 4 4 4 5 6 6 7
linia 2 2 3 5 4 5 6 6 8 5 7 8 6 7 8 8
linia 3 14 6 5 12 16 20 12 12 21 24 10 16 14 6 10
După aşezarea ı̂n ordine crescătoare a muchiilor după cost avem:
A 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
linia 1 1 1 6 4 7 2 3 3 1 6 2 5 2 4 4
linia 2 5 3 8 8 8 4 6 8 2 7 5 6 6 5 7
linia 3 5 6 6 10 10 12 12 12 14 14 16 16 20 21 24
În figura 5.7, sunt ilustraţi paşii algoritmului lui Kruskal aplicat pe graful considerat.
La ı̂nceput se iniţializează pădurea de arbori, fiecare arbore fiind alcătuit dintr–un singur
nod, acesta fiind şi rădăcina arborelui. Apoi la fiecare pas se alege o muchie şi se verifică
dacă extremităţile acesteia fac parte din arbori diferiţi. Dacă răspunsul este afirmativ atunci
muchia respectivă este selectată, altfel se trece la muchia următoare.

122
Fig. 5.7: Algoritmului lui Kruskal exemplificat pe graful din figura 5.6

La pasul ı̂ntâi evaluăm muchia (1, 5), şi deoarece cele două extremităţi fac parte din arbori
distincţi, vom selecta această muchie (L = {(1, 5)}). Reunim arborii din care fac parte cele
două extremităţi, (vezi figura 5.7 (1)).
La pasul al doilea, se ia ı̂n considerare muchia (1, 3) şi mulţimea muchilor selectate devine
L = {(1, 5), (1, 3)} (vezi figura 5.7 (2)).
La pasul al patrulea, ajungem la muchia (4, 8) ce are costul 10. Nodul 4 face parte din
arborele de rădăcină 4 iar nodul 8 face parte din arborele de rădăcină 6. Deoarece extremităţile
muchiei sunt amplasate ı̂n arbori distincţi selectăm muchia curentă pentru arborele parţial
de cost minim, L = {(1, 5), (1, 3), (6, 8), (4, 8)}. În urma reuniunii arborilor corespunzători
celor două noduri se obţine configuraţia din figura 5.7 (4).

123
Subliniem faptul că arborii reprezentaţi ı̂n figura 5.7 sunt diferiţi de arborii ce con-
duc la obţinerea soluţiei problemei: arborii din figură constituie suportul necesar pentru
reprezentarea structurii de date pentru mulţimi disjuncte, ce este utilizată pentru efectuarea
eficientă a operaţiilor F ind şi Merge. Acest lucru justifică faptul că deşi la pasul al patrulea
selectăm muchia (4, 8) pentru arborele parţial de cost minim, ea nu se regăseşte ı̂n configuraţia
(4) (nodul 4 este unit direct cu nodul 6, vezi figura 5.7). Singura legătură dintre un arbore
suport pentru reprezentarea unei mulţimi şi un arbore folosit pentru obţinerea arborelui parţial
de cost minim, este mulţimea de noduri ce este comună.
La paşii următori, cinci şi şase, sunt alese muchiile (7, 8) şi respectiv (2, 4): L = {(1, 5), (1, 3),
(6, 8), (4, 8), (7, 8), (2, 4)}.
La pasul al şaptelea se evaluează muchia (3, 6). Nodurile 3 şi 6 fac parte din arbori diferiţi,
astfel ı̂ncât muchia curentă poate fi selectată pentru soluţia problemei, L = {(1, 5), (1, 3), (6, 8),
(4, 8), (7, 8), (2, 4), (3, 6)}.

5.4 Exerciţii
1. Să se realizeze o subrutină ce ı̂ntoarce numărul de noduri dintr–un arbore oarecare.

2. Se dă o secvenţă formată din n numere naturale d1 , d2 , . . . , dn . Să se realizeze un


algoritm prin care să se verifice dacă există un arbore cu n noduri ale căror grade sunt
d1 , d2 , . . . , dn .
Dacă există un astfel de arbore, acesta va fi reprezentat prin liste ale nodurilor. O listă
a unui nod conţine numărul nodului urmat de fii săi.
Intrare Ieşire
1123113 DA
1 2 3 4
2 5 6
3 7
4
5
6
7
(Timişoara-pregătire, 1996)

3. Fie un graf G cu n vârfuri şi m muchii de cost pozitiv. Alegând un nod, numit nod
central, să se determine un subarbore al lui G astfel ı̂ncât drumurile de la nodul central
la toate celelalte noduri să aibă lungimea minimă.

4. Fiind date n puncte ı̂n spaţiul R3 determinate prin coordonatele (x, y, z), să se elaboreze
un algoritm ce determină sfera de rază minimă cu centrul ı̂ntr–unul din punctele date
şi care conţine ı̂n interiorul ei toate cele n puncte.

5. Se consideră procesul de proiectare a unei plăci electronice cu N componente (1 ≤ N ≤


100). Pentru fiecare componentă electronică C se cunoaşte numărul de interconexiuni.
Se consideră graful determinat de mulţimea pinilor şi mulţimea interconectărilor posi-
bile ale tuturor componentelor, precum şi lungimile lor. Dintre toate modalităţile de
interconectare posibile se cere cea corespunzătoare arborelui de acoperire minim (inter-
conectarea pentru care suma tuturor circuitelor imprimate are lungimea minimă).

124
Datele de intrare sunt alcătuite din descrierile mai multor plăci electronice. Descrierea
fiecărei plăci conţine numărul N de componente şi numărul M de interconexiuni. O
conexiune se caracterizează prin trei valori: două vârfuri, u şi v, precum şi lungimea
acesteia l.
Datele de ieşire constau dintr-un răspuns pentru fiecare mulţime de date de test, compus
din numărul testului (se ı̂ncepe numerotarea cu 1), precum şi costul interconectării de
lungime minimă.

Exemplul 5.8 Pentru datele de intrare

5 7
1 2 40 2 3 35 1 5 27 1 4 37 4 5 30 2 4 58 3 4 60
0 0

vom obţine rezultatul

Cazul 1: [1 5] [4 5] [2 3] [1 2]
Interconectarea de cost minim are valoarea 132.

În figura 5.8 este reprezentat graful corespunzător datelor de intrare.

Fig. 5.8: Descrierea conexiunilor posibile dintre componentele unei plăci electronice

6. Realizaţi o subrutină nerecursivă care să determine cheia minimă dintr–un arbore oare-
care.

7. Determinaţi ı̂nălţimea unui arbore oarecare printr–o funcţie nerecursivă.

8. Pentru asigurarea securităţii activităţilor dintr–un combinat chimic s–a apelat la o com-
panie de pompieri. Desfăşurarea activităţilor companiei presupune stabilirea locurilor
de instalare a comandamentului precum şi a posturilor de supraveghere. Pentru aceasta
sunt disponibile ı̂n cadrul combinatului n puncte de control. Pentru fiecare pereche de
puncte de control se cunoaşte dacă există o legătură directă ı̂ntre ele şi, ı̂n caz afirmativ,
distanţa dintre ele. Se cunoaşte, de asemenea, faptul că ı̂ntre oricare două puncte de
control există un drum direct sau indirect.
Odată stabilite amplasamentele comandamentului şi ale punctelor de supraveghere ı̂n
câte unul dintre cele n puncte de control este posibil să se ajungă de la comandament

125
la fiecare punct de supraveghere parcurgând un anumit drum. Evident este de dorit ca
lungimea acestui drum să fie minimă şi valoarea maximă dintre lungimile drumurilor
minime pentru fiecare punct de supraveghere ı̂n parte să fie cât mai mică posibilă.
Se cere să se determine punctul de control ı̂n care trebuie instalat comandamentul cât
şi drumurile ce vor fi parcurse de la comandament la fiecare punct de supraveghere,
astfel ı̂ncât aceste drumuri să aibă valoarea lungimii minimă şi cel mai lung dintre ele
să fie cât mai scurt posibil.

126
Capitolul 6

Grafuri orientate

6.1 Noţiuni de bază


Fie o mulţime finită V = {x1 , x2 , . . . , xn }. Fie E ⊆ V × V (unde V × V este produsul
cartezian al mulţimii V cu ea ı̂nsăşi).
În cazul unui graf orientat, noţiunea de muchie este ı̂nlocuită cu noţiunea de arc (o
pereche de noduri (x, y) devine ordonată, adică (x, y) 6= (y, x)). Pentru un arc (x, y) ∈ E,
vârful x reprezintă extremitatea iniţială a arcului, iar vârful y reprezintă extremitatea finală.
Vom spune că vârfurile x şi y sunt adiacente.

Definiţia 6.1 Un graf orientat este o pereche ordonată G = (V, E), unde V este o
mulţime de vârfuri sau noduri, iar E este o mulţime de arce.

Exemplul 6.1 Fie graful orientat G = (V, E), V = {1, 2, 3, 4, 5, 6, 7, 8},


E = {(1, 2), (1, 3), (1, 4), (2, 1), (3, 5), (4, 6), (5, 2), (5, 7), (6, 4), (6, 8), (7, 5), (7, 6)}. Acest graf
se poate reprezenta ca ı̂n figura 6.1.

Definiţia 6.2 Un graf parţial al unui graf orientat G = (V, E) este un graf orientat
G1 = (V, E1 ) unde E1 ⊂ E.

Exemplul 6.2 Pentru graful G din exemplul anterior (G = (V, E)), considerăm graful
parţial G1 = (V, E1 ), cu E1 = {(1, 3), (2, 1), (3, 5), (4, 6), (5, 2), (5, 7), (6, 4), (6, 8)}, şi V =
{1, 2, 3, 4, 5, 6, 7, 8}.

Definiţia 6.3 Un subgraf al unui graf orientat G = (V, E) este un graf orientat H =
(V1 , E1 ) unde V1 ⊂ V iar arcele din E1 sunt toate arcele din E ce au ambele extremităţi ı̂n
mulţimea V1 (E1 = E|V1 ×V1 ).

Exemplul 6.3 Fie subgrafurile H1 şi H2 ale grafului G din exemplul anterior: H1 = (V1 , E1 ),
unde V1 = {1, 2, 3, 5}, iar E1 = {(1, 2), (1, 3), (2, 1), (3, 5), (5, 2)}, şi H2 = (V2 , E2 ), unde
V = {4, 6, 7, 8} şi E2 = {(4, 6), (6, 4), (6, 8), (7, 6)}.

Definiţia 6.4 Gradul exterior al unui vârf d+ (x) este egal cu numărul arcelor ce au ca ex-
tremitate iniţială pe x (d+ (x) = |{(x, u)|(x, u) ∈ E, u ∈ V }|). Gradul interior al unui vârf
d− (x) este egal cu numărul arcelor ce au ca extremitate finală pe x (d− (x) = |{(u, x)|(u, x) ∈
E, u ∈ V }|).

127
Fig. 6.1: Un exemplu de graf orientat

Definiţia 6.5 Se numeşte lanţ o secvenţă de arce L = {l1 , . . . , lp } cu proprietatea că oricare
două arce consecutive ale secvenţei au o extremitate comună. (L = [v0 , v1 , . . . , vp ] unde
(vi , vi+1 ) ∈ E sau (vi+1 , vi ) ∈ E, ∀i = 0, p − 1).

Definiţia 6.6 Un drum D = [v0 , v1 , . . . , vp ] este o succesiune de vârfuri cu proprietatea că


oricare două vârfuri vecine sunt adiacente ((vi , vi+1 ) ∈ E, i = 0, p − 1).

Dacă vârfurile v0 , v1 , . . . , vp sunt distincte două câte două, drumul se numeşte elementar.

Exemplul 6.4 L1 = {(6, 8), (7, 6), (7, 5), (3, 5), (1, 3)} şi L2 = {(6, 8), (6, 4), (1, 4)} sunt lanţuri
de la vârful 8 la vârful 1, iar D1 = {(1, 3), (3, 5), (5, 7), (7, 6), (6, 8)} şi
D2 = {(1, 4), (4, 6), (6, 8)} sunt drumuri elementare de la vârful 1 la vârful 8.

Definiţia 6.7 Un drum D pentru care v0 = vp se numeşte circuit.

Definiţia 6.8 Se numeşte circuit hamiltonian un circuit elementar ce trece prin toate
vârfurile grafului. Un graf ce admite un circuit hamiltonian se numeşte graf hamiltonian
orientat.

Definiţia 6.9 Un drum D ce conţine fiecare arc exact o singură dată se numeşte drum eu-
lerian. Dacă v0 = vp şi drumul este eulerian atunci circuitul se numeşte circuit eulerian.
Un graf ce conţine un circuit eulerian se numeşte graf eulerian orientat.

Definiţia 6.10 Un graf se numeşte conex dacă pentru orice pereche de vârfuri x şi y există
un lanţ Lxy de la x la y.

Definiţia 6.11 Un graf orientat este complet dacă oricare două vârfuri sunt adiacente.

Metodele utilizate pentru reprezentarea unui graf neorientat se pot adapta ı̂n mod natural
pentru reprezentarea unui graf orientat, ı̂nlocuind noţiunea de muchie cu cea de arc.

128
Fig. 6.2: Arbore de acoperire ı̂n lăţime pentru graful orientat din figura 6.1

6.2 Parcurgerea grafurilor


În urma vizitării unui graf neorientat, de obicei nu rezultă numai un arbore de acoperire ci o
pădure de arbori de acoperire.
În urma unei parcurgeri ı̂n lăţime a unui graf orientat, se ı̂ntâlnesc următoarele tipuri de
arce:

• arc al arborelui de acoperire - arcul (u, v) este un arc al arborelui de acoperire ı̂n lăţime.

• arc de ı̂ntoarcere - arcul (u, v) se numeşte arc de ı̂ntoarcere dacă are sensul contrar unui
drum de la v la u ı̂n arborele de acoperire (v u) (se spune că u este un descendent
al lui v şi v este un strămoş al lui u).

• arc de traversare - arcul (u, v) este un arc de traversare dacă v nu este nici descendent
direct, nici strămoş al lui u.

În figura 6.2 este prezentat arborele de acoperire ı̂n lăţime corespunzător grafului din
figura 6.1 având rădăcina 1: arcele (2, 1), (7, 5), (6, 4) sunt arce de ı̂ntoarcere, (5, 2), (7, 6)
sunt arce de traversare, iar celelalte sunt arce ale arborelui de acoperire (de exemplu (1, 3),
(6, 8)).
Vom prezenta schiţa unui algoritm mai general pentru parcurgerea unui graf (a se vedea
algoritmul 46). Pentru aceasta se utilizează două mulţimi de noduri, V izitat şi Neexplorat,
unde V izitat reprezintă mulţimea nodurilor vizitate iar Neexplorat (Neexplorat ⊂ V izitat)
reprezintă mulţimea nodurilor vizitate dar neexplorate (noduri ce prezintă vecini ı̂ncă nevizitaţi).
În urma unei parcurgeri ı̂n adâncime a unui graf orientat, fiecare arc din mulţimea arcelor
poate fi ı̂ncadrat ı̂ntr-unul dintre tipurile următoare:

• arc al arborelui de acoperire - arcul (u, v) este un arc al arborelui de acoperire dacă
call
df s(u) apelează df s(v) (df s(u) df s(v)).

• arc de ı̂naintare - arcul (u, v) este un arc de ı̂naintare dacă este paralel cu un drum de
la u la v din arborele de acoperire (u v) (nu face parte din arborele de acoperire).

129
Algoritm 46 Algoritm de vizitare a unui graf (model general)

( ParcurgereGraf(u, G)
1: procedure
u - vârful de unde se porneşte vizitarea
Input:
G - graful
2: V izitat ← {u}
3: N eexplorat ← V izitat
4: while (N eexplorat 6= ∅) do
5: extrage un nod x din N eexplorat
6: determină y, următorul vecin al lui x ce nu a fost vizitat
7: if (y = N U LL) then
8: elimină x din N eexplorat
9: else
10: if (y ∈
/ V izitat) then
11: V izitat ← V izitat ∪ {y}
12: N eexplorat ← N eexplorat ∪ {y}
13: end if
14: end if
15: end while
16: end procedure

• arc de ı̂ntoarcere - arcul (u, v) se numeşte arc de ı̂ntoarcere dacă are sensul contrar unui
drum de la v la u din arborele de acoperire (v u).

• arc de traversare - arcul (u, v) este un arc de traversare dacă df s(v) a fost apelat şi s-a
terminat ı̂nainte de apelul lui df s(u).

Fig. 6.3: Arbore de acoperire ı̂n adâncime pentru graful orientat din figura 6.1

Pentru fiecare nod v al unui graf vom introduce două numere, prenumv şi postnumv ,
numere ce depind de ordinea ı̂n care sunt ı̂ntâlnite nodurile acestuia ı̂n timpul vizitării ı̂n
adâncime: prenumv marchează momentul când este ı̂ntâlnit nodul v pentru prima oară

130
iar postnumv momentul ı̂n care prelucrarea nodului v s-a ı̂ncheiat. Variabila counter este
iniţializată cu valoarea 1:
1: procedure Prenum(v)
2: prenumv ← counter
3: counter ← counter + 1
4: end procedure
şi
1: procedure Postnum(v)
2: postnumv ← counter
3: counter ← counter + 1
4: end procedure
Un arc (u, v) va avea următoarele proprietăţi, ı̂n funcţie de una din cele patru categorii
de arce introduse anterior ı̂n care se ı̂ncadrează:

1. arc al arborelui de acoperire: prenumu < prenumv şi postnumu > postnumv ;

2. arc de ı̂naintare: prenumu < prenumv şi postnumu > postnumv ;

3. arc de ı̂ntoarcere: prenumu > prenumv şi postnumu < postnumv ;

4. arc de traversare: prenumu > prenumv şi postnumu > postnumv .

Arborele de acoperire ı̂n adâncime corespunzător grafului din figura 6.1 ilustrează aceste
tipuri de arce: arc de ı̂naintare – (1, 4), arce de ı̂ntoarcere - (2, 1), (7, 5), (4, 6) arc de traver-
sare - (5, 2), arce ale arborelui de acoperire - (1, 2), (1, 3), (3, 5), (5, 7), (7, 6), (6, 8), (6, 4) (a
se vedea figura 6.3).

Algoritm 47 Algoritm de vizitare ı̂n adâncime pentru un graf orientat


1: procedure DFSNum(k, n, V ecin)

k
 - nodul curent vizitat
Input: n - numărul de noduri din graf


V ecin - matricea de adiacenţă a grafului
2: vizitatk ← 1 ⊲ marcarea nodului curent ca fiind vizitat
3: call P renum(k)
4: call V izitare(k) ⊲ vizitarea nodului curent
5: for i ← 1, n do
6: if (vizitati = 0) ∧ (vecink,i = 1) then
7: call DF SN um(i, n, V ecin) ⊲ apelul recursiv al subrutinei DF SN um pentru nodul
i
8: end if
9: end for
10: call P ostnum(k)
11: end procedure

Lema 6.5 Fiind dat un graf orientat G şi două noduri oarecare u, v ∈ G ce aparţin aceluiaşi
arbore de acoperire ı̂n adâncime rezultat ı̂n urma parcurgerii cu metoda DF S a grafului G
avem:

1. dacă (u, v) este un arc al arborelui de acoperire sau un arc de ı̂naintare sau un arc de traver-
sare ⇒ postnumu > postnumv ;

131
2. dacă (u, v) este un arc de ı̂ntoarcere ⇒ postnumu < postnumv .

Lema 6.6 Fiind dat un graf orientat G pentru oricare două noduri u, v ∈ G avem:

1. v este un descendent al lui u ı̂n pădurea de arbori de acoperire rezultaţi ı̂n urma vizitării ı̂n
adâncime a grafului G ⇔ intervalul [prenumv , postnumv ] este inclus ı̂n intervalul [prenumu ,
postnumu ];
2. nu există nici o legătură ı̂ntre u şi v ı̂n pădurea de arbori de acoperire rezultaţi ı̂n urma
vizitării ı̂n adâncime a grafului G ⇔ intervalele [prenumu , postnumu ] şi [prenumv , postnumv ]
sunt disjuncte;
3. situaţii prenumu < prenumv < postnumu < postnumv sau prenumv < prenumu < postnumv
< postnumu nu sunt posibile.

Lema 6.7 Fiind dat un graf neorientat G, dacă pentru două noduri oarecare u, v ∈ G avem
relaţia prenumu < prenumv < postnumu atunci:

• prenumu < prenumv < postnumv < postnumu ;


• există un drum de la u la v ı̂n G.

Exemplul 6.8 În urma apelării procedurii DFSNum(1, 8, Vecin) (a se vedea algoritmul
47), secvenţa de apeluri recursive ale procedurii DFSNum este ilustrată prin arborele de acoperire
ı̂n adâncime din figura 6.3.
Numerotarea nodurilor ı̂n preordine şi postordine, rezultată ı̂n urma vizitării este următoarea:

1 2 3 4 5 6 7 8
prenum 1 2 4 8 5 6 7 10
postnum 16 3 15 9 14 13 12 11
Pentru arcul (1, 4) avem prenum1 = 1, postnum1 = 16, prenum4 = 8 şi postnum4 = 9.
Deoarece relaţia prenumu < prenumv < postnumu este adevărată conform lemei 6.7 ar
trebui să avem prenumu < prenumv < postnumv < postnumu şi să existe un drum de la u
la v, lucruri care sunt adevărate (prenum1 < prenum4 < postnum4 < postnum1 şi există un
drum de la 1 la 4 ı̂n arborele de acoperire ı̂n adâncime).

6.3 Sortarea topologică

Fig. 6.4: Un graf orientat aciclic

132
Definiţia 6.12 Un graf orientat şi care nu posedă circuite se numeşte graf orientat aciclic
(directed acyclic graph - DAG).
Lema 6.9 Într-un graf orientat aciclic, dacă prenumu < prenumv şi există un drum de la
u la v ı̂n G, atunci prenumu < prenumv < postnumv < postnumu .
În practică, există mai multe situaţii ı̂n care o mulţime de activităţi sau sarcini, trebuie
organizate ı̂ntr-o anumită ordine, ı̂ntre acestea existând, de obicei, o mulţime de restricţii sau
dependenţe. În activitatea de construire a unei clădiri, anumite activităţi nu pot fi ı̂ncepute
decât după finalizarea altor activităţi: spre exemplu, nu se poate ı̂ncepe ridicarea pereţilor
unei construcţii decât după turnarea fundaţiei şi finalizarea structurii de rezistenţă, nu se
poate realiza instalaţia electrică dacă nu au fost ridicate zidurile, ş.a.m.d.
Sau dacă un student doreşte să ı̂şi alcătuiască un plan de studiu individual ce va conţine
pe lângă cursurile obligatorii, şi o serie de cursuri opţionale, va trebui să ţină cont de anul de
studiu ı̂n care se predă fiecare materie, precum şi de cerinţele obligatorii ale acestora: un curs
nu poate fi inclus ı̂n planul de studiu individual al unui student decât dacă acesta a parcurs
şi a obţinut creditele la toate materiile anterioare cerute explicit ı̂n programa cursului.

Algoritm 48 Algoritm de sortare topologică a unui graf orientat (prima variantă)

( SortTop1(n, V ecin)
1: procedure
n - numărul de noduri din graf
Input:
V ecin - vector ce conţine listele cu vecini ai fiecărui nod
2: for k ← 1, n do
3: dminusk ← 0
4: end for
5: for ∀(u, v) ∈ E do
6: dminusv = dminusv + 1
7: end for
8: Q←∅ ⊲ se iniţializează coada
9: for k ← 1, n do
10: if (dminusk = 0) then
11: Q⇐k ⊲ se inserează ı̂ntr–o coadă nodurile cu gradul interior 0
12: end if
13: end for
14: while (Q 6= ∅) do
15: Q⇒k ⊲ se extrage din coadă un nod
16: L⇐k ⊲ se inserează nodul ı̂ntr-o listă
17: w ← V ecink ⊲ v ia valoarea capului listei de vecini a nodului k
18: while (w 6= N U LL) do
19: dminusw.nodeIndex ← dminusw.nodeIndex − 1
20: if (dminusw.nodeIndex = 0) then
21: Q ⇐ w.nodeIndex ⊲ se inserează ı̂n coadă nodul w.nodeIndex
22: end if
23: w ← w.next ⊲ se trece la următorul vecin
24: end while
25: end while
26: end procedure

Dependenţa dintre două activităţi A şi B o putem modela prin introducerea a două noduri
ı̂n graf xi şi xj , asociate celor două activităţi. Dacă activitatea A trebuie realizată ı̂naintea
activităţii B, atunci se adaugă arcul (xi , xj ).

133
Definiţia 6.13 Se numeşte sortare topologică pentru un graf orientat G = (V, E) o or-
donare {x1 , x2 , . . . , xn } a nodurilor grafului astfel ı̂ncât pentru orice arc (xi , xj ) să avem
i < j.
Prin urmare o sortare topologică presupune aranjarea liniară a vârfurilor unui graf astfel
ı̂ncât toate arcele sale să fie orientate de la stânga la dreapta.
Lema 6.10 Dacă un graf orientat G admite o sortare topologică atunci G este aciclic.
Lema 6.11 Dacă un graf orientat G este aciclic atunci el admite o sortare topologică.
Observaţia 6.12 Într-un graf orientat aciclic există cel puţin un nod al cărui grad interior
este 0 (graful nu posedă arce care să aibă nodul v drept extremitate finală).
Pornind de la această observaţie se schiţează următorul algoritm [78]: ı̂ntr-un graf G se
determină un nod v astfel ı̂ncât gradul său interior să fie zero (d− (v) = 0); se adaugă acest
nod la o listă ce va conţine ordonarea topologică, se şterge nodul din graf ı̂mpreună cu toate
arcele ce ı̂l au ca extremitate iniţială, şi se reia algoritmul pentru graful G′ = (V ′ , E ′ ), unde
V ′ = V \ {v}, E ′ = E|V ′ ×V ′ .
Algoritmul 48 se termină ı̂n cel mult n paşi (|V | = n). Dacă se termină mai devreme,
atunci graful G nu este aciclic (la un moment dat nu mai există nici un nod v astfel ı̂ncât
d− (v) = 0).

Fig. 6.5: Sortare topologică cu algoritmul 48 pentru graful orientat din figura 6.4

Exemplul 6.13 Fie graful orientat din figura 6.5. Vârful 3 are d− (v) = 0. Se adaugă acest
nod la lista finală ce va conţine nodurile ordonate, se şterge nodul ı̂mpreună cu arcele care ı̂l
au drept extremitate iniţială. În graful rezultat, vârful 1 are proprietatea că d− (v) = 0. Se
adaugă la lista de rezultate, şi se elimină din graf ı̂mpreună cu arcele ce pleacă din el. Se
continuă procedeul până când graful devine vid (ı̂ntregul proces poate fi urmărit ı̂n figura 6.5).

134
Algoritm 49 Algoritm de sortare topologică a unui graf orientat (a doua variantă)

( SortTop2(n, V ecin)
1: procedure
n - numărul de noduri din graf
Input:
V ecin - matricea de adiacenţă a grafului
2: for k ← 1, n do
3: prenumk ← 0, postnumk ← 0
4: vizitatk ← 0
5: end for
6: for k ← 1, n do
7: if (vizitatk = 0) then
8: call DF SN um(k, n, V ecin)
9: end if
10: end for
11: se ordonează descrescător nodurile după postnumk
12: end procedure

Lema 6.14 Un graf orientat G este aciclic dacă şi numai dacă ı̂n urma unei vizitări ı̂n
adâncime a acestuia nu este ı̂ntâlnit nici un arc de ı̂ntoarcere.

Ideea algoritmului 49 o reprezintă lema 6.14[118].

Exemplul 6.15 Să considerăm ca date de intrare pentru algoritmul 49 graful orientat din
figura 6.4. După etapa de iniţializare (liniile 2 - 5) avem:

1 2 3 4 5 6 7
prenum 0 0 0 0 0 0 0
postnum 0 0 0 0 0 0 0
vizitat 0 0 0 0 0 0 0
Se apelează mai ı̂ntâi DFSNum(1, 7, Vecin). Secvenţa rezultată de apeluri recursive este
următoarea: DFSNum(1, 7, Vecin) → DFSNum(2, 7, Vecin) → DFSNum(4, 7, Vecin)
→ DFSNum(5, 7, Vecin) → DFSNum(7, 7, Vecin) → DFSNum(6, 7, Vecin).
În urma acestei secvenţe valorile vectorilor prenum şi postnum sunt următoarele:

1 2 3 4 5 6 7
prenum 1 2 0 3 4 6 5
postnum 12 11 0 10 9 7 8
vizitat 1 1 0 1 1 1 1
Mai rămâne nevizitat un singur nod, 3, drept pentru care vom mai avea un apel
DFSNum(3, 7, Vecin) din procedura principală SortTop2():

1 2 3 4 5 6 7
prenum 1 2 13 3 4 6 5
postnum 12 11 14 10 9 7 8
vizitat 1 1 1 1 1 1 1
Ordonarea descrescătoare a nodurilor mulţimii V după valorile vectorului postnum, con-
duce la următoarea configuraţie:

135
postnum 14 12 11 10 9 8 7
3 1 2 4 5 7 6
Astfel, sortarea topologică a nodurilor grafului obţinută ı̂n urma aplicării algoritmului 49
este: 3, 1, 2, 4, 5, 7, 6.

Observaţia 6.16 O optimizare a algoritmului 49 se referă la adăugarea nodului v ı̂ntr-o


stivă atunci când se termină vizitarea acestui nod v şi se calculează postnumv . Această stivă
va conţine la final nodurile grafului ı̂n ordinea descrescătoare a valorilor postnumv .
Prin urmare, nu mai este nevoie să ordonăm descrescător valorile vectorului postnum
pentru a obţine o sortare topologică a nodurilor grafului G, iar complexitatea algoritmului 49
devine O(|V | + |E|).

6.4 Componente tare conexe

Fig. 6.6: Graf orientat. Componente tare conexe.

Fie G = (V, E) un graf orientat unde V = {1, 2, . . . , n} este mulţimea nodurilor şi E este
mulţimea arcelor (E ⊆ V × V ).

Definiţia 6.14 O componentă tare conexă a unui graf orientat G este o mulţime maxi-
mală de vârfuri U ⊆ V , astfel ı̂ncât, pentru fiecare pereche de vârfuri {u, v} (u, v ∈ U) există
atât un drum de la u la v cât şi un drum de la v la u. Prin urmare se spune că vârfurile u
şi v sunt accesibile unul din celălalt.

Un graf orientat ce are o singură componentă tare conexă astfel ı̂ncât U = V este un graf
tare conex. În cazul ı̂n care un graf orientat nu este tare conex atunci el se poate descompune
ı̂n mai multe componente tare conexe. O astfel de componentă este determinată de un subgraf
al grafului iniţial.

Definiţia 6.15 Graful orientat G se numeşte tare conex dacă pentru orice pereche de
vârfuri u 6= v, există un drum de la nodul u la nodul v şi un drum de la nodul v la nodul u.

Se poate arăta că relaţia de tare conexitate este o relaţie de echivalenţă. O relaţie (notată
cu ∼) este o relaţie de echivalenţă dacă prezintă proprietăţile de reflexivitate, simetrie şi
tranzitivitate:

136
• reflexivitate: a ∼ a;

• simetrie: a ∼ b ⇒ b ∼ a;

• tranzitivitate: a ∼ b, b ∼ c ⇒ a ∼ c.

O clasă de echivalenţă este mulţimea tuturor elementelor care se află ı̂n relaţia ∼ ([x]∼ =
{y|x ∼ y}). Relaţia de echivalenţă determină o partiţie ı̂n clase de echivalenţă a mulţimii
peste care a fost definită. Prin urmare relaţia de tare conexitate determină o partiţie a
mulţimii V . Clasele de echivalenţă determinate de relaţia de tare conexitate sunt componen-
tele tare conexe.
Pentru determinarea componentelor tare conexe există mai mulţi algoritmi, dintre care
amintim algoritmul lui Tarjan, algoritmul lui Kosaraju şi algoritmul lui Gabow.

6.4.1 Algoritmul lui Kosaraju


Algoritmul lui Kosaraju-Sharir a fost prezentat de Aho, Hopcroft şi Ullman ı̂n lucrarea lor [2],
fiind preluat dintr-un manuscris al lui S. Rao Kosaraju (M. Sharir l-a prezentat ı̂n lucrarea
[110]).
Algoritmul foloseşte graful transpus GT asociat grafului iniţial G ([11], [23], [30]), fiind
compus din următorii paşi:

Pas 1. Se realizează parcugerea grafului cu algoritmul de vizitare ı̂n adâncime, pornind de la


un nod arbitrar. În timpul vizitării, se realizează numerotarea ı̂n postordine a nodurilor
ı̂n cadrul vectorului postnum.

Pas 2. Se obţine un nou graf GT = (V, E T ) prin inversarea sensului arcelor grafului G (E T =
{(u, v)|(v, u) ∈ E, u, v ∈ V }).

Pas 3. Se caută nodul nevizitat din graful GT ce are cel mai mare număr atribuit ı̂n urma
parcurgerii de la Pasul 1. Din acest nod se iniţiază parcugerea grafului cu algoritmul
de vizitare ı̂n adâncime.

Pas 4. Dacă mai rămân noduri nevizitate atunci se reia Pasul 3, altfel algoritmul se termină.

Fiecare arbore rezultat ı̂n urma parcurgerii de la Pasul 3 constituie o componentă tare conexă
a grafului G.
În continuare se prezintă implementarea ı̂n limbajul C a algoritmului anterior:
Listing 6.1: kosaraju.c
#include <s t d i o . h>
#include <mem. h>

#define MAX 100


#define TRUE 1
#define FALSE 0

char v e c i n [MAX] [MAX] ; // M a t r i c e a de a d i a c e n t a


char v i z i t a t [MAX] ; // Vecto r c e p a s t r e a z a s t a r e a unui nod : v i z i t a t sau n e v i z i t a t .
i nt n ; // Numarul de v a r f u r i d i n g r a f

i nt nump ;
i nt postnum [MAX] ; // Numarul a s o c i a t f i e c a r u i v a r f l a v i z i t a r e a i n p o s t o r d i n e .

137
void d f s ( i nt k ) {
i nt i ;

v i z i t a t [ k ] = TRUE;
for ( i = 0 ; i < n ; i ++)
i f ( ( v i z i t a t [ i ] == FALSE) && ( v e c i n [ k ] [ i ] > 0 ) )
dfs ( i ) ;
nump++;
postnum [ k ] = nump ;
}

void d f s 1 ( i nt k ) {
i nt i ;

v i z i t a t [ k ] = TRUE;
for ( i = 0 ; i < n ; i ++)
i f ( ( v i z i t a t [ i ] == FALSE) && ( v e c i n [ k ] [ i ] > 0 ) )
dfs1 ( i ) ;
p r i n t f ( ”%d ” , k ) ;
}

void r e a d I n p u t ( void ) {
i nt i , j ;

p r i n t f ( ”n = ” ) ; s c a n f ( ”%d” , &n ) ;
do{
p r i n t f ( ” nod1 nod2 : ” ) ; s c a n f ( ”%d %d” , &i , &j ) ;
i f ( i >= 0 )
vecin [ i ] [ j ] = 1;
} while ( i >= 0 ) ;
}

void main ( void ) {


i nt i , j , k , tmp ;
i nt maxim ;
i nt nod ;

readInput ( ) ;
// prima eta pa
memset ( v i z i t a t , 0 , s i z e o f ( v i z i t a t ) ) ;
nump = 0 ;
for ( i = 0 ; i < n ; i ++)
i f ( v i z i t a t [ i ] == FALSE)
dfs ( i ) ;
// eta pa a doua
memset ( v i z i t a t , 0 , s i z e o f ( v i z i t a t ) ) ;
for ( i = 0 ; i < n ; i ++)
for ( j = i ; j < n ; j ++) {
tmp = v e c i n [ i ] [ j ] ;
vecin [ i ] [ j ] = vecin [ j ] [ i ] ;
v e c i n [ j ] [ i ] = tmp ;
}
k = 0;
while (TRUE) {
maxim = 0 ;
for ( i = 0 ; i < n ; i ++)
i f ( ( v i z i t a t [ i ] == FALSE) && ( maxim < postnum [ i ] ) ) {
maxim = postnum [ i ] ;
nod = i ;

138
}
i f ( maxim == 0 )
break ;
k++;
p r i n t f ( ”Componenta %d : ” , k ) ;
d f s 1 ( nod ) ;
p r i n t f ( ” \n” ) ;
}
}

Exemplul 6.17 După parcurgerea ı̂n adâncime a grafului 6.6, valorile vectorilor vizitat şi
postnum sunt următoarele:

1 2 3 4 5 6 7 8
postnum 8 7 5 6 1 2 3 4
vizitat 1 1 1 1 1 1 1 1

Fig. 6.7: Graful transpus GT corespunzător grafului din figura 6.6

Se construieşte graful transpus, GT (a se vedea figura 6.7). Se caută primul nod u ı̂ncă
nevizitat (vizitatu = 0), căruia ı̂i corespunde cea mai mare valoare postnumu . În acest mod,
se identifică componenta tare conexă compusă numai din nodul 1: {1}.
Următorul nod, ca valoarea a vectorului postnum ordonat descrescător, este 2. Din nodul
2 se parcurg nodurile 3 şi 4, rezultând o altă componentă tare conexă: {2, 3, 4}.
Următorul nod nevizitat u ce are valoarea prenumu maximă este nodul 8. Se identifică
mai ı̂ntâi componenta tare conexă {7, 8} şi apoi {5, 6}.

Analiza algoritmului
Algoritmul lui Kosaraju realizează două parcurgeri ale tuturor elementelor grafului G: prima
dată parcurge graful G şi a doua oară parcurge graful GT . Prin urmare, complexitatea
timp a algoritmului este Ω(|V | + |E|) ı̂n cazul ı̂n care graful este reprezentat prin liste de
adiacenţă şi este pătratică ı̂n |V |2 (O(|V |2 )) atunci când graful este reprezentat prin matricea
de adiacenţă.

6.4.2 Algoritmul lui Tarjan


Algoritmul lui Tarjan [117] este considerat drept o ı̂mbunătăţire a algoritmului lui Kosaraju
prin aceea că nu mai este nevoie de parcurgerea grafului G de două ori.

139
Algoritmul iniţiază o parcurgere ı̂n adâncime a nodurilor grafului G pornind de la un nod
oarecare. O componentă tare conexă a grafului G, dacă există, constituie un subarbore al
arborelui de acoperire ı̂n adâncime, iar rădăcina acestui subarbore este reprezentantul clasei
de echivalenţă.
Prin urmare componentele tare conexe se obţin prin descompunerea arborelui/arborilor
de acoperire ı̂n adâncime dacă eliminăm anumite arce ale acestora. Un nod este capul unei
componente tare conexe (sau rădăcina), dacă acesta constituie rădăcina subarborelui core-
spunzător componentei. Arcul ce are nodul-cap drept extremitate finală este cel ce trebuie
eliminat. După ce determinăm toate nodurile-cap, subarborii arborelui/arborilor de acoperire
ı̂n adâncime ce ı̂i au drept rădăcini, sunt componentele tare conexe.
Algoritmul lui Tarjan are drept scop determinarea nodurilor-cap. Pentru a păstra o ordine
a vizitării acestora pe parcursul algoritmului, nodurile vor fi adăugate pe o stivă. Ele vor fi
extrase din stivă ı̂n momentul ı̂n care procedura de vizitare DFS a unui nod este ı̂ncheiată: se
determină dacă nodul curent este rădăcina unei componente tare conexe, şi, ı̂n caz afirmativ,
toate nodurile care au fost vizitate din nodul curent ı̂n cadrul parcurgerii ı̂n adâncime sunt
marcate ca fiind elemente ale componentei tare conexe.
Vom numerota toate nodurile grafului ı̂n preordine (altfel spus, acest număr indică pen-
tru nodul curent numărul de noduri vizitate ı̂naintea sa), valorile fiind păstrate ı̂n vectorul
prenum (cititorul este invitat să revadă şi secţiunea despre Muchie critică, cea de-a doua
soluţie). Pentru un nod u ∈ G definim lowu astfel:

prenumu

lowu = min prenumx , dacă [u, x] este arc de ı̂ntoarcere sau de traversare şi x ∈ S


lowy , ∀y descendent direct al lui u

Dacă dintr-un vârf u ∈ G există un arc de ı̂ntoarcere sau de traversare către un nod v ∈ G
ı̂n afara subarborelui de vizitare ı̂n adâncime determinat de u, atunci acest nod v trebuie să
fi fost vizitat ı̂naintea lui u (prenumu > prenumv ). Dacă un astfel de nod nu există atunci
lowu = prenumu . Stiva S păstrează nodurile grafului, pe măsură ce sunt vizitate, ele fiind
adăugate la S. Dacă u este un nod-cap (lowu = prenumu ) se extrag din stivă toate nodurile
dintre vârful stivei şi u inclusiv: acestea formează o componentă tare conexă.
Se observă că algoritmul lui Tarjan (algoritmul 50) seamănă foarte mult cu algoritmul 22
de determinare a muchiei critice (varianta a II-a).

Exemplul 6.18 La ı̂nceput valorile vectorilor vizitat şi prenum sunt următoarele:

1 2 3 4 5 6 7 8
prenum 0 0 0 0 0 0 0 0
vizitat 0 0 0 0 0 0 0 0
Primul element nevizitat este nodul 1 (linia 9), prin urmare se apelează DFSTarjan(1,
8, Vecin). Rezultă o secvenţă de apeluri recursive:
DFSNum(1, 8, Vecin) → DFSNum(2, 8, Vecin) → DFSNum(3, 8, Vecin)
→ DFSNum(6, 8, Vecin) → DFSNum(5, 8, Vecin).

1 2 3 4 5 6 7 8
prenum 1 2 3 0 5 4 0 0
vizitat 1 1 1 0 1 1 0 0
low 1 2 0 0 4 4 0 0
Stiva conţine următoarele elemente (vârful stivei fiind ı̂n dreapta): {1, 2, 3, 6, 5}.

140
Algoritm 50 Algoritmul lui Tarjan pentru determinarea componentelor tare conexe

( Tarjan(n, V ecin)
1: procedure
n - numărul de noduri din graf
Input:
V ecin - matricea de adiacenţă
2: for i ← 1, n do
3: vizitati ← 0
4: end for
5: counter ← 0
6: S←∅
7: for i ← 1, n do
8: if (vizitati = 0) then
9: call DF ST arjan(i, n, V ecin) ⊲ vizitarea ı̂n adâncime a grafului
10: end if
11: end for
12: end procedure
13: procedure DFSTarjan(k, n, V ecin)
14: S⇐k
15: vizitatk ← 1
16: counter ← counter + 1
17: prenumk ← counter, lowk ← counter
18: for i ← 1, n do
19: if (vecink,i = 1) then
20: if (vizitati = 0) then
21: call DF ST arjan(i, n, V ecin)
22: lowk ← M in(lowk , lowi )
23: else
24: if (i ∈ S) then
25: lowk ← M in(lowk , prenumi )
26: end if
27: end if
28: end if
29: end for
30: if (lowk = prenumk ) then
31: repeat
32: S⇒u
33: Output ”u”
34: until (u = k)
35: end if
36: end procedure

low5 = min{prenum6 , low5 } = min{4, 5} = 4 (deoarece deşi vizitat6 = 1, avem 6 ∈ S,


adică nodul 6 se află pe stivă). Astfel ı̂n momentul terminării apelului DFSTarjan(5, 8,
Vecin), low5 = 4.
Acum la nivelul lui DFSTarjan(6, 8, Vecin), ı̂n urma revenirii din DFSTarjan(5, 8,
Vecin), se calculează low6 = min{low6 , low5 } = min{4, 4} = 4 (linia 22).
Nodul 6 este un nod-cap (low6 = prenum6 ) (vezi linia 30), şi prin urmare se extrag de pe
stivă toate elementele dintre vârful stivei şi elementul 6 inclusiv, rezultând prima componentă
tare conexă: {5, 6}. Stiva rămâne cu elementele (vârful stivei fiind ı̂n dreapta) {1, 2, 3}.
Din nodul 3 se continuă cu vizitarea nodului 8, ı̂ncă nevizitat:

141
DFSNum(3, 8, Vecin) → DFSNum(8, 8, Vecin) → DFSNum(7, 8, Vecin).
Conţinutul stivei este {1, 2, 3, 8, 7}.
La nivelul apelului DFSNum(7, 8, Vecin) nu se ia ı̂n considerare la calculului valorii lui
low7 nodul 6: deşi există arcul (7, 6), nodul 6 a fost vizitat (vizitati 6= 0) şi nu se mai află
pe stivă (i ∈
/ S). Astfel low7 = min{low7 , prenum8 } = min{7, 6} = 6.
La nivelul lui DFSNum(8, 8, Vecin), low8 = min{low8 , low7 } = min{6, 6} = 6 (linia
22). Avem că (low8 = prenum8 ) (linia 30) şi prin urmare 8 este un nod-cap. Se extrag de pe
stivă toate elementele dintre vârful stivei şi elementul 8 inclusiv, rezultând a doua componentă
tare conexă: {7, 8}.
Stiva rămâne cu elementele (vârful stivei fiind ı̂n dreapta) {1, 2, 3}.

1 2 3 4 5 6 7 8
prenum 1 2 3 0 5 4 7 6
vizitat 1 1 1 0 1 1 1 1
low 1 2 0 0 4 4 6 6
În momentul terminării apelului DFSTarjan(3, 8, Vecin), low3 = 2.
Revenind la nivelul apelului DFSTarjan(2, 8, Vecin), se calculează low2 = min{low2 , low3 } =
min{2, 2} = 2, şi se continuă cu următorul nod ı̂ncă nevizitat, 4: DFSNum(2, 8, Vecin) →
DFSNum(4, 8, Vecin).
Se adaugă nodul 4 pe stivă (stiva devine {1, 2, 3, 4}), low4 = prenum4 = 8.
low4 se calculează din low4 = min{low4 , prenum3 } = min{8, 3} = 3 (există arcul (4, 3),
nodul 3 a fost vizitat - vizitat3 = 1 şi 3 ∈ S - nodul 3 se află pe stivă).
1 2 3 4 5 6 7 8
prenum 1 2 3 8 5 4 7 6
vizitat 1 1 1 1 1 1 1 1
low 1 2 2 3 4 4 6 6
Ne ı̂ntoarcem la nivelul apelului DFSTarjan(2, 8, Vecin), low2 = min{low2 , low4 } =
min{2, 3} = 2. Arcul (2, 5) nu este luat ı̂n considerare la calculul lui low2 deoarece nodul 5 a
fost vizitat (vizitat5 = 1) şi 5 nu se regăseşte pe stivă. Astfel low2 rămâne cu valoarea 2.
Deoarece low2 = prenum2 , extragem de pe stivă elementele ce determină cea de-a treia
componentă tare conexă: {4, 3, 2}. Pe stivă mai rămâne un singur element, {1}, ce va
determina ultima componentă tare conexă.
În final, valorile low şi prenum sunt următoarele:

1 2 3 4 5 6 7 8
prenum 1 2 3 8 5 4 7 6
vizitat 1 1 1 1 1 1 1 1
low 1 2 2 3 4 4 6 6

6.4.3 Algoritmul lui Gabow


Algoritmul a fost introdus de către Joseph Cheriyan and Kurt Mehlhorn ı̂n 1996 [28] şi apoi
independent de către Harold Gabow ı̂n 1999 [56].
Algoritmul construieşte un graf H [56] ce reprezintă o contracţie a grafului original G: unul
sau mai multe noduri din G pot să corespundă unui nod din H. La ı̂nceput, se iniţializează
H = G. Se ı̂ncepe construirea unui drum P alegându-se un nod oarecare v ∈ H.
La fiecare pas al algoritmului se ı̂ncearcă să se augmenteze drumul P = P ∪ {w} =
[v1 , . . . , vk , w] prin parcurgerea tuturor arcelor (vk , w):

142
Fig. 6.8: Drumul P ı̂n algoritmul lui Gabow pentru graful din figura 6.6

- dacă w ∈
/ P , atunci se adaugă nodul w la drumul P (P = [v1 , . . . , vk , w]);

- dacă w ∈ P , fie w = vj . Contractăm circuitul {vj , vj+1 , . . . , vk , w} ı̂n graful H: ı̂n


locul mulţimii de vârfuri {vj , vj+1, . . . , vk } va rămâne doar reprezentantul acesteia, e.g.
nodul ce are valoarea minimă ı̂n urma numerotării ı̂n preordine;

- dacă nu avem nici un arc neverificat care să aibă nodul vk ca extremitate iniţială, se
marchează nodul vk ca aparţinând unei componente conexe. Se şterge nodul vk din
graful H şi din drumul P , ı̂mpreună cu toate arcele ce ı̂l au ca extremitate. Dacă
P 6= ∅ atunci se continuă algoritmul ı̂ncercându-se augmentarea drumului P . În caz
contrar, se ı̂ncearcă iniţializarea unui drum nou P ı̂n H.

Şi acest algoritm are la bază metoda de parcurgere ı̂n adâncime a unui graf (DFS). În
timpul vizitării, algoritmul utilizează două stive S şi P : S conţine toate nodurile ce nu au
fost atribuite ı̂ncă unei componente tare conexe, ı̂n ordinea ı̂n care au fost ı̂ntâlnite, iar P
conţine toate nodurile despre care ı̂ncă nu se poate spune nimic cu privire la apartenenţa lor
la componente conexe diferite (conţine nodurile drumului P din graful H). Altfel spus, se
observă faptul că stiva P păstrează nodurile rădăcină ce formează o subsecvenţă a secvenţei
nodurilor ce se află la un moment dat ı̂n cealaltă stivă S.
În cadrul secvenţei următoare, pentru un nod i adiacent cu nodul curent k, se verifică
dacă nu a mai fost vizitat. Dacă vizitati = 0 atunci se continuă vizitarea ı̂n adâncime
(call DFSGabow(i, n, Vecin)). Altfel, se verifică dacă nodul i nu a fost deja asignat unei
componente tare conexe, ı̂n caz afirmativ eliminându-se din drumul P circuitul {vi , . . . , vk , vi }:
1: if (vizitati = 0) then
2: call DF SGabow(i, n, V ecin)
3: else
4: if (i ∈ S) then ⊲i
5: while (prenumi < prenumpeek(P )) do
6: P ⇒u
7: end while
8: end if
9: end if
La finalul procedurii DFSGabow, se verifică dacă nodul curent este identic cu cel aflat ı̂n
vârful stivei P . În caz afirmativ, se extrag de pe stivă toate nodurile dintre vârful curent

143
al stivei S şi nodul k, şi se marchează ca fiind elemente ale componentei tare conexe a cărei
rădăcină este nodul k:
if (k = peek(P )) then
P ⇒u
repeat
S⇒u
Output ”u”
until (u = k)
end if

Algoritm 51 Algoritmul lui Gabow pentru determinarea componentelor tare conexe


1: procedure Gabow(n, V ecin)
2: for i ← 1, n do
3: vizitati ← 0
4: end for
5: counter ← 0
6: S ← ∅, P ← ∅
7: for i ← 1, n do
8: if (vizitati = 0) then
9: call DF SGabow(i, n, V ecin)
10: end if
11: end for
12: end procedure
13: procedure DFSGabow(k, n, V ecin)
14: vizitatk ← 1
15: S ⇐ k, P ⇐ k
16: counter ← counter + 1
17: prenumk ← counter
18: for i ← 1, n do
19: if (vecink,i = 1) then
20: if (vizitati = 0) then
21: call DF SGabow(i, n, V ecin)
22: else
23: if (i ∈ S) then ⊲ i nu a fost asignat ı̂ncă unei componente tare conexe
24: while (prenumi < prenumpeek(P )) do
25: P ⇒u
26: end while
27: end if
28: end if
29: end if
30: end for
31: if (k = peek(P )) then
32: P ⇒u
33: repeat
34: S⇒u
35: Output ”u”
36: until (u = k)
37: end if
38: end procedure

144
6.5 Exerciţii
1. (Compilator ) În trecut compilatoarele erau foarte simple. În acele vremuri oamenii
preferau să includă tot programul ı̂ntr-un singur fişier. Dacă cineva făcea o modificare ı̂n
program, trebuia recompilat tot codul sursă. Creşterea lungimii programelor conducea
la timpi de compilare tot mai mari, ceea ce constituia o piedică ı̂n ceea ce priveşte
creşterea complexităţii algoritmilor implementaţi.
De aceea programatorii au dezvoltat o tehnică pentru eliminarea compilărilor redun-
dante. Ei au ı̂nceput prin a sparge programele ı̂n mai multe module mici pe care le
compilau separat. Astfel, pentru orice modificare care se opera ı̂ntr-un anumit modul,
se compila numai acesta, şi nu ı̂ntreaga aplicaţie. Fiecare modul conţine la ı̂nceput lista
celorlaltor module pe care le foloseşte.
Modulul A trebuie recompilat numai dacă a fost modificat sau are ı̂n lista sa un modul
B care a fost recompilat la rândul său. În celelalte cazuri nu este necesară recompilarea
modulului A.
Problema cere să se realizeze un algoritm şi pe baza acestuia un program, care să decidă
ce module trebuie recompilate şi care nu. Pentru a avea un timp de recompilare minim,
va trebui să căutam compilarea unui număr cât mai mic de linii.
Prima linie a datelor de intrare conţine numărul de module N (1 ≤ N ≤ 100). Urmează
descrierea modulelor. Prima linie a descrierii conţine numele modulului. A doua linie
conţine numărul liniilor din codul sursă. A treia linie conţine numărul M (0 ≤ M < N)
de module de care depinde modulul actual. Linia următoare conţine numele acestor
module, separate printr–un spaţiu. Numele unui modul nu depăşeşte 20 de caractere.
Descrierea modulelor este urmată de mai multe blocuri, câte unul pentru fiecare versiune
a programului. Prima linie a fiecărui bloc conţine numărul k (1 ≤ k ≤ N) de module
ce au suferit modificări de la recompilarea versiunii precedente.
Linia următoare conţine numele modulelor, separate prin spaţiu, ı̂n care au survenit
modificări. După ultimul bloc există o singură linie ce conţine doar numărul 0.
Pentru fiecare versiune a programului se scrie o linie ce conţine numărul liniilor codului
sursă care au trebuit să fie recompilate.
Intrare Ieşire
3 127
MATH
20
0
MAIN
100
2
MATH IO
IO
7
0
3
MATH IO MAIN
0
(Propusa la CEOI 1997)

2. Se dă un număr de k (k < 1000) reguli, numerotate de la 1 la k. O regulă are forma


x → y unde x şi y sunt propoziţii cu următoarea semnificaţie: dacă propoziţia x este
adevarată, atunci proproziţia y este adevărată.

145
Să considerăm pentru k = 5 următoarele reguli:
(1) 1 → 2 (2) 1 → 3 (3) 3 → 4 (4) 4 → 5 (5) 1 → 5
Profesorul cere să se demonstreze regula 1 → 5. Demonstraţia optimă constă ı̂n apli-
carea directă a regulii (5).
(2) (1) (3) (4)
Un elev demonstrează regula (5) astfel: 2 1 3 4 (1 → 3, 1 → 2, 3 → 4, 4 → 5). Aceasta
(1)
este o demonstraţie corectă, ce conţine ı̂n plus regula 1 → 2. Profesorul ar fi fost
(2) (3) (4)
mulţumit cu demonstraţia 2 3 4 (1 → 3, 3 → 4, 4 → 5).
Să se realizeze un algoritm ce utilizează drept date de intrare k, x (propoziţia care se
consideră adevărată), y (propoziţia ce trebuie demonstrată), şirul celor k reguli cât şi
şirul numerelor de ordine ale regulilor ce constituie demonstraţia elevului.
Algoritmul verifică dacă şirul numerelor de ordine ale regulilor constituie o demonstraţie.
În caz negativ va afişa ′ NU ′ , iar ı̂n caz afirmativ va afişa ′ DA′ , urmat pe linia următoare
de demonstraţia elevului din care au fost eliminate afirmaţiile inutile. Se consideră că
un şir de reguli fără reguli ı̂n plus constituie o demonstraţie corectă a propoziţiei y
pornind de la propozitia x, dacă renunţarea la orice regulă ar conduce la o secvenţă
prin care y nu se mai poate deduce din x.
(Marele premiu PACO, 1997)

3. După cum se poate observa prin reclamele de la televizor, multe companii cheltuiesc
foarte mulţi bani pentru a convinge oamenii că oferă cele mai bune servicii la cel mai
scăzut preţ. O companie de telefoane oferă cercuri de apel (calling circles).
Un abonat poate să facă o listă cu persoanele pe care le sună cel mai frecvent (şi care
constituie cercul său de prieteni). Dacă acesta sună pe cineva inclus ı̂n această listă,
şi persoana respectivă este, de asemenea, abonată la aceeaşi companie, va beneficia de
un discount mai mare decât pentru o convorbire telefonică cu cineva din afara listei.
O altă companie a aflat de această iniţiativă şi se oferă să determine ea lista de
cunoştinţe cu care un abonat vorbeşte cel mai frecvent la telefon.
LibertyBellPhone este o companie nouă de telefoane ce se gândeşte să ofere un plan
de apeluri mult mai avantajos decât alte companii. Ea oferă nu numai reduceri pentru
”cercul de apel”, cât şi determină pentru un abonat acest cerc. Iată cum procedează:
compania păstrează numerele tuturor persoanelor participante la fiecare apel telefonic.
În afara unui abonat, cercul său de apel constă din toate persoanele pe care le sună şi
care ı̂l sună, direct sau indirect.
De exemplu, dacă Ben ı̂l sună pe Alexander, Alexander o sună pe Dolly şi Dolly ı̂l
suna pe Ben, atunci ei toţi fac parte din acelaşi cerc. Dacă Dolly ı̂l mai sună şi pe
Benedict iar Benedict o sună pe Dolly, atunci Benedict este ı̂n acelaşi cerc cu Dolly,
Ben şi Alexander. În fine, dacă Alexander ı̂l sună pe Aron dar Aaron nu ı̂l sună pe
Alexander, Ben, Dolly sau Benedict, atunci Aaron nu este ı̂n cerc.
Să se realizeze un algoritm ce determină cercurile de apel, cunoscându–se lista apelurilor
telefonice dintre abonaţi.
(Finala ACM 1995, Calling Circles)

4. La facultatea X există două alternative pentru ca studenţii să aibă timp să asimileze
cunoştinţele:

146
(a) Mărirea zilei la 30 de ore, şi

(b) Reducerea programei şcolare.

Optând pentru a doua variantă, Liga Studenţilor introduce o platformă program care:

(a) Stabileşte care sunt materiile necesare pentru a putea studia o nouă materie.
De exemplu, pentru a studia cursul ”Management” este nevoie de cursul ”Teorie
Economică” şi de cursul ”Marketing”.

(b) Stabileşte care sunt materiile cu adevărat utile dintre toate cele studiate ı̂n fac-
ultate. De exemplu, cursul de ”Măsurări electrice” nu este util la absolut nimic.

(c) Cere să se elimine din programă materiile care nu sunt nici folositoare, nici nu
servesc (direct sau indirect) la ı̂nvăţarea unor materii folositoare.

(d) Cere să se indice care dintre materii nu pot fi predate ı̂n nici o ordine. De ex-
emplu, cursul ”Mecanică” se bazează pe cursul ”Ecuaţii Diferenţiale”, dar cursul
de ”Ecuaţii Diferenţiale” ı̂şi preia exemplele din ”Mecanică”. Prin urmare nu ex-
istă nici o ordine ı̂n care aceste materii să fie predate fără a introduce cunoştinţe
nedemonstrate.

5. Distribuirea cărţilor cerute de către cititorii de la sala de lectură a unei biblioteci este
făcută de către un robot ce are posibilitatea să ajungă la orice carte ce poate fi solicitată.
Din păcate, rafturile unde sunt depozitate cărţile sunt dispuse astfel ı̂ncât robotul nu
poate lua cărţile ı̂ntr–un singur drum. După recepţionarea comenzilor de la mai mulţi
cititori, robotul cunoaşte poziţia cărţilor ı̂n rafturi şi drumurile către acestea. Din
păcate, de la poziţia unei cărţi, robotul nu se poate deplasa decât spre anumite cărţi.
Acesta porneşte şi culege cărţile ce sunt accesibile ı̂ntr–un drum, apoi porneşte de la
o altă pozitie de carte şi culege acele cărţi ce sunt accesibile din acel punct şi aşa mai
departe.
Datele de intrare constau din numărul de cărţi n precum şi numărul m de legături
dintre poziţiile acestora (1 ≤ n ≤ 100, 11 ≤ m ≤ 10000), urmate de m perechi de
numere naturale, ce semnifică legăturile directe ı̂ntre poziţiile cărţilor.
Datele de ieşire constau din drumurile pe care le face robotul pentru a culege cărţile
cerute.

6. Se consideră o mulţime de n elevi dintr-o clasă. Fiecare elev are cunoştinţe mai avansate
ı̂ntr-un anumit domeniu. Pentru ridicarea nivelului clasei, dirigintele vrea să-i aranjeze
ı̂n grupuri astfel ı̂ncât toţi elevii dintr-un grup să ajungă să cunoască, ı̂ntr-o anumită
perioadă de timp, toate cunoştinţele tuturor celorlalţi colegi din acelaşi grup.
Grupurile de elevi nu ı̂şi vor schimba cunoştinţele ı̂ntre ele. Se ştie că un elev nu se
poate face ı̂nţeles de către oricine. El are o listă de preferinţe faţă de care el le va
ı̂mpărtăşi cunoştintele sale. Relaţia de preferinţă nu este simetrică.
Să se determine numărul minim de grupuri ı̂n care va fi ı̂mpărţită clasa precum şi
componenţa acestora.

147
Fig. 6.9: Poziţiile cărţilor ı̂ntr-o bibliotecă precum şi posibilităţile de deplasare ale robotului

148
Capitolul 7

Heap-uri

Un heap (eng. heap = movilă) reprezintă o structură de date abstractă organizată sub forma
unei structuri ierarhice (de arbore) şi care respectă următoarea proprietate: pentru oricare
două noduri ale arborelui, A şi B, astfel ı̂ncât tataB = A, avem cheieA ≥ cheieB (sau
cheieA ≤ cheieB ). Fiecare nod va avea o valoare asociată numită cheie. Se observă faptul că,
dacă este ı̂ndeplinită proprietatea că ∀A, B, tataB = A, cheieA ≥ cheieB , atunci ı̂ntotdeauna
cheia de valoare maximă se va afla ı̂n rădăcina arborelui.
Structura de date heap poate fi implementată sub mai multe forme: 2 − 3 heap [116], heap
binar [9], heap binomial [125], heap Fibonacci [54], heap ternar, treap 1 [7], [109].
Principalele operaţii definite pentru un heap H sunt:
1. FindMin(H; p) - determină cheia având cea mai mică valoare şi ı̂ntoarce o referintă
către nodul ce o conţine;
2. DeleteMin(H) - şterge nodul ce conţine cheia de valoare minimă şi reorganizează struc-
tura prin refacerea proprietăţii de heap;
3. Insert(H,p) - inserează nodul p ı̂n heap-ul H;

4. Delete(H,p) - şterge nodul p din heap-ul H;

5. DecreaseKey(H,p,v) - modifică valoarea cheii nodului p din heap-ul H, atribuindu-i


valoarea v şi reorganizează această structură, păstrându-i proprietatea de heap;

6. MergeHeaps(H1 , H2 , H) - unifică două heap-uri, desemnate prin H1 şi H2 , ı̂ntr-una


nouă H, ce va conţine elementele celor două heap-uri.
Tabelul 7.1 ([30]) ilustrează complexitatea-timp a acestor operaţii pentru diferite tipuri
de implementări ale structurii de heap.
Structura de date de tip heap prezintă multiple aplicaţii dintre care amintim algoritmul
de ordonare heapsort a unei secvenţe de elemente, diferiţi algoritmi din teoria grafurilor
(algoritmi pentru determinarea drumurilor de cost minim) sau algoritmi de selecţie (pentru
determinarea valorii minime, maxime, mediane dintr-un vector).

7.1 Heap-uri binare (Min-heapuri sau Max -heapuri)


Definiţia 7.1 Un min-heap este un arbore binar complet ı̂n care valoarea memorată ı̂n
orice nod al său este mai mică sau egală decât valorile memorate ı̂n nodurile fii ai acestuia.
1
http://acs.lbl.gov/~aragon/treaps.html

149
Table 7.1: Complexitatea-timp a principalelor operaţii ale unui heap

Heap binar Heap binomial Heap Fibonacci


FindMin Θ(1) O(log n) Θ(1)
DeleteMin Θ(log n) Θ(log n) O(log n)
Insert Θ(log n) O(log n) Θ(1)
Delete Θ(log n) Θ(log n) O(log n)
DecreaseKey Θ(log n) Θ(log n) Θ(1)
MergeHeaps Θ(n) O(log n) Θ(1)

Fig. 7.1: Un exemplu de heap

Într-un mod similar se poate defini un max-heap, drept un arbore binar complet ı̂n care
valoarea memorată ı̂n orice nod este mai mare sau egală decât valorile memorate ı̂n nodurile
fii ai acestuia.

Această structură poate fi interpretată drept un arbore parţial ordonat ce este un arbore
binar complet cu n vârfuri.
Reprezentarea cea mai adecvată pentru un heap binar (min-heap) este reprezentarea
secvenţială. Pentru n noduri vom utiliza un tablou A cu dimensiunea egală cu numărul
de noduri. a1(reprezintă cheia nodului rădăcină al arborelui.
(
2·i dacă 2 · i ≤ n 2·i+1 dacă 2 · i + 1 ≤ n
Lef ti = , Righti =
nu există dacă 2 · i > n nu există dacă 2 · i + 1 > n
(
⌊ 2i ⌋ dacă i > 1
T atai =
nu există dacă i = 1

Exemplul 7.1 Fie heap-ul binar din figura 7.1. Acesta poate fi descris de următoarea struc-
tură de date secvenţială:

Nod 1 2 3 4 5 6 7 8 9 10
cheie 3 5 8 7 8 8 11 10 18 9
Conform definiţiei avem:

150
Nod 1 2 3 4 5 6 7 8 9 10
Left 2 4 6 8 10 - - - - -
Right 3 5 7 9 - - - - - -
Tata - 1 1 2 2 3 3 4 4 5

7.1.1 Inserarea unui element


Procedura Insert (a se vedea algoritmul 52) va insera elementul X ı̂n heap-ul reprezentat de
A. Se adaugă noul element pe ultimul nivel, pe prima poziţie liberă ı̂ntâlnită ı̂n parcurgerea
pe nivel de la stânga la dreapta. Se verifică dacă este respectată proprietatea structurii de
tip heap de către configuraţia curentă a elementelor şi se reorganizează această structură, ı̂n
cazul ı̂n care proprietatea nu este ı̂ndeplinită: dacă noul element are valoarea etichetei mai
mică decât cea a părintelui său, atunci face schimb de poziţii cu acesta. Procesul se repetă
până când noul element este fie rădăcina, fie are eticheta mai mare decât cea a părintelui
său.
Algoritm 52 Algoritm de inserare ı̂ntr-un heap
1: procedure Insert(A, last, N, X)
2: if (last ≥ n) then
3: Output {′ Heap plin!′ }
4: return
5: end if
6: last ← last + 1
7: alast ← x ⊲ adaugă elementul cel nou pe ultima poziţie
8: i ← last
9: while ((i > 1) ∧ (ai < a⌊ i ⌋ )) do
2
10: ai ↔ a⌊ i ⌋ ⊲ interschimbă valorile lui ai şi a⌊ i ⌋
2 2
11: i← ⌊ 2i ⌋
12: end while
13: end procedure

Fig. 7.2: Inserarea valorii 3 ı̂ntr-un heap

151
Exemplul 7.2 În figura 7.2 pot fi urmărite operaţiile necesare pentru inserarea unui nod
având cheia asociată de valoare 3. Se adaugă elementul având valoarea cheii 3, pe ultima
poziţie a heap-ului. Deoarece valoarea cheii ultimului element 3 este mai mică decât valoarea
cheii părintelui său 8, se interschimbă valorile cheilor. Se compară din nou, valoarea cheii
nodului curent (cheia cu valoarea 3) cu valoarea cheii asociată părintelui său (nodul 1). Se
interschimbă din nou valorile cheilor. La final, heap-ul arată ca ı̂n ultima configuraţie din
figura 7.2.

7.1.2 Ştergerea elementului minim


Procedura DeleteMin (a se vedea algoritmul 53) va elimina nodul rădăcină din heap, ı̂ntorcând
valoarea acestuia prin intermediul variabilei minim, şi va reorganiza heap-ul.
În linia 6 se păstrează valoarea rădăcinii ı̂n variabila minim. Instrucţiunea următoare ia
ultimul element şi ı̂l aduce pe prima poziţie. În linia 8, se decrementează numărul de elemente
existente la momentul respectiv ı̂n heap. Apoi se păstrează ı̂n variabila j indicele elementului
ce conţine cea mai mică valoare a cheii, dintre cheile corespunzătoare descendenţilor nodului
curent (liniile 11-15).
În linia 17 se interschimbă poziţia părintelui cu poziţia celui mai mic dintre descendenţii
săi, dacă este necesar. Instrucţinea de ciclare while (liniile 10 - 22) se părăseşte atunci când
s-a ajuns la o frunză sau când nu se mai poate ı̂nainta ı̂n jos ı̂n arbore (linia 20).

Algoritm 53 Algoritm de ştergere a elementului minim dintr-un heap


1: procedure DeleteMin(A, last, N ; minim)
2: if (last = 0) then
3: Output {′ Heap vid!′ }
4: return
5: end if
6: minim ← a1
7: a1 ← alast
8: last ← last − 1
9: i←1
10: while (i ≤ ⌊ last
2 ⌋) do
11: if ((2 · i = last) ∨ (a2·i < a2·i+1 )) then
12: j ←2·i
13: else
14: j ←2·i+1
15: end if
16: if (ai > aj ) then
17: ai ↔ aj
18: i←j
19: else
20: return
21: end if
22: end while
23: end procedure

În figura 7.3 pot fi urmăriţi paşii necesari pentru ştergerea nodului de valoare minimă din
heap-ul considerat drept exemplu.

152
Fig. 7.3: Ştergerea valorii minime dintr-un heap

7.1.3 Crearea unui heap

Algoritm 54 Algoritm de creare a unui heap (prima variantă)


1: procedure NewHeap1(A, n)
2: for i ← 2, n do
3: call Insert(A, i − 1, n, ai )
4: end for
5: end procedure

Prima metodă de construire a unui heap binar se bazează pe următoarea tehnică: se pleacă
iniţial cu heap-ul format dintr-un singur element şi se inserează pe rând, toate elementele ı̂n
heap-ul nou creat (a se vedea algoritmul 54).
Exemplul 7.3 Să construim o structură de heap din şirul de elemente 10, 7, 9, 5, 7 . . .. Se
pleacă cu heap-ul format dintr-un singur nod, 10. Apoi, se inserează elementul cu valoarea 7
ı̂n heap. Deoarece 7 > 10, se interschimbă valorile ı̂ntre ele (a se vedea figura 7.4). Urmează
să se insereze nodul a cărui valoare asociată este 9 (acest nod va fi descendentul drept al
rădăcinii, după cum se poate observa din figura 7.4). La final, se adaugă nodul a cărui cheie
are valoarea 5, şi se restabileşte proprietatea de heap prin interschimbări succesive.
Analizând numărul de operaţii efectuate de procedura Insert, se observă că o valoare
nou introdusă poate să ajungă până ı̂n rădăcina arborelui. Astfel, numărul de operaţii
(interschimbări) ı̂n cazul cel mai defavorabil va fi egal cu ı̂nălţimea arborelui, h. Deoarece
avem un arbore binar complet cu cel mult n noduri, ı̂nălţimea acestuia este cel mult [log n]
(h ≤ [log n]).
Complexitatea timp ı̂n cazul cel mai defavorabil al procedurii NewHeap este egală cu suma
timpilor necesari inserării tuturor celor n valori, ı̂n cazul cel mai defavorabil. Vom ţine cont
de faptul că, ı̂n cazul unui arbore binar complet, numărul nodurilor de pe nivelul i este 2i ,
cu excepţia ultimului nivel unde numărul de noduri este mai mic.
n [log n] [log n]
X X X
i
T (n) = TInsert (i) ≤ i · 2 ≤ log n 2i = O(n log n) (7.1)
k=1 i=1 i=1

153
Fig. 7.4: Crearea unui heap prin inserări succesive

Cea de-a doua metodă se bazează pe ideea de a construi un heap prin combinări (uni-
uni) repetate de heap-uri, strategie cu o eficienţă superioară metodei anterioare. Iniţial, se
porneşte cu o pădure de arbori, compuşi fiecare numai dintr-un singur nod, rădăcina. La
fiecare pas i, se construieşte un heap nou din ai şi două heap-uri de dimensiunile apropiate:
heap-ul având rădăcina a2·i şi cel cu rădăcina a2·i+1 (a se vedea algoritmul 55).

Algoritm 55 Algoritm de creare a unui heap (a doua variantă)


1: procedure NewHeap2(A, n)
2: for i ← ⌊ n2 ⌋, 1 ST EP − 1 do
3: call P ush(A, i, n)
4: end for
5: end procedure

Se observa faptul că subrutina Push (a se vedea algoritmul 56) este asemănătoare cu
subrutina DeleteMin, scopul lui Push fiind acela de a reorganiza un heap ı̂ntre limitele f irst
şi last ale tabloului A.

Exemplul 7.4 Să construim o structură de heap având drept date de intrare elementele
secvenţei:
1 2 3 4 5 6 7
10 7 9 5 7 8 6
După cum se poate observa din figura 7.5, pentru i = 3 se va construi heap-ul compus din
elementele de valori 9, 8 şi 6, pentru i = 2 se va construi heap-ul compus din elementele de
valori 7, 5 şi 7, iar pentru i = 1 se va construi heap-ul ce are drept rădăcină elementul de
valoare 10, descendentul stâng fiind heap-ul construit la pasul i = 2 iar descendentul drept
fiind heap-ul construit la pasul i = 3.

Fie x un nod aflat pe nivelul i (i = 0, h − 1, h = log n). Acest nod va suferi cel mult
h − i deplasări ı̂n cadrul structurii de heap, migrând către frunzele arborelui. Numărul de

154
Algoritm 56 Algoritm de reorganizare a unui heap
1: procedure Push(A, f irst, last)
2: i ← f irst
3: while (i ≤ ⌊ last
2 ⌋) do
4: if ((last = 2 · i) ∨ (a2·i < a2·i+1 )) then
5: j ←2·i
6: else
7: j ←2·i+1
8: end if
9: if (ai > aj ) then
10: ai ↔ aj
11: i←j
12: else
13: i ← last
14: end if
15: end while
16: end procedure

Fig. 7.5: Crearea unui heap prin reorganizări succesive

operaţii (deplasări) efectuate de algoritmul NewHeap2 pentru a construi o structură de heap


cu n elemente este:
h−1 h h h
X
i j=h−i
X
h−j
X
h j X j
T (n) ≤ 2 (h − i) = j2 = 2 j ≤n < 2n = O(n) (7.2)
i=0 j=1 j=1
2 j=1
2j

7.2 Ordonare prin metoda HeapSort


Metoda de ordonare HeapSort [129] are la bază algoritmul general prezentat ı̂n subrutina
SortX (a se vedea algoritmul 57). Vom presupune că elementele şirului iniţial se află ı̂ntr-o
structură de date de tip listă L, iar rezultatul va fi obţinut ı̂n structura de date de tip heap
S. Iniţial structura S nu conţine nici un element.
Subrutina Insert(x, S; S) va adăuga elementul x structurii de date S. Subrutina

155
Algoritm 57 Algoritm de ordonare folosind heap-uri (prima variantă)
1: procedure SortX(A, n)
2: for f iecare x ∈ L do
3: call Insert(x, S; S)
4: end for
5: while (S 6= ∅) do
6: call M in(S; y)
7: ouput {y}
8: call Delete(y, S; S)
9: end while
10: end procedure

Min(S; y) ı̂ntoarce valoarea minimă a lui S ı̂n y, iar Delete(y, S; S) şterge elementul y
din S.
După câteva modificări ale algoritmului iniţial de ordonare se obţine algoritmul HeapSort
(a se vedea algoritmul 58): instrucţiunile din liniile 2-4 formează heap-ul ı̂ntr-o manieră
incrementală; pentru toate elementele, ı̂n linia 6 se elimină cel mai mic element din faţa
heap-ului şi se reface proprietatea de arbore parţial ordonat (linia 7) ı̂ntre limitele 1 şi i − 1.

Algoritm 58 Algoritm de ordonare folosind heap-uri (a doua variantă)


1: procedure HeapSort(A, n)
2: for i ← ⌊ 2i ⌋, 1 ST EP − 1 do
3: call P ush(A, i, n)
4: end for
5: for i ← n, 2 ST EP − 1 do
6: a1 ↔ ai
7: call P ush(A, 1, i − 1)
8: end for
9: end procedure

Implementarea ı̂n limbajul C a algoritmului de ordonare a unei secvenţe de numere


folosind heap-uri este următoarea:
Listing 7.1: sortheap.c
#include <s t d i o . h>

#define MAXN 100

/∗ ∗
∗ F u n c t i e pentr u c i t i r e a v a l o r i l o r d a t e l o r de i n t r a r e .
∗/
i nt r e a d I n p u t ( i nt ∗a ) {
i nt i , n ;

p r i n t f ( ”n=” ) ; s c a n f ( ”%d” , &n ) ;


for ( i = 1 ; i <= n ; i ++) {
p r i n t f ( ” a[%d]= ” , i ) ; s c a n f ( ”%d” , &a [ i ] ) ;
}
return n ;
}

156
/∗ ∗
∗ F u n c t i e pentr u a f i s a r e a r e z u l t a t e l o r .
∗/
void l i s t ( i nt ∗a , i nt n ) {
i nt i ;

for ( i = 1 ; i <= n ; i ++)


p r i n t f ( ”%d , ” , a [ i ] ) ;
p r i n t f ( ” \n” ) ;
}

/∗ ∗
∗ Reorganizarea unei movile .
∗ @param s t a r t − i n d i c e l e p r i m u l u i element a l s e c v e n t e i
∗ @param f i n i s h − i n d i c e l e l u l t i m u l u i element a l s e c v e n t e i
∗ @param a − v e c t o r u l c e p a s t r e a z a v a l o r i l e m o v i l e i
∗/
void push ( i nt s t a r t , i nt f i n i s h , i nt ∗ a ) {
i nt i , j , aux ;

i = start ;
while ( i <= f i n i s h / 2 ) {
i f ( 2 ∗ i == f i n i s h | | a [ 2 ∗ i ] < a [ 2 ∗ i + 1 ] )
j = 2 ∗ i;
else
j = 2 ∗ i + 1;
if (a [ j ] < a [ i ]) {
aux = a [ i ] ; a [ i ] = a [ j ] ; a [ j ] = aux ;
i = j;
}
else
return ;
}
}

i nt a [MAXN] ;

void main ( void ) {


i nt n = r e a d I n p u t ( a ) ;
i nt i , j , aux ;

// o r g a n i z a r e a unui heap
for ( i = n / 2 ; i > 0 ; i −−)
push ( i , n , a ) ;

// d e t e r m i n a r e a minimului s i r e o r g a n i z a r e a heap−u l u i
j = n;
while ( j > 1 ) {
aux = a [ 1 ] ; a [ 1 ] = a [ j ] ; a [ j ] = aux ;
j −−;
push ( 1 , j , a ) ;
}
l i s t (a , n ) ;
}

157
7.3 Aplicatie - Coadă cu prioritate
Definiţia 7.2 O coadă cu prioritate este o structură de date abstractă formată din ele-
mente ce au asociată o valoare numită cheie sau prioritate şi care suportă următoarele opera-
ţii:
• Insert(Q, X) - inserează elementul x ı̂n coada cu prioritate denumită Q;
• ExtractMax(Q) - extrage din coada cu prioritate Q, elementul de valoare maximă.

Multitasking-ul este o metodă prin intermediul căreia mai multe procese utilizează ı̂n co-
mun resursele calculatorului (inclusiv procesorul). În situaţia unui calculator cu un singur
procesor, se spune că ı̂n orice moment rulează cel mult un proces, ceea ce ı̂nseamnă că proce-
sorul execută instrucţiunile unui singur proces la un moment dat. Sarcina alegerii procesului
care să se afle ı̂n execuţie la un moment dat este o problemă de planificare. Operaţia de a
opri un proces aflat ı̂n execuţie, pentru a-i aloca altui proces, aflat ı̂n aşteptare, un timp pro-
cesor se numeşte schimbare de context (eng. context switch). Realizarea frecventă a acestor
schimbări de context crează iluzia execuţiei ı̂n paralel a mai multor programe. În cazul unui
calculator cu mai multe procesoare, multitasking-ul permite execuţia unui număr de procese
mai mare decât numărul de procesoare.
Sistemul de operare este cel care se ocupă de planificarea proceselor, planificare ce se
ı̂ncadrează ı̂ntr-una din următoarele strategii:
• ı̂n cazul sistemelor cu multiprogramare, procesul curent se află ı̂n execuţie până ı̂n
momentul ı̂n care realizează o operaţie ce presupune aşteptarea după un eveniment
extern (o operaţie de I/O), sau până când planificatorul de procese forţează eliberarea
procesorului.

• ı̂ntr-un sistem de tip time-sharing, fiecare proces va elibera procesorul ı̂n mod voluntar,
după expirarea cuantei de timp alocate acestuia, sau ı̂n urma apariţiei unui eveniment
hardware cum ar fi o ı̂ntrerupere.

• ı̂n cadrul sistemelor real-time, unui proces aflat ı̂n stare de aşteptare i se garantează
accesul la procesor ı̂n cazul apariţiei unui eveniment extern. Astfel de sisteme sunt
proiectate pentru controlul unor dispozitive mecanice cum ar fi roboţii industriali.
Comutarea ı̂ntre procese consumă timp procesor pentru ca planificatorul de procese să
ı̂ngheţe starea unui proces şi să dezgheţe starea altui proces (schimbare de context). Dacă
mai multe procese concurente sunt executate pe acelaşi procesor şi toate efectuează diverse
calcule, atunci timpul total de execuţie va fi mai mare decât timpul de execuţie al unui
program secvenţial echivalent. Creşterea vitezei sistemului se poate realiza prin ı̂ntreţeserea
(intercalarea) diferitelor faze ale mai multor procese.
În cadrul unui proces se pot identifica două tipuri de faze din punct de vedere logic:
faza de calcul şi faza de I/O. Faza de calcul se realizează exclusiv la nivelul procesorului
utilizându-se la maxim funcţiunile acestuia. Faza de I/O (intrare/ieşire) presupune un aport
mai mare din partea perifericelor (imprimante, hard discuri, plăci de reţea etc), procesul
aşteptând ca un periferic să-şi termine sarcina. În timp ce un proces se află ı̂ntr-o fază de
I/O aşteptând după finalizarea unei operaţii de către un dispozitiv periferic, un proces aflat
ı̂n faza de calcul poate ocupa procesorul şi efectua calculele programate.
În cazul multitasking-ului preemtiv, planificatorul de procese alocă cuante de timp procesor
egale fiecărui proces, asigurându-le astfel un tratament echitabil. De asemenea, sistemul poate
răspunde rapid unui eveniment extern (cum ar fi sosirea unor date) ce presupune procesarea

158
de către procesul curent sau de către un altul. În cazul multitasking-ului non-preemtiv,
planificatorul ı̂i dă controlul unui proces până când acesta se termină, sau eliberează singur
procesorul.
Sistemele de operare bazate pe kernelul Windows NT 4.0 folosesc un planificator de
procese preemtiv cu priorităţi. În momentul ı̂n care trebuie să aleagă un proces pentru a-i da
controlul procesorului, planificatorul de procese va alege procesul ce are prioritatea cea mai
mare şi este gata de execuţie, utilizând o strategie de tip round rubin: dacă avem trei procese
având aceeaşi prioritate A, B şi C, şi un proces D de prioritate mai mică, planificatorul va
alege mai ı̂ntâi procesul A, apoi procesul B, urmat de procesul C, procesului D alocându-i
procesorul numai ı̂n situaţia ı̂n care celelalte trei procese nu sunt ı̂n starea gata de execuţie (de
exemplu sunt blocate ı̂n aşteptarea efectuării unei operaţii I/O). Pentru procesele de prioritate
mică, şi care nu au fost executate deloc ı̂n ultimele, să zicem, 3 secunde, planificatorul de
procese aplică următorul algoritm pentru a evita fenomenul de ı̂nfometare: le atribuie o
prioritate temporară foarte mare, ridicându-le la ı̂nceputul cozii de procese, şi le alocă o
cuantă de timp mai lungă decât ı̂n mod normal.
Sistemele de operare bazate pe un kernel de tip Unix folosesc un planificator bazat pe
mai multe cozi de priorităţi ce foloseşte aceaşi strategie de tip round rubin ı̂n cadrul fiecărei
cozi. Procesele noi sunt inserate ı̂ntr-o coadă corespunzătoare unor priorităţi mai mari, şi pe
măsură ce petrec un timp procesor mai ı̂ndelungat, sunt inserate ı̂ntr-o coadă corespunzătoare
unor priorităţi mai mici. Sistemele de operare de generaţie mai nouă, bazate pe un kernel
Unix, incrementează prioritatea unui proces aflat ı̂n stare de ı̂nfometare (starvation) până
când acesta este executat, după care prioritatea acestuia este resetată la valoarea pe care
procesul o avea ı̂nainte de a ı̂ncepe ı̂nfometarea.
Vom simula un planificator de procese bazat pe o coadă de priorităţi: la un moment
dat, planificatorul alege procesul de prioritate minimă, ı̂i dă controlul procesorului pentru o
perioadă de timp variabilă ı̂nsă limitată. După trecerea acelei perioade de timp, prioritatea
procesului este incrementată cu o valoare ce depinde direct proporţional de timpul procesor
petrecut de acesta, şi este inserat ı̂n coada de priorităţi pe poziţia corespunzătoare.
În cadrul fişierului task.h definim structura de date Process precum şi două funcţii ce
au legătură cu prelucrarea informaţiilor asociate unui proces:
• long p(Process a); - ı̂ntoarce prioritatea unui proces;

• void execute(int id); - simulează execuţia unui proces pentru o anumită perioadă
de timp.

Listing 7.2: task.h


/∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗∗ ∗∗ ∗∗∗ ∗∗ Task . h ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗∗ ∗∗∗ ∗∗∗/
#i fndef TASK
#define TASK

#include <s t d i o . h>


#include < s t d l i b . h>
#include <dos . h>

typedef str u ct p r o c e s s {
i nt i d ;
long p r i o r i t y ;
} Process ;

long p ( P r o c e s s a ) ;
void e x e c u t e ( i nt i d ) ;

159
#endif

În fişierul heap.h se defineşte o coadă de priorităţi (PriorityQueue) implementată sub


forma unui heap:

• Process* deleteMin(PriorityQueue*); - ı̂ntoarce procesul de prioritate minimă având


grijă să-l şteargă din coada de priorităţi;

• void makeNull(PriorityQueue*); - goleşte (videază) o coadă de priorităţi;

• void insertTask(PriorityQueue*, Process); - adaugă un nou proces ı̂n coada de


priorităţi.

Coada de priorităţi se comportă ca un container de obiecte.


Listing 7.3: heap.h
/∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗∗ ∗∗ ∗∗∗ ∗∗ Heap . h ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗∗ ∗∗∗ ∗∗∗/
#i fndef HEAP
#define HEAP

#include <mem. h>


#include ” t a s k . h”

#define MAX SIZE 100

/∗ ∗
∗ D e f i n i t i a s t r u c t u r i i PriorityQueue
∗/
typedef str u ct p r i o r i t y q u e u e {
P r o c e s s e l e m e n t s [ MAX SIZE+ 1 ] ;
i nt l a s t ;
} PriorityQueue ;

Process ∗ deleteMin ( PriorityQueue ∗ ) ;


void makeNull ( P r i o r i t y Q u e u e ∗ ) ;
void i n s e r t T a s k ( P r i o r i t y Q u e u e ∗ , P r o c e s s ) ;

#endif

Implementarea funcţiilor declarate ı̂n fişierul header heap.h, se realizează ı̂n cadrul fişierului
heap.c:

• Process* deleteMin(PriorityQueue*); - ı̂ntoarce procesul de prioritate minimă. Se mută


elementul de pe ultima poziţie pe prima poziţie şi se reorganizează structura de date
de tip heap (prin reorganizare ı̂nţelegem rearanjarea elementelor cu un număr minim
de operaţii astfel ı̂ncât să se păstreze proprietatea de heap).

• void makeNull(PriorityQueue*); - videază structura de date de tip heap.

• void insertTask(PriorityQueue*, Process); - adaugă un nou proces ı̂n heap la sfârşit, şi
caută poziţia corespunzătoare acestui proces nou astfel ı̂ncât proprietatea de heap să
fie păstrată.

Fişierul manager.c conţine funcţii ce implementează comportamentul planificatorului de


procese.

160
void init(int p) - crează un proces cu identificatorul p şi prioritatea egală cu momentul
de timp ı̂n care a fost creat, pe care ı̂l inserează ı̂n coada de priorităţi.
void select(void) - la un moment dat, selectează procesul aflat ı̂n vârful cozii, ı̂i dă con-
trolul procesorului pentru o perioadă de timp, actualizează prioritatea acestuia şi-l inserează
ı̂n coada de priorităţi.
Dacă dorim ca managementul cozii de priorităţi să fie realizat prin intermediul unei liste
ı̂n locul structurii de tip heap, este suficient să ı̂nlocuim linia #include "heap.h" cu linia
#include "list.h".

7.4 Exerciţii
1. Să se implementeze structura de date abstractă coadă cu prioritate cu ajutorul:

a) unui vector ordonat,

b) unei liste simplu ı̂nlănţuită,

şi să se elaboreze algoritmii pentru operaţiile Insert şi Extract. Analizaţi ı̂n aceste
cazuri complexitatea operaţiilor.

2. Să se descrie algoritmi având complexitate logaritmică ce implementează următoarele


operaţii efectuate asupra unei cozi cu prioritate.

• DecreaseKey(H, k, x) - valoarea elementului k din heap-ul H se micşorează


(hk ← min{hk , x}), operaţia fiind, eventual, urmată de restaurarea heap-ului.

• IncreaseKey(H, k, x) - valoarea elementului k din heap-ul H este mărită (hk ←


max {hk , x}), operaţia fiind urmată de restaurarea heap-ului.

Coada cu prioritate este implementată cu ajutorul unui heap.

3. Implementare următoare a algoritmului Heapsort este foarte scurtă şi rapidă, având
avantajul că nu utilizează un vector suplimentar pentru ordonare2 :
Listing 7.4: heapsortv2.c
void h e a p s o r t ( i nt a r r [ ] , unsigned i nt N) {
i nt t ;
unsigned i nt n = N, p a r e n t = N/ 2 , index , c h i l d ;

for ( ; ; ) {
i f ( parent > 0) {
t = a r r [−− p a r e n t ] ;
} else {
n−−;
i f ( n == 0 )
return ;
t = arr [ n ] ;
arr [ n] = arr [ 0 ] ;
}

index = parent ;
2
http://en.wikibooks.org/wiki/Algorithm_Implementation/Sorting/Heapsort

161
c h i l d = index ∗ 2 + 1 ;
while ( c h i l d < n ) {
i f ( ( c h i l d + 1 < n ) && ( arr [ child + 1] > arr [ child ] ) ) {
c h i l d ++;
}

if ( arr [ child ] > t ) {


arr [ index ] = arr [ c h i l d ] ;
index = c h i l d ;
c h i l d = index ∗ 2 + 1 ;
} else {
break ;
}
}
arr [ index ] = t ;
}
}

Ordonaţi crescător următoarea secvenţă de numere, pe baza funcţiei anterioare, utili-


zând numai o coală de hârtie şi un creion: 5, 100, 25, 20, 10, 5, 7, 80, 90, 1.

4. Să se elaboreze un algoritm de timp O(n · lg k) pentru a interclasa k liste ordonate,


unde n este numărul total de elemente din listele de intrare.

5. Adriana este o mare colecţionară de timbre. În fiecare zi se duce la magazinul de pe


strada ei pentru a-şi mări colecţia. Într-o zi, vânzătorul (nimeni altul decât Balaurul
Arhirel) s-a gândit să-i facă o surpriză. A scos dintr-un dulap vechi nişte timbre foarte
valoroase pe care erau scrise cu fir de aur şi de argint numere naturale. Ştiind că fetiţa
nu are bani prea mulţi, Balaurul i-a spus următoarele: ”Eu pot să ı̂mpart timbrele ı̂n m
intervale de forma [1, . . . , mi ]. Tu poţi să iei din orice interval o singură subsecvenţă
de maxim k elemente. Desigur, dacă ai ales o subsecvenţă din intervalul i vei plăti o
anumită sumă . . . ”
Adriana s-a gândit că ar fi frumos să-şi numeroteze toate cele n pagini ale clasorului
ei cu astfel de timbre. Fiind şi o fetiţă pofticioasă şi-a zis: ”Tare aş vrea să mănânc o
ı̂ngheţată din banii pe care ı̂i am la mine, dar nu ştiu dacă o să-mi ajungă să plătesc
timbrele. Cum să fac? ”
Fiind cunoscute cele m intervale, precum şi costurile acestora, ajutaţi-o pe Adriana să
cumpere timbrele necesare numerotării clasorului, plătind o sumă cât mai mică.
Date de intrare
Pe prima linie a fişierului timbre.in se află n m k. n reprezintă numărul de pagini ale
clasorului, m reprezintă numărul de intervale, iar k lungimea maximă a unei subsecvenţe.
Pe următoarele m linii se află două numere separate printr-un spaţiu, mi ci , unde mi
reprezintă marginea superioară a intervalului i, iar ci costul acestuia.
Date de ieşire
Pe prima linie a fişierului timbre.out se va afla Smin, reprezentând suma minimă pe
care trebuie să o plătească Adriana pentru a cumpăra timbrele necesare numerotării
clasorului.
Restricţii: 0 < N < 1001, 0 < M < 10001, 0 < K < 1001, 0 < mi < 100000,
0 < ci < 10000.

162
Pentru a numerota toate cele n pagini ale clasorului, Adriana are nevoie de timbre cu
numerele de la 1 la n.
Exemplu
timbre.in timbre.out
4 3 2
5 3
3
2 1
6 2
Luăm subsecvenţa {1, 2} din al doilea interval şi subsecvenţa {3, 4} din al treilea inter-
val. Obţinem astfel costul minim 3.
(InfoArena, Timbre)

6. Paftenie barbarul a fost capturat de către duşmanii săi cei mai temuţi şi aruncat ı̂ntr-o
temniţă. Temniţa este, de fapt, un grid de dimensiune R × C. În anumite celule există
dragoni, unele sunt ocupate de pereţi, iar altele sunt libere. Paftenie trebuie să iasă din
temniţă mergând numai prin celule libere (o celulă are maxim 4 vecini), şi asta stând
cât mai departe de fioroşii dragoni ale căror flăcări ı̂i pot deteriora vestimentaţia (astfel
ı̂ncât valoarea minimă dintre distanţele până la cel mai apropiat dragon din fiecare din
celulele traseului său să fie maxim).
Să se determine un traseu al lui Paftenie pentru a ieşi din temniţă astfel ı̂ncât distanţa
minimă până la cel mai apropiat dragon din fiecare dintre celulele traseului său să fie
maximă.
Date de intrare
Pe prima linie a fişierului de intrare barbar.in sunt date două numere ı̂ntregi R şi
C, reprezentând numărul liniilor, respectiv al coloanelor temniţei. Pe următoarele R
linii se află câte C caractere, neseparate prin spaţii, cu următoarele semnificaţii: ’.’
celulă liberă, ’∗’ perete, ’D’ dragon, ’I’ punctul de plecare al lui Paftenie, ’O’ ieşirea din
temniţă.
Date de ieşire
Fişierul de ieşire barbar.out va conţine pe prima linie un singur număr, reprezentând
valoarea maximă pentru minima dintre distanţele până la cel mai apropiat dragon din
fiecare din celulele traseului. În caz că nu există soluţie se va afişa valoarea −1.
Restricţii: 1 ≤ R, C ≤ 1.000.
Se garantează că ı̂n temniţă există cel puţin un dragon.
Exemplu Pentru datele de intrare

10 10
..........
.I....D...
..........
..D...D...
.*........
D*........
*...D.....
..****....
...O......
..........

163
o soluţie posibilă este următoarea, unde valoarea maximă dintre distanţele minime este
2:

..........
.Iooo.D...
....o.....
..D.o.D...
.*..oo....
D*...ooooo
*...D....o
..****...o
...Ooooooo
..........

(preONI 2005, Barbar)

7. Se dă un şir de numere ı̂ntregi şi două numere x şi y (x, y ∈ N). Să se determine
subsecvenţa de sumă maximă a cărei lungime se află ı̂n intervalul [x, y].

164
Capitolul 8

Distanţe ı̂n grafuri

Reţeaua de drumuri europene, naţionale şi judeţene dintr-o ţară, reţeaua feroviară reprezentând
infrastructura căilor ferate absolut necesară pentru realizarea transportului de mărfuri şi
călători cu trenul, reţeaua de fibră optică folosită pentru traficul de date ı̂n Internet, reţeaua
de transport a energiei electrice folosită pentru alimentarea cu energie electrică a consumato-
rilor casnici şi a celor industriali, reţeaua de canalizare dintr-un oraş folosită pentru evacuarea
deşeurilor, reţeaua CAT V a unui operator de televiziune prin cablu, reţeaua de linii de auto-
buz ce face parte din sistemul public de transport al unui oraş sunt exemple tipice de grafuri
cu care interacţionăm ı̂n viaţa de zi cu zi.
Putem spune că reţelele de transport şi cele de comunicaţii de date ne influenţează ı̂n
mod direct modul de viaţă, devenind elemente indispensabile ale omului modern, şi astfel,
problemele referitoare la studiul căilor de comunicaţie, drumurilor, conexiunilor, capătă un
interes special.
De obicei suntem interesaţi de aspecte precum drumul cel mai scurt sau cel mai lung,
drumul cel mai ieftin sau drumul care se poate parcurge cel mai repede, drumul cel mai
sigur. Teoria grafurilor ne pune la dispoziţie mijloacele pentru aflarea răspunsului la multe
astfel de ı̂ntrebări.
Definiţiile pentru drum, lanţ, ciclu, circuit au fost prezentate ı̂n capitolele anterioare,
ı̂mpreună cu multe alte concepte teoretice. Lungimea unui lanţ este determinată de numărul
muchiilor sale. În mod analog, lungimea unui drum este egală cu numărul arcelor sale.
Într-un graf G se introduce o funcţie de cost ce asociează o valoare reală fiecărei muchii
sau arc, după caz:
c : E −→ R sau c : V × V −→ R
Notăm costul unui drum ca fiind suma costurilor arcelor componente. Astfel pentru un
drum D = [v0 , v1 , . . . , vm ], vi ∈ V , de lungime m, costul acestuia va fi dat de către următoarea
formulă:
m−1
X
c(D) = c([vi , vi+1 ]). (8.1)
i=0

Notăm cu Ms,t mulţimea drumurilor dintre nodul sursă xs şi nodul destinaţie xt , xs , xt ∈
V . Pentru două noduri oarecare xs şi xt , se doreşte să se determine un drum δs,t de la xs la
xt a cărui valoare să fie optimă (minimă sau maximă):
opt
c(δs,t )= min c(δs,t ). (8.2)
δs,t ∈Ms,t

165
8.1 Drumul minim de la un vârf la celelalte vârfuri
8.1.1 Algoritmul lui Moore
Fie G = (V, E) un graf orientat unde V = {x1 , x2 , . . . , xn } este mulţimea nodurilor şi E este
mulţimea arcelor (E ⊆ V × V ). Pentru reprezentarea grafului vom utiliza, ca metodă de
reprezentare, listele de adiacenţă (liste de vecini).
Fie u un vârf al grafului numit sursă. Dorim să determinăm pentru fiecare vârf w ∈
V, w 6= u, dacă există, un drum de lungime minimă de la u la w. Lungimea unui drum se
defineşte ca fiind numărul arcelor ce ı̂l compun.
Algoritmul lui Moore (a se vedea algoritmul 59) se bazează pe structura algoritmului de
parcurgere ı̂n lăţime al unui graf (Breadth First Search - a se vedea algoritmul 16). În lucrarea
[98], Moore ı̂şi prezintă algoritmul astfel:

Write 0 on the source. Then look at all the neighbors of the source and write
1 on them. Then look at all the neighbors of nodes with 1 on them and write 2
on them. And so on.

Vom utiliza un vector de distanţe D, unde dk reprezintă costul drumului minim de la vârful u
la vârful xk , iar tatak păstrează vârful anterior lui xk , pe drumul de cost minim de la nodul
u la nodul xk .

Algoritm 59 Algoritmul lui Moore (prima versiune)


1: procedure Moore1(k, n, V ecin; D, T ata)

k
 - nodul sursă
Input: n - numărul de noduri din graf


V ecin - vector ce conţine capetele listelor de vecini

D
 - vectorul distanţelor de la nodul sursă la celelalte noduri
Output: T ata - vector ce conţine pentru fiecare nod k, predecesorul acestuia pe drumul


de cost minim de la nodul sursă la nodul k
2: for i ← 1, n do
3: di ← +∞
4: end for
5: dk ← 0 ⊲ distanţa de la un nod la el ı̂nsuşi este 0
6: Q⇐k ⊲ inserarea nodului curent k ı̂n coadă
7: while (Q 6= ∅) do ⊲ cât timp coada nu este vidă
8: Q⇒k ⊲ extrage nodul curent din coadă
9: v ← vecink ⊲ se pleacă cu primul vecin din lista de vecini
10: while (v 6= N U LL) do
11: if (dv.nodeIndex = +∞) then ⊲ v.nodeIndex este indicele nodului vecin
12: dv.nodeIndex ← dk + 1, tatav.nodeIndex ← k
13: Q ⇐ v.nodeIndex ⊲ inserarea nodului v.nodeIndex ı̂n coada
14: end if
15: v ← v.next ⊲ se trece la următorul vecin
16: end while
17: end while
18: end procedure

166
Exemplul 8.1 Fie un graf orientat reprezentat prin intermediul următoarelor liste cu vecini:

1: (2, 4) 7: (8)
2: (3, 6) 8: (6, 7)
3: (2) 9: ()
4: (1, 5) 10: (11)
5: (4, 6, 9) 11: (10)
6: (7, 8)
În urma aplicării algoritmului lui Moore pentru acest graf şi pentru nodul 1 drept sursă,
se obţin următoarele valori pentru vectorii D şi Tata:

În urma aplicării algoritmului lui Moore pentru acest graf şi având nodul 1 drept sursă,
se obţin următoarele valori pentru vectorii d şi tata:
1 2 3 4 5 6 7 8 9 10 11
D 0 1 2 1 2 2 3 3 3 ∞ ∞
Tata 1 2 1 4 2 6 6 5
Urmărind valorile calculate, putem trage concluzia că drumul de lungime minimă de la
nodul 1 la nodul 9, are costul 3 şi este compus din nodurile [1, 4, 5, 9].

Algoritmul 60 calculează lungimea drumurilor minime de la un nod sursă la toate nodurile


accesibile din acesta, ı̂n cazul ı̂n care lungimea unui drum se defineşte ca fiind suma costurilor
asociate arcelor ce ı̂l compun.

Algoritm 60 Algoritm lui Moore (a doua versiune)


1: procedure Moore2(k, n, C; D, T ata)

k - nodul sursă

Input: n - numărul de noduri din graf


C - matricea costurilor
2: for i ← 1, n do
3: di ← +∞
4: end for
5: dk ← 0 ⊲ distanţa de la un nod la el ı̂nsuşi este 0
6: Q⇐k ⊲ inserarea nodului curent k ı̂n coadă
7: while (Q 6= ∅) do ⊲ cât timp coada nu este vidă
8: Q⇒k ⊲ extrage nodul curent din coadă
9: for fiecare vecin (v = xi ) al lui xk do
10: if (dk + ck,i < di ) then
11: di ← dk + ck,i , tatai ← k
12: Q⇐i ⊲ inserarea nodului i ı̂n coada
13: end if
14: end for
15: end while
16: end procedure

8.1.2 Algoritmul lui Dijkstra


Să considerăm G = (V, E) un graf orientat unde V = {1, 2, . . . , n} este mulţimea nodurilor
iar E este mulţimea arcelor (E ⊆ V × V ). Pentru reprezentarea grafului vom utiliza matricea

167
costurilor C: 
0
 , dacă i = j
ci,j = ∞ , dacă (i, j) ∈
/ E, i 6= j


d > 0 , dacă (i, j) ∈ E
Fie u un vârf al grafului numit sursă. Dorim să determinăm pentru fiecare vârf w ∈
V, w 6= u, dacă există, un drum de lungime minimă de la u la w. Lungimea unui drum se
defineşte ca fiind suma costurilor asociate arcelor ce ı̂l compun.
Fie p = [x1 , x2 , . . . , xk ] un drum elementar. Se numeşte vârf intermediar orice vârf u ∈
{x2 , . . . , xk−1 } unde u 6= x1 , u 6= xk (vârful intermediar este diferit de extremităţile drumului
p).
Algoritmul lui Dijkstra [38] utilizează metoda generală de elaborare a algoritmilor Greedy,
drumurile de lungime minimă fiind generate ı̂n ordinea crescătoare a lungimii lor. Vom nota
cu S mulţimea nodurilor grafului G pentru care se cunoaşte (s-a calculat) drumul de lungime
minimă de la sursă la un astfel de nod. La ı̂nceput, mulţimea S este compusă doar din
nodul sursă. La fiecare pas, se adaugă la mulţimea S un nod xk ∈ V \ S cu proprietatea că,
drumul de la sursă la acest nod xk , este cel mai mic dintre toate drumurile posibile de la
sursă la noduri din mulţimea V \ S, cu proprietatea că, un astfel de drum are drept noduri
intermediare numai elemente din mulţimea S. Vom utiliza un tablou D, ce va păstra pentru
fiecare nod xk , lungimea drumului cel mai scurt de la sursă ce trece numai prin noduri din
mulţimea S (a se vedea algoritmul 61).

Algoritm 61 Algoritmul lui Dijkstra (schema generală)


1: procedure Dijkstra1(u, n, C)
2: S ← {u}
3: for i ← 1, n do
4: di ← cu,i
5: end for
6: for i ← 1, n − 1 do
7: k ← min{dk |xk ∈ V \ S}
8: S ← S ∪ {xk }
9: for each xj ∈ V \ S do
10: dj ← min{dj , dk + ck,j }
11: end for
12: end for
13: end procedure

După ce am identificat un astfel de nod xk , mulţimea S se modifică astfel: S = S ∪ {xk }.


Este posibil ca lungimea unui drum de la sursă la un nod xj ∈ V \ S, ce are drept noduri
intermediare noduri din mulţimea S, să se modifice datorită faptului că, mai ı̂nainte, nodul
xk nu fusese luat ı̂n considerare, deoarece nu aparţinea mulţimii S. Astfel, se poate ca un
drum de la sursă la nodul xj , ce trece prin nodul xk , să fie mai mic decât drumul anterior de
la sursă la nodul xj , ce nu avea printre nodurile sale intermediare vârful xk :
dk + ck,j < dj .
Vom utiliza trei vectori (a se vedea algoritmul 62):
• Vizitat - vector caracteristic pentru mulţimea S, unde:
(
1 , dacă nodul xj ∈ S
vizitatj =
0 , dacă nodul xj ∈ V \ S

168
Algoritm 62 Algoritmul lui Dijkstra
1: function DistantaMinima(n, V izitat, D)
2: min ← ∞
3: for j ← 1, n do
4: if ((vizitatj = 0) ∧ (dj < min)) then
5: min ← dj
6: j0 ← j
7: end if
8: end for
9: if (min = ∞) then
10: return −1
11: else
12: return j0
13: end if
14: end function

15: procedure Dijkstra2(u, n, C)


16: vizitatu ← 1
17: du ← 0, tatau ← 0
18: for i ← 1, n do
19: if (i 6= u) then
20: vizitati ← 0
21: di ← cu,i , tatai ← u
22: end if
23: end for
24: while (true) do
25: k ← DistantaM inima(n, vizitat, d)
26: if (k < 0) then
27: break ⊲ forteaza iesirea dintr-o instructiune de ciclare
28: else
29: vizitatk ← 1
30: for i ← 1, n do
31: if ((vizitati = 0) ∧ (dk + ck,i < di )) then
32: tatai ← k
33: di ← dk + ck,i
34: end if
35: end for
36: end if
37: end while
38: end procedure

• D - vectorul distanţelor de la nodul u la celelalte noduri ale grafului. Aşa cum am


precizat anterior, dj păstrează costul drumului de lungime minimă de la nodul sursă u
la nodul xj , drum ce are drept noduri intermediare numai elemente din mulţimea S.
În momentul iniţial vom avea dj = cu,j .
Notăm cu xk nodul ales la un moment dat. Atunci costul drumului de lungime minimă
dj de la nodul sursă u la un nod xj , se modifică numai dacă dk + ck,j < dj , fiind
actualizat astfel: dj = dk + ck,j .
• tataj - păstrează pentru fiecare nod xj , nodul anterior xk (xk ∈ S) pe drumul de cost

169
minim de la u la xj . La ı̂nceput,
(
0 , dacă nodul j = u sau cu,j = ∞
tataj =
u , ı̂n rest (j 6= u şi cu,j 6= ∞).

Un element al acestui vector se poate modifica atunci când se modifică valoarea ele-
mentului dj astfel ı̂ncât dj = dk + ck,j , caz ı̂n care tataj = k.

Observaţia 8.2 Algoritmul lui Dijkstra prezintă asemănări cu algoritmul de căutare ı̂n
lăţime, vectorul T ata păstrând la sfârşitul procesului de calcul al distanţelor un arbore al
drumurilor minime ce are drept rădăcină nodul sursă.

Demonstrarea corectitudinii algoritmului

Fig. 8.1: O cale specială mai scurtă

În momentul ı̂n care alegem nodul xk ∈ V \ S având proprietatea că drumul de la sursă la
acel nod xk este cel mai mic dintre toate drumurile posibile de la sursă la noduri din mulţimea
V \ S, ce trec numai prin noduri din mulţimea S, acel drum este cel mai mic drum de la
sursă la xk dintre toate drumurile posibile. Denumim drum special un drum de la sursă la
un nod xk ce are drept noduri intermediare numai elemente din mulţimea S.
Să presupunem prin reducere la absurd, că există un drum de lungime mai mică de la
nodul sursă la nodul xk , ce nu are toate nodurile intermediare din mulţimea S. Fie xj ∈ V \S,
primul nod ce nu aparţine mulţimii S pe drumul de la nodul sursă la nodul xk . Atunci, drumul
de la nodul sursă u la nodul xk se compune dintr-un drum de la u la xj , şi un drum de la xj la
xk . Datorită modului de alegere al lui xj , drumul de la u la xj are drept noduri intermediare
numai elemente din mulţimea S (xj este primul nod pe drumul de la nodul sursă la nodul
xk , care nu aparţine mulţimii S), deci este un drum special şi are o lungime mai mică decât
drumul special de la sursă la nodul xk . Prin urmare, am identificat un alt drum special de
lungime mai mică, ceea ce contrazice modul de alegere al nodului xk .
Trebuie să demonstrăm că, ı̂n orice moment, dk păstrează lungimea celui mai scurt drum
special de la nodul sursă u la nodul xk ∈ V \ S. În momentul ı̂n care nodul xk este adăugat
mulţimii S, avem grijă să verificăm dacă nu există un drum special de la nodul u la un nod
xj ∈ V \ S care să aibă o lungime mai mică. Să presupunem că, pentru un nod xj fixat, există
un nod w ∈ S astfel ı̂ncât drumul special u xk ∪ xk w ∪ (w, xj ) să aibă o lungime mai
mică (a se vedea figura 8.1). Deoarece nodul w a fost adăugat mulţimii S ı̂naintea nodului
xk , avem dw ≤ dk .

170
Prin urmare
dw + cw,j ≤ dk + cw,j < dk + costxk w + cw,j , (8.3)
adică drumul special de la sursă la nodul xj ce trece prin nodul w are lungimea mai mică
decât drumul special compus din drumul de la nodul sursă la nodul xk , drumul de la nodul
xk la nodul w şi arcul (w, xj ).

Fig. 8.2: Un exemplu de graf orientat ponderat

Exemplul 8.3 Să presupunem că nodul sursă este nodul 1 pentru graful din figura 8.2. După
etapa de iniţializare, vom avea următoarele valori:

1 2 3 4 5
D 1 ∞ 21 ∞
Tata 0 1 1 1 1
Vizitat 1 0 0 0 0
În cadrul primei iteraţii, după selectarea nodului 2, se evaluează următoarele distanţe:
d2 + c2,3 < d3 ⇔ 1 + 8 < +∞
d2 + c2,4 < d4 ⇔ 1 + ∞ < 21
d2 + c2,5 < d5 ⇔ 1 + 4 < +∞

1 2 3 4 5
D 1 9 21 5
Tata 0 1 2 1 2
Vizitat 1 1 0 0 0
În timpul celei de-a doua iteraţii se evaluează următoarele condiţii:
d5 + c5,3 < d3 ⇔ 5 + 3 < 9
d5 + c5,4 < d4 ⇔ 5 + ∞ < 21

1 2 3 4 5
D 1 8 21 5
Tata 0 1 5 1 2
Vizitat 1 1 0 0 1
După pasul al treilea, vom actualiza lungimea unui singur drum:
d3 + c3,4 < d4 ⇔ 8 + 12 < 21

171
1 2 3 4 5
D 1 8 20 5
Tata 0 1 5 3 2
Vizitat 1 1 1 0 1
La sfâşitul ultimei iteraţii valorile vectorilor D, Tata, Vizitat sunt următoarele:

1 2 3 4 5
D 1 8 20 5
Tata 0 1 5 3 2
Vizitat 1 1 1 1 1

Implementarea ı̂n limbajul C a algoritmului 62 este următoarea:


Listing 8.1: dijkstra.c
#include <s t d i o . h>
#include <v a l u e s . h>

#define MAXN 100


#define TRUE 1
#define FALSE 0
#define MAXINT 30000

/∗ ∗
∗ C i t i r e a d a t e l o r de i n t r a r e − + i n f i n i t = −1
∗/
void r e a d I n p u t ( i nt ∗ n , i nt c [ ] [MAXN] , i nt ∗ u ) {
i nt i , j ;

p r i n t f ( ”n = ” ) ; s c a n f ( ”%d” , n ) ;
for ( i = 0 ; i < ∗n ; i ++)
for ( j = 0 ; j < ∗n ; j ++) {
s c a n f ( ”%d” , &c [ i ] [ j ] ) ;
i f ( c [ i ] [ j ] < 0)
c [ i ] [ j ] = MAXINT;
}
p r i n t f ( ” Nodul i n i t i a l : ” ) ; s c a n f ( ”%d” , u ) ;
}

i nt minim ( i nt n , i nt d [ ] , char v i z i t a t [ ] ) {
i nt j , j 0 ;
i nt min ;

min = MAXINT;
for ( j = 0 ; j < n ; j ++)
i f ( ! v i z i t a t [ j ] && d [ j ] < min ) {
min = d [ j ] ;
j0 = j ;
}
i f ( min == MAXINT)
return −1;
else
return j 0 ;
}

/∗ ∗
∗ F u n c t i a v e r i f i c a daca a > b + c .
∗/

172
i nt mai mare ( long a , long b , long c ) {
return ( a > b+c ) ? 1 : 0 ;
}

void d i j k s t r a ( i nt u , i nt n , i nt c [ ] [MAXN] , i nt d [ ] , i nt t a t a [ ] ) {
/∗ v e c t o r c e p a s t r e a z a s t a r e a unui nod : v i z i t a t sau n e v i z i t a t ∗/
char v i z i t a t [MAXN] ;
i nt i , j , k ;

// i n i t i a l i z a r i
v i z i t a t [ u ] = TRUE;
t a t a [ u ] = −1;
for ( i = 0 ; i < n ; i ++)
i f ( i != u ) {
v i z i t a t [ i ] = FALSE ;
d[ i ] = c [u ][ i ];
tata [ i ] = u ;
}
// p a r t e a p r i n c i p a l a
for ( i = 1 ; i < n ; i ++) {
k = minim ( n , d , v i z i t a t ) ;
i f (k < 0)
break ;
v i z i t a t [ k ] = TRUE;
// a c t u a l i z a r e
for ( j = 0 ; j < n ; j ++)
i f ( ! v i z i t a t [ j ] && mai mare ( d [ j ] , d [ k ] , c [ k ] [ j ] ) ) {
tata [ j ] = k ;
d[ j ] = d[k] + c [k ][ j ];
}
}
}

void printDrum ( i nt k , i nt t a t a [ ] ) {
i f ( t a t a [ k ] >= 0 )
printDrum ( t a t a [ k ] , t a t a ) ;
else
printf (” [ ” );
p r i n t f ( ”%d ” , k ) ;
}

void p r i n t S o l u t i o n ( i nt u , i nt n , i nt d [ ] , i nt t a t a [ ] ) {
i nt i ;

for ( i = 0 ; i < n ; i ++)


i f ( i != u )
i f ( d [ i ] == MAXINT)
p r i n t f ( ”Nu e x i s t a drum de l a v a r f u l %d l a v a r f u l %d . \ n” , u , i ) ;
else {
p r i n t f ( ” D i s t a n t a de l a v a r f u l %d l a v a r f u l %d e s t e %d : ” , u , i , d [ i ] ) ;
printDrum ( i , t a t a ) ;
p r i n t f ( ” ] \ n” ) ;
}
}

void main ( void ) {


/∗ numarul de n o d u r i d i n g r a f ∗/
i nt n ;
/∗ m a t r i c e a c o s t u r i l o r ∗/

173
i nt c [MAXN] [MAXN] ;
/∗ no dul f a t a de c a r e s e c a l c u l e a z a d r u m u r i l e de lung ime minima ∗/
i nt u ;
/∗ t a t a [ k ] − p a r i n t e l e v a r f u l u i k i n a r b o r e l e r e z u l t a t ∗/
i nt t a t a [MAXN] ;
/∗ d i s t a n t a de l a u l a f i e c a r e nod ∗/
i nt d [MAXN] ;

r e a d I n p u t (&n , c , &u ) ;
d i j k s t r a (u , n , c , d , tata ) ;
printSolution (u , n , d , tata ) ;
}

Utilizarea structurii coadă cu prioritate ı̂n algoritmul lui Dijkstra


Complexitatea-timp a algoritmului lui Dijkstra poate fi ı̂mbunătăţită prin utilizarea structurii
de date coadă cu prioritate (a se vedea algoritmul 63).

Algoritm 63 Algoritmul lui Dijkstra (varianta ce foloseşte o coadă cu prioritate)


1: procedure Dijkstra3(u, n, C)
2: for i ← 1, n do
3: di ← ∞
4: tatai ← N U LL
5: call Insert(Q, i, di ) ⊲ inserează ı̂n coadă elementul xi cu prioritatea di
6: end for
7: du ← 0
8: call DecreaseKey(Q, u, du ) ⊲ actualizează prioritatea vârfului u la du
9: S←∅
10: for i ← 1, n − 1 do
11: k ← DeleteM in(Q) ⊲ şterge elementul de prioritate minimă din coadă
12: S ← S ∪ {xk }
13: for f iecare xj ∈ V \ S, astfel ı̂ncât (xk , xj ) ∈ E do
14: if (dj > dk + ck,j ) then
15: dj ← dk + ck,j
16: tataj ← k
17: call DecreaseKey(Q, j, dj ) ⊲ actualizează prioritatea vârfului xj la dj
18: end if
19: end for
20: end for
21: end procedure

Analizând algoritmul 63, obţinem că timpul de lucru al acestuia depinde de formula:

T (n, m) = O(n · TInsert + n · TDeleteM in + m · TDecreaseKey ) (8.4)

unde n reprezintă numărul de noduri ale grafului G iar m reprezintă numărul de muchii ale
aceluiaşi graf. Cele trei operaţii ı̂ntâlnite ı̂n cadrul algoritmlui sunt (a se vedea şi capitolul
7):

1. DeleteMin(Q) - şterge nodul ce conţine cheia de valoare minimă şi reorganizează struc-
tura prin refacerea proprietăţii de coadă cu prioritate.

174
2. Insert(Q,p,x) - inserează nodul p ı̂n coada cu prioritate Q, unde x reprezintă priori-
tatea nodului p.

3. DecreaseKey(Q,p,v) - modifică valoarea priorităţii nodului p din coada cu prioritate


Q, atribuindu-i valoarea v şi reorganizează această structură.

Astfel, complexitatea algoritmului pentru diferite implementări ale cozii cu prioritate Q


va fi [30]:

1. Listă liniară dublu ı̂nlănţuită neordonată:

T (n, m) = O(n · 1 + n · n + m · 1) = O(n2 ) (8.5)

2. Arbore 2 − 3:
T (n, m) = O(n · 1 + n · n + m · 1) = O(n2 ) (8.6)

3. Heap-uri Fibonacci:

T (n, m) = O(n · 1 + n · n + m · 1) = O(n2 ). (8.7)

8.2 Drumuri minime ı̂ntre toate perechile de vârfuri


Fie G = (V, E) un graf orientat, unde V = {x1 , x2 , . . . , xn } reprezintă mulţimea nodurilor
şi E reprezintă mulţimea arcelor (E ⊆ V × V ). Fiecărui arc din E i se asociază o valoare
pozitivă ci,j (ci,j ≥ 0) ce reprezintă lungimea arcului. Se doreşte calcularea drumurilor de
lungime minimă pentru oricare pereche de noduri xi , xj .
Pentru a determina drumurile minime ı̂ntre toate perechile de vârfuri, putem alege un
algoritm ce determină drumurile minime de sursă unică (drumurile minime de la acel vârf la
toate celelalte), şi pe care să-l aplicăm pentru fiecare vârf al grafului.
Un astfel de algoritm este algoritmul lui Dijkstra, algoritm ce a cunoscut multiple ı̂mbunătă-
ţiri faţă de varianta orginală, ce se bazează pe utilizarea unor structuri de date avansate (de
exemplu heap-uri Fibonacci), menite să-i optimizeze timpul de execuţie. Astfel, complexitatea-
timp ı̂n cazul cel mai defavorabil pentru calculul celor mai scurte drumuri, pornind dintr-un
vârf al unui graf cu n noduri şi m arce, este O(m + n log n). Dacă aplicăm acest algoritm
pentru toate cele n vârfuri vom obţine un timp de lucru de O(mn + n2 log n).
Algoritmul 64 prezintă o variantă a algoritmului lui Dijkstra ı̂n care drumurile de lungime
minimă ı̂ntre oricare două vârfuri sunt calculate incremental şi intercalat [35]. Matricea D
va păstra pentru fiecare pereche [xi , xj ] lungimea di,j a drumului minim dintre cele explorate
până la momentul curent. Coada de priorităţi C păstrează toate perechile (i, j), ordonate
crescător ı̂n funcţie de prioritatea di,j a fiecăreia. La fiecare pas, se extrage din coadă perechea
(i, j) având prioritatea cea mai mică şi se ı̂ncearcă extinderea drumului [xi , xj ] cu exact un
arc la fiecare extremitate a drumului. Tehnica utilizată este cea a relaxării, ı̂ntâlnită şi ı̂n
cadrul algoritmului lui Bellman, cu scopul de a micşora costul drumului de la xi la xj , prin
parcurgerea tuturor arcelor ce părăsesc vârful xj şi a celor ce sosesc ı̂n vârful xi .

8.2.1 Algoritmul lui Roy-Floyd-Warshall


Fie p = [x1 , x2 , . . . , xk ] un drum elementar. Definim un vârf intermediar orice vârf u ∈
{x2 , . . . , xk−1 }, u 6= x1 , u 6= xk (vârful intermediar este diferit de extremităţile drumului p).

175
Algoritm 64 Algoritmul lui Dijkstra pentru toate perechile de vârfuri
1: procedure DijkstraAll(n, C; D)
2: for i ← 1, n do
3: for j ← 1, n do
4: if ((i, j) ∈ E) then ⊲ sau (ci,j > 0) ∧ (ci,j < ∞)
5: di,j ← ci,j
6: else
7: di,j ← ∞
8: end if
9: C ⇐ (i, j) cu prioritatea di,j
10: end for
11: end for
12: while (C = 6 ∅) do
13: C ⇒ (i, j)
14: for k ← 1, n do
15: if (di,j + cj,k < di,k ) then
16: di,k ← di,j + cj,k
17: actualizează prioritatea perechii (i, k) din C la di,k
18: end if
19: end for
20: for k ← 1, n do
21: if (ck,i + di,j < dk,j ) then
22: dk,j ← ck,i + di,j
23: actualizează prioritatea perechii (k, j) din C la dk,j
24: end if
25: end for
26: end while
27: end procedure

Fie U = {x1 , x2 , . . . , xk } o mulţime de noduri. Notăm cu MUi,j mulţimea drumurilor de


la xi la xj ce au drept noduri intermediare elemente din mulţimea U. Fie p drumul de cost
minim de la xi la xj (p ∈ MUi,j ). Un astfel de drum este elementar deoarece am presupus că
nu există cicluri al căror cost să fie negativ.
(k)
Notăm di,j costul drumului minim de la xi la xj ce are noduri intermediare numai din
mulţimea {x1 , x2 , . . . , xk }. Dacă xk ∈ / p (xk nu aparţine drumului p) atunci drumul de
cost minim de la xi la xj având toate nodurile intermediare din mulţimea {x1 , x2 , . . . , xk−1 }
va fi şi drum de cost minim de la xi la xj cu toate nodurile intermediare din mulţimea
{x1 , x2 , . . . , xk }:
(k) (k−1)
di,j = di,j (8.8)
Dacă xk este un vârf intermediar al drumului p, fie p1 şi p2 subdrumuri ale lui p, p = p1 ⊕ p2
(p1 drumul de la xi la xk având nodurile intermediare din mulţimea {x1 , x2 , . . . , xk } şi p2
drumul de la xk la xj având nodurile intermediare din mulţimea {x1 , x2 , . . . , xk }). Deoarece
p1 şi p2 sunt drumuri de cost minim şi xk ∈
/ {x1 , x2 , . . . , xk−1 } vom avea:
(k−1) (k)
di,k = di,k ,
(k−1) (k)
dk,j = dk,j .

Prin urmare
(k) (k−1) (k−1)
c(p) = c(p1 ) + c(p2 ) ⇒ di,j = di,k + dk,j (8.9)

176
Algoritmul Roy-Floyd-Warshall va construi un şir de matrici D (0) , D (1) , . . . , D (k) , . . . , D (n) .
(k)
Pentru fiecare k, 1 ≤ k ≤ n, di,j va conţine lungimea drumului minim de la nodul xi la nodul
xj ce are drept noduri intermediare numai noduri din mulţimea {x1 , x2 , . . . , xk }.
Matricea D (0) se va iniţializa astfel:

0
 , dacă i = j
(0)
di,j = +∞ , dacă (xi , xj ) ∈
/ E, i 6= j (8.10)


d > 0 , dacă (xi , xj ) ∈ E

(un drum ce nu are nici un vârf intermediar este determinat de costul legăturii directe dintre
xi şi xj ).
Drumul de lungime minimă de la xi la xj ce trece numai prin nodurile {x1 , x2 , . . . , xk }
(k) (k−1)
fie nu conţine nodul xk , caz ı̂n care di,j = di,j , fie ı̂l conţine, şi atunci are loc relaţia
(k) (k−1) (k−1) (k)
di,j = di,k + dk,j . Prin urmare, valoarea elementului di,j se va calcula astfel:
(k) (k−1) (k−1) (k−1)
di,j = min{di,j , di,k + dk,j } (8.11)

Deşi această ultimă relaţie sugerează un algoritm recursiv, o abordare iterativă este mai
eficientă atât ı̂n ceea ce priveşte complexitatea timp, cât şi din punctul de vedere al nece-
sarului de memorie. Se observă că, matricea D (k) nu mai este necesară de ı̂ndată ce matricea
D (k+1) a fost calculată.
(n) (n)
D (n) = (di,j ), di,j = δi,j , ∀xi , xj ∈ V. (8.12)

Algoritm 65 Algoritmul Floyd-Warshall


1: procedure FloydWarshall1(n, C; D (n))
2: for i ← 1, n do
3: for j ← 1, n do
(0)
4: di,j ← ci,j
5: end for
6: end for
7: for k ← 1, n do
8: for i ← 1, n do
9: for j ← 1, n do
(k) (k−1) (k−1) (k−1)
10: di,j ← min {di,j , di,k + dk,j }
11: end for
12: end for
13: end for
14: end procedure

În algoritmul 65 se poate renunţa la indicii superiori, obţinându-se astfel o economie de


spaţiu de la Θ(n3 ) la Θ(n2 ) (a se vedea algoritmul 66).
Algoritmul 66 ne permite să calculăm distanţa minimă ı̂ntre oricare pereche de noduri
apaţinând unui graf G. Se pune problema de a determina şi componenţa drumului minim
(mulţimea vârfurilor intermediare) dintre două noduri, nu numai valoarea distanţei minime.
Acest lucru se poate realiza prin intermediul unei alte matrici P (k) asociată matricii D (k) .
(k)
Un element Pi,j va păstra nodul intermediar k ce conduce la cea mai mică valoare pentru

177
Algoritm 66 Algoritmul Floyd-Warshall (varianta optimizată)
1: procedure FloydWarshall2(n, C; D)
2: for i ← 1, n do
3: for j ← 1, n do
4: di,j ← ci,j
5: end for
6: end for
7: for k ← 1, n do
8: for i ← 1, n do
9: for j ← 1, n do
10: di,j ← min {di,j , di,k + dk,j }
11: end for
12: end for
13: end for
14: end procedure

(k) (k)
di,j conform formulei 8.11. Daca pi,j = 0 atunci cel mai scurt drum dintre nodurile xi şi xj
este cel direct.
(0)
Valoarea elementelor matricii iniţiale P (0) este pi,j = 0.
(k)
Valoarea unui element pi,j se va calcula conform formulei următoare, ţinând cont de
relaţia 8.11: (
(k−1) (k−1) (k−1) (k−1)
(k) pi,j , dacă di,j ≤ di,k + dk,j
pi,j = (k−1) (k−1) (k−1) (8.13)
k , dacă di,j > di,k + dk,j
Se va calcula secvenţa de matrici P (0) , P (1) , . . ., P (n) ı̂mpreună cu secvenţa D (0) , D (1) ,
. . ., D (n) . La final vom avea: P = P (n) , D = D (n) .
Printr-un raţionament analog, se poate demonstra că nu este nevoie să construim efectiv
şirul de matrici P (0) , P (1) , . . ., P (n) . Algoritmul 67 prezintă varianta extinsă a algoritmului
66, ı̂n cadrul acestuia fiind adăugat şi suportul pentru determinarea drumului de cost minim
dintre oricare pereche de noduri a grafului.

Exemplul 8.4 Fie graful din figura 8.2. Matricea costurilor asociată acestui graf este următorul:
 
0 1 +∞ 21 +∞
+∞ 0 8 +∞ 4 
 
C =  3 +∞ 0
 12 +∞ 
+∞ +∞ 9 0 +∞
+∞ +∞ 3 +∞ 0
Şirul de matrice D k , rezultat al aplicării algoritmului Roy-Floyd-Warshall pentru acest
graf, este:

178
Algoritm 67 Algoritmul Floyd-Warshall (reconstruirea drumului optim)
1: procedure FloydWarshall3(n, C; D, P )
2: for i ← 1, n do
3: for j ← 1, n do
4: di,j ← ci,j
5: pi,j ← 0
6: end for
7: end for
8: for k ← 1, n do
9: for i ← 1, n do
10: for j ← 1, n do
11: if (di,j > di,k + dk,j ) then
12: di,j ← di,k + dk,j }
13: pi,j ← k
14: end if
15: end for
16: end for
17: end for
18: end procedure
19: procedure PrintDrum(i, j, P )
20: if (pi,j = 0) then
21: Output {(i,j)}
22: else
23: call P rintDrum(i, pi,j , P )
24: call P rintDrum(pi,j , j, P )
25: end if
26: end procedure

   
0 1 ∞ 21 ∞ 0 1 ∞ 21 ∞

 ∞ 0 8 ∞ 4  

 ∞ 0 8 ∞ 4  
D0 =
 3 ∞ 0 12 ∞ 
 D1 =
 3 4 0 12 ∞ 

 ∞ ∞ 9 0 ∞   ∞ ∞ 9 0 ∞ 
∞ ∞ 3 ∞ 0 ∞ ∞ 3 ∞ 0
   
0 1 9 21 5 0 1 9 21 5

 ∞ 0 8 ∞ 4 


 11 0 8 20 4 

D2 =
 3 4 0 12 8 
 D3 =
 3 4 0 12 8 

 ∞ ∞ 9 0 ∞   12 13 9 0 17 
∞ ∞ 3 ∞ 0 6 7 3 15 0
   
0 1 9 21 5 0 1 8 20 5

 11 0 8 20 4 


 10 0 7 19 4 

D4 = 
 3 4 0 12 8 
 D5 = 
 3 4 0 12 8 

 12 13 9 0 17   12 13 9 0 17 
6 7 3 15 0 6 7 3 15 0

Închiderea tranzitivă
Definiţia 8.1 Fie G = (V, E) un graf orientat, simplu şi finit, V = {x1 , x2 , . . . , xn }.
Închiderea tranzitivă a grafului G este un graf G+ = (V, E + ), unde E + = {(xi , xj )|

179
∃ un drum de la xi la xj ı̂n G}.

Definiţia 8.2 O relaţie binară ρ peste o mulţime A = {1, 2, . . . , n} se defineşte ca fiind o


submulţime a produsului cartezian A × A (ρ ⊆ A × A). (a, b) ∈ ρ se mai poate scrie aρb.

Definiţia 8.3 Închiderea tranzitivă a relaţiei binare ρ este o relaţie binară ρ∗ cu proprietatea
că ∀i, j ∈ {1, . . . , n}, (i, j) ∈ ρ∗ ⇔ ∃i1 , . . . , im ∈ {1, 2, . . . , n} astfel ı̂ncât (i1 , i2 ) ∈ ρ, (i2 , i3 ) ∈
ρ, . . ., (im−1 , im ) ∈ ρ şi i = i1 , j = im .

Notăm ρn = ρ ◦ ρ ◦ . . . ρ, unde ρn reprezintă compunerea relaţiei ρ cu ea ı̂nsăşi de n ori.


| {z }
n ori
Atunci ρ∗ se poate defini ı̂n modul următor:
[
ρ∗ = ρn = ρ ∪ (ρ ◦ ρ) ∪ . . . ∪ (ρ ◦ ρ ◦ . . . ◦ ρ) ∪ . . . (8.14)
n∈N

O relaţie binară ρ peste mulţimea {1, 2, . . . , n} se poate reprezenta printr-o matrice R,


ale cărei elemente sunt definite astfel:
(
1 , dacă (i, j) ∈ ρ
ri,j = (8.15)
0 , dacă (i, j) ∈ /ρ

Pentru relaţia ρ2 = ρ ◦ ρ ı̂i corespunde matricea R × R, pentru relaţia ρ3 = ρ ◦ ρ ◦ ρ ı̂i


corespunde matricea R × R × R etc.
Dacă notăm cu R(k) matricea corespunzătoare relaţiei ρk , atunci avem R(k) = Rk =
. . × R}. Conform formulei 8.14, pentru determinarea ρ∗ este nevoie de calculul unei
| × .{z
R
k ori
serii de matrici R0 , R1 , . . . , Rn , unde un element ri,j
k
∈ Rk se calculează după formula:
(
k−1 k−1 k−1
k 1 , dacă ri,j = 1 sau (ri,k = 1 ∧ rk,j = 1)
ri,j = (8.16)
0 , altfel .

Închiderea tranzitivă a unei relaţii binare prezintă multiple aplicaţii, ca subproblemă ı̂n
cazul unor probleme computaţionale, cum ar fi: construirea unui automat de parsare utilizat
la realizarea unui compilator, evaluarea interogărilor recursive efectuate asupra unei baze de
date sau analiza elementelor accesibile ı̂ntr-o reţea de tranziţie asociată unui sistem paralel
sau distribuit.
Prima variantă de a determina ı̂nchiderea tranzitivă presupune atribuirea unui cost unitar
(= 1) arcelor mulţimii E, urmată de aplicarea algoritmului Roy-Floyd-Warshall. Vom obţine
o matrice, ı̂n care, di,j = +∞ dacă nu există un drum de la xi la xj sau di,j = c < n dacă
există.
În cadrul celei de-a doua variante de calcul (a se vedea algoritmul 68), se ı̂nlocuiesc ı̂n
algoritmul 65 operaţiile min cu ∨ (sau logic) şi respectiv + cu ∧ (şi logic).
Semnificaţia valorii unui element al matricii D (k) este următoarea: dki,j = 1 dacă există un
drum de la xi la xj ı̂n graful G având vârfurile intermediare numai din mulţimea {x1 , . . . , xk }.
(
1 , dacă i = j sau (xi , xj ) ∈ E
d0i,j = (8.17)
0 , dacă i 6= j şi (xi , xj ) ∈
/E
Există astfel un drum de la xi la xj ı̂n graful G având vârfurile intermediare numai
din mulţimea {x1 , . . . , xk } (dki,j = 1) dacă există un drum de la xi la xj având vârfurile

180
Algoritm 68 Algoritm de calcul a ı̂nchiderii tranzitive
1: procedure InchidereTranzitiva(n, C; D)
2: for i ← 1, n do
3: for j ← 1, n do
4: if ((i = j) ∨ (ci,j 6= +∞)) then
5: d0i,j ← 1
6: else
7: d0i,j ← 0
8: end if
9: end for
10: end for
11: for k ← 1, n do
12: for i ← 1, n do
13: for j ← 1, n do
14: dki,j ← dk−1 k−1 k−1
i,j ∨ (di,k ∧ dk,j )
15: end for
16: end for
17: end for
18: end procedure

intermediare numai din mulţimea {x1 , . . . , xk−1 } sau dacă există un drum de la xi la xk
având vârfurile intermediare numai din mulţimea {x1 , . . . , xk−1 } şi dacă există un drum de
la xk la xj având vârfurile intermediare numai din mulţimea {x1 , . . . , xk−1 }:

dki,j = dk−1 k−1 k−1


i,j ∨ (di,k ∧ dk,j ) (8.18)

Algoritm 69 Algoritm de calcul a ı̂nchiderii tranzitive (variantă)


1: procedure InchidereTranzitiva(n, C; D)
2: for i ← 1, n do
3: for j ← 1, n do
4: if ((i = j) ∨ (ci,j 6= +∞)) then
5: di,j ← 1
6: else
7: di,j ← 0
8: end if
9: end for
10: end for
11: for k ← 1, n do
12: for i ← 1, n do
13: if (di,k = 1) then
14: for j ← 1, n do
15: di,j ← di,j ∨ dk,j
16: end for
17: end if
18: end for
19: end for
20: end procedure

181
Exemplul 8.5 Fie graful din figura 8.2, din care am eliminat arcul (1, 2). Matricea cos-
turilor asociată acestui graf modificat este:
 
0 ∞ ∞ 21 ∞
 ∞ 0 8 ∞ 4 
 
C=  3 ∞ 0 12 ∞ 

 ∞ ∞ 9 0 ∞ 
∞ ∞ 3 ∞ 0
   
1 0 0 1 0 1 0 0 1 0

 0 1 1 0 1 


 0 1 1 0 1 

D0 =
 1 0 1 1 0 
 D1 =
 1 0 1 1 0 

 0 0 1 1 0   0 0 1 1 0 
0 0 1 0 1 0 0 1 0 1
   
1 0 0 1 0 1 0 0 1 0
 0 1 1 0 1   1 1 1 1 1 
   
D2 = 
 1 0 1 1 0 
 D3 = 
 1 0 1 1 0 

 0 0 1 1 0   1 0 1 1 0 
0 0 1 0 1 1 0 1 1 1
   
1 0 1 1 0 1 0 1 1 0
 1 1 1 1 1   1 1 1 1 1 
   
D4 = 
 1 0 1 1 0  D5 =  1
  0 1 1 0 

 1 0 1 1 0   1 0 1 1 0 
1 0 1 1 1 1 0 1 1 1

8.3 Exerciţii
1. (Cutii n-dimensionale) Să considerăm o cutie n-dimensională, dată prin lungimea
fiecărei laturi (o cutie bidimensională este un dreptunghi, o cutie tridimensională este
un paralelipiped etc.).
Problema cere analizarea unui grup de k cutii n-dimensionale: să se elaboreze un algo-
ritm ce determină o secvenţă de cutii de lungime maximă (b1 , b2 , . . .) din grupul de k
cutii, astfel ı̂ncât fiecare cutie bi să poată fi introdusă ı̂n cutia bi+1 .
O cutie D = (d1 , d2 , . . . , dn ) poate fi inclusă ı̂ntr-o cutie E = (e1 , e2 , . . . , en ) numai dacă
există o permutare a mulţimii valorilor laturilor cutiei D astfel ı̂ncât lungimea fiecărei
laturi a cutiei D, după reorganizare, să nu depăşească lungimea laturii corespunzătoare
din cutia E.
De exemplu cutia (2, 6) poate fi introdusă ı̂n cutia (7, 3) ((6, 2) ⊂ (7, 3)), iar cutia
(1, 6, 13) poate fi introdusă ı̂n cutia (11, 15, 3) ((6, 13, 1) ⊂ (11, 15, 3)).
Cutiile egale pot fi introduse una ı̂ntr-alta. Măsurile laturilor sunt numere reale mai
mici sau egale cu 1000. Numărul de cutii nu depăşeste 100, iar numărul de dimensiuni
nu depăşeşte 20.
(Tuymaada, 1997)

2. Sunteţi angajat la o companie ce asigură servicii de transport mărfuri en-gross. Clienţii


au magazine ı̂n oraşe din toată ţara şi ei sunt aprovizionaţi din depozitele locale ale
companiei.

182
Pentru o aprovizionare optimă, compania vrea să investească ı̂n construirea unui depozit
central din care să se aprovizioneze toate zonele; va fi, deci, necesar ca acest depozit să
fie plasat ı̂ntr-unul din oraşe, ı̂n aşa fel ı̂ncât timpul total de livrare din acest depozit
către toate oraşele să fie minim. Camioanele companiei transportă marfa la depozitele
locale şi se ı̂ntorc la depozitul central. Timpul de livrare este format din timpul necesar
parcurgerii drumului de la depozitul central la oraş şi ı̂napoi (se presupune că şoferul
va urma de fiecare dată calea cea mai rapidă, iar ı̂n fiecare oraş depozitul local se află
amplasat la intrare) iar timpul de descărcare al camionului la destinaţie este de exact 30
de minute. Drumurile ce leagă oraşele sunt de aceeaşi calitate, dar pot exista drumuri
ce iau mai mult timp ı̂ntr-un sens decât ı̂n sens invers. Pot fi, de asemenea, drumuri
cu sens unic.
Pentru a simplifica modelul, s-a stabilit pentru fiecare oraş o listă cu toate drumurile
ce pleacă din oraş spre celelalte oraşe şi cât timp ia parcurgerea fiecărui drum.
(Concurs ACM Zona Pacificul de Sud)

3. Se dă reţeaua hidrografică a unei ţări constituită din mulţimea râurilor şi afluenţii lor.
Cele N râuri (2 ≤ n ≤ 1000) sunt numerotate de la 1 la N. Legătura dintre un râu v
şi un afluent al său u este specificată prin perechea (u, v).
Pentru a se putea face estimări cu privire la potenţialul risc de inundaţie pe cursul unui
râu trebuie să se calculeze debitul fiecărui râu ı̂n parte.
Debitul unui izvor se defineşte ca fiind cantitatea de apă ce trece prin secţiunea izvorului
ı̂n unitatea de timp. Debitul râului u la vărsare va fi egal cu debitul izvorului râului u
plus suma debitelor afluenţilor la vărsare ı̂n râul u.
Se cere să se realizeze un algoritm care să calculeze debitul la vărsare al fiecărui râu.
4. Să se determine drumul de lungime minimă necesar deplasării unui cal pe o tablă de şah
(având dimensiunile 8×8), de la un punct de plecare la unui de sosire, ştiind că pe tablă
există şi căsuţe-capcană. Calul ı̂n drumul său nu poate trece printr-o căsuţă-capcană.
(ACM, Eastern Europe, 1994)

5. Un traducător găseşte o carte scrisă cu litere latine, ı̂n care ı̂nsă ordinea literelor din
alfabet este schimbată. La sfârşitul cărţii, se află un index de cuvinte complet şi necon-
tradictoriu.
Se cere să se elaboreze un algoritm ce determină ordinea literelor din noul alfabet.
(ONI, 1991)

6. Profesorul Heif realizează nişte experimente cu o specie de albine din America de Sud
pe care le-a descoperit ı̂n timpul unei expediţii ı̂n jungla Amazonului. Mierea produsă
de aceste albine este superioară din punct de vedere calitativ mierii produse de albinele
din Europa sau din America de Nord. Din păcate, aceste albine nu se ı̂nmulţesc ı̂n
captivitate. Profesorul Heiff crede că poziţiile larvelor (albine lucrătoare, regina) din
fagure depind de condiţiile de mediu, care sunt diferite ı̂n laborator faţă de pădurea
tropicală.
Ca un prim pas pentru a-şi verifica teoria, profesorul Heiff doreşte să calculeze distanţa
dintre poziţiile larvelor. Pentru aceasta el măsoară distanţa dintre celulele fagurelui ı̂n
care sunt plasate larvele. El a numerotat celulele alegând ı̂n mod arbitrat una, căreia
i-a atribuit valoarea 1, celelalte celule rămase fiind apoi numerotate circular, ı̂n sensul
acelor de ceas, ca ı̂n figură.

183
__ __ __ __
__/ \__/ \__/ \__/ \__
__/ \__/ \__/53\__/ \__/ \__
/ \__/ \__/52\__/54\__/ \__/ \
\__/ \__/51\__/31\__/55\__/ \__/
/ \__/50\__/30\__/32\__/56\__/ \
\__/49\__/29\__/15\__/33\__/57\__/
/ \__/28\__/14\__/16\__/34\__/ \
\__/48\__/13\__/ 5\__/17\__/58\__/
/..\__/27\__/ 4\__/ 6\__/35\__/ \
\__/47\__/12\__/ 1\__/18\__/59\__/
/..\__/26\__/ 3\__/ 7\__/36\__/ \
\__/46\__/11\__/ 2\__/19\__/60\__/
/..\__/25\__/10\__/ 8\__/37\__/ \
\__/45\__/24\__/ 9\__/20\__/61\__/
/..\__/44\__/23\__/21\__/38\__/ \
\__/70\__/43\__/22\__/39\__/62\__/
/ \__/69\__/42\__/40\__/63\__/ \
\__/ \__/68\__/41\__/64\__/ \__/
/ \__/ \__/67\__/65\__/ \__/ \
\__/ \__/ \__/66\__/ \__/ \__/
\__/ \__/ \__/ \__/ \__/
\__/ \__/ \__/ \__/

De exemplu, două larve poziţionate ı̂n celulele 19 şi 30 se află la distanţa de 5 celule.
Unul din drumurile minime ce unesc cele două celule trece prin celulele 19-7-6-5-15-30.
Să se scrie un algoritm ce calculează distanţa minimă dintre oricare două celule.
Datele de intrare constau din două numere naturale u şi v (u, v ≤ 10.000) reprezentând
numărul celulelor ı̂ntre care se doreşte să se determine distanţa minimă.
(ACM Final, 1999)

7. Într-o ţară există N orase, unele dintre ele legate prin autostrăzi cu dublu sens. Se
presupune că din orice oraş se poate ajunge ı̂n oricare altul. Taxa este aceeaşi pentru
orice autostradă (1 Ron), dar trebuie plătită din nou la intrarea pe o nouă autostradă.
Distanţa dintre două oraşe este considerată egală cu taxa minimă pentru a ajunge
dintr-unul ı̂n altul.
Se doreşte introducerea unui abonament a cărui plată permite circulaţia pe autostrăzi
fără nici o taxă suplimentară. Costul abonamentului este de 100 de ori mai mare decât
distanţa maximă ı̂ntre două perechi de oraşe.
Să se scrie un algoritm ce calculează costul abonamentului.
(CEOI, 1996)

8. În fiecare noapte, un prinţ intră ı̂n subsolurile unui castel unde locuieşte o prinţesă.
Drumul către prinţesă este ca un labirint. Sarcina prinţului este să se grăbească şi să
găsească drumul prin labirint către prinţesă, deoarece trebuie să se ı̂ntoarcă ı̂n zori.
Acesta are unelte potrivite cu ajutorul cărora poate să spargă numai un singur perete
pentru a-şi scurta drumul către destinaţie.
Să se elaboreze un algoritm care:

184
(a) determină lungimea celui mai scurt drum din labirint din locul ı̂n care prinţul
intră ı̂n labirint până ı̂n locul ı̂n care trăieşte prinţesa;

(b) determină lungimea celui mai scurt drum ı̂n condiţiile de mai sus, dacă se sparge
un perete şi numai unul.

Castelul are o formă dreptunghiulară cu m × n camere (1 ≤ n, m ≤ 100). Fiecare


cameră poate avea pereţi spre E, S, V şi N codificaţi cu un număr ı̂ntre 1 şi 14: 1, 2,
4, 8. Se ştie că nu există camere fără nici un perete (codul > 0) şi nici camere care să
aibă pereţi ı̂n toate direcţiile (codul < 15).
Lungimea celui mai scurt drum se defineşte ca fiind numărul de camere dintre sursă şi
destinaţie inclusiv.
Datele de intrare constau din M linii ce conţin fiecare N numere ı̂ntregi reprezentând
codificarea pereţilor din fiecare cameră, urmate de coordonatele prinţului şi ale prinţesei.
De exemplu, pentru datele de intrare

5 8
14 10 10 10 10 10 10 9
12 10 10 10 10 10 10 3
5 14 9 14 8 11 14 9
4 10 2 8 3 12 10 1
6 11 14 2 10 2 10 3
1 1 5 8

avem rezultatul 26 şi 12.


(ACM Bucureşti, 1999)

185
Appendix A

Metoda Backtracking

Metoda Backtracking este una dintre cele mai cunoscute metode de elaborare a algoritmilor.
Metoda se aplică numai ı̂n situaţia ı̂n care nu există nici o altă modalitate de rezolvare a
problemei propuse deoarece timpul de execuţie depinde exponenţial de dimensiunea datelor
de intrare. Se utilizează o structură de tip stivă iar metoda poate fi implementată atât
iterativ cât şi recursiv. Metoda se aplică acelor probleme ı̂n care soluţia se poate reprezenta
sub forma unui vector x = (x1 , x2 , . . . , xn ) ∈ A1 × A2 × . . . × An , unde mulţimile Ai , (i = 1, n)
sunt finite şi nevide (|Ai| = ni > 0). În plus, pentru fiecare problemă ı̂n parte este necesar ca
soluţia x1 , x2 , . . . , xn să satisfacă anumite condiţii interne ρ(x1 , x2 , . . . , xn ) (vezi algoritmul
70).

Algoritm 70 Algoritm Backtracking (varianta generală)


1: procedure Backtracking(n, A)
2: k←1
3: xk ← prima valoare din afara domeniului de valori
4: while (k > 0) do
5: gasit ← f alse
6: while (∃ valori neverif icate pentru xk ) ∧ (gasit 6= true) do
7: xk ← urmatoarea valoare neverif icata
8: if (ρk (x1 , x2 , . . . , xk ) = true) then
9: gasit ← true
10: end if
11: end while
12: if (gasit = true) then
13: if (k = n) then
14: call Af is Solutie(x1 , . . . , xn )
15: else
16: k ←k+1
17: xk ← prima valoare din afara domeniului de valori
18: end if
19: else
20: k ←k−1
21: end if
22: end while
23: end procedure

De exemplu, să considerăm două mulţimi, S1 = {a, b, c} şi S2 = {m, n}. Se doreşte să se
determine perechile (x1 , x2 ), cu x1 ∈ A1 şi x2 ∈ A2 , cu proprietatea că dacă x1 este a sau b

186
atunci x2 nu poate fi n. Rezolvarea conduce la următoarele variantele: (a, m), (b, m), (c, m), (c, n).
Din cele şase soluţii posibile (a, m), (a, n), (b, m), (b, n), (c, m), (c, n), numai acestea patru
ı̂ndeplinesc condiţiile interne.
Mulţimea A = A1 × A2 × ... × An se numeşte spaţiul soluţiilor posibile, iar elementele
x ∈ A ce satisfac condiţiile interne se numesc soluţii rezultat. Ne propunem determinarea
tuturor soluţiilor rezultat, eventual pentru a alege dintre ele pe aceea ce minimizează sau
maximizează o funcţie obiectiv.
Metoda Backtracking evită generarea tuturor soluţiilor posibile (toate elementele pro-
dusului cartezian A1 × A2 × ... × An ). Construirea unei soluţii se face ı̂n mai mulţi paşi,
elementele vectorului X primind valori pe rând: elementului xk ∈ Ak , k = 1, n, i se atribuie
o valoare numai după ce au fost atribuite valori pentru componentele x1 ∈ A1 , x2 ∈ A2 ,
. . ., xk−1 ∈ Ak−1 . Metoda trece la atribuirea unei valori pentru xk+1 ∈ Ak+1 doar dacă
xk ı̂mpreună cu x1 , x2 , . . . , xk−1 verifică condiţiile de continuare, notate cu ρk (x1 , x2 , . . . , xk ).
Dacă aceste condiţii ρk (x1 , x2 , . . . , xk ) sunt ı̂ndeplinite se trece la căutarea unei valori pentru
elementul xk+1 ∈ Ak+1 al soluţiei.
Neı̂ndeplinirea condiţiilor are următoarea semnificaţie: pentru orice alegere xk+1 ∈ Ak+1 ,
. . . , xn ∈ An , nu vom putea ajunge la o soluţie rezultat ı̂n care condiţiile interne să fie
ı̂ndeplinite. În această situaţie va trebui să ı̂ncercăm o altă alegere pentru xk , acest lucru fiind
posibil doar dacă nu am epuizat toate valorile disponibile din mulţimea Ak . Dacă mulţimea
Ak a fost epuizată va trebui să micşorăm valoarea variabilei k cu o unitate (k ← k − 1), şi să
trecem la alegerea unei alte valori pentru elementul xk−1 ∈ Ak−1 . Micşorarea valorii curente
a variabilei k cu o unitate dă numele metodei şi semnifică faptul că atunci când nu putem
avansa, vom urmări ı̂napoi secvenţa curentă din soluţie. Faptul că valorile v1 , v2 , . . . , vk−1 ,
ale componentelor x1 , x2 , . . . , xk−1 , satisfac condiţiile de continuare, nu este suficient pentru
a garanta obţinerea unei soluţii ale cărei prime k − 1 componente coincid cu aceste valori.
O alegere bună a condiţiilor de continuare conduce la reducerea numărului de calcule,
fiind de dorit ca aceste condiţii de continuare să fie nu numai necesare, dar şi suficiente
pentru obţinerea unei soluţii. Condiţiile interne devin chiar condiţii de continuare pentru
k = n. Orice vector soluţie se obţine progresiv ı̂ncepând cu prima componentă şi deplasându-
ne către ultima, cu eventuale reveniri asupra valorilor atribuite anterior. Anumite valori sunt
consumate ı̂n urma unor atribuiri sau ı̂ncercări de atribuire eşuate din cauza neı̂ndeplinirii
condiţiilor de continuare.
Există două variante faţă de metoda standard:

• soluţiile pot avea număr variabil de componente;

• dintre soluţii se alege cea care minimizează sau maximizează o funcţie cost sau o funcţie
obiectiv dată.

Exemplul A.1 Enunţul problemei: Se cere să se genereze permutări de n elemente.


Rezolvare: Spaţiul soluţiilor este A = ×ni=1 Ai , Ai = {1, 2, . . . , n}, |Ai | = n. Soluţia va
fi obţinută ı̂n vectorul x = (x1 , x2 , . . . , xn ) ∈ A1 × A2 × . . . An .
La pasul k se ı̂ncearcă atribuirea unui alt element din mulţimea Ak lui xk . Vom trece la
pasul k + 1 doar dacă sunt ı̂ndeplinite condiţiile de continuare: (x1 , x2 , . . . , xk ) nu conţine
elemente care se repetă. Deoarece această verificare se face la fiecare pas, este suficient să
verificăm dacă valoarea elementului xk nu se regăseşte printre valorile x1 , x2 , . . . , xk−1 (vezi
funcţia CanContinue() din algoritmul 71).

Exemplul A.2 Fie n = 4. Atunci Ai va fi compus din elementele Ai = {1, 2, 3, 4}.


Pentru k = 1, x1 va primi valuarea 1 (vezi linia 15): 1

187
Pentru k = 2, se verifică x2 = 1, ı̂nsă nu se respectă condiţiile de continuare (linia 16).
Astfel se trece la următoarea valoare, x2 = x1 + 1 = 2 (linia 15). Pentru această configuraţie
condiţiile de continuare sunt ı̂ndeplinite, şi se trece la pasul următor, k = k + 1 = 3 (linia
24): 1 2
Se verifică valorile 1, 2 şi 3 pentru x3 (linia 16), dintre care, numai ultima satisface
condiţiile de continuare: 1 2 3
Ajunşi la ultimul pas, k = 4, se verifică valorile 1, 2, 3 şi 4 pentru x4 . În urma verificării
condiţiilor de continuare pentru x4 = 4, se urmăreşte trecerea la pasul k = k+1 = 5. Deoarece
suntem la ultimul element al vectorului x (linia 21), se obţine prima soluţie, ce se şi afişează:
1 2 3 4
Mai departe, nu mai există valori netestate pentru x4 (linia 14), şi revenim la nivelul
anterior k = k −1 = 3 (linia 28). Aici următoarea valoare din domeniul de valori neverificată
este x3 = 4 (linia 15). Soluţia parţială respectă condiţiile de continuare (linia 16), astfel ı̂ncât
se trece la nivelul următor (linia 24), k = k + 1 = 4: 1 2 4
Pentru x4 sunt verificate valorile, 1, 2, 3, 4 (liniile 15–16), dintre acestea numai valoarea
3, conducând la o soluţie finală (linia 21): 1 2 4 3 Algoritmul se continuă ı̂n acelaşi
mod până se obţin toate soluţiile: 

 (1, 2, 3, 4)




 (1, 2, 4, 3)


(1, 3, 2, 4)





 (1, 3, 4, 2)


(1, 4, 2, 3)


(1, 4, 3, 2)




 (2, 1, 3, 4)




 (2, 1, 4, 3)


(2, 3, 1, 4)





 (2, 3, 4, 1)


...

O variantă de implementare a algoritmului 71 ı̂n limbajul C este:


#include <stdio.h>

#define TRUE 1
#define FALSE 0
#define NN 100

int n;
int a[NN];

/**
* Functia citeste valorile datelor de intrare.
*/
void readInput(void) {
//se citeste numarul de elemente n
printf("n="); scanf("%d", &n);
}

188
Algoritm 71 Algoritm pentru generare permutări (varianta backtracking)
Input: n - numărul de elemente din mulţimea iniţială
1: function CanContinue(A, k)
2: for i ← 1, k − 1 do
3: if (ai = ak ) then
4: return f alse
5: end if
6: end for
7: return true
8: end function

9: procedure Permutari(n)
10: k←1
11: xk ← 0
12: while (k > 0) do
13: gasit ← f alse
14: while (xk + 1 ≤ n) ∧ (gasit 6= true) do
15: xk ← xk + 1
16: if ((CanContinue(X, k)) = true) then
17: gasit ← true
18: end if
19: end while
20: if (gasit = true) then
21: if (k = n) then
22: Output {x1 , . . . , xn }
23: else
24: k ←k+1
25: xk ← 0
26: end if
27: else
28: k ←k−1
29: end if
30: end while
31: end procedure

/**
* Functia afiseaza solutia calculata a problemei.
*/
void list(void) {
int i;

for (i = 1; i <= n; i++)


printf("%d ", a[i]);
printf("\n");
}

/**
* Functia de continuare: aici se verifica daca elementul de pe pozitia k
* nu se mai afla in sirul A.
*/

189
int canContinue(int k) {
int i;

for (i = 1; i <= k - 1; i++)


if (a[k] == a[i])
return FALSE;
return TRUE;
}

void run(void) {
int i;
int gata;

//initializare
i = 1; a[i] = 0;
while (i > 0) {
gata = FALSE;
while ((a[i] + 1 <= n) && !gata) {
/**
* cat timp exista elementul urmator si nu sunt verificate
* conditiile de continuare
*/

//treci la elementul urmator


a[i]++;
//verifica conditiile de continuare
if (canContinue(i))
gata = TRUE;
//sau: gata = canContinue(i);
}

if (gata)
//daca s-au verificat conditiile de continuare
if (i == n)
//daca suntem la ultimul element afisam solutia
list();
else { //altfel trecem la elementul urmator
i++;
a[i] = 0;
}
else
//altfel revenim la un element anterior si alegem o alta valoare
i--;
}
}

void main(void) {
read_data();
run();
}

Exemplul A.3 Enunţul problemei: Să se genereze combinări de n luate câte m (0 ≤

190
m ≤ n).
Rezolvare: Spaţiul soluţiilor este A = ×m i=1 Ai , Ai = {1, 2, . . . , n}, |Ai | = n. Soluţia va
fi obţinută ı̂n vectorul x = (x1 , x2 , . . . , xm ) ∈ A1 × A2 × . . . Am (vezi algoritmul 72).
La pasul k se ı̂ncearcă atribuirea unui alt element din mulţimea Ak lui xk . Condiţia de
continuare se referă la proprietatea că elementele vectorului soluţie trebuie să respecte relaţia
x1 < x2 < . . . < xk . Acest lucru se obţine foarte uşor prin următorul procedeu: la trecerea la
pasul k + 1, variabila xk+1 va primi, ca valoare iniţială, valoarea curentă a variabilei xk (vezi
linia 15 din algorimul 72).
Pentru n = 5 şi m = 3 avem: A = ×3i=1 Ai = A1 × A2 × A3 , unde A1 = A2 = A3 =
{1, 2, 3, 4, 5}. Soluţiile obţinute sunt:


 (1, 2, 3)




 (1, 2, 4)




 (1, 2, 5)




 (1, 3, 4)

(1, 3, 5)


 (1, 4, 5)



 (2, 3, 4)




 (2, 3, 5)




 (2, 4, 5)


(3, 4, 5)

Algoritm 72 Algoritm pentru generare combinări (varianta backtracking)


1: procedure Combinari(n, m)
2: k←1
3: xk ← 0
4: while (k > 0) do
5: gasit ← f alse
6: while (xk + 1 ≤ n) ∧ (gasit 6= true) do
7: xk ← xk + 1
8: gasit ← true
9: end while
10: if (gasit = true) then
11: if (k = m) then
12: Output {x1 , . . . , xm }
13: else
14: k ←k+1
15: xk ← xk−1
16: end if
17: else
18: k ←k−1
19: end if
20: end while
21: end procedure

În continuare prezentăm o implementare a algoritmului 72 ı̂n limbajul C:

191
#include <stdio.h>

#define TRUE 1
#define FALSE 0
#define NN 100

int n, m;
int a[NN];

/**
* Functia citeste valorile datelor de intrare.
*/
void readInput(void) {
printf("n="); scanf("%d", &n);
printf("m="); scanf("%d", &m);
}

/**
* Functia afiseaza solutia calculata de functia run().
*/
void list(void) {
int i;

for (i = 1; i <= m; i++)


printf("%d ", a[i]);
printf("\n");
}

void run(void) {
int i;
int gata;

//initializare
i = 1; a[i] = 0;
while (i > 0) {
gata = FALSE;
while ((a[i] + 1 <= n) && !gata) {
/**
* cat timp exista elementul urmator si nu sunt verificate
* conditiile de continuare, treci la elementul urmator
*/
a[i]++;
gata = TRUE;
}

if (gata)
//daca s-au verificat conditiile de continuare
if (i == m)
//daca suntem la ultimul element afisam solutia
list();
else { //altfel trecem la elementul urmator
i++;

192
a[i] = a[i - 1];
}
else
//altfel revenim la un element anterior si alegem o alta valoare
i--;
}
}

void main(void) {
readInput();
run();
}

Exemplul A.4 Problema damelor


Enunţul problemei: Pe o tablă de şah cu n linii şi n coloane (n × n pătrate), să se
poziţioneze n dame astfel ı̂ncât să nu se atace reciproc.
Rezolvare: Tabla de şah va fi reprezentată prin matricea A = (aij ), i, j = 1, n. Două
dame aflate ı̂n poziţiile aij şi akl se vor ataca reciproc dacă:

i = k sau j = l sau |i − k| = |j − l|

Prima observaţie care se poate deduce se referă la faptul că două dame nu trebuie să se
afle pe aceeaşi linie sau pe aceeaşi coloană.
Notăm cu V = {1, 2, . . . , n} mulţimea celor n coloane ale tablei de şah. Vectorul soluţiei
rezultat X = (x1 , x2 , . . . , xn ) ∈ V × ... × V are următoarea semnificaţie: componenta i a
vectorului (i = 1, n) reprezintă linia i de pe tabla de şah. Valoarea xi a acestei componente
reprezintă coloana pe care se aşează dama de pe linia i (vezi algoritmul 73).
La ı̂nceput, toate damele sunt ı̂n afara tablei de şah, prin urmare xi = 0, i = 1, n.
La pasul k vom ı̂ncerca aşezarea damei k (de pe linia k) pe coloana următoare (xk + 1).
Iniţial dama a k-a este poziţionată ı̂n afara tablei (xk = 0 ∈/ V ). O damă poate fi aşezată pe
o altă coloană dacă vechea poziţie satisface condiţia xk < n. Noua poziţie (xk + 1) trebuie să
satisfacă condiţiile interne ρk :
• ∀i, 1 ≤ i < k, xi 6= xk + 1 (nu sunt pe aceeaşi coloană);
• |i − k| =
6 |xi − (xk + 1)| (nu sunt pe aceeaşi diagonală).

Dacă xk < n şi ρk sunt adevarăte, dama de pe linia k va fi poziţionată pe coloana xk + 1


şi se trece la pasul k + 1, corespunzător poziţionării damei k + 1. În caz contrar, dama k nu
poate fi aşezată pe tablă şi va trebui să reluăm poziţionarea damei k − 1.

Exemplul A.5 Problema colorării hărţilor


Enunţul problemei: Să se coloreze o hartă reprezentând n ţări folosind m culori
etichetate 1, . . . , m.
Rezolvare: Datele necesare pentru descrierea hărţii vor fi reprezentate de o matrice
A = (aij ), i, j = 1, n, ale cărei elemente au următoarea semnificaţie:
(
1 , dacă ţara i are frontieră comună cu ţara j
aij =
0 , ı̂n caz contrar

Matricea A ∈ Mn×n este simetrică şi elementele de pe diagonala principală sunt nule:
aij = aji şi aii = 0.

193
Algoritm 73 Algoritm Problema damelor
Input: n - numărul de linii/coloane de pe tabla de şah
1: function CanContinue(A, k)
2: for j ← 1, k − 1 do
3: if (aj = ak ) ∨ ((k − j) = |ak − aj |) then
4: return f alse
5: end if
6: end for
7: return true
8: end function

9: procedure DameBacktracking(n)
10: i←1
11: xi ← 0
12: while (i > 1) do
13: gasit ← f alse
14: while (xi + 1 ≤ n) ∧ (gasit 6= true) do
15: xi ← xi + 1
16: if (CanContinue(X, i) = true) then
17: gasit ← true
18: end if
19: end while
20: if (gasit = true) then
21: if (i = n) then
22: call Af is Solutie(x1 , . . . , xn )
23: else
24: i←i+1
25: xi ← 0
26: end if
27: else
28: i ← i−1
29: end if
30: end while
31: end procedure

Fie C = {c1 , c2 , . . . , cm } mulţimea culorilor.


Vectorul soluţiei rezultat X = (x1 , x2 , . . . , xn ) ∈ C × ... × C, are următoarea semnificaţie:
ţara i are repartizată culoarea xi ∈ C, i = 1, n. La ı̂nceput, fiecărei ţări i se va atribui valoarea
0.
La pasul k ı̂ncercăm să atribuim o nouă culoare (xk + 1) pentru ţara k, k = 1, n, dacă
sunt ı̂ndeplinite condiţiile:

1. xk < m (dacă mai există alte culori neatribuite pentru ţara k);
2. pentru ∀i, 1 ≤ i < k cu ai,k = 1 avem xi 6= xk + 1.

Dacă cele două condiţii anterioare sunt ı̂ndeplinite, atunci noua culoare pentru ţara k va
fi xk + 1, şi se va trece la pasul k + 1 (stabilirea unei culori pentru ţara k + 1). În caz contrar,
ţării k nu i se mai poate atribui o altă culoare şi ne ı̂ntoarcem la pasul k − 1, corespunzător
alegerii unei noi culori pentru ţara k − 1. Algoritmul se termină atunci când nu mai există
nici o culoare neverificată pentru ţara 1 (vezi algoritmul 74).

194
Algoritm 74 Algoritm Problema colorării hărţilor

A - matricea de adiacenţă/de vecinătate a ţărilor

Input: n - numărul de ţări


m - numărul de culori disponibile
1: function CanContinue(A, X, k)
2: for j ← 1, k − 1 do
3: if (xj = xk ) ∧ (aj,k = 1) then
4: return f alse
5: end if
6: end for
7: return true
8: end function

9: procedure ColorBacktracking(A, n, m)
10: k←1
11: xk ← 0
12: while (k > 0) do
13: gasit ← f alse
14: while (xk + 1 ≤ m) ∧ (gasit 6= true) do
15: xk ← xk + 1
16: gasit ← CanContinue(A, X, k)
17: end while
18: if (gasit = true) then
19: if (k = n) then
20: call Af is Solutie(x1 , . . . , xn )
21: else
22: k ←k+1
23: xk ← 0
24: end if
25: else
26: k ←k−1
27: end if
28: end while
29: end procedure

Exemplul A.6 Enunţul problemei: Se dă o sumă s şi n tipuri de monede având valorile
a1 , a2 , . . . , an lei. Realizaţi un algoritm care să determine o modalitate de plată a sumei s
utilizând un număr minim de monede.
Rezolvare:
#include <stdio.h>

#define FALSE 0
#define TRUE 1

#define NN 100
#define MAX 10000

int suma_plata; //suma ce trebuie platita


int n; //numarul de monezi

195
int limit[NN]; //limit[i] numarul maxim de monede de tipul val[i]
int val[NN]; //val[i] valoarea unei monede
int taken[NN]; //cate monede de valoare val[i] au fost luate
int minim; //numarul minim de monede cu care se poate face plata
int keep[NN]; //solutia optima pina la momentul curent

/**
* Se citesc datele de intrare: suma de plata, numarul de
* tipuri de monezi si valoarea fiecarui tip.
*/
void readInput(void) {
int i;

printf("Suma= "); scanf("%d", &suma_plata);


printf("Numarul de tipuri de monede: "); scanf("%d", &n);

for (i = 0; i < n; i++) {


printf("val[%d]=", i); scanf("%d", &val[i]);
limit[i] = suma_plata / val[i];
}
}

/**
* Se verifica daca sunt satisfacute conditiile de continuare:
* suma partiala sa nu depaseasca valoarea totala de platit.
* @param k - pasul curent
*/
int canContinue(int k) {
int i, suma_tmp;

suma_tmp = 0;
for (i = 0; i <= k; i++)
suma_tmp += val[i] * taken[i];
if ((k == n - 1 && suma_tmp == suma_plata)
|| (k != n - 1 && suma_tmp <= suma_plata))
return TRUE;
else
return FALSE;
}

/**
* Se pastreaza solutia actuala (taken) numai daca este
* mai buna decat cea anterioara (keep).
*/
void final(void) {
int i;
int monezi = 0;

//se numara cate monede sunt in solutie


for (i = 0; i < n; monezi += taken[i], i++);
if (monezi < minim) {
minim = monezi;

196
for (i = 0; i < n; i++)
keep[i] = taken[i];
}
}

/**
* Se afiseaza solutia optima sau mesajul ’Nu avem solutie.’
*/
void print(void) {
int i, first = 0;

if (minim != MAX) { //verificam daca am obtinut o solutie


printf("%d =", suma_plata);
for (i = 0; i < n; i++)
if (keep[i] != 0) {
printf((first == 1) ? " + " : " ");
printf("%d X %d", keep[i], val[i]);
first = 1;
}
printf("\n");
}
else
printf("Nu avem solutie! \n");
}

/**
* Implementarea metodei backtracking.
*/
void run(void) {
int i;
int gata; /* are valoarea TRUE cand sunt verificate conditiile
de continuare */

minim = MAX;
i = 0; taken[i] = -1;
while (i >= 0) {
gata = FALSE;
while ((taken[i] + 1 <= limit[i]) && !gata) {
/* cat timp exista elementul urmator si nu sunt verificate conditiile
de continuare */
taken[i]++; //treci la elementul urmator
//verifica conditiile de continuare
if (canContinue(i))
gata = TRUE;
//sau: gata = canContinue(i);
}
if (gata) //daca s-au verificat conditiile de continuare
if (i == n-1)
//daca suntem la ultimul element pastram solutia
final();
else { //altfel trecem la elementul urmator
i++; taken[i] = -1;

197
}
else
//altfel revenim la un element anterior si alegem o alta valoare
i--;
}
}

void main(void) {
readInput();
run();
print();
}

198
Bibliografie

[1] A. Aho, J. Hopcroft, J. Ullman, On finding lowest common ancestors in trees, Proc. 5th
ACM Symp. Theory of Computing (STOC), pp. 253-265, 1973.
[2] A. V. Aho, J. E. Hopcroft, J. D. Ulmann, Data Structures and algorithms, Addison-
Wesley, 1983.
[3] A. V. Aho, J. D. Ulmann, Foundation of Computer Science, Computer Science Press,
1992.
[4] R. K. Ahuja, J. B. Orlin, A Fast and Simple Algorithm for the Maximum Flow Problem,
Operations Research, vol. 37(5), pp. 748–759, 1989.
[5] R. Ahuja, T. Magnanti, J. Orlin, Network Flows, Prentice Hall, 1993.
[6] R. Andonie, I. Gârbacea, Algoritmi fundamentali, o perspectivă C++, Editura Libris,
1995.
[7] C. Aragon, R. Seidel, Randomized Search Trees, in Proc. of 30th IEEE Symposium on
Foundations of Computer Science, pp. 540–546, 1989.
[8] A. Atanasiu, Concursuri de informatică. Probleme propuse (1994), Editura Petrion,
Bucureşti, 1995.
[9] M.D. Atkinson, J.-R. Sack, N. Santoro, T. Strothotte, Min-max heaps and generalized
priority queues, Programming techniques and Data structures. Comm. ACM, vol. 29(10),
pp. 996-1000, 1986.
[10] M. Augenstein, A. Tenenbaum, Program efficiency and data structures, Proceedings of
the eighth SIGCSE technical symposium on Computer science education, pp. 21–27,
1977.
[11] S. Baase, Computer algorithms. Introduction to Design and Analysis, Addison-Wesley,
1992.
[12] P. Băzăvan, Elemente de Teoria Algoritmilor, Editura Sitech, Craiova, 2007.
[13] R. Bellman, On a routing problem, Quarterly Applied Mathematics, XVI(1), pp. 87-90,
1958.
[14] M. A. Bender, M. Farach-Colton, The LCA problem revisited, Proceedings of the 4th
Latin American Symposium on Theoretical Informatics, LNCS, vol. 1776, Springer-
Verlag, pp. 88-94, 2000.
[15] J. Bentley, R. Sedgewick, Fast Algorithms for Sorting and Searching Strings, Proceedings
of the 8th Annual ACM-SIAM Symposium on Discrete Algorithms, 1997.

199
[16] J. Bentley, R. Sedgewick, Ternary Search Trees, Dr. Dobb’s Journal, 1998.

[17] C. Bereanu, Algoritmica Grafurilor, Editura Sitech, Craiova, 2006.

[18] C. Berge, Graphes et hypergraphes, Dunod, Paris 1970.

[19] O. Berkman, D. Breslauer, Z. Galil, B. Schieber, şi U. Vishkin, Highly parallelizable


problems, in Proceedings of the 21st Annual ACM Symposium on Theory of Computing,
pp. 309-319, 1989.

[20] O. Berkman, U. Vishkin, Recursive Star-Tree Parallel Data Structure, SIAM Journal on
Computing, vol.22(2), pp.221-242, 1993.

[21] O. Boruvka, O jistém problému minimálnı́m (About a certain minimal problem), Acta
Societ. Scient. Natur. Moravicae, 3, pp. 37-58, 1926.

[22] R. P. Brent, An improved Monte Carlo factorization algorithm, BIT Numerical Mathe-
matics, Springer, vol. 20(2), pp. 176–184, 1980.

[23] D. D. Burdescu, Analiza complexităţii algoritmilor, Editura Albastră, Cluj–Napoca,


1998.

[24] D. D. Burdescu, M. Brezovan, M. Coşulschi, Structuri de date arborescente cu aplicaţii


ı̂n Pascal şi C, Reprografia Universităţii din Craiova, 2000.

[25] D. D. Burdescu, Liste, arbori, grafuri, Editura Sitech, Craiova, 2005.

[26] B. Chazelle, A minimum spanning tree algorithm with inverse-Ackerman type complexity,
J. ACM, 47, pp. 1028-1047, 2000.

[27] D. Cherition, R. E. Tarjan, Finding minimum spanning trees, SIAM Journal on Com-
puting, vol. 5, pp. 724-741, 1976.

[28] J. Cheriyan, K. Mehlhorn, Algorithms for dense graphs and networks on the random
access computer, Algorithmica, vol. 15, pp. 521-549, 1996.

[29] B. V. Cherkassky,
√ Algorithm for construction of maximal flows in networks with com-
plexity of O(V 2 E), Mathematical Methods of Solution of Economical Problems, vol.
7, pp. 112–125, 1977.

[30] T. H. Cormen, C. E. Leiserson, R. L. Rivest, Introducere ı̂n Algoritmi, Computer Libris


Agora, Cluj–Napoca, 1999.

[31] M. Coşulschi, M. Gabroveanu, Algoritmi o abordare pragmatică, Editia a 2-a, Universi-


taria, Craiova, 2004.

[32] C. Croitoru, Tehnici de bază ı̂n optimizarea combinatorie, Editura Univ. Al. I. Cuza Iaşi,
Iaşi, 1992.

[33] G. B. Dantzig, Linear programming and extensions, University Press, Princeton, 1962.

[34] S. Dasgupta, C. H. Papadimitriou, U. V. Vazirani, Algorithms, McGraw–Hill, 2006.

[35] C. Demetrescu, G. F. Italiano, Engineering Shortest Path Algorithms, WEA, Springer,


LNCS 3059, pp. 191–198, 2004.

200
[36] R. B. Dial, Algorithm 360: shortest-path forest with topological ordering [H], Communi-
cations of the ACM, vol. 12:11, pp. 632–633, 1969.

[37] Dicţionarul explicativ al limbii române, Academia Română, Institutul de Lingvistică


Iorgu Iordan, Editura Univers Enciclopedic, 1998.

[38] E. W. Dijkstra, A note on two problems in connections with graphs, Numerische Math-
ematik, 1, pp. 269-271, 1959.

[39] Y. Dinitz, Algorithm for solution of a problem of maximum flow in a network with power
estimation, Doklady Akademii nauk SSSR, vol. 11, pp. 1277-1280, 1970.

[40] Y. Dinitz, Dinitz’ Algorithm: The Original Version and Even’s Version, in Oded Goldre-
ich, Arnold L. Rosenberg, and Alan L. Selman, Theoretical Computer Science: Essays
in Memory of Shimon Even, Springer, pp. 218-240, 2006.

[41] J. Edmonds, Paths, Trees and Flowers, Canadian J. Math, vol. 17, pp. 449-467, 1965.

[42] J. Edmonds, R. M. Karp, Theoretical improvements in algorithmic efficiency for network


flow problems, Journal of the ACM, vol. 19(2), pp. 248-264, 1972.

[43] J. Egerváry, Matrixok kombinatorius tulajdonságairól, (in Hungarian), Matematikai és


Fizikai Lapok, vol. 38 pp. 16-28, 1931.

[44] J. Farey, On a Curious Property of Vulgar Fractions, London, Edinburgh and Dublin
Phil. Mag. 47, 385, 1816.

[45] J. Feng, G. Li, J. Wang, L. Zhou, Finding and ranking compact connected trees for
effective keyword proximity search in XML documents, Information Systems, vol. 35(2),
pp. 186–203, 2010.

[46] L. R. Ford, Network flow theory, Technical Report P-923, RAND, Santa Monica, CA,
1956.

[47] L. R. Ford, Jr., D. R. Fulkerson, Maximal Flow Through a Network, Canadian Journal
of Mathematics, 8, pp. 399–404, 1956.

[48] L. R. Ford, Jr., D. R. Fulkerson, A Simple Algorithm for Finding Maximal Network
Flows and an Application to the Hitchcock Problem, Canadian Journal of Mathematics,
9, pp. 210–218, 1957.

[49] L. R. Ford, Jr., D. R. Fulkerson, Flows in Networks, Princeton University Press, Prince-
ton, 1962.

[50] C. L. Foster, The Design and Analysis of Algorithms, Springer Verlag, 1992.

[51] J.-C. Fournier, Graph Theory and Applications, Wiley-Blackwell, 2009.

[52] E. Fredkin, Trie Memory, Communications of the ACM, 3:(9), pp. 490, 1960.

[53] M. Fredman, R. Sedgewick, R. Sleator, R. Tarjan, The pairing heap: A new form of
self-adjusting heap, Algorithmica, 1, pp. 111–129, 1986.

[54] M.L. Fredman, R.E. Tarjan, Fibonacci heaps and their use in improved network opti-
mization algorithms, Journal of the ACM, vol. 34, pp. 596-615, 1987.

201
[55] H. N. Gabow, R. E. Tarjan, A linear-time algorithm for a special case of disjoint set
union, Proceedings of the 15th ACM Symposium on Theory of Computing (STOC), pp.
246-251, 1983.

[56] H. N. Gabow, Path-based depth-first search for strong and biconnected components, In-
formation Processing Letters, pp. 107-114, 2000.

[57] D. Gale, L. S. Shapley, College Admissions and the Stability of Marriage, American
Mathematical Monthly, vol. 69, pp. 9–14, 1962.
5
[58] Z. Galil, An O(V 3 E f rac23 ) algorithm for the maximal flow problem, Acta Informatica,
vol. 14, pp. 221–242, 1980.

[59] Z. Galil, A. Naamad, An O(EV (log V )2 ) algorithm for the maximal flow problem, Jour-
nal of Computer and System Sciences, vol. 21(2), pp. 203–217, 1980.

[60] C. Giumale, Introducere ı̂n analiza algoritmilor, Editura Polirom, Iaşi, 2004.

[61] A. V. Goldberg, A new max-flow algorithm, Technical Report MIT/LCS/TM-291, Lab-


oratory for Computer Science, MIT, 1985.

[62] A. V. Goldberg, R. E. Tarjan, A new approach to the maximum flow problem, in Pro-
ceedings of the 18th ACM Symposium on Theory of Computing, ACM, pp. 136–146,
1986.

[63] A. V. Goldberg, É. Tardos, R. E. Tarjan, Network flow algorithms, Algorithms and
Combinatorics, vol. 9. in B. Korte, L. Lovsz, H. J. Prmel, A. Schrijver, Editors, Paths,
Flows, and VLSI-Layout, Springer-Verlag, Berlin, pp.101-164, 1990.

[64] R. L. Graham, D. E. Knuth, O. Patashnik, Concrete Mathematics: A Foundation for


Computer Science, 2nd Edition, Addison-Wesley, 1994.

[65] D. Gries, The Science of Programming, Springer Verlag, Heidelberg, New–York, 1981.

[66] L. Guo, F. Shao, C. Botev, J. Shanmugasundaram, XRANK: ranked keyword search over
XML documents, in Proceedings of the 2003 ACM SIGMOD International Conference
on Management of Data, pp. 16–27, 2003.

[67] D. Gusfield, Algorithms on Strings, Trees, and Sequences, Cambridge University Press,
1997.

[68] G. H. Hardy, E. M. Wright, An Introduction to the Theory of Numbers, 5th Edition,


Oxford University Press, 1979.

[69] D. Harel, R. E. Tarjan, Fast algorithms for finding nearest common ancestors, SIAM
Journal on Computing, vol. 13(2), pp. 338-355, 1984.

[70] T. E. Harris, F. S. Ross, Fundamentals of a Method for Evaluating Rail Net Capacities,
Research Memorandum RM-1573, The RAND Corporation, Santa Monica, California,
1955.

[71] I. N. Herstein, I. Kaplansky, Matters mathematical, 2nd Edition, Chelsea Publishing


Company, 1978.

202
5
[72] J. E. Hopcroft, R. Karp, An n 2 algorithm for maximum matchings in bipartite graphs,
SIAM Journal on Computing, vol. 2(4), pp.225-231, 1973.

[73] V. Hristidis , N. Koudas , Y. Papakonstantinou, D. Srivastava, Keyword Proximity


Search in XML Trees, IEEE Transactions on Knowledge and Data Engineering, vol.
18(4), pp. 525–539, 2006.

[74] D. A. Huffman, A Method for the Construction of Minimum-Redundancy Codes, Pro-


ceedings of the I.R.E., 1952, pp. 1098-1102.

[75] C. Ionescu, I. Zsako, Structuri arborescente, Editura Tehnică, Bucureşti, 1990.

[76] V. Jarnı́k, O jistém problému minimálnı́m (About a certain minimal problem), Práce
Moravské Prı́rodovedecké Spolecnosti, 6, pp. 57–63, 1930.

[77] D. Jungnickel, Graphs, Networks and Algorithms, Algorithms and Computation in Math-
ematics, vol. 5, 3rd Edition, Springer, 2008.

[78] A. B. Kahn, Topological sorting of large networks, Communications of the ACM, vol.
5(11), pp. 558-562, 1962.

[79] D. Karger, P. Klein, R. Tarjan, A randomized linear-time algorithm to find minimum


spanning trees, Journal of ACM, 42, pp. 321328, 1995.

[80] A. V. Karzanov, Determining the maximum flow in the network by the method of pre-
flows, Doklady Akademii nauk SSSR 15, pp. 434-437, 1974.

[81] B. W. Kernigham, D. M. Ritchie, The C Programming Language, 2nd Edition, Prentice


Hall, 1988.

[82] D. C. Kozen, The Design and Analysis of Algorithms. Texts and Monographs in Com-
puter Science, Springer, 1993.

[83] D. Kőnig, Theorie der endlichen und unendlichen Graphen, Leipzig: Akademische Ver-
lagsgesellschaft, 1936. Translated from German by Richard McCoart, Theory of finite
and infinite graphs, Birkhäuser, 1990.

[84] J. Kleinberg, E. Tardos, Algorithm Design, Addison-Wesley, 2005.

[85] D. E. Knuth, Arta programării calculatoarelor, vol. 1 Algoritmi fundamentali, Teora,


Bucureşti, 1999.

[86] D. E. Knuth, Arta programării calculatoarelor, vol. 2 Algoritmi seminumerici, Teora,


Bucureşti, 2000.

[87] D. E. Knuth, Arta programării calculatoarelor, vol. 3 Sortare şi Căutare, Teora, Bu-
cureşti, 2001.

[88] J. B. Kruskal, On the shortest spanning subtree of a graph and the traveling salesman
problem, Proc. of the American Mathematical Society, 7, pp. 48-50, 1956.

[89] H. W. Kuhn, On combinatorial properties of matrices, Logistics Papers (George Wash-


ington University), vol. 11, pp. 1-11, 1955.

203
[90] H. W. Kuhn, The Hungarian Method for the assignment problem, Naval Research Lo-
gistics Quarterly, vol. 2, pp. 83-97, 1955.

[91] H. W. Kuhn, Variants of the Hungarian method for assignment problems, Naval Research
Logistics Quarterly, vol. 3, pp. 253-258, 1956.

[92] Y. Li , C. Yu , H. V. Jagadish, Schema-free XQuery, in Proceedings of the Thirtieth


International Conference on Very Large Databases (VLDB), pp. 72–83, 2004.

[93] L. Livovschi, H. Georgescu, Analiza şi sinteza algoritmilor, Ed. Ştiinţifică şi Enciclope-
dică, Bucureşti, 1986.

[94] V. M. Malhotra, M. P. Kumar, S. N. Maheshwari, An O(|V |3 ) algorithm for finding


maximum flows in networks, Information Processing Letters, 7(6), pp.277–278, 1978.

[95] K. Mehlhorn, Data Structures and Algorithms: Graph Algorithms and NP-Completeness,
Springer Verlag, 1984.
p
[96] S. Micali, V. V. Vazirani, An O( |V | · |E|) algorithm for finding maximum matching
in general graphs, Proc. 21st IEEE Symp. Foundations of Computer Science, pp. 17-27,
1980.

[97] V. Mitrana, Provocarea algoritmilor, Editura Agni, Bucureşti, 1994.

[98] E. F. Moore, The shortest path through a maze, in Proceedings of International Sympo-
sium on the Theory of Switching, Part II, pp. 285-292, 1959. (prezentat la simpozion la
Universitatea Harvard in aprilie 1957)

[99] R. Motwani, Average-case analysis of algorithms for matchings and related problems,
Journal of the ACM, vol. 41(6), pp. 1329-1356, 1994.

[100] J. Munkres, Algorithms for the Assignment and Transportation Problems, Journal of
the Society for Industrial and Applied Mathematics, vol. 5(1), pp. 32-38, 1957.

[101] G. Nivasch, Cycle detection using a stack, Information Processing Letters, vol. 90(3),
pp. 135–140, 2004.

[102] I. Odăgescu, F. Furtună, Metode şi tehnici de programare, Computer Libris Agora,
Cluj–Napoca, 1998.

[103] S. Pettie, A faster all-pairs shortest path algorithm for real-weighted sparse graphs, in
Proceedings of 29th International Colloquium on Automata, Languages, and Program-
ming (ICALP02), LNCS Vol. 2380, pp. 85-97, 2002.

[104] S. Pettie, V. Ramachandran, Computing shortest paths with comparisons and additions,
in Proceedings of the 13th Annual ACM-SIAM Symposium on Discrete Algorithms
(SODA02), SIAM, pp. 267-276, 2002.

[105] S. Pettie, V. Ramachandran, An optimal minimum spanning tree algorithm, Journal of


ACM, 49:1634, 2002.

[106] R. C. Prim, Shortest connection networks and some generalizations, Bell System Tech-
nical Journal, 36, pp. 1389-1401, 1957.

204
[107] B. Schieber, U. Vishkin, On finding lowest common ancestors: Simplification and par-
allelization, SIAM J. Comput., vol. 17, pp. 1253-1262, 1988.

[108] A. Schrijver, On the history of the transportation and maximum flowproblems, Mathe-
matical Programming, vol. 91, issue 3, pp. 437-445, 2002.

[109] R. Seidel, C. Aragon, Randomized Search Trees, Algorithmica, vol. 16, pp. 464–497,
1996.

[110] M. Sharir, A strong-connectivity algorithm and its applications in data fow analysis,
Computers and Mathematics with Applications, vol. 7(1), pp. 67–72, 1981.

[111] Y. Shiloach, U. Vishkin, An O(n2 log n) parallel max-flow algorithm, Journal of Algo-
rithms, vol. 3(2), pp. 128-146, 1982.

[112] S. Skiena, The Algorithm Design Manual, 2nd Edition, Springer, 2008.

[113] D. D. Sleator, An O(EV log V ) algorithm for maximum network flow, Technical Report,
STAN-CS-80-831, 1980.

[114] D. D. Sleator, R. E. Tarjan, A data structure for dynamic trees, Journal of Computer
Sciences, vol. 26, pp. 362–391, 1983.

[115] J. Stasko, J. Vitter, Pairing heaps: Experiments and analysis, Communications of the
ACM, vol. 30(3), pp. 234-249, 1987.

[116] T. Takaoka, Theory of 2-3 heaps, Discrete Applied Mathematics, Vol. 126(1), 5th An-
nual International Computing and Combinatories Conference (COCOON’99), pp. 115–
128, 2003.

[117] R. Tarjan, Depth–first search and linear graph algorithms, SIAM Journal on Computing,
vol. 1(2), pp. 146–160, 1972.

[118] R. E. Tarjan, Edge-disjoint spanning trees and depth-first search, Algorithmica, vol.
6(2), pp. 171-185, 1976.

[119] R. E. Tarjan, Applications of path compression on balanced trees, Journal of the ACM,
vol. 26(4), pp. 690-715, 1979.

[120] R. E. Tarjan, A simple version of Karzanov’s blocking flow algorithm, Operation Re-
search Letters, vol. 2, pp.265–268, 1984.

[121] I. Tomescu, Grafuri şi programare liniară, Ed. Didactică şi Pedagogică, Bucureşti, 1975.

[122] I. Tomescu, Probleme de combinatorică şi teoria grafurilor, Ed. Didactică şi Pedagogică,
Bucureşti, 1981.

[123] I. Tutunea, Algoritmi, logică şi programare, Reprografia Universităţii din Craiova, 1993.

[124] I. Tutunea, S. Pescăruş, Algoritmi şi programe Pascal. (Culegere de probleme), Repro-
grafia Universităţii din Craiova, 1994.

[125] J. Vuillemin, A data structure for manipulating priority queues, Communications of


the ACM, vol. 21:4, pp. 309–315, 1978.

205
[126] J. Vuillemin, A unifying look at data structures, Communications of the ACM, vol.
23:4, pp. 229-239, 1980.

[127] G. A. Waissi, A new Karzanov-type O(n3 ) max-flow algorithm, Mathematical and Com-
puter Modelling, vol. 16(2), pp.65–72, 1992.

[128] H. S. Wilf, Algorithms and Complexity, Internet edition, 1996.

[129] J. W. J. Williams, Algorithm 232 - Heapsort, Communications of the ACM, vol. 7(6),
pp. 347-348, 1964.

[130] Y. Xu, Y. Papakonstantinou, Efficient keyword search for smallest LCAs in XML
databases, in Proceedings of the 2005 ACM SIGMOD International Conference on Man-
agement of Data, pp. 527–538, 2005.

[131] D. Zaharie, Introducere ı̂n proiectarea şi analiza algoritmilor, Eubeea, Timişoara, 2008.

206

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