Documente Academic
Documente Profesional
Documente Cultură
5 Heap-uri 97
5.1 Heap-uri binare (Min-heapuri sau Max -heapuri) . . . . . . . . . . . . . . . . . 98
5.1.1 Inserarea unui element . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
1
5.1.2 Ştergerea elementului minim . . . . . . . . . . . . . . . . . . . . . . . . 99
5.1.3 Crearea unui heap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
5.1.4 Ordonare folosing heap-uri - HeapSort . . . . . . . . . . . . . . . . . . 104
5.2 Exerciţii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
2
Capitolul 1
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ă.
1. claritate - la fiecare pas trebuie să specifice operaţia pe care urmează să o efectueze
algoritmul asupra datelor de intrare;
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;
• 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.
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 realizată
niciodată. Acest comportament este opus celui corespunzător structurii repetitive condiţio-
nată 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ţi-
tului (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:
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).
Î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).
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}
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|
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:
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).
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.
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.
Definiţia 1.3 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.4 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 → ∞).
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)).
12
Definiţia 1.6 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 }.
Teorema 1.10 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.10 O funcţie f are o creştere exponenţială dacă ∃c > 1 a.ı̂. 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.
Teorema 1.13
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)).
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
13
• Pentru a, b > 1, a, b ∈ R∗ avem loga n ∈ Θ(logb n).
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 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
2: A(i)
3: end for
14
4: i←i+1
5: end while
Exemplul 1.16 Să considerăm un alt exemplu: se dă un şir A cu P n numere reale, şi se
doreşte calcularea elementelor unui şir B astfel ı̂ncât să avem bi = 1i · ij=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
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 ?? 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 eva-
luare a blocurilor principale, cum ar fi instrucţiunile de ciclare, atribuindu-le direct valori
corespunzătoare complexităţii timp, atunci când este posibil:
15
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:
În urma analizei de complexitate, putem concluziona faptul că cea de-a doua variantă a
algoritmului rulează ı̂ntr-un timp liniar.
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 ).
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.
16
√
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 final se obţine faptul că T (n) = O(log n log log n).
17
1.2.4 Teorema master
Metoda master pune la dispoziţie o variantă de rezolvare a unor recurenţe de forma
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.17 (Teorema master) [29] Fie a ≥ 1 şi b > 1 două constante, f (n) o funcţie
asimptotic pozitivă, şi T (n) definită de relaţia de recurenţă
n
T (n) = aT ( ) + f (n),
b
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.18 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);
18
• 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ă.
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
Algoritmul 7 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 8).
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.
19
• peek():object – ı̂ntoarce valoarea obiectului din vârful stivei fără a-l extrage.
1.3 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.
20
(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. Andrei are N beţişoare de lungimi nu neapărat diferite. El vrea să afle ı̂n câte moduri
poate alege trei beţişoare astfel ı̂ncât să poată forma cu ele un triunghi.
Dându-se lungimile beţişoarelor aflaţi ı̂n câte moduri se pot alege trei dintre ele astfel
ı̂ncât să se poată forma un triunghi cu ele.
Date de intrare
Pe prima linie a fişierului nrtri.in se află N, numărul de beţişoare. Pe următoarea
linie se află N numere separate prin spaţii ce reprezintă lungimile beţişoarelor.
Date de ieşire
Fişierul nrtri.out conţine un singur număr ce reprezintă numărul cerut de problemă.
1 ≤ N ≤ 2.000, 1 ≤ lungimea unui beţisor ≤ 30.000.
Se consideră triunghiuri doar cele care au lungimea fiecărei laturi mai mică decât suma
celorlalte două (nu se iau ı̂n considerare triunghiurile degenerate).
Exemplu
nrtri.in nrtri.out
4
1
2 3 7 4
(http://varena.ro/problema/nrtri)
21
Capitolul 2
Definiţia 2.1 Se numeşte graf o pereche ordonată G = (V, E), unde V este o mulţime finită
de noduri şi E este o mulţime finită de muchii.
Elementele xi ∈ V = V (G) se numesc noduri sau vârfuri. Elementele mulţimii E =
E(G) sunt arce sau muchii. O muchie (xk , xl ) ∈ E se mai notează şi [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 este finit dacă mulţimile V şi E sunt finite. Un graf G se numeşte simplu dacă
oricare două vârfuri ale sale sunt extremităţi pentru cel mult o muchie şi nu există bucle.
Se numeşte multigraf un graf simplu G ı̂n care putem avea şi muchii paralele.
22
Un pseudograf este un multigraf ı̂n care putem avea şi bucle.
În continuare vom considera un graf neorientat, simplu şi finit.
Definiţia 2.2 Fie G = (V, E) un graf neorientat, simplu şi finit şi e = (x, y) ∈ E o muchie
a lui G. Atunci spunem că
1. muchia e conectează nodurile x şi y;
Mulţimea
NG (x) = {y|y ∈ V şi (x, y) ∈ E}
se numeşte vecinătatea nodului x.
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 finită.
Definiţia 2.3 Gradul unui vârf este egal cu numărul muchiilor incidente cu vârful x şi se
notează cu dG (x) (dG (x) = |NG (x)|). Un vârf cu gradul 0 (dG (x) = 0) se numeşte vârf
izolat.
Definiţia 2.4 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).
b) K5 este graful complet cu 5 vârfuri. Vârfurile 3 şi 5 sunt adiacente (a se vedea figura
2.1 b)).
Definiţia 2.5 Un graf parţial al unui graf dat G = (V, E) este un graf G1 = (V, E1 )
unde E1 ⊂ E.
Definiţia 2.6 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:
23
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).
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.7 Un graf neorientat, simplu şi finit G = (V, E) se numeşte p-regulat dacă
toate vârfurile au gradul p (∀u ∈ V , dG (v) = p).
Un graf se numeşte regulat dacă ∃p ∈ N astfel ı̂ncât G să fie p-regulat.
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).
24
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.
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.
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).
Un lanţ simplu este un lanţ ce conţine numai muchii distincte. Lanţul care nu este
format numai din muchii distincte se numeşte lanţ compus.
Definiţia 2.11 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.12 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.
Teorema 2.8 Fie G = (V, E) un graf şi m = |E|. Dacă ∀u ∈ V , dG (u) ≥ 2m, atunci G
admite cicluri.
Teorema 2.9 Fie G = (V, E) un graf astfel ı̂ncât |V | ≥ 3. Dacă |E| ≥ |V | atunci G conţine
cicluri.
Teorema 2.10 Fie G = (V, E) un graf. Dacă ı̂ntre două vârfuri distincte u şi v (u, v ∈ V ,
u 6= v) există două drumuri diferite atunci G conţine cicluri.
25
Definiţia 2.13 Un graf planar este un graf ce poate fi reprezentat ı̂ntr-un plan astfel ı̂ncât
fiecărui nod să ı̂i corespundă un punct din plan şi fiecărei muchii să ı̂i corespundă o curbă
simplă care uneşte punctele corespunzătoare extremităţilor iar aceste curbe să se intersecteze
cel mult la capete.
Definiţia 2.14 Un graf etichetat este un graf ı̂n care fiecare muchie şi vârf poate avea
asociată o etichetă.
Definiţia 2.15 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 ).
Propoziţia 2.11 Un graf este bipartit dacă şi numai dacă nu conţine cicluri de lungime
impară.
Exemplul 2.12 În figura 2.4 a) este ilustrat un graf bipartit (V1 = {1, 2, 3}, V2 = {4, 5}),
iar ı̂n cazul b) avem un graf bipartit complet, K2,3 .
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;
26
• 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.
Observaţia 2.13 Pentru un graf neorientat această matrice este simetrică (ai,j = aj,i).
Exemplul 2.14 Matricea de adiacenţă A corespunzătoare grafului din figura 2.5 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
27
Fig. 2.6: Un exemplu de graf neorientat ponderat
Exemplul 2.15 Matricea costurilor corespunzătoare grafului din figura 2.6 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
28
Observaţia 2.16 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.
4. 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.18 Listele de adiacenţă pentru graful din figura 2.5 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 .
29
Exemplul 2.19 Pentru exemplul din figura 2.5 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
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
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.7):
typedef struct nod {
int nodeIndex;
struct nod *next;
}NOD;
• 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
30
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).
5. 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.
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.20 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:
31
Fig. 2.8: Arbore de acoperire ı̂n lăţime
Pentru graful considerat ı̂n figura 2.5 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.8 este reprezentat arborele de acoperire ı̂n lăţime rezultat ı̂n urma parcurgerii grafului
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.
32
Implementarea ı̂n limbajul C a algoritmului 9 este următoarea:
Listing 2.1: parcurgerebf.c
#include <stdio.h>
#include <mem.h>
/**
* Initializarea cozii circulare.
*/
void initQueue(void) {
vida = TRUE;
first = 0;
last = MAXN-1;
}
/**
* Intoarce pentru pozitia k, urmatoarea pozitie din coada.
*/
int next(int k) {
return (k + 1) % MAXN;
}
/**
* Insereaza elementul a carui valoare este pastrata in variabila v in coada.
*/
void enQueue(int v) {
last = next(last);
coada[last] = v;
if (vida == TRUE) {
vida = FALSE;
}
}
/**
* Extrage un element din coada.
*/
int deQueue(void) {
int v = coada[first];
first = next(first);
if (first == next(last)) {
vida = TRUE;
33
}
return v;
}
/**
* Parcurge in latime graful pornind de la nodul de start k.
*/
void bfs(int k) {
int i;
char vizitat[MAXN];
/**
* Se citesc numarul de noduri precum si matricea de adiacenta.
*/
void readInput(void) {
int i, j;
int main() {
readInput();
initQueue();
bfs(0);
return 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.
34
Deoarece matricea vecin este simetrică, pentru a reduce numărul de citiri, se vor
solicita numai valorile aflate deasupra diagonalei principale.
• 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.
• Structura de date abstractă de tip coadă este implementată static sub forma unei cozi
circulare.
• 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.
• 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.
În figura 2.9 poate fi urmărit arborele de acoperire pentru parcurgerea D a grafului G
prezentat ı̂n figura 2.5.
Metoda de parcurgere D este o combinaţie ı̂ntre metoda de parcurgere ı̂n lăţime (a se
vedea secţiunea 2.3.1) şi metoda de parcurgere ı̂n adâncime (a se vedea secţiunea 2.3.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 10).
1
http://msdn.microsoft.com/en-us/library/1fdeehz6.aspx
2
http://en.wikibooks.org/wiki/C_Programming/Strings#The_memset_function
35
Fig. 2.9: Arbore de acoperire pentru parcurgerea D
• 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
36
care a fost vizitat nodul curent), şi se continuă algoritmul, cu următorul vecin ı̂ncă
nevizitat al nodului curent.
Pentru graful considerat (a se vedea figura 2.5), ı̂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.10). 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:
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).
• muchiile [1, 2], [2, 5], [3, 5], [4, 6], [5, 7], [6, 7], [6, 8] sunt muchii ale arborelui de acoperire;
Implementarea algoritmului se poate realiza fie ı̂n variantă recursivă (a se vedea algoritmul
11), fie ı̂n variantă nerecursivă (a se vedea algoritmul 12). Î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 11) ı̂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 =
37
Algoritm 11 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
1)) şi se reia secvenţa de operaţii pentru nodul i cu această proprietate, printr-un apel recursiv
(call DFS(i)) (linia 6).
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ţă.
Algoritmul 12 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 12.13-12.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 11 ı̂n limbajul C:
Listing 2.2: dfsrec.c
#include <stdio.h>
#include <mem.h>
/**
* Se citesc numarul de noduri precum si matricea de adiacenta.
*/
void readInput(void) {
38
Algoritm 12 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
int i, j;
/**
* Parcurgerea in adancime.
*/
void dfs(int k) {
int i;
39
vizitat[k] = 1;
printf("%d ",k);
for (i = 0; i < n; i++) {
if ((vizitat[i] == 0) && (vecin[k][i] == 1)) {
dfs(i);
}
}
}
int main() {
readInput();
dfs(nodStart);
return 0;
}
40
astfel ı̂ncât fiecare graf Gi = (Vi , Ei ), ∀i = 1, m, este conex (Ei = E|Vi ×Vi ).
Un graf G = (V, E) se poate scrie ca o reuniune disjunctă a componentelor sale conexe.
Notăm cu p(G) numărul componentelor conexe ale grafului G.
Exemplul 2.24 Graful din figura 2.11 are două componente conexe: {1, 2, 3, 4, 5, 6} şi {7, 8, 9}.
Muchia [2, 5] este muchie critică.
( 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 13 prezintă
descrierea formalizată ı̂n pseudo-cod):
41
Pas 1. La ı̂nceput, toate nodurile sunt marcate ca nevizitate.
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.
Prin urmare, 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 14).
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 14 ı̂n limbajul C este următoarea:
Listing 2.3: muchiecriticav1.c
#include <stdio.h>
#include <mem.h>
/**
* Se citesc numarul de noduri precum si matricea de adiacenta.
*/
void readInput(void) {
42
Algoritm 14 Algoritm de determinare a muchiilor critice (prima variantă)
1: procedure MuchiiCriticeI(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
int i, j;
/**
* Parcurgerea in adancime a grafului pornind din nodul de start k.
*/
void dfs(int k) {
int i;
vizitat[k] = TRUE;
printf("%d ", k);
for (i = 0; i < n; i++) {
if ((vizitat[i] == FALSE) && (vecin[k][i] == 1)) {
dfs(i);
}
}
43
}
/**
* Functia verifica daca graful este conex.
*/
int conex(void) {
int i;
memset(vizitat, 0, sizeof(vizitat));
dfs(0);
for (i = 0; i < n; i++) {
if (vizitat[i] == FALSE) {
return FALSE;
}
}
return TRUE;
}
int main() {
int i, j;
readInput();
for (i = 1; i < n; i++) {
for (j = 0; j < i; j++) {
if (vecin[i][j] == TRUE) {
vecin[i][j] = vecin[j][i] = 0;
if (conex() == FALSE) {
printf("Muchia (%d,%d) este critica! \n", i, j);
}
vecin[i][j] = vecin[j][i] = 1;
}
}
}
return 0;
}
Observaţia 2.25 Am presupus că numerotarea nodurilor se face de la 0 şi că există un nod
cu eticheta 0.
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.26 O muchie nu este critică dacă ea face parte din cel puţin un ciclu elemen-
tar al grafului respectiv.
44
Î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 [74], [123]:
prenumu
lowu = min prenumx , dacă [u, x] este muchie de ı̂ntoarcere (2.7)
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 15). 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).
( MuchiiCriticeII(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 j ← 1, n do
13: if (vecink,j = 1) then
14: if (vizitatj = 0) then
15: tataj ← k
16: call DF S critic(j, n, V ecin)
17: lowk ← M in(lowk , lowj )
18: if (prenumk < lowj ) then
19: Output {’Muchia (k,j) este critica’}
20: end if
21: else
22: if (tatak 6= j) then
23: lowk ← M in(lowk , prenumj )
24: end if
25: end if
26: end if
27: end for
28: end procedure
45
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 j este vecin cu nodul curent k, şi nu a fost ı̂ncă vizitat,
valoarea lui lowk se calculează astfel: lowk ← min {lowk , lowj }. Dacă [k, j] reprezintă o
muchie de ı̂ntoarcere (nodul j este vecin cu nodul curent k, nodul j a fost vizitat, nodul j
nu este tatăl nodului k), atunci valoarea lowk se calculează după formula
void readInput(void) {
int i, j;
void dfs(int k) {
int j;
vizitat[k] = 1;
counter++;
low[k] = prenum[k] = counter;
for (j = 0; j < n; j++) {
if (vecin[k][j] == 1) {
if (vizitat[j] == 0) {
46
tata[j] = k;
dfs(j);
low[k] = min(low[k], low[j]);
if (prenum[k] < low[j]) {
printf("%d -> %d \n", k, j);
}
} else {
if (tata[k] != j) {
low[k] = min(low[k], prenum[j]);
}
}
}
}
/*printf("prenum[%d] = %d low[%d] = %d\n", k, prenum[k], k, low[k]);*/
}
void critic(void) {
memset(vizitat, 0, sizeof(vizitat));
counter = 0;
dfs(0);
}
int main(void) {
printf("\n");
readInput();
critic();
return 0;
}
Exemplul 2.27 Pentru graful din figura 2.5, valorile prenum şi low sunt cele din figura 2.12
(valoarea din partea dreaptă a unui vârf reprezintă valoarea sa la numerotarea ı̂n preordine,
prenum, iar numărul din stânga reprezintă valoarea calculată low). Condiţia prenumu <
47
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ţă.
Fig. 2.13: a) Nodul 3 este punct de articulaţie. b) Nodurile 2 şi 3 sunt puncte de articulaţie. c) Graf fără
puncte de articulaţie.
48
Un alt algoritm se bazează pe metoda de parcurgere a grafurilor ı̂n adâncime (a se vedea
algoritmul 17). Algoritmul a fost realizat de către R. E. Tarjan [74], [123]. Reamintim că ı̂n
cazul parcurgerii DFS, muchiile grafului iniţial G se ı̂mpart ı̂n:
• muchii de ı̂ntoarcere.
1. nodul u este rădăcina arborelui de acoperire ı̂n adâncime şi acesta are cel puţin doi
copii;
2. nodul u are un descendent v ∈ V astfel ı̂ncât nici unul din descendenţii nodului v să
nu constituie extremitate unei muchii de ı̂ntoarcere către unul din strămoşii nodului u
din arborele de acoperire ı̂n adâncime (∃v copil al nodului u astfel ı̂ncât ∀w descendent
al nodului v, w ∈ V , ∄ [w, x] muchie de ı̂ntoarcere unde x este un strămoş al nodului u
ı̂n arborele de acoperire ı̂n adâncime - a se vedea figura 2.14).
Fig. 2.14: Ce condiţie trebuie să ı̂ndeplinească un nod u ca să nu fie punct de articulaţie?
Prima condiţie se poate verifica cu ajutorul vectorului tata (tatau reprezintă părintele
nodului u ı̂n arborele de acoperire ı̂n adâncime) şi păstrând un contor cu numărul de copii
ai fiecărui nod.
A doua condiţie se poate verifica păstrând pentru fiecare nod u două valori: prenumu ce
reprezintă momentul ı̂n care este ı̂ntâlnit nodul u pentru prima dată şi lowu ce este definită
astfel:
prenumu
lowu = min prenumx , dacă [u, x] este muchie de ı̂ntoarcere (2.8)
lowy , ∀y descendent direct al lui u.
Se observă că definiţia lowu este similară cu cea ı̂ntâlnită ı̂n a doua metodă de determinare
a muchiilor critice dintr-un graf. Astfel dacă pentru un nod u şi un copil al acestuia v este
ı̂ndeplinită condiţia (tatav = u) ∧ (prenumu ≤ lowv ), atunci u este punct de articulaţie ı̂n
graful G.
49
Algoritm 17 Algoritm de determinare a punctelor de articulaţie (a doua variantă)
2.7 Exerciţii
1. Un graf neorientat cu n noduri, G = (V, E) se numeşte graf scorpion dacă posedă trei
noduri speciale:
50
Fig. 2.15: Un exemplu de graf neorientat cu 8 vârfuri
3. Se dă un graf neorientat G = (V, E). Să se elaboreze un algoritm ce verifică dacă ı̂n
graful G există cel puţin un ciclu.
4. Se dă un graf neorientat G = (V, E). Să se elaboreze un algoritm ce verifică dacă ı̂n
graful G există cel puţin un ciclu de lungime impară.
5. Se dă un graf neorientat G = (V, E). Să se elaboreze un algoritm ce determină numărul
de triunghiuri formate de nodurile grafului (numărul de cicluri având lungimea egală
cu valoarea 3).
7. Fiind dat un graf neorientat G = (V, E), să se elaboreze un algoritm eficient ce deter-
mină dacă graful G este bipartit.
9. Se dă o matrice A având n linii şi m coloane, având ca valori numerele 0 sau 1 (A ∈
Mn×m ({0, 1})). Să se elaboreze un algoritm ce determină numărul de insule din cadrul
matricei (o insulă este formată dintr-un un grup de elemente vecine având valoarea 1,
elementele fiind vecine pe verticală sau orizontală).
51
Capitolul 3
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
52
Fig. 3.2: Două figuri geometrice ce pot fi desenate folosind o singură linie
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.
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ă.
53
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 18).
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.
54
În algoritmul 19 este prezentată funcţia EulerRec() pentru determinarea unui ciclu eu-
lerian.
Procedura FindCycle(G) (a se vedea algoritmul 19) 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 .
Fig. 3.4: Grafurile parţiale obţinute ı̂n urma primelor două etape ale algoritmului 19
Exemplul 3.5 Să aplicăm algoritmul 19 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
55
Fig. 3.5: Grafurile parţiale obţinute ı̂n urma etapelor 3 şi 4 ale algoritmului 19
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]).
Exemplul 3.6 Să aplicăm algoritmul 20 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:
La pasul următor, se extrage valoarea 9 de pe stivă şi astfel u devine 9. La sfârşitul acestuia
56
Algoritm 20 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:
În programul următor se prezintă o variantă de implementare ı̂n C++ a algoritmului 20:
Listing 3.1: rosenstiehl.cpp
#include <vector>
#include <stack>
#include <list>
#include <iostream>
#include <fstream>
#include <iomanip>
#include <conio.h>
57
fin >> n;
ma = Matrice(n, Linie(n, 0));
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
fin >> ma[i][j];
}
}
fin.close();
return n;
}
void print(list<int>& l) {
list<int>::iterator it;
s.push(u);
while (!s.empty()) {
u = s.top();
s.pop();
v = 0;
while (v < n) {
if (ma[u][v] == 1) {
ma[u][v] = ma[v][u] = 0;
s.push(u);
u = v;
v = 0;
} else {
v++;
}
}
l.push_back(u);
}
}
int main() {
Matrice ma;
list<int> l = list<int>();
int n = readInput(ma);
58
return 0;
}
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
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.
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 - clasa template stack 4 . Am folosit o stivă de numere ı̂ntregi: stack<int> s;.
• lista - clasa template 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>();.
59
După cum se poate observa din programul anterior, pentru reprezentarea internă a
grafului s-a folosit matricea de adiacenţă.
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].
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.7 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.
7
http://www.sgi.com/tech/stl/Iterators.html
60
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.
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.
61
• Pentru graful din figura 3.7 avem:
Nod 1 2 3 4 5 6 7 8
dG 3 4 3 4 4 5 3 4
62
Fig. 3.7: Exemplu de graf pentru problema comis-voiajorului
Exemplul 3.13 Matricea costurilor corespunzătoare grafului din figura 3.7 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.8: Rezultatul rulării programului pentru determinarea ciclului hamiltonian optim
63
Algoritm 21 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
64
#define LIMIT 100000 // lungimea maxima a unei muchii echivalenta cu infinit
/**
* Functia citeste valorile datelor de intrare.
*/
int readInput(Matrice a) {
int n;
int i, j, max = 0;
scanf("%d", &n);
for (i = 1; i <= n; i++) {
for (j = 1; j <= n; j++) {
scanf("%d", &a[i][j]);
if (max < a[i][j]) {
max = a[i][j];
}
if (a[i][j] < 0) {
a[i][j] = LIMIT;
}
}
}
cost_optim = n * max;
return n;
}
/**
* Functia verifica daca a + b > c.
*/
int mai_mare(long a, long b, long c) {
if (a + b > c) {
return 1;
} else {
return 0;
}
}
/**
* Functia afiseaza solutia calculata a problemei.
*/
void AfisareSolutie(int n) {
int i;
65
printf("%d, ", x_optim[i]);
}
printf("%d]\n", x_optim[n + 1]);
}
/**
* Functia de continuare: aici se verifica daca elementul de pe pozitia k
* nu se mai afla in sirul A.
*/
int CanContinue(int n, Matrice a, int k, long cost, Vector vizitat, Vector x) {
if ((vizitat[x[k]] == 1) || (mai_mare(cost, a[x[k - 1]][x[k]], cost_optim))
|| ((k != n + 1) && (x[k] == 1))) {
return 0;
} else {
return 1;
}
}
/**
* Functia salveaza solutia optima (circuitul hamiltonian de cost minim)
* determinata/gasita pana in momentul curent.
*/
void EvaluareSolutie(int n, long cost, Vector x) {
int i;
cost_optim = cost;
for (i = 1; i <= n + 1; i++) {
x_optim[i] = x[i];
}
AfisareSolutie(n);
}
/**
* Metoda backtracking implementata.
*/
void ComisVoiajorBacktracking(int n, Matrice a) {
Vector x;
Vector vizitat;
int k, gasit;
long cost;
memset(vizitat, 0, sizeof(vizitat));
x[1] = 1;
cost = 0;
k = 2; x[k] = 1;
while (k > 1) {
gasit = 0;
while ((x[k] + 1 <= n) && (gasit == 0)) {
x[k] = x[k] + 1;
gasit = CanContinue(n, a, k, cost, vizitat, x);
}
if (gasit == 1) {
66
if (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;
if (k > 1) {
cost = cost - a[x[k - 1]][x[k]];
}
}
}
}
int main() {
Matrice a;
int n = readInput(a);
ComisVoiajorBacktracking(n, a);
return 0;
}
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:
...
scanf("%d", &a[i][j]);
if (max < a[i][j]) {
max = a[i][j];
}
if (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:
67
int CanContinue(int n, Matrice a, int k, long cost, Vector vizitat, Vector x) {
if ((vizitat[x[k]] == 1) || (mai_mare(cost, a[x[k - 1]][x[k]], cost_optim))
|| ((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 :
int 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 EvaluareSolutie(int n, long cost, Vector x) {
int i;
cost_optim = cost;
for (i = 1; i <= n + 1; i++) {
x_optim[i] = x[i];
}
AfisareSolutie(n);
}
Partea principală a programului, motorul, este constituită din funcţia:
void ComisVoiajorBacktracking(int n, Matrice a) {
Vector x;
Vector vizitat;
int k, gasit;
long cost;
memset(vizitat, 0, sizeof(vizitat));
x[1] = 1;
cost = 0;
k = 2; x[k] = 1;
while (k > 1) {
gasit = 0;
while ((x[k] + 1 <= n) && (gasit == 0)) {
x[k] = x[k] + 1;
gasit = CanContinue(n, a, k, cost, vizitat, x);
}
if (gasit == 1) {
if (k == n + 1) {
8
http://en.wikipedia.org/wiki/Arithmetic_overflow
68
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;
if (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
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.
69
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.
70
Fig. 3.9: Pasii efectuati de algoritm pentru determinarea ciclului hamiltonian asociat grafului din figura 3.7
71
Fig. 3.10: Pasii efectuati de algoritm pentru determinarea ciclului hamiltonian asociat grafului din figura 3.7
(continuare)
72
3.3 Exerciţii
1. Să se determine un ciclu hamiltonian, dacă există, ı̂n grafurile din figura 3.11.
2. Să se verifice dacă există şi, ı̂n caz afirmativ, să se pună ı̂n evidenţă un ciclu eulerian
ı̂n grafurile din figura 3.12.
73
3. Se dă un graf neorientat conex G = (V, E), n = |V |, m = |E|. Să se elaboreze un
algoritm ce determină numărul de cicluri de lungime k.
4. Se dă un graf orientat G = (V, E). Ce condiţii trebuie să fie ı̂ndeplinite astfel ı̂ncât
graful G să fie eulerian?
74
Capitolul 4
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:
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 poate constitui 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 simplu G = (V, E) neorientat,
conex şi aciclic.
Teorema 4.1 (Proprietăţile arborilor liberi) Fie G = (V, E) un graf simplu neorientat.
Următoarele afirmaţii sunt echivalente:
Definiţia 4.3 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ă.
Definiţia 4.4 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.
75
1
2 3
4 5 6 7
8 9 10 11 12 13 14 15
Definiţia 4.5 Adâncimea unui vârf este dată de lungimea lanţului de la rădăcină la acel
vârf. Înălţimea unui vârf se determină ca fiind valoarea celui mai lung lanţ 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 defineşte drept 1+ adâncimea vârfului respectiv. Rădăcina se află pe nivelul 1.
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.
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ă un vârf are un singur
descendent 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.7 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 nod al unui arbore binar plin are doi descendenţi, descendentul stâng
şi descendentul 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ă, nodurile unui arbore binar plin se numerotează ı̂n ordinea nivelurilor, iar
ı̂n cadrul unui nivel, de la stânga la dreapta.
Definiţia 4.8 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.
76
1
2 3
4 5 6 7
8 9 10 11 12
1. expresii cu paranteze;
2. forma standard ;
2 8
3 5 9
4 6 7
1. expresii cu paranteze - expresia ı̂ncepe cu rădăcina arborelui şi, după fiecare vârf k,
urmează expresiile subarborilor ce au ca rădăcini descendenţii vârfului respectiv, sepa-
rate 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))).
77
2. forma standard (stâng-drept) - se folosesc doi vectori identificaţi prin Stang şi Drept.
Se indică rădăcina arborelui, iar pentru fiecare vârf k se precizează descendentul său
stâng şi/sau drept, astfel:
(
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
Luând ca model o listă liniară dublu ı̂nlăţuită, putem folosi o structură de date asemănătoare
pentru reprezentarea unui arbore binar. Un nod al arborelui are următoarea structură:
typedef struct nod {
TipOarecare data;
struct nod* stang;
struct nod* drept;
} Nod;
Nod *rad; - rad este o variabilă de tip pointer la Nod (variabila rad păstrează adresa
unei zone de memorie ce conţine date numerice de tip Nod). rad desemnează adresa
nodului rădăcină al arborelui.
În figura 4.5 avem reprezentarea arborelui din figura 4.4.
3. reprezentarea tip tată - pentru fiecare vârf k se indică tatăl nodului respectiv, astfel:
(
i , dacă nodul i este părintele nodului k
tatak =
0 , dacă nu există părintele nodului k.
78
Fig. 4.5: Exemplu de arbore binar cu 8 noduri
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:
(
⌊ k2 ⌋ , k≥2
T atak =
nu există , k = 1.
( (
2·k , 2·k ≤n 2·k+1 , 2·k+1≤n
Stangk = Dreptk =
nu există , 2 · k > n. nu există , 2 · k + 1 > n.
79
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.
19) şi există şi descendentul său drept. În continuare, se reia algoritmul trecând ı̂n
descendentul drept (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 este necesar să ştim din ce
descendent venim, stâng sau drept, vom utiliza şi vectorul tata.
Observaţia 4.3 Această procedură poate fi utilizată şi pentru celelalte moduri de par-
curgere, modificările necesare referindu-se la momentul când trebuie vizitat nodul curent
(prin apelul procedurii Vizitare - instrucţiunea call Vizitare(k)).
80
Algoritm 22 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 izitare(k) ⊲ prelucrarea informaţiei din nodul vizitat
7: k ← stangk
8: end while
9: call V izitare(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
void readInput(void) {
int k;
81
}
k = rad;
q = 0;
while (q == 0) {
while (stang[k] != 0) {
vizitare(k);
k = stang[k];
}
vizitare(k);
while ((q == 0) && (drept[k] == 0)) {
found = 0;
while ((q == 0) && (found == 0)) {
j = k;
k = tata[k];
if (k == 0) {
q = 1;
} else {
if (j == stang[k]) {
found = 1;
}
}
}
}
if (q == 0) {
k = drept[k];
}
}
}
int main() {
readInput();
preord(rad);
return 0;
}
82
fie stiva devine vidă. Dacă se găseşte un nod ce are şi descendent drept, atunci nodul
curent devine descendentul drept, şi se reia procesul de ı̂naintare ı̂n arbore spre stânga.
Parcurgerea se termină ı̂n momentul ı̂n care stiva S este vidă (a se vedea algoritmul
23).
void readInput(void) {
int k;
83
printf("n = "); scanf("%d", &n);
printf("rad = "); scanf("%d", &rad);
k = rad;
q = 0;
cap = -1;
while (q == 0) {
while (stang[k] > 0) {
stack[++cap] = k;
k = stang[k];
}
vizitare(k);
while ((q == 0) && (drept[k] == 0)) {
if (cap < 0) {
q = 1;
} else {
k = stack[cap--];
vizitare(k);
}
}
if (q == 0) {
k = drept[k];
}
}
}
int main() {
readInput();
inord(rad);
return 0;
}
84
vedea algoritmul 24).
2 8
3 5 9
6 7
• fiecărui operator ı̂i corespunde un nod neterminal având drept subarbori stâng şi drept
cei doi operanzi corespunzători.
85
Fig. 4.8: Arbore asociat expresiei aritmetice 2 · ((a + b) − (c + b))
unde Infk este informaţia asociată vârfului k. Această informaţie este caracterizată 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 asociată
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 anterioară
astfel:
pentru orice vârf k aparţinând arborelui, avem
(
Infj < Infk , pentru toate vârfurile j ce aparţin subarborelui stâng al vârfului k
Infk < Infj , pentru toate vârfurile j ce aparţin subarborelui drept al vârfului k.
86
Crearea unui arbore de căutare
Crearea (construirea) unui arbore de căutare se face aplicând ı̂n mod repetat operaţia de
inserare.
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 (a se vedea algoritmul 25):
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.
• 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, 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.
87
Algoritm 26 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
88
Mai ı̂ntâi se va căuta nodul ce se doreşte să fie eliminat, folosind, de exemplu, subrutina
SearchNode(p, Key).
Să presupunem că am găsit nodul ce urmează a fi şters. Avem următoarele situaţii, tratate
ı̂n algoritmul 28:
1. nodul ce urmează să fie şters este un nod terminal - se efectuează ştergerea având grijă
să se ı̂nlocuiască legătura din nodul său 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 său va conţine acum noua legătură către descendentul fostului fiu;
• se determină cel mai din stânga nod (notat B) din subarborele drept al nodului
ce trebuie şters A (a se vedea algoritmul 27); acesta va fi şters ı̂n mod fizic, dar
la un pas ulterior;
• toate informaţiile legate de datele conţinute ı̂n nodul B vor fi copiate ı̂n nodul A;
89
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.
90
Algoritm 28 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
Exemplul 4.4 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.
Dacă la un moment dat, se doreşte inserarea unui cuvânt ce deja există ı̂n arbore, atunci
această operaţie nu se va realiza.
91
NODE *rad;
if (p != NULL) {
ind = strcmp(p->word, str);
if (ind < 0) {
return Search(p->right, str);
} else {
if (ind > 0) {
return Search(p->left, str);
} else {
return p;
}
}
} else {
return NULL;
}
}
p = (NODE*)malloc(sizeof(NODE));
p->left = p->right = NULL;
p->word = (char*)malloc(sizeof(char) * (strlen(str) + 1));
strcpy(p->word, str);
return p;
}
if (p == NULL) {
p = CreateNODE(str);
} else {
ind = strcmp(p->word, str);
if (ind < 0) {
p->right = Insert(p->right, str);
} else {
if (ind > 0) {
p->left = Insert(p->left, str);
}
}
}
return p;
}
92
if (p != NULL) {
VisitInord(p->left);
printf("[%s] ", p->word);
VisitInord(p->right);
}
}
if (parent->right == curent) {
parent->right = curent->right;
} else {
parent ->left = curent->right;
}
return curent;
}
if (p != NULL) {
ind = strcmp(p->word, str);
if (ind < 0) {
p->right = DeleteNODE(p->right, str);
} else {
if (ind > 0) {
p->left = DeleteNODE(p->left, str);
} else {
if ((p->left == NULL) || (p->right == NULL)) {
if (p->left != NULL) {
tmp = p->left;
} else {
tmp = p->right;
}
FreeNODE(p);
p = tmp;
} else {
tmp = p->right;
tmp = LeftmostNODE(p, tmp);
tmp->left = p->left;
93
tmp->right = p->right;
FreeNODE(p);
p = tmp;
}
}
}
}
return p;
}
int main() {
char word[100];
char ch;
NODE *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("Cuvant: "); scanf("%s", word);
}
switch (ch) {
case ’1’: rad = Insert(rad, word); break;
case ’2’: p = Search(rad, word);
if (!p) {
printf("Cuvantul %s nu a fost gasit!\n", word);
} else {
printf("Cuvantul %s exista in arbore!\n", word);
}
break;
case ’3’: rad = DeleteNODE(rad, word); break;
case ’4’: VisitInord(rad); printf("\n"); break;
case ’0’: exit(1);
}
}
return 0;
}
94
4.3 Exerciţii
1. Fiind dat un arbore binar T , să se elaboreze un algoritm nerecursiv care numără
nodurile frunză ale acestui arbore.
2. Fiind dat un arbore binar T , să se elaboreze un algoritm ce determină nivelul arborelui
având cele mai multe noduri frunză.
3. Fiind dat un arbore binar T , să se elaboreze un algoritm nerecursiv ce determină
ı̂nălţimea acestuia.
4. Fiind dat un arbore binar T , să se elaboreze un algoritm pentru a determina toate
nodurile arborelui T aflate la distanţa k faţă de rădăcina acestuia.
5. Se dă un arbore binar T de rădăcină r. Să se elaboreze un algoritm ce determină pentru
un nod p al arborelui cel mai apropiat nod frunză de acesta.
6. Se dă un arbore binar T de rădăcină r. Să se elaboreze un algoritm ce determină
lăţimea arborelui T . Lăţimea unui nivel este dată de numărul de noduri aflate pe acel
nivel. Lăţimea unui arbore este valoarea cea mai mare dintre lăţimile tuturor nivelelor
arborelui.
7. Fiind dat un arbore binar T având nodul r drept rădăcină, să se elaboreze un algoritm
ce determină un arbore binar T oglindit ce reprezintă oglinditul arborelui iniţial T .
De exemplu, ı̂n figura 4.11 se poate vizualiza rezultatul procesului de oglindire al unui
arbore binar.
8. Se dau două secvenţe de numere ce reprezintă rezultatele parcurgerilor ı̂n preordine şi
inordine al unui arbore binar T . Să se elaboreze un algoritm ce construieşte arborele
binar T pe baza acestor secvenţe.
9. Fiind dat un arbore binar T , să se elaboreze un algoritm pentru a determina diametrul
acestui arbore. Diametrul unui arbore binar T este dat de numărul de noduri pe lanţul
de lungime maximă dintre oricare două noduri ale arborelui.
10. Se dă un arbore binar T de rădăcină r. Să se elaboreze un algoritm ce parcurge/afişează
nodurile frunză ı̂n ordinea apariţiei lor de la stânga la dreapta.
11. Se dă un arbore binar T de rădăcină r. Să se elaboreze un algoritm ce parcurge toate
nodurile de pe frontiera arborelui ı̂n sens trigonometric.
De exemplu, pentru arborele binar din figura 4.12, ”frontiera” sa este dată de următoarea
secvenţă: 1, 2, 4, 8, 9, 10, 11, 12, 7, 3.
95
Fig. 4.12: Frontiera unui arbore binar
12. Se dă un arbore binar T de rădăcină r şi un nod al acestuia p. Să se elaboreze un
algoritm ce determină succesorul nodului p la vizitarea ı̂n inordine.
13. Se dă un arbore binar T de rădăcină r. Să se elaboreze un algoritm ce determină dacă
arborele este echilibrat ı̂n ı̂nălţime.
Un arbore binar de rădăcină r este echilibrat ı̂n ı̂nălţime dacă subarborii stâng şi drept
sunt, fiecare, echilibraţi ı̂n ı̂nălţime iar diferenţa dintre ı̂nălţimea subarborelui stâng şi
ı̂nălţimea subarborelui drept este cel mult 1.
14. Să se elaboreze un algoritm ce construieşte un arbore binar de căutare T fiind dat
rezultatul parcurgerii arborelui T pe nivele.
15. Fiind dat un arbore binar T , să se elaboreze un algoritm ce verifică dacă arborele este
un arbore binar de căutare.
16. Fiind dat un arbore binar de căutare T , să se elaboreze un algoritm ce determină nodul
a cărui cheie are valoarea minimă / maximă din arbore.
17. Fiind dat un arbore binar de căutare T şi o cheie k, să se elaboreze un algoritm ce
determină nodul predecesor şi nodul succesor nodului u a cărui cheie este k, ı̂n cadrul
vizitării ı̂n inordine.
18. Fiind daţi doi arbori binari de căutare T1 şi T2 , să se elaboreze un algoritm ce determină
toate valorile cheilor celor doi arbori T1 şi T2 ı̂n ordine (algoritm de interclasare pentru
arbori binari de căutare).
19. Se dă un arbore binar de căutare T şi două chei k1 şi k2 . Să se elaboreze un algoritm
ce determină toate nodurile ale căror chei se află ı̂n intervalul [k1 , k2 ].
96
Capitolul 5
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 nodul A este părintele nodului B (tataB = A),
avem relaţia cheieA ≥ cheieB (sau cheieA ≤ cheieB ). Fiecare nod X va avea o valoare
asociată numită cheieX . Se observă faptul că ı̂ntotdeauna cheia de valoare maximă/minimă
se va afla ı̂n rădăcina arborelui ı̂n funcţie de condiţia aleasă: ∀A, B, astfel ı̂ncât tataB = A,
avem cheieA ≥ cheieB sau cheieA ≤ cheieB .
Structura de date heap poate fi implementată sub mai multe forme: 2 − 3 heap [122], heap
binar [9], heap binomial [132], heap Fibonacci [54], heap ternar, treap 1 [7], [114].
Principalele operaţii definite pentru un heap H sunt următoarele:
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;
Tabelul 5.1 ([29]) 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).
1
http://acs.lbl.gov/~aragon/treaps.html
97
Table 5.1: Complexitatea-timp a principalelor operaţii ale unui 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, unde a1 reprezintă cheia nodului rădăcină al arborelui.
Pentru această
( reprezentare avem următoarele relaţii:
(
2·i dacă 2 · i ≤ n 2·i+1 dacă 2 · i + 1 ≤ n
Stangi = , Drepti =
nu există dacă 2 · i > n nu există dacă 2 · i + 1 > n
(
i
⌊2⌋ dacă i > 1
T atai =
nu există dacă i = 1.
Exemplul 5.1 Să considerăm heap-ul binar din figura 5.1. Acesta poate fi descris de următoarea
structură de date secvenţială:
Nod 1 2 3 4 5 6 7 8 9 10
Cheie 3 5 8 8 7 11 8 10 18 9
98
Conform relaţiilor anterioare, vom avea următoarea componenţă pentru vectorii Stang,
Drept şi Tata:
Nod 1 2 3 4 5 6 7 8 9 10
Stang 2 4 6 8 10 - - - - -
Drept 3 5 7 9 - - - - - -
Tata - 1 1 2 2 3 3 4 4 5
Exemplul 5.2 În figura 5.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 5.2.
99
Fig. 5.2: Inserarea valorii 3 ı̂ntr-un heap
Î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 ??).
În figura 5.3 pot fi urmăriţi paşii necesari pentru ştergerea nodului de valoare minimă din
heap-ul considerat drept exemplu.
100
Algoritm 30 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: i ← last
21: end if
22: end while
23: 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 31).
Exemplul 5.3 Să construim o structură de heap pornind de la şirul de valori 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 5.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 5.4. La final, se adaugă nodul a cărui cheie
are valoarea 5, şi se restabileşte proprietatea de heap prin interschimbări succesive.
101
Fig. 5.4: Crearea unui heap prin inserări succesive
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 subrutinei NewHeap1 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 un nivel 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 (k) ≤ i · 2 ≤ log n 2i = O(n log n). (5.1)
k=1 i=1 i=1
Cea de-a doua metodă de creare a unui heap se bazează pe ideea de a construi un heap
prin combinări (reuniri) repetate de heap-uri, strategie cu o eficienţă superioară metodei
prezentate anterior. 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 dimensiuni apropiate: heap-ul având rădăcina 2 · i şi cel cu rădăcina 2 · i + 1 (a se vedea
algoritmul 32).
Se observă faptul că subrutina Push (a se vedea algoritmul 33) este asemănătoare cu
subrutina DeleteMin. Scopul subrutinei Push este de a reorganiza un heap ı̂ntre limitele
f irst şi last ale tabloului A.
Exemplul 5.4 Să construim o structură de heap având drept date de intrare elementele
secvenţei:
102
Algoritm 33 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
poz 1 2 3 4 5 6 7
A 10 7 9 5 7 8 6
După cum se poate observa din figura 5.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. Astfel numărul
de operaţii (deplasări) efectuate ı̂n cadrul algoritmului 32 (NewHeap2) pentru a construi o
103
structură de tip heap binar cu n elemente este:
h−1
X h
X h
X Xh
j=h−i j j
T (n) ≤ 2i (h − i) = j2h−j = 2h j
≤ n j
< 2n = O(n). (5.2)
i=0 j=1 j=1
2 j=1
2
Subrutina Min(S; y) ı̂ntoarce valoarea minimă a lui S ı̂n y, iar subrutina Delete(y, S)
şterge elementul y din S.
După câteva modificări ale subrutinei de ordonare SortX (algoritmul 34) se obţine sub-
rutina Heapsort (a se vedea algoritmul 35): instrucţiunile din liniile 2-4 au rolul de a forma
heap-ul printr-o construcţie incrementală; apoi pentru toate elementele, se elimină cel mai
mic element din faţa heap-ului şi se reface proprietatea de arbore parţial ordonat ı̂ntre limitele
1 şi i − 1 (liniile 6 şi 7).
104
Listing 5.1: sortheap.c
#include <stdio.h>
/**
* Functie pentru citirea valorilor datelor de intrare.
*/
int readInput(int *a) {
int i, n;
/**
* Functie pentru afisarea rezultatelor.
*/
void list(int *a, int n) {
int i;
/**
* Functia realizeaza reorganizarea unei movile.
* @param start - indicele primului element al secventei
* @param finish - indicelel ultimului element al secventei
* @param a - vectorul ce pastreaza valorile movilei
*/
void push(int start, int finish, int *a) {
int i, j, aux;
i = start;
while (i <= finish / 2) {
if ((2 * i == finish) || (a[2 * i] < a[2 * i + 1])) {
j = 2 * i;
} else {
j = 2 * i + 1;
}
105
}
}
}
int main() {
int a[MAXN];
int i, j, aux;
int n = readInput(a);
j--;
push(1, j, a);
}
list(a, n);
return 0;
}
5.2 Exerciţii
1. Să se implementeze structura de date abstractă coadă cu priorităţi cu ajutorul:
ş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 priorităţi.
106
Listing 5.2: heapsortv2.c
void heapsort(int arr[], unsigned int N) {
int t;
unsigned int n = N, parent = N/2, index, child;
for (;;) {
if (parent > 0) {
t = arr[--parent];
} else {
n--;
if (n == 0) {
return;
}
t = arr[n];
arr[n] = arr[0];
}
index = parent;
child = index * 2 + 1;
while (child < n) {
if ((child + 1 < n) && (arr[child + 1] > arr[child])) {
child++;
}
if (arr[child] > t) {
arr[index] = arr[child];
index = child;
child = index * 2 + 1;
} else {
break;
}
}
arr[index] = t;
}
}
5. 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].
6. K concurenţi participă la o ı̂ntrecere. Fiecare dintre ei trebuie să facă N tururi. Toţi
concurenţii pleacă ı̂n acelaşi timp de la linia de pornire. La ı̂nceput fiecare concurent
se simte normal, ı̂nsă când termină o tură el ı̂şi pierde din capacitatea de a conduce.
Astfel fiecare cursă este cu o milisecundă mai ı̂nceată decât cea dinainte. La ı̂nceput,
concurentul i face un tur ı̂n msi milisecunde (msi este un număr natural nenul). Reg-
ulile spun că fiecare concurent i are dreptul la fiecare pi (1 ≤ pi ≤ N) să primească
atunci când trece linia de sosire o băutură energizantă ce ı̂l readuce la forma normală.
107
Fiecare concurent va trece linia de sosire de N ori (vom număra ultima trecere, ı̂nsă nu
şi pe cea de la ı̂nceput)
Date de intrare
Pe prima linie din fişierul competitie.in se vor găsi două numere, K şi N. Pe
următoarele K linii se găsesc două numere msi şi pi .
Date de ieşire
Fişierul competitie.out va conţine numărul maxim de concurenţi ce trec linia de start
ı̂n acelaşi timp.
Restricţii: K ≤ 10000, N ≤ 1000, msi ≤ 1000000, 1 ≤ pi ≤ N.
Exemplu
competitie.in competitie.out
4 3
26 2
39 3 2
45 1
56 2
(Shumen 2010, Competitie)
Date de ieşire
În fişierul de ieşire magazin.out se va tipări, pentru fiecare operaţie de vânzare, suma
ı̂ncasată de magazin.
Restricţii: 1 ≤ N ≤ 300.000, 1 ≤ K ≤ 100, 1 ≤ x ≤ 1.000.000.000 pentru toate
obiectele adăugate; 1 ≤ q ≤ K pentru toate obiectele vândute. Se garantează că există
q obiecte ı̂n stoc la momentul vânzării.
Exemplu
108
competitie.in competitie.out
10 3 5
1 5 4
1 8 10
1 3
2 2
1 10
1 4
1 12
2 2
1 15
2 3
(Magazin, Cătălin Frâncu)
109
Capitolul 6
Arbori oarecare
Definiţia 6.1 Fiind dată o mulţime M de elemente denumite noduri, vom numi arbore o
submulţime finită de noduri astfel ı̂ncât:
• legături fiu-frate: acest mod de reprezentare utilizează două tablouri, denumite fiu şi
frate, unde f iuk reprezintă primul descendent al vârfului k, iar f ratek desemnează
un descendent al tatălui nodului k, ce urmează după k. Între descendenţii unui vârf
se presupune că definim o relaţie de ordine, astfel ı̂ncât, atunci când ı̂i vizităm, acest
lucru se face, ı̂ntr-o anumită ordine: de obicei, descendenţii unui nod sunt vizitaţi de
la cel mai mic (ca valoare a etichetei asociate nodului) la cel mai mare.
De exemplu, descendenţii nodului 4 din figura 6.1 sunt 7, 8, 9 şi 10.
Rădăcina arborelui din figura 6.1 este nodul 1 (Rad = 1).
2 3 4
5 6 7 8 9 10
Dacă identificăm Fiu cu Stang şi Frate cu Drept, ı̂n cadrul reprezentării fiu-frate,
atunci unui arbore oarecare i se poate asocia un arbore binar.
110
Table 6.1: Reprezentarea cu legături fiu-frate a arborelui din figura 6.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
Pentru reprezentarea unui arbore oarecare, modelul de reprezentare fiu-frate ı̂n varianta
cu alocare statică poate fi uşor extins la o variantă cu alocare dinamică, ce utilizează
pointeri pentru legăturile către primul descendent respectiv pentru următorul frate.
Astfel, un nod al arborelui poate avea următoarea structură:
typedef struct nod {
TipOarecare data;
struct nod* fiu;
struct nod* frate;
} Nod;
rad
1 NULL
2 3 4 NULL
NULL
5 6 NULL 7 8 9 10 NULL
Fig. 6.2: Exemplu de arbore oarecare cu 10 noduri reprezentat prin legături fiu-frate
În figura 6.2 poate fi urmărită reprezentarea arborelui din figura 6.1 folosind legături
fiu-frate, varianta cu pointeri.
• 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 C cu n componente:
(
0 , dacă vârful respectiv nu are descendenţi
Ck =
j , unde j indică adresa (coloana) unde ı̂ncepe lista descendenţilor vârfului k.
111
Listele de descendenţi se păstrează prin intermediul unei matrice L cu 2 linii şi N − 1
coloane (L ∈ M2×n (N)):
L1,k - un descendent al unui nod al arborelui a cărui listă conţine coloana k a matricei
date. (
0 , dacă descendentul respectiv este ultimul
L2,k =
j , unde j indică coloana unde se află următorul descendent, frate cu L1,k .
Table 6.2: Reprezentarea arborelui din figura 6.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
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
static sau dinamic. Acest model 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 noi descendenţi, atunci când intrările alocate pentru aceşti descendenţi sunt deja
ocupate nu poate conduce decât la realocarea spaţiului de memorie cu un cost reflectat
ı̂n complexitatea algoritmului. Pentru un număr limitat superior de descendenţi putem
defini tipul Nod astfel
#define NMAX 100
typedef struct nod {
TipOarecare data;
struct nod* children[NMAX];
} Nod;
unde data constituie informaţia asociată unui nod, iar children este un tablou ce
păstrează adresele tuturor descendenţilor unui nod. Dacă childrenn = NULL atunci
nodul curent va avea n copii (descendenţi) de la children0 la childrenn−1 .
Altfel, putem defini tipul Nod
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. Din acest motiv, se alocă spaţiu pentru un tablou de pointeri
către structura de tip Nod ı̂n acel moment prin instrucţiunea
children = (Nod**)malloc(sizeof(Nod*) * no of children).
În figura 6.3 este reprezentat arborele din figura 6.1 folosind reprezentarea cu liste cu
descendenţi.
• vectorul Tata - pentru fiecare nod k, tatak indică nodul său părinte.
112
rad
1 3
2 0 3 2 4 4
5 0 6 0 7 0 8 0 9 0 10 0
Fig. 6.3: Exemplu de arbore oarecare cu 10 noduri reprezentat prin liste cu descendenţi
Table 6.3: Reprezentarea arborelui din figura 6.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, ı̂n anumite condiţii. De obicei, reprezentarea prin vectorul tata nu este
folosită singură ci ı̂mpreună cu alte moduri de reprezentare, ca o completare la acestea.
De exemplu, reprezentarea unui nod propusă la primul punct, poate fi modificată 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;
În figura 6.4, reprezentarea din figura 6.2 este ı̂mbunătăţită prin legătura tata.
• parcurgerea ı̂n A-preordine: se vizitează mai ı̂ntâi rădăcina, şi apoi, ı̂n ordine, subar-
borii săi [95], [130] (a se vedea algoritmul 36).
113
rad
NULL
1 NULL
NULL
2 3 4 NULL
NULL
5 6 NULL 7 8 9 10 NULL
Fig. 6.4: Exemplu de arbore oarecare cu 10 noduri reprezentat prin legături fiu-frate-tata
Parcurgerea ı̂n A-preordine este exemplificată pe arborele oarecare ilustrat ı̂n figura
6.1: metoda conduce la aceleaşi rezultate ca şi metoda de parcurgere ı̂n preordine a
arborelui binar asociat, 1, 2, 3, 5, 6, 4, 7, 8, 9, 10.
• parcurgerea ı̂n 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 [95], [130]. În urma par-
curgerii ı̂n A-postordine a arborelui din figura 6.1 se vor vizita nodurile ı̂n ordinea
următoare: 2, 5, 6, 3, 7, 8, 9, 10, 4, 1.
114
6.3 Arbori de acoperire de cost minim
Fie G = (V, E) un graf simplu, neorientat, finit şi fie c : E −→ R o funcţie de cost ce asociază
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 :
Lema 6.1 (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.
adică am obţinut un arbore de acoperire T ′′ ce prezintă un cost mai mic decât T ′ , contradicţie
cu faptul că T ′ este un arbore de acoperire de cost minim.
Lema 6.2 (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.
115
Înlocuind muchia f cu muchia e ı̂n arborele T ′ , obţinem un alt arbore de acoperire T ′′
unde 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.
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. În general, 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 ).
Mulţimea T k+1 se obţine din mulţimea T k prin reuniunea arborilor Tik şi Tjk (i 6= j) ca
urmare a adăugării muchiei (u′ , v ′ ), unde u′ ∈ Tik şi v ′ ∈ Tjk (T k+1 = T k \{Tik , Tjk }∪{Tik ∪Tjk }).
În final, la pasul n − 1, mulţimea T n−1 va fi compusă dintr-un singur element, T n−1 =
{T1n−1}, acesta fiind arborele de acoperire de cost minim. Algoritmii cei mai cunoscuţi pentru
determinarea arborilor de acoperire de cost minim sunt următorii:
116
2. Algoritmul lui Prim (a se vedea algoritmul 38);
Algoritmul lui Prim implementat simplu are o complexitate O(n2 )[29] şi atinge o com-
plexitate de O(m log n) dacă se folosesc heap-uri Fibonacci [54], sau pairing heaps [121].
În tabelul 6.4 sunt prezentaţi mai mulţi algoritmi elaboraţi de-a lungul timpului, pentru
determinarea arborelui de acoperire minimal şi complexităţile lor. Karger, Klein şi Tarjan [81]
pornind de la algoritmul lui Boruvka au realizat un algoritm randomizat pentru determinarea
arborelui de acoperire minimal, având o complexitate liniară, iar Chazelle [25] a dezvoltat
un algoritm având complexitatea O(n · α(m, n)) (unde α(m, n) este inversa funcţiei lui Ack-
erman). Pe de altă parte, Pettie şi Ramachandran [108] au propus un algoritm demonstrat
ca fiind optimal, având complexitatea cuprinsă ı̂ntre O(n + m) şi O(n · α(m, n)).
117
Table 6.4: Algoritmi pentru determinarea arborelui de acoperire minim
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 poate utiliza matricea
costurilor C (C ∈ Mn×n (R)):
0
, dacă i = j
ci,j = ∞ , dacă (i, j) ∈ /E
d > 0 , dacă (i, j) ∈ E
118
Exemplul 6.3 Să considerăm graful din figura 6.5:
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ă.
119
• tata - conţine pentru fiecare nod k ∈
/ S nodul u ∈ S astfel ı̂ncât ck,u = 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 astfel ı̂ncât
tataj = k.
120
Fig. 6.6: Exemplu de graf ponderat - aplicaţie algoritmul lui Prim
121
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 (a se vedea algoritmul 41) este aproape
identic cu algoritmul lui Dijkstra (a se vedea algoritmul 55).
După cum am subliniat, implementarea optimă se obţine prin folosirea unor structuri de
date avansate - heap-uri Fibonacci sau pairing heaps (a se vedea algoritmul 42).
122
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 [29]. Operaţiile de bază
ale acestei structuri de date sunt următoarele [2]:
• init(x, B) - crează o mulţime B formată dintr-un singur element x;
Exemplul 6.5 Pentru a 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 vom folosi 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, vom avea:
1 2 3 4 5 6 7 8
A ’a’ ’b’ ’e’ ’x’ ’y’ ’z’ ’u’ ’v’
C 1 2 3 1 1 2 3 2
123
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.
Procedura Init alocă un nod şi iniţializează câmpurile acestuia. Adresa nodului alocat
este păstrată ı̂n Listk (a se vedea algoritmul 44).
Funcţia Find 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 memorate. Se parcurge
lista folosind metoda de căutare liniară şi se ı̂ncearcă identificarea unui element având valoarea
x. Dacă elementul este găsit, atunci se ı̂ntoarce capul listei din care face parte. Dacă, până
la sfârşit, nu a fost identificat elementul x, atunci funcţia returnează valoarea NULL.
124
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 dintr-o listă, avem nevoie şi 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).
Se poate observa foarte uşor (a se vedea algoritmul 45), faptul că arborele obţinut ı̂n urma
unor operaţii repetate de reuniune Merge poate deveni degenerat (o structură mai apropiată
de o listă liniară decât o structură de arbore). Pentru a putea să evităm asemenea situaţii,
reuniunea se poate realiza ţinând cont de una din următoarele euristici:
1. numărul de elemente (noduri) din fiecare arbore - rădăcina arborelui ce are mai puţine
noduri (notăm arborele cu TB ) va deveni descendentul direct al rădăcinii arborelui ce
posedă mai multe noduri (notat cu TA ). Astfel, toate nodurile din arborele TA vor
rămâne cu aceeaşi valoare a adâncimii ı̂n cadrul arborelui rezultat, pe când nodurile
din arborele TB vor avea valoarea adâncimii incrementată cu 1. De asemenea, arborele
rezultat ı̂n urma reuniunii celor doi arbori TA şi TB va avea cel puţin de două ori mai
multe noduri decât arborele TB .
În continuare sunt descrise subrutinele Init, Find şi Merge actualizate pentru a folosi
caracteristica numărului de elemente din fiecare arbore.
125
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 Find 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.
În continuare sunt descrise subrutinele Init, Find şi Merge actualizate pentru a folosi
caracteristica ı̂nălţimii unui arbore.
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 Find ş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 Find(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:
126
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
Pentru a păstra muchiile grafului, vom utiliza o matrice A cu 3 linii şi m coloane (A ∈
127
M3×m (R)): primele două linii conţin extremităţile muchiilor, iar linia a 3-a costul muchiei
respective.
A1,k - indicele nodului ce reprezintă prima extremitate a muchiei.
A2,k - indicele nodului ce reprezintă a doua extremitate a muchiei.
A3,k - lungimea muchiei respective.
Pentru a se aplica o strategie Greedy, datele de intrare, muchiile grafului, se vor ordona
crescător ı̂n funcţie de costul asociat. Deoarece graful parţial respectiv este un arbore cu n
noduri, rezultatul final va avea un număr de n − 1 muchii.
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. L este
o structură de date de tip listă ce va păstra muchiile alese ı̂n timpul procesului de calcul,
muchii ce formează arborele parţial de cost minim.
Când sunt preluate ca date de intrare, muchiile vor fi prezentate ı̂n ordinea crescătoare
a extremităţilor lor, de forma u v cost, cu u < v, indicii nodurilor ce formează extremităţile
unei muchii.
Exemplul 6.6 Să considerăm graful din figura 6.6. Muchiile grafului şi costurile asociate
lor sunt următoarele:
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 6.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.
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 (a se vedea figura 6.7 (1)).
La pasul al doilea, se ia ı̂n considerare muchia (1, 3) şi mulţimea muchiilor selectate devine
L = {(1, 5), (1, 3)} (a se vedea figura 6.7 (2)).
La pasul al treilea, alegerea muchiei (6, 8) va conduce la reuniunea arborilor de rădăcini
6 şi 8, iar L = {(1, 5), (1, 3), (6, 8)} (figura 6.7 (3)).
La pasul al patrulea, ajungem la muchia (4, 8) ce are costul 10. Nodul 4 face parte din ar-
borele 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, iar L devine 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 6.7 (4).
128
Fig. 6.7: Algoritmului lui Kruskal exemplificat pe graful din figura 6.6
La paşii următori, cinci şi şase, sunt alese muchiile (7, 8) şi respectiv (2, 4): mulţimea L
este actualizată astfel 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)}.
Observaţia 6.7 Subliniem faptul că arborii reprezentaţi ı̂n figura 6.7 sunt diferiţi de arborii
ce conduc la obţinerea soluţiei problemei: arborii din figură constituie suportul necesar pentru
reprezentarea structurii de date pentru mulţimi disjuncte, structură ce este utilizată pentru
efectuarea eficientă a operaţiilor Find ş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
129
configuraţia (4) (nodul 4 este unit direct cu nodul 6, a se vedea figura 6.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ă.
6.4 Exerciţii
1. Să se realizeze o subrutină ce ı̂ntoarce numărul de noduri dintr-un arbore oarecare T .
3. Se dă un arbore oarecare T , reprezentat prin vectorul tata. Să se elaboreze un algoritm
pentru a determina ı̂nălţimea arborelui.
5. Fiind dat un arbore oarecare T , să se elaboreze un algoritm ce determină toate nodurile
u cu proprietatea că numărul de copii ai nodului u este mai mare decât numărul de
copii ai părintelui său.
8. Fiind dat un arbore oarecare T , să se elaboreze un algoritm ce determină pentru fiecare
nod u ∈ T numărul descendenţilor din subarborele a cărui rădăcină este u.
10. Fiind dat un arbore oarecare T de rădăcină r şi un număr natural k, să se elaboreze
un algoritm ce determină toate nodurile aflate la distanţa k faţă de rădăcină.
11. Se dă un arbore oarecare T cu n noduri. Să se elaboreze un algoritm eficient ce verifică
pentru o pereche de noduri (u, v) dacă nodul u este un strămoş al nodului v.
130
Să se elaboreze un algoritm care, dintre toate modalităţile de interconectare posibile,
să determine o interconectare corespunzătoare arborelui de acoperire de cost minim
(interconectarea pentru care suma tuturor circuitelor imprimate are lungimea minimă).
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ă.
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
Cazul 1: [1 5] [4 5] [2 3] [1 2]
Interconectarea de cost minim are valoarea 132.
Fig. 6.8: Descrierea conexiunilor posibile dintre componentele unei plăci electronice
13. Se dă un graf neorientat conex ponderat G = (V, E). Să se elaboreze un algoritm ce
determină un arbore de acoperire al grafului G ı̂n care costul maxim al unei muchii este
minim dintre toţi arborii de acoperire ai lui G.
14. Se dă un graf neorientat conex ponderat G = (V, E). Să presupunem că se cunoaşte un
arbore parţial de cost minim T (arbore minim de acoperire) corespunzător lui G. Să se
elaboreze un algoritm eficient care actualizează arborele T dacă ı̂n graful G se adaugă
un vârf nou u şi muchii incidente cu acesta.
131
Capitolul 7
Grafuri orientate
Definiţia 7.1 Un graf orientat (digraf ) 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.
Î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) dacă x 6= y.
Definiţia 7.2 Fie G = (V, E) un graf orientat, finit şi e = (x, y) ∈ E un arc al lui G.
Atunci spunem că
Într-un digraf nu putem avea mai multe arce care să aibă aceeaşi pereche de noduri ca
extremitate iniţială şi finală, ı̂nsă putem avea bucle.
Se numeşte multigraf orientat un graf orientat G ı̂n care putem avea şi arce paralele.
Definiţia 7.3 Un graf parţial al unui graf orientat G = (V, E) este un graf orientat
G1 = (V, E1 ) unde E1 ⊂ E.
Exemplul 7.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 7.4 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 ).
132
Fig. 7.1: Un exemplu de graf orientat
Exemplul 7.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 7.5 Gradul exterior al unui vârf d+ G (x) este egal cu numărul arcelor ce au ca
+
extremitate iniţială pe x (dG (x) = |{(x, u)| (x, u) ∈ E, u ∈ V }|). Gradul interior al
unui vârf d− −
G (x) este egal cu numărul arcelor ce au ca extremitate finală pe x (dG (x) =
|{(u, x)|(u, x) ∈ E, u ∈ V }|).
Definiţia 7.6 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).
Un drum D este simplu dacă arcele sale (v0 , v1 ), (v1 , v2 ), . . ., (vp−1 , vp ) sunt distincte două
câte două. Dacă vârfurile v0 , v1 , . . . , vp sunt distincte două câte două, drumul se numeşte
elementar. Lungimea drumului D este p.
Exemplul 7.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 7.8 Un drum D pentru care v0 = vp se numeşte circuit. Un circuit este ele-
mentar dacă nodurile sale sunt distincte.
Definiţia 7.9 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 7.10 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.
133
Definiţia 7.11 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 7.12 Un graf orientat G = (V, E) este complet dacă oricare două vârfuri dis-
tincte sunt adiacente: ∀u, v ∈ V fie (u, v) ∈ E, fie (v, u) ∈ E, fie ambele.
Propoziţia 7.5 Într-un graf orientat complet G = (V, E), unde n = |V |, m = |E| avem
relaţia
n(n − 1)
≤ m ≤ n(n − 1). (7.1)
2
n(n−1)
Lema 7.6 Numărul de grafuri orientate complete cu n vârfuri este 3 2 .
Definiţia 7.13 Un graf orientat G = (V, E) se numeşte graf turneu dacă ı̂ntre oricare
două vârfuri distincte u 6= v avem fie (u, v) ∈ E, fie (v, u) ∈ E.
Propoziţia 7.7
1. Orice graf turneu G = (V, E) este un graf complet.
n(n−1)
2. Numărul de grafuri turneu cu n vârfuri este 2 2 .
3. Într-un graf turneu există un drum elementar ce trece prin toate vârfurile.
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.
Fig. 7.2: Arbore de acoperire ı̂n lăţime pentru graful orientat din figura 7.1
În urma unei parcurgeri ı̂n lăţime a unui graf orientat, se ı̂ntâlnesc următoarele tipuri de
arce:
134
• 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 7.2 este prezentat arborele de acoperire ı̂n lăţime corespunzător grafului din
figura 7.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)).
( 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
Vom prezenta schiţa unui algoritm mai general pentru parcurgerea unui graf (a se vedea
algoritmul 47). 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-una dintre următoarele categorii:
• 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).
• 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).
135
Fig. 7.3: Arbore de acoperire ı̂n adâncime pentru graful orientat din figura 7.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ă
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 7.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 7.3).
Lema 7.8 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:
136
Algoritm 48 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 k ca fiind vizitat
3: call P renum(k)
4: call V izitare(k) ⊲ vizitarea nodului curent k
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 pt nodul i
8: end if
9: end for
10: call P ostnum(k)
11: end procedure
1. dacă (u, v) este un arc al arborelui de acoperire sau un arc de ı̂naintare sau un arc de traver-
sare atunci avem relaţia postnumu > postnumv ;
2. dacă (u, v) este un arc de ı̂ntoarcere atunci avem relaţia postnumu < postnumv .
Lema 7.9 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. nu sunt posibile situaţiile prenumu < prenumv < postnumu < postnumv sau
prenumv < prenumu < postnumv < postnumu .
Lema 7.10 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 7.11 În urma apelării procedurii DFSNum(1, 8, Vecin) (a se vedea algorit-
mul 48), secvenţa de apeluri recursive ale procedurii DFSNum este ilustrată prin arborele de
acoperire ı̂n adâncime din figura 7.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 7.10 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).
137
7.3 Sortarea topologică
Definiţia 7.14 Un graf orientat şi care nu posedă circuite se numeşte graf orientat aciclic
(directed acyclic graph - DAG).
Lema 7.12 Î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.
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 ).
Definiţia 7.15 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 7.13 Dacă un graf orientat G admite o sortare topologică atunci G este aciclic.
Lema 7.14 Dacă un graf orientat G este aciclic atunci el admite o sortare topologică.
Observaţia 7.15 Î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ă).
138
Algoritm 49 Algoritm de sortare topologică a unui graf orientat (algoritmul lui Kahn)
( SortTop1(n, V ecin; L)
1: procedure
n - numărul de noduri din graf
Input:
V ecin - vector ce conţine listele cu vecini ai fiecărui nod
Output: L - lista nodurilor ordonate ı̂n urma sortării topologice
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
Exemplul 7.16 Fie graful orientat din figura 7.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 7.5).
Lema 7.17 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.
139
Fig. 7.5: Sortare topologică cu algoritmul 49 pentru graful orientat din figura 7.4
( SortTop2(n, V ecin; L)
1: procedure
n - numărul de noduri din graf
Input:
V ecin - matricea de adiacenţă a grafului
Output: L - lista nodurilor ordonate ı̂n urma sortării topologice
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 şi se păstrează astfel ordonate ı̂n lista L
12: end procedure
Exemplul 7.18 Să considerăm ca date de intrare pentru algoritmul 50 graful orientat din
figura 7.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
140
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:
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 50
este: 3, 1, 2, 4, 5, 7, 6.
Definiţia 7.16 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 7.17 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.
141
Fig. 7.6: Graf orientat. Componente tare conexe.
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:
• 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.
Definiţia 7.18 Se numeşte graf condensat Gcc al unui graf orientat G = (V, E) un graf
orientat ı̂n care fiecărui nod din Gcc ı̂i corespunde o componentă tare conexă din G, iar
(ucc, v cc ) este un arc din Gcc dacă ∃u1 ∈ CC1 şi v1 ∈ CC2 astfel ı̂ncât (u1 , v1 ) ∈ E (CC1 este
componenta conexă a lui G ce ı̂i corespunde nodului ucc, iar CC2 este componenta conexă a
lui G ce ı̂i corespunde nodului v cc ).
Teorema 7.20 Graful condensat Gcc al unui graf orientat G = (V, E) este un graf orientat
aciclic.
Pentru determinarea componentelor tare conexe există mai mulţi algoritmi, dintre care
amintim algoritmul lui Tarjan, algoritmul lui Kosaraju şi algoritmul lui Gabow.
142
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.
Exemplul 7.21 După parcurgerea ı̂n adâncime a grafului 7.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
În continuare se construieşte graful transpus, GT (a se vedea figura 7.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, ı̂n ordinea descrescătoare a valorilor vectorului postnum, 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}.
Observaţia 7.22 Se poate simplifica algoritmul, prin eliminarea numerotării ı̂n postordine
a nodurilor. Pentru aceasta se va utiliza o stivă S ce va memora nodurile ı̂n momentul ı̂n
care toţi descendenţii unui nod au fost vizitaţi. Această stivă are acelaşi rol ca şi ı̂n cadrul
celui de-al doilea algoritm pentru sortarea topologică. Algoritmul lui Kosaraju se modifică
astfel:
143
Pas 1. Se iniţializează o stivă S. Se realizează parcugerea grafului cu algoritmul de vizitare ı̂n
adâncime, pornind de la un nod arbitrar. În timpul vizitării unui nod u, ı̂nainte de părăsirea
acestuia (după ce toate nodurile adiacente cu nodul u au fost vizitate), nodul u se adaugă pe
stiva S. Practic, la implementarea recursivă, ı̂nainte de părăsirea subrutinei, nodul curent se
adaugă pe stivă.
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ă primul nod nevizitat din graful GT ı̂n ordinea determinată de extragerea din stiva S.
Din acest nod se iniţiază parcugerea grafului cu algoritmul de vizitare ı̂n adâncime. Nodurile
ı̂ntâlnite ı̂n cadrul acestei parcurgeri formează o componentă tare conexă.
Pas 4. Dacă mai rămân noduri ale grafului GT nevizitate ı̂n stiva S atunci se reia Pasul 3, altfel
algoritmul se termină.
void init(STACK* s) {
s->top = -1;
}
int empty(STACK* s) {
return (s->top < 0);
}
144
void push(STACK* s, int x) {
s->top++;
s->a[s->top] = x;
}
int peek(STACK* s) {
return s->a[s->top];
}
void pop(STACK* s) {
s->top--;
}
visit[u] = 1;
for (v = 1; v <= n; v++) {
if ((vecin[u][v] == 1) && !visit[v]) {
dfs(n, v);
}
}
push(&stk, u);
}
visit[u] = 1;
ctc[ctcNo] = u;
for (v = 1; v <= n; v++) {
if ((vecinT[u][v] == 1) && !visit[v]) {
dfsT(n, v);
}
}
}
void findCTC(int n) {
int i, u;
// prima etapa
init(&stk);
for (i = 1; i <= n; i++) {
if (!visit[i]) {
dfs(n, i);
}
}
// a doua etapa
memset(visit, 0, sizeof(visit));
while (!empty(&stk)) {
145
u = peek(&stk);
pop(&stk);
if (!visit[u]) {
ctcNo++;
dfsT(n, u);
}
}
}
int main() {
int n, m, i, u, v;
vecin[u][v] = 1;
vecinT[v][u] = 1;
}
return 0;
}
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ţă.
Those little essays have been packaged into The Stanford GraphBase (1994),
and I still enjoy using and modifying them. My favorite is the implementation
of Tarjan’s beautiful algorithm for strong components, which appears on pages
512-519 of that book.
146
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 cores-
punză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 51) seamănă foarte mult cu algoritmul 15
de determinare a muchiei critice (varianta a II-a).
Exemplul 7.23 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}.
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.
147
Algoritm 51 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
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:
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
148
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 elementelor vectorilor 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
7.5 Exerciţii
1. Se dă un graf orientat G = (V, E) şi două noduri u şi v, u, v ∈ V . Să se elaboreze un
algoritm ce determină dacă există un drum de la u la v.
2. Se dă un graf orientat G = (V, E). Să se elaboreze un algoritm ce verifică dacă graful
G este aciclic.
3. Se dă un graf orientat G = (V, E) şi două noduri u şi v, u, v ∈ V . Să se elaboreze un
algoritm ce determină toate drumurile de la u la v ı̂n graful G.
4. Se dă un graf orientat G = (V, E). Să se elaboreze un algoritm ce determină un circuit
eulerian ı̂n graful G.
149
5. Se dă un graf orientat G = (V, E). Să se elaboreze un algoritm ce determină dacă graful
G este unic conex (singly connected ).
Un graf orientat este unic conex dacă pentru orice pereche de noduri u, v ∈ V , u 6= v,
există cel mult un drum simplu de la u la v.
6. Să se elaboreze un algoritm ce determină dacă un graf orientat G = (V, E) este semi-
conex (semiconnected ).
Un graf orientat G este semiconex dacă pentru orice pereche de noduri u, v ∈ V , u 6= v,
există fie un drum simplu de la u la v, fie un drum simplu de la v la u.
7. (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.
150
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)
9. 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.
151
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.
Fig. 7.8: Poziţiile cărţilor ı̂ntr-o bibliotecă precum şi posibilităţile de deplasare ale robotului
10. Să se elaboreze un algoritm eficient ce determină dacă un graf orientat G = (V, E)
posedă un nod cu proprietatea că gradul interior are valoarea |V | − 1 iar gradul exterior
are valoarea 0.
11. Subrutina SortTop2 (a se vedea algoritmul 50) se poate rescrie astfel ı̂ncât să se renunţe
la ordonarea realizată la sfârşitul subrutinei. Acest lucru se poate obţine prin utilizarea
unei stive ce va păstra fiecare nod al grafului ı̂n timpul parcurgerii ı̂n A-postordine,
parcugere a arborelui de vizitare ı̂n adâncime asociat grafului.
Să se realizeze un program ce implementează algoritmul descris.
Indicaţie
O variantă de implementare este cea propusă de procedura SortTop3 (a se vedea algo-
ritmul 52).
152
Algoritm 52 Algoritm de sortare topologică a unui graf orientat
( SortTop3(n, V ecin; L)
1: procedure
n - numărul de noduri din graf
Input:
V ecin - matricea de adiacenţă a grafului
Output: L - lista nodurilor ordonate ı̂n urma sortării topologice
2: for k ← 1, n do
3: vizitatk ← 0
4: end for
5: S ← ∅, L ← ∅
6: for k ← 1, n do
7: if (vizitatk = 0) then
8: call DF ST op(k, n, V ecin; S)
9: end if
10: end for
11: while (S 6= ∅) do
12: S⇒u
13: L⇐u
14: end while
15: end procedure
16: procedure DFSTop(k, n, V ecin; S)
k
- nodul curent vizitat
Input: n - numărul de noduri din graf
V ecin - matricea de adiacenţă a grafului
Output: S - stiva ce păstrează nodurile vizitate ı̂n postordine
17: vizitatk ← 1
18: for i ← 1, n do
19: if ((vizitati = 0) ∧ (vecink,i = 1)) then
20: call DF ST op(i, n, V ecin; S)
21: end if
22: end for
23: S⇐k
24: end procedure
153
Capitolul 8
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 = [x0 , x1 , . . . , xm ], xi ∈ V , de lungime m, costul acestuia va fi dat de către
următoarea formulă:
m−1
X
c(D) = c([xi , xi+1 ]). (8.1)
i=0
Notăm cu Ms,t mulţimea drumurilor dintre nodul sursă xs şi nodul destinaţie xt , unde
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
154
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. Reamintim că lungimea
unui drum se defineşte ca fiind numărul arcelor ce ı̂l compun.
Algoritmul lui Moore (a se vedea algoritmul 53) se bazează pe structura algoritmului de
parcurgere ı̂n lăţime al unui graf (Breadth First Search - a se vedea algoritmul 9). În lucrarea
[100], autorul ı̂ş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ă lungimea drumului minim de la vârful
u la vârful xk , iar tatak păstrează vârful anterior lui xk , pe drumul de lungime minimă de la
nodul u la nodul xk .
155
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:
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 lungimea 3 şi este compus din nodurile [1, 4, 5, 9].
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 cost minim de la u la w. Costul 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 [37] utilizează metoda generală de elaborare a algoritmilor Greedy,
drumurile de cost minim fiind generate ı̂n ordinea crescătoare a valorilor acestor costuri. Vom
nota cu S mulţimea nodurilor grafului G pentru care se cunoaşte (s-a calculat) drumul de
cost 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 astfel ı̂ncât drumul
de la sursă la acest nod xk , este cel mai scurt dintre toate drumurile posibile de la sursă la
noduri din mulţimea V \ S, drumuri ce au drept noduri intermediare numai elemente din
mulţimea S.
Vom utiliza un tablou D, ce va păstra pentru fiecare nod xk , costul drumului cel mai scurt
de la sursă ce trece numai prin noduri din mulţimea S (a se vedea algoritmul 54).
După ce am identificat un astfel de nod xk , mulţimea S se modifică prin adăugarea nodului
xk : S = S ∪ {xk }. Costul unui drum de la sursă la un nod xj ∈ V \ S, ce are drept noduri
intermediare noduri din mulţimea S, este posibil să se modifice datorită faptului că, anterior,
nodul xk nu fusese luat ı̂n considerare ca vârf intermediar, deoarece nu aparţinea mulţimii
S. Se poate astfel ca, un drum de la sursă la nodul xj ∈ V \ S, ce trece şi prin nodul xk , să
156
Algoritm 54 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
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 . (8.3)
În cadrul algoritmului 55 vom utiliza trei vectori cu următoarele semnificaţii:
• tataj - păstrează pentru fiecare nod xj , nodul anterior xk (xk ∈ S) pe drumul de cost
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 tata păstrând la sfârşitul procesului de calcul al distanţelor un arbore al
drumurilor de cost minim ce are drept rădăcină nodul sursă.
157
Algoritm 55 Algoritmul lui Dijkstra
1: function DistantaMinima(n, V izitat, D)
2: min ← ∞
3: j0 ← −1
4: for j ← 1, n do
5: if ((vizitatj = 0) ∧ (dj < min)) then
6: min ← dj
7: j0 ← j
8: end if
9: end for
10: return j0
11: end function
158
Fig. 8.1: O cale specială mai scurtă
are drept noduri intermediare numai elemente din mulţimea S (xj este primul nod pe drumul
de la nodul sursă la nodul xk , ce nu aparţine mulţimii S), deci este un drum special ce are
un cost mai mic decât drumul special de la sursă la nodul xk . Prin urmare, am identificat un
drum special distinct având un cost mai mic, ceea ce contrazice modul de alegere al nodului
xk .
Trebuie să demonstrăm că, ı̂n orice moment, dk păstrează costul 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ă un cost 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ă un cost mai
mic (a se vedea figura 8.1). Deoarece nodul w a fost adăugat mulţimii S ı̂naintea nodului xk ,
avem dw ≤ dk .
Prin urmare
dw + cw,j ≤ dk + cw,j < dk + costxk w + cw,j , (8.4)
adică drumul special de la sursă la nodul xj ce trece prin nodul w are costul mai mic decât
costul drumului special compus din drumul de la nodul sursă la nodul xk , drumul de la nodul
xk la nodul w şi arcul (w, xj ).
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:
159
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:
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:
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 costul unui singur drum:
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
160
#define NIL -1
/**
* Citirea datelor de intrare - +infinit = -1
*/
void readInput(int* n, int c[][MAXN], int* u) {
int i, j;
min = MAXINT;
j0 = NIL;
for (j = 0; j < n; j++) {
if (!vizitat[j] && (d[j] < min)) {
min = d[j];
j0 = j;
}
}
return j0;
}
/**
* Functia verifica daca a > b + c.
*/
int mai_mare(long a, long b, long c) {
return (a > b+c) ? 1 : 0;
}
//initializari
vizitat[u] = TRUE;
tata[u] = NIL;
for (i = 0; i < n; i++) {
161
if (i != u) {
vizitat[i] = FALSE;
d[i] = c[u][i];
tata[i] = u;
}
}
//partea principala
for(i = 1; i < n; i++) {
k = minim(n, d, vizitat);
if (k == NIL) {
break;
}
vizitat[k] = TRUE;
//actualizare
for (j = 0; j < n; j++) {
if (!vizitat[j] && mai_mare(d[j], d[k], c[k][j])) {
tata[j] = k;
d[j] = d[k] + c[k][j];
}
}
}
}
int main() {
int n; // numarul de noduri din graf
int c[MAXN][MAXN]; // matricea costurilor
int u; // nodul fata de care se calculeaza drumurile de lungime minima
162
int tata[MAXN]; // tata[k] - parintele varfului k in arborele rezultat
int d[MAXN]; // distanta de la u la fiecare nod
readInput(&n, c, &u);
dijkstra(u, n, c, d, tata);
printSolution(u, n, d, tata);
return 0;
}
Analizând algoritmul 56, obţinem că timpul de lucru al acestuia depinde de formula:
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 algoritmului sunt (a se vedea şi capitolul
5):
1. DeleteMin(Q) - şterge nodul ce conţine cheia de valoare minimă şi reorganizează struc-
tura prin refacerea proprietăţii de coadă cu priorităţi.
163
2. Insert(Q,p,x) - inserează nodul p ı̂n coada cu priorităţi Q, unde x reprezintă priori-
tatea nodului p.
2. Arbore 2 − 3:
3. Heap-uri Fibonacci:
Exemplul 8.4 În figura 8.3 avem un graf orientat aciclic. Rezultatul sortării topologice a
nodurilor acestui graf este 1, 2, 5, 4, 7, 6, 3, după cum se poate vedea şi ı̂n figură.
La pasul 1, nodul curent este nodul 1 (a se vedea figura 8.4 (a)):
164
Algoritm 57 Algoritmul de calcul a distanţelor ı̂ntr-un graf orientat aciclic
1: procedure DAGDist(k, n, C; D, T ata)
k - nodul sursă
Input: n - numărul de noduri din graf
C - matricea costurilor
2: call T opSort(n, V ecin; L)
3: for i ← 1, n do
4: di ← +∞, tatai ← 0
5: end for
6: dk ← 0
7: for i ← 1, n do
8: u ← Li ⊲ nodul aflat pe poziţia i ı̂n ordinea topologică
9: for each e = (u, v) ∈ E do
10: if (dv > du + cu,v ) then
11: dv ← du + cu,v
12: tatav ← u
13: end if
14: end for
15: end for
16: end procedure
La pasul al doilea, nodul curent este nodul 2 (a se vedea figura 8.4 (b)):
La pasul al treilea, nodul curent este nodul 5 (a se vedea figura 8.4 (c)):
La pasul al patrulea, nodul curent este nodul 4 (a se vedea figura 8.4 (d)):
La al cincilea pas, nodul curent este nodul 7 (a se vedea figura 8.4 (e)):
La al şaselea pas, nodul curent este nodul 6 (a se vedea figura 8.4 (f )):
La ultimul pas, nodul curent este nodul 3, din care nu pleacă niciun arc.
Observaţia 8.5 Deşi ı̂n exemplul prezentat, primul nod al listei obţinute ı̂n urma sortării
topologice este identic cu nodul sursă (cel de la care se doreşte să se calculeze drumul de cost
minim la fiecare nod al grafului), ı̂n general, algoritmul ı̂ntoarce rezultate corecte şi atunci
când cele două noduri sunt distincte.
165
Fig. 8.4: Determinarea drumurilor de cost minim de la nodul 1 la toate celelalte noduri ale grafului orientat
aciclic din figura 8.3
Pornind de la algoritmul 57 se pot determina cele mai lungi drumuri de la nodul sursă la
toate celelalte noduri ale grafului G, dacă se efectuează una dintre următoarele modificări:
De asemenea, tot prin intermediul acestui algoritm se poate determina un drum critic
maximal ı̂ntr-un graf.
166
Algoritm 58 Algoritmul Bellman-Ford-Moore
1: function BellmanFord1(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 ← +∞, tatai ← 0
4: end for
5: dk ← 0 ⊲ distanţa de la un nod la el ı̂nsuşi este 0
6: for i ← 1, n − 1 do
7: for each e = (u, v) ∈ E do
8: if (dv > du + cu,v ) then
9: dv ← du + cu,v
10: tatav ← u
11: end if
12: end for
13: end for
14: for each e = (u, v) ∈ E do
15: if (dv > du + cu,v ) then
16: return f alse ⊲ Există un ciclu de cost negativ!
17: end if
18: end for
19: return true
20: end function
Însă nici aceşti algoritmi nu pot calcula rezultatul corect dacă ı̂n cadrul grafului orientat
este semnalată prezenţa unor circuite având costul negativ.
Cel mai cunoscut algoritm este algoritmul Bellman-Ford-Moore, numele acestuia consti-
tuind o recunoaştere a muncii depuse de către cercetătorii R. E. Bellman [13], L. R. Ford Jr.
[46] şi E. F. Moore [100], care au publicat independent algoritmi foarte apropiaţi (a se vedea
algoritmul 58).
opt,k
Fie δs,t drumul de cost minim de la s la t, drum de lungime k (este format din k arce).
Drumurile de lungime 2 le putem determina din drumurile de lungime 1, apoi drumurile
de lungime 3 le putem determina folosind drumurile de lungime 2 . . . În general, drumurile
de lungime k + 1 le putem determina folosind drumurile de lungime k.
Drumul de cost minim de la s la t de lungime k, se găseşte printre drumurile de la s la t
opt,k
de lungime k (δs,t ∈ Mks,t).
Se poate determina costul drumului minim de la nodul s la un nod v, de lungime k + 1,
pe baza drumurilor minime de la nodul s de lungime k astfel:
opt,k+1 opt,k
c(δs,v ) = min{c(δs,u ) + cu,v }, ∀v ∈ V. (8.9)
Notăm cu dks,t costul drumului minim de la s la t, drum ce are lungimea cel mult k. Astfel,
conform definiţiei, avem relaţia:
opt,1 opt,2 opt,k
dks,t = min{c(δs,t ), c(δs,t ), . . . , c(δs,t )}. (8.10)
dk+1 k
s,v = min {ds,u + cu,v }, ∀v ∈ V. (8.11)
(u,v)∈E
167
Costurile drumurilor minime de la nodul s la celelalte noduri v (∀v ∈ V ) ale grafului G
se vor afla ı̂n dn−1
s,v , unde n = |V |.
Existenţa unor circuite de cost negativ se verifică cu ajutorul condiţiei
dn−1 n−1
s,v > ds,u + cu,v , ∀(u, v) ∈ E. (8.12)
Algoritmul 59 este o variantă a algoritmului 58 ı̂n care arcele nu mai sunt tratate ı̂ntr-o
ordine apriori, ele fiind abordate ı̂n ordinea dată de nodul sursă.
168
Algoritm 60 Algoritmul Bellman-Ford-Moore (a treia variantă)
1: function BellmanFord3(k, n, C, V ecin; D, T ata)
2: for i ← 1, n do
3: di ← +∞, tatai ← 0, inQueuei ← 0, counti ← 0
4: end for
5: Q ← ∅, dk ← 0 ⊲ distanţa de la un nod la el ı̂nsuşi este 0
6: Q ⇐ k, inQueuek ← 1
7: while (Q =6 ∅) do
8: Q ⇒ u, inQueueu ← 0
9: p ← V ecinu
10: while (p 6= N U LL) do
11: w ← p.nodeIndex
12: if (dw > du + cu,w ) then
13: dw ← du + cu,w , tataw ← u
14: if (inQueuew = 0) then
15: if (countw > n) then
16: return f alse ⊲ Există un ciclu de cost negativ!
17: else
18: Q ⇐ w, inQueuew ← 1, countw ← countw + 1
19: end if
20: end if
21: end if
22: p ← p.next
23: end while
24: end while
25: return true
26: end function
Se cuvine să menţionăm faptul că aceşti algoritmi sunt folosiţi la rutarea pachetelor de
date ı̂n cadrul reţelei globale Internet:
• protocolul OSPF (Open Shortest Path First - OSPF1 , OSPFv22 ) utilizează algoritmul
lui Dijkstra;
• protocoalele RIP (Routing Information Protocol - RIP3 , RIPv24 ) şi IGRP (Interior
Gateway Routing Protocol - IGRP5 utilizează algoritmul lui Bellman-Ford-Moore.
169
Pentru a determina drumurile de cost minim ı̂ntre toate perechile de vârfuri, putem alege
un algoritm ce determină drumurile minime de sursă unică (drumurile de cost minim de la
un 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 61 prezintă o variantă a algoritmului lui Dijkstra ı̂n care drumurile de cost
minim ı̂ntre oricare două vârfuri sunt calculate incremental şi intercalat [34]. Matricea D
va păstra pentru fiecare pereche [xi , xj ] costul di,j al drumului minim dintre cele explorate
până la momentul curent. Coada de priorităţi C va conţine toate perechile de noduri (i, j),
ordonate crescător ı̂n funcţie de prioritatea di,j a fiecăreia. La fiecare pas, se extrage din
coadă perechea de noduri (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ă ı̂n cadrul algoritmilor prezentaţi anterior (de exemplu algoritmului lui
Bellman-Ford-Moore), 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 .
170
8.2.1 Algoritmul lui Roy-Floyd-Warshall
Fie p = [x1 , x2 , . . . , xk ] un drum elementar. Definim drept 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).
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 numai 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.13)
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.14)
Cu ajutorul algoritmului Roy-Floyd-Warshall se va calcula un şir de matrici D (0) , D (1) , . . . ,
(k)
D (k) , . . . , D (n) . Pentru fiecare k, 1 ≤ k ≤ n, un element di,j al unei matrici D (k) va păstra
valoarea costului drumului minim de la nodul xi la nodul xj , drum ce are drept noduri
intermediare numai noduri din mulţimea {x1 , x2 , . . . , xk }.
Matricea D (0) va fi iniţializată astfel:
0
, dacă i = j
(0)
di,j = +∞ , dacă (xi , xj ) ∈
/ E, i 6= j (8.15)
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 cost minim de la xi la xj ce trece numai prin nodurile {x1 , x2 , . . . , xk } fie nu
(k) (k−1) (k)
conţine nodul xk , caz ı̂n care di,j = di,j , fie ı̂l conţine, şi atunci are loc relaţia di,j =
(k−1) (k−1) (k)
di,k + dk,j . Prin urmare, valoarea elementului di,j se poate calcula astfel:
(k) (k−1) (k−1) (k−1)
di,j = min{di,j , di,k + dk,j } (8.16)
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 necesaru-
lui de memorie. Se observă că, matricea D (k) este utilizată doar la calculul matricei D (k+1) ,
după aceea ea nemaifiind folosită.
Rezultatul final este păstrat ı̂n matricea D (n) :
(n) (n)
D (n) = (di,j ), di,j = δi,j , ∀xi , xj ∈ V, (8.17)
171
(n)
unde di,j = costul drumului minim de la nodul xi la nodul xj , drum ce are drept noduri
intermediare numai elemente din mulţimea {x1 , x2 , . . . , xn }.
Algoritmul 62 prezintă modul de calcul al elementelor din matricea D (n) .
Algoritmul 63 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 al matricii P (k) va păstra nodul intermediar k ce conduce la cea mai mică
(k) (k)
valoare pentru di,j conform formulei 8.16. Daca pi,j = 0, atunci cel mai scurt drum dintre
nodurile xi şi xj este cel direct.
(0)
Elementele matricii iniţiale P (0) vor fi toate nule (pi,j = 0).
172
(k)
Valoarea unui element pi,j al matricii P (k) se va calcula conform formulei următoare,
ţinându-se cont de relaţia 8.16:
(
(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.18)
k , dacă di,j > di,k + dk,j .
Împreună cu secvenţa de matrici D (0) , D (1) , . . ., D (n) , se vor calcula şi elementele secvenţei
de matrici P (0) , P (1) , . . ., P (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 64 prezintă varianta extinsă a algoritmului
63, ı̂n cadrul acestuia fiind adăugat şi suportul pentru determinarea drumului de cost minim
dintre oricare pereche de noduri a grafului.
Exemplul 8.6 Fie graful din figura 8.2. Matricea costurilor asociată acestui graf este următoarea:
0 1 +∞ 21 +∞
+∞ 0 8 +∞ 4
C= 3 +∞ 0 12 +∞
+∞ +∞ 9 0 +∞
+∞ +∞ 3 +∞ 0
173
Şirul de matrice D (k) , rezultat al aplicării algoritmului Roy-Floyd-Warshall pentru acest
graf, este ilustrat
ı̂n continuare:
0 1 ∞ 21 ∞ 0 1 ∞ 21 ∞
∞ 0 8 ∞ 4 ∞ 0 8 ∞ 4
D(0) =
3 ∞ 0 12 ∞
D(1) =
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
D(2) =
3 4 0 12 8
D(3) =
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
D(4) =
3 4 0 12 8
D(5) =
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, finit, şi V = {x1 , x2 , . . . , xn }.
Închiderea tranzitivă a grafului G este un graf G+ = (V, E + ), unde E + se defineşte astfel:
E + = {(xi , xj )| ∃ un drum de la xi la xj ı̂n G}.
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 .
O relaţie binară ρ peste mulţimea {1, 2, . . . , n} se poate reprezenta cu ajutorul unei ma-
trice R ∈ Mn×n ({0, 1}), ale cărei elemente sunt definite astfel:
(
1 , dacă (i, j) ∈ ρ
ri,j = (8.20)
0 , dacă (i, j) ∈ / ρ.
174
(k)
serii de matrici R0 , R1 , . . . , Rn , unde un element ri,j ∈ 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.21)
0 , ı̂n caz contrar.
Închiderea tranzitivă a unei relaţii binare prezintă multiple aplicaţii, ca subproblemă ı̂n
cadrul 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 calcul pentru 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 65), se ı̂nlocuiesc ı̂n
algoritmul 62 operaţiile ’min’ cu ’∨’ (sau logic) şi respectiv ’+’ cu ’∧’ (şi logic).
(k)
Semnificaţia valorii unui element al matricii D (k) este următoarea: di,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 }.
Elementele matricii D (0) se iniţializează astfel:
(
(0) 1 , dacă i = j sau (xi , xj ) ∈ E
di,j = (8.22)
0 , dacă i 6= j şi (xi , xj ) ∈
/ E.
175
la xk la xj având vârfurile intermediare numai din mulţimea {x1 , . . . , xk−1 }:
(k) (k−1) (k−1) (k−1)
di,j = di,j ∨ (di,k ∧ dk,j ). (8.23)
Exemplul 8.7 Fie graful din figura 8.2, din care am eliminat arcul (1, 2). Matricea cos-
turilor asociată acestui graf modificat este următoarea:
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
176
8.3 Diametrul unui graf
Definiţia 8.4 Diametrul unui graf G = (V, E) este dat de lungimea celui mai mare drum
dintre toate drumurile de lungime minimă ı̂ntre oricare două noduri u şi v ale grafului.
Dacă graful G este un arbore fără rădăcină (graf neorientat aciclic conex), iar lungimea
distanţei dintre două noduri este dată de numărul de muchii al lanţului cel mai scurt, atunci
diametrul grafului G, diam(G), se poate calcula cu ajutorul următorului algoritm:
Pas 1. Se alege un nod oarecare s ∈ V şi se parcurge graful G folosind metoda de parcurgere
ı̂n lăţime BF S(s). Fie u ultimul nod vizitat ı̂n cadrul acestei parcurgeri.
177
Pas 2. Se parcurge ı̂n lăţime graful G plecând din nodul u. Fie v ultimul nod la care se ajunge
ı̂n cadrul acestei parcurgeri.
Definiţia 8.5 Se numeşte graf pe mai multe niveluri un graf orientat G = (V, E) ı̂n
care:
Într-un astfel de graf, se cere să de identifice un drum de cost minim de la vârful iniţial
xs la vârful final xt .
Dacă d este un drum optim (de cost minim) de la nodul xs la nodul xt , atunci pentru
orice xi vârf intermediar, drumurile de la xs la xi şi cel de la xi la xt sunt optime.
Fie xj ∈ Vi , i ∈ {1, 2, . . . , n − 1}. Notăm cu dij costul drumului minim de la xj la xt
(considerăm că vom avea dij = +∞ dacă nu există vreun drum de la xj la xt ).
Aceste relaţii presupun utilizarea metodei programării dinamice, mai exact metoda ı̂nainte
(a se vedea algoritmul 68).
178
Algoritm 68 Algoritm de calcul al drumului minim ı̂ntr-un graf pe nivele
1: procedure GrafPeNivele(n, C)
2: dn ← 0, nextn ← 0
3: for i ← n − 1, 1 ST EP − 1 do
4: di ← ∞, nexti ← 0
5: for j ← i + 1, n do
6: if ((cij < ∞) ∧ (di > cij + dj )) then
7: di ← cij + dj
8: nexti ← j
9: end if
10: end for
11: end for
12: if (d1 = ∞) then
13: Output {’Nu exista drum’}
14: else
15: k←1
16: while (k 6= 0) do
17: Output {k}
18: k ← nextk
19: end while
20: end if
21: end procedure
Exemplul 8.8 Fie graful pe mai multe niveluri din figura 8.5. Numărul de vârfuri este
n = 12.
În urma aplicării algoritmului prezentat pentru acest graf şi având vârfurile 1 drept sursă,
respectiv 12 destinaţie, se obţin următoarele valori pentru vectorii D şi Next:
1 2 3 4 5 6 7 8 9 10 11 12
D 11 10 8 9 8 2 ∞ 7 1 ∞ 2 0
Next 4 5 6 6 9 9 0 12 12 0 12 0
179
Prin urmare, drumul de lungime minimă de la 1 la 12 are costul 11 şi trece prin nodurile
[1, 4, 6, 9, 12].
8.5 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. 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.
3. 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)
4. 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)
180
Capitolul 9
Studierea fluxului ı̂n reţele de transport ı̂şi are originea ı̂n analiza unor probleme de transport
[112]. Un graf orientat poate constitui o structură ce intervine ı̂n modelarea procesul de
transport dintre un producător şi un consumator prin intermediul unei reţele de transport,
iar ceea ce se trimite pe un drum nu poate depăşi capacitatea sa de transport. La destinaţie
nu poate să ajungă o cantitate mai mare decât cea care a fost realizată de către producător.
În jurul nostru există foarte multe reţele cum ar fi reţeaua electrică, reţeaua de apă,
reţeaua de drumuri, reţeaua de legături telefonice, ce au devenit elemente indispensabile ale
modului de viaţă actual. O astfel de reţea de transport poate asigura curgerea unui lichid
printr-o reţea de conducte, deplasarea curentului prin reţele electrice, transportul de mărfuri
de-a lungul unei reţele de drumuri etc.
Fig. 9.1: Diagrama schematica a reţelei de căi ferate din vestul Uniunii Sovietice şi a ţărilor din estul Europei.
Fluxul maxim este de 163.000 de tone de material din Rusia către Europa de Est. [70]
181
Două dintre lucrările considerate a avea un caracter de pionierat ı̂n acest domeniu ([127]
şi [70]) ı̂şi bazează studiul de caz pe reţeaua feroviară existentă ı̂n fosta U.R.S.S. (cf. [112]).
Aşa cum observă şi autorii lucrării [63], o abordare a acestei probleme folosea algoritmi având
la bază programarea liniară, mai exact metoda simplex [32], ı̂nsă algoritmii dezvoltaţi inde-
pendent de această metodă au condus la rezultate mai bune ı̂n ceea ce priveşte complexitatea
timp.
Prima formulare a problemei determinării fluxului maxim ı̂ntr-o reţea de transport a fost
făcută de T. E. Harris ı̂n lucrarea [70]:
Există două probleme celebre pentru care s-a demonstrat matematic că sunt duale una
celeilalte: problema fluxului maxim ı̂ntr-o reţea de transport şi problema determinării tăieturii
de capacitate minimă.
În concluzie, putem asimila o reţea de transport cu un graf orientat ı̂n cadrul căruia arcele
facilitează fluxul de materiale ı̂ntre noduri, flux limitat de capacitatea maximă a fiecărui arc.
Procesul presupune stabilirea unui flux de resurse de la un producător (nodul sursă) către
un consumator (nodul destinaţie). Prin flux aici ı̂nţelegem debitul de deplasare (transport)
a resurselor.
Problema fluxului maxim ı̂ntr-o reţea de transport se reduce la a calcula debitul maxim al
unui flux de resurse de la producător la consumator, cu respectarea restricţiilor impuse de
capacitatea maximă de transport a fiecărui arc.
1. G = (V, E) este un graf orientat, unde fiecărui arc (u, v) ∈ E ı̂i este asociată o valoare
pozitivă denumită capacitate, c(u, v) ≥ 0;
2. există două noduri speciale, s, t ∈ V , s - sursă, t - destinaţie;
3. ∀u ∈ V \ {s, t}, nodul u se găseşte pe cel puţin un drum de la sursă la destinaţie.
Definiţia 9.2 Definim fluxul asociat reţelei de transport < G, c, s, t > drept o funcţie
f : V × V → R ce satisface următoarele condiţii:
182
P
3. ∀u ∈ V \ {s, t} avem v∈V f (u, v) = 0 (conservarea fluxului).
Definiţia 9.3 Se spune că arcul (u, v) este saturat dacă f (u, v) = c(u, v).
Dacă f (u, v) > 0 se spune că fluxul părăseşte nodul u, iar dacă, pentru arcul (u, v) avem
f (u, v) < 0 (echivalent cu f (v, u) > 0) spunem că fluxul intră ı̂n nodul u.
Definiţia 9.4 Definim valoarea fluxului f ca fiind valoarea fluxului total ı̂n nodul sursă:
X
|f | = f (s, v). (9.1)
v∈V
Drept urmare, valoarea fluxului ı̂n exces ce părăseşte nodul sursă s este:
X X
|f | = f (s, v) − f (u, s). (9.4)
v∈V,f (s,v)>0 u∈V,f (u,s)>0
Definiţia 9.5 Problema fluxului maxim presupune determinarea unui flux de valoare
maximă de la s la t pentru reţeaua de transport < G, c, s, t > [47], [48].
adică valoarea fluxului f este egală cu diferenţa dintre suma fluxurilor ce intră ı̂n destinaţie
minus suma fluxurilor ce părăsesc destinaţia.
Din regula de conservare a fluxului rezultă că:
X X
f (u, v) = f (v, u), ∀u ∈ V \ {s, t} (9.6)
v∈V,f (u,v)>0 v∈V,f (v,u)>0
Cu alte cuvinte, suma fluxurilor ce intră ı̂ntr-un nod u este egală cu suma fluxurilor ce
părăsesc acelaşi nod u.
Într-o reţea de transport avem următoarea relaţie:
X X X
( f (u, v) − f (w, u)) = 0. (9.7)
u∈V v∈V,f (u,v)>0 w∈V,f (w,u)>0
183
Pe de altă parte avem
X X X X X
( f (u, v) − f (w, u)) = ( f (s, v) − f (u, s))+
u∈V v∈V,f (u,v)>0 w∈V,f (w,u)>0 v∈V,f (s,v)>0 u∈V,f (u,s)>0
X X
( f (t, v) − f (u, t))
v∈V,f (t,v)>0 u∈V,f (u,t)>0
X X
= |f | + ( f (t, v) − f (u, t))
v∈V,f (t,v)>0 u∈V,f (u,t)>0
(9.8)
Observaţia 9.3 Pentru două mulţimi oarecare A, B (A, B ⊆ V ), f (A, B) se defineşte astfel:
XX
f (A, B) = f (u, v). (9.10)
u∈A v∈B
Propoziţia 9.4 Fluxul asociat unei reţele de transport < G, c, s, t > are următoarele proprie-
tăţi:
1. ∀A ⊆ V , f (A, A) = 0;
2. ∀A, B, C ⊆ V avem:
f (A ∪ B, C) = f (A, C) + f (B, C) − f (A ∩ B, C)
f (A, B ∪ C) = f (A, B) + f (A, C) − f (A, B ∩ C); (9.12)
f (A \ B, C) = f (A, C) − f (B, C)
f (C, A \ B) = f (C, A) − f (C, B); (9.13)
Definiţia 9.6 Fie f1 , f2 două fluxuri ı̂ntr-o reţea de transport < G, c, s, t > şi α ∈ R. Atunci
definim următoarele operaţii cu fluxuri:
184
Fig. 9.2: Un exemplu de reţea de transport
Observaţia 9.5 1. În figura 9.2 poate fi vizualizat un exemplu de reţea de transport. În cadrul
acesteia sunt indicate doar fluxurile pozitive. Astfel dacă pentru arcul (u, v) avem f (u, v) > 0,
aceasta va fi etichetată ”f(u,v)/c(u,v)”. Dacă f (u, v) ≤ 0, atunci arcul va fi etichetat doar
cu valoarea capacităţii ”c(u,v)”.
2. Dacă avem f1 = f (u, v) ≥ 0 şi f2 = f (v, u) ≥ 0, f1 ≥ f2 > 0 (a se vedea figura 9.3 (a)),
atunci putem aplica următoarea transformare (figura 9.3 (b)): arcul (u, v) este etichetat cu
”(f1 − f2 )/c1 ”, iar arcul (v, u) este etichetat cu ”c2 ” (regula de anulare).
În urma aplicării acestei tranformări, se elimină fluxurile negative din reţea ı̂n acelaşi timp
fiind ı̂ndeplinite restricţiile din definiţia unei reţele de transport.
Exemplul 9.6 În figura 9.2 este prezentat un exemplu de reţea de transport şi un flux f .
Ca o ilustrare a celor discutate vom calcula suma fluxurilor pentru nodul 1:
X
f (1, v) = f (1, s) + f (1, 2) + f (1, 3) + f (1, 4) + f (1, t)
v∈V (9.14)
= −f (s, 1) + (−1) + 12 + 0 + 0 = −11 − 1 + 12 = 0
f (1, 2) = 0 − 1 = −1
Sau
X
f (4, v) = f (4, s) + f (4, 1) + f (4, 2) + f (4, 3) + f (4, t)
v∈V (9.15)
= 0 + 0 + (−f (2, 4)) + 7 + 4 = −11 + 7 + 4 = 0
185
Tăietură. Fluxul de-a lungul unei tăieturi
Definiţia 9.7 O < s, t > - tăietură este o partiţie (A, B) a lui V (A ∩ B = ∅, A ∪ B = V ,
A, B ⊆ V ) cu proprietatea că s ∈ A şi t ∈ B.
Definiţia 9.8 Capacitatea unei < s, t > - tăieturi (A, B) este suma capacităţilor arcelor
ce au o extremitate ı̂n mulţimea A şi cealaltă extremitate ı̂n mulţimea B:
X
c(A, B) = c(u, v). (9.16)
u∈A,v∈B
not
X
|f | = f ({s}, V \ {s}) = f (s, v) (fluxul net ce părăseşte nodul sursă). (9.18)
v∈V
Lema 9.7 Pentru < G, c, s, t > reţea de transport, (A, B) o < s, t >-tăietură şi f un flux ı̂n
reţeaua de transport, avem
|f | = f (A, B) . (9.19)
(valoarea fluxului ı̂n reţeaua de transport este egală cu valoarea fluxului de-a lungul tăieturii).
Demonstraţie: Să ne reamintim că dacă (A, B) este o < s, t >-tăietură, avem A ∩ B = ∅,
A ∪ B = V . Atunci din propoziţia 9.4 obţinem
f (A, V ) = f (A, A ∪ B) = f (A, A) + f (A, B) − f (A, A ∩ B) = f (A, B) (9.20)
deoarece f (A, A) = 0 şi f (A, A ∩ B) = f (A, ∅) = 0.
Astfel pentru o < s, t >-tăietură avem f (A, B) = f (A, V ). Aplicând identitatea A =
(A \ {s}) ∪ {s} ı̂n relaţia 9.20, obţinem:
f (A, V ) = f (A \ {s}, V ) + f ({s}, V ) − f (∅, V ) = f (A \ {s}, V ) + f ({s}, V ) (9.21)
Pe de altă parte avem identitatea:
f (A \ {s}, V ) = 0. (9.22)
Din ultimele trei relaţii, 9.20, 9.21 şi 9.22, concluzionăm faptul că:
9.20 9.21 9.22
f (A, B) = f (A, V ) = f ({s}, V ) + f (A \ {s}, V ) = f ({s}, V ) = |f |. (9.23)
Corolarul 9.8 Următoarea inegalitate este adevărată pentru un flux oarecare f ı̂n reţeaua
de transport < G, c, s, t > şi (A, B) o < s, t >-tăietură:
|f | ≤ c(A, B) . (9.24)
Demonstraţie: Din Lema 9.7 avem:
L9.7 def
XX def X X def
|f | = f (A, B) = f (u, v) ≤ c(u, v) = c(A, B). (9.25)
u∈A v∈B u∈A v∈B
Tăietura minimă reprezintă modalitatea cea mai eficientă / simplă de a ı̂ntrerupe fluxul
(curgerea) de la s la t.
Problema tăieturii minime se referă la a determina o < s, t > - tăietură a cărei capacitate
să fie minimă.
186
9.2 Graf rezidual. Drum de ameliorare. Flux maxim–
tăietură minimă
Definiţia 9.9 Definim capacitatea reziduală a unui arc (u, v) aparţinând grafului G ast-
fel:
cR (u, v) = c(u, v) − f (u, v) . (9.26)
Definiţia 9.10 Graful rezidual [47] corespunzător grafului G = (V, E), capacităţii c şi
fluxului f este graful GR = (V, ER ) având funcţia de capacitate cR , unde ER se defineşte
astfel:
ER = {(u, v) ∈ V × V |cR (u, v) > 0}. (9.27)
Observaţia 9.9 Între două noduri u şi v din graful G vom avea cel mult două arce ı̂n graful
rezidual GR :
1. dacă (u, v) ∈ E şi f (u, v) < c(u, v) atunci există arcul (u, v) ı̂n graful rezidual ((u, v) ∈ ER )
şi cR (u, v) = c(u, v) − f (u, v) (cR (u, v) > 0);
2. dacă (u, v) ∈ E şi f (u, v) > 0 atunci există arcul (v, u) ı̂n graful rezidual ((v, u) ∈ ER ) şi
cR (v, u) = f (u, v).
Altfel spus, ı̂n graful rezidual GR pot să apară arce noi, care nu existau ı̂n graful iniţial
G. Un alt element ce merită subliniat se referă la faptul că dacă, ı̂ntre două noduri nu există
nici un arc ı̂n graful iniţial G, atunci nu va exista nici un arc ı̂ntre cele două noduri nici ı̂n
graful rezidual GR . Prin urmare, numărul total de arce din graful rezidual GR este cel mult
de două ori mai mare decât numărul arcelor din graful G.
Lema 9.10 Fie < G, c, s, t > o reţea de transport şi f un flux ı̂n această reţea de transport.
Dacă fR este un flux ı̂n reţeaua de transport < GR , cR , s, t > (GR este graful rezidual asociat
grafului G), atunci f + fR este un flux ı̂n < G, c, s, t >, iar |f + fR | = |f | + |fR |.
Demonstraţie: Pentru a arăta că f + fR este un flux ı̂n reţeaua de transport < G, c, s, t >
trebuie să verificăm dacă condiţiile din definiţia 9.2 sunt ı̂ndeplinite:
1. restricţie de capacitate
Avem fR (u, v) ≤ cR (u, v), ∀u, v ∈ V .
2. antisimetrie
3. conservarea fluxului
X X
(f + fR )(u, v) = (f (u, v) + fR (u, v))
v∈V v∈V
X X
= f (u, v) + fR (u, v) = 0 + 0 = 0 (9.30)
v∈V v∈V
187
Să demonstrăm că |f + fR | = |f | + |fR |:
X X
|f + fR | = (f + fR )(s, v) = (f (s, v) + fR (s, v))
v∈V v∈V
X X
= f (s, v) + fR (s, v) = |f | + |fR |. (9.31)
v∈V v∈V
not
Corolarul 9.11 Dacă f ′ = f +fR , unde fR este un flux ı̂n reţeaua de transport < GR , cR , s, t >
a grafului rezidual, atunci f ′ este un flux ı̂n < G, c, s, t > şi |f ′| = |f | + |fR | > |f |.
Lema 9.12 Fie f un flux ı̂n reţeaua de transport < G, c, s, t > şi GR graful rezidual asociat
cu G. Atunci avem:
1. funcţia fR este un flux maxim ı̂n reţeaua de transport < GR , cR , s, t > dacă f + fR este un
flux maxim ı̂n reţeaua de transport < G, c, s, t >;
2. valoarea funcţiei este aditivă: |f + fR | = |f | + |fR |, |f − fR | = |f | − |fR |;
3. dacă f este un flux oarecare şi f ∗ este fluxul maxim ı̂n reţeaua de transport < G, c, s, t >,
atunci valoarea fluxului maxim ı̂n reţeaua de transport < GR , cR , s, t > este |f ∗ | − |f |.
Definiţia 9.11 Fiind dată o reţea de transport < G, c, s, t > şi un flux f ı̂n această reţea,
atunci o cale de creştere d (drum de ameliorare, drum de augmentare) este un drum de la
s la t ı̂n graful rezidual GR .
Numim capacitate reziduală a unui drum de creştere d, cantitatea maximă a fluxului
ce poate fi transportat de-a lungul acestui drum de ameliorare d:
Observaţia 9.13 Unui drum de ameliorare ı̂n graful rezidual GR ı̂i corespunde un lanţ ı̂n
graful G.
Definiţia 9.12 Un arc (u, v) se numeşte arc critic dacă face parte dintr-un drum de ame-
liorare d şi dacă capacitatea sa reziduală este egală cu capacitatea reziduală a drumului de
ameliorare (c(u, v) = cR (d)).
Lema 9.14 Fie o reţea de transport < G, c, s, t >, f un flux ı̂n această reţea, şi d o cale
de creştere (drum) ı̂n GR , graful rezidual asociat reţelei de transport şi fluxului f . Atunci
funcţia fR : V × V → R descrisă prin
cR (d)
, dacă (u, v) ∈ d
fR (u, v) = −cR (d) , dacă (v, u) ∈ d
0 , altfel
este un flux pentru reţeaua de transport < GR , cR , s, t >, iar |fR | = cR (d).
Teorema 9.15 (flux maxim - tăietură minimă) [46], [48], [49]. Fie o reţea de transport
< G, c, s, t > şi f un flux ı̂n această reţea. Următoarele afirmaţii sunt echivalente:
188
1. f este un flux maxim ı̂n reţeaua de transport;
2. nu există nici o cale de creştere (drum de ameliorare) ı̂n reţeaua de transport < G, c, s, t >;
3. există o < s, t >-tăietură a lui G pentru care |f | = c(A, B) (există o < s, t >-tăietură pentru
care valoarea fluxului maxim este egală cu capacitatea tăieturii).
Demonstraţie: Vom demonstra această teoremă abordând următoarea serie de implicaţii:
1) ⇒ 2) ⇒ 3) ⇒ 1).
1) ⇒ 2)
Să presupunem prin reducere la absurd că există un drum de ameliorare ı̂n reţeaua de trans-
port < G, c, s, t > pentru un flux maxim |f |.
Dacă există un drum de ameliorare atunci există un flux nenul fp ı̂n reţeaua < GR , cR , s, t >.
Fie f ′ fluxul construit astfel: f ′ = f + fp . Din corolarul 9.11 obţinem că f ′ este un flux
ı̂n reţeaua de transport < G, c, s, t > şi |f ′| > |f |.
Am determinat astfel un flux f ′ ı̂n < G, c, s, t > a cărui valoare este mai mare decât
cea a fluxului f . Contradicţie cu faptul că f este un flux maximal ı̂n reţeaua de transport
< G, c, s, t >.
2) ⇒ 3)
Dacă nu există nici un drum de ameliorare ı̂n < G, c, s, t > atunci nu există nici un drum de
la s la t ı̂n graful rezidual GR (GR = (V, ER )).
Să notăm S = {v ∈ V |∃ un drum de la s la v ı̂n GR } şi T = V \ S.
Deoarece nu există nici un drum de la s la t ı̂n GR ⇒ t ∈ T . De asemenea avem că s ∈ S,
S ∩ T = ∅, S ∪ T = V . Prin urmare S, T este o < s, t >-tăietură.
Din cauza faptului că nu există nici nici un drum de la s la t ı̂n GR ⇒ nu există arce
ı̂n GR care să aibă o extremitate ı̂n S şi cealaltă extremitate ı̂n T ⇒ ∀u ∈ S, v ∈ T , dacă
∃(u, v) ∈ E atunci f (u, v) = c(u, v).
Aplicând Lema 9.7 obţinem că |f | = f (S, T ) = c(S, T ).
3) ⇒ 1)
Din Corolarul 9.8 avem |f | ≤ c(A, B), ∀(A, B) < s, t >-tăietură a lui G.
Deoarece |f | = c(A, B) rezultă că f este fluxul maxim ce se poate obţine ı̂n reţeaua de
transport < G, c, s, t >.
În concluzie, valoarea maximă a unui flux ı̂ntr–o reţea de transport < G, c, s, t > este
egală cu valoarea capacităţii tăieturii minime pentru aceeaşi reţea de transport.
Corolarul 9.16 Fie f un flux ı̂ntr-o reţea de transport < G, c, s, t >. Notăm cu Af mulţimea
nodurilor ce aparţin unui drum de ameliorare. Fie Bf = V \ Af . Atunci f este un flux
maximal dacă şi numai dacă t ∈ Bf .
Mai mult, Af , Bf reprezintă o < s, t >-tăietură minimă (de cost minim): |f | = c(Af , Bf ) .
189
Algoritm 69 Algoritmul Ford–Fulkerson (varianta simplificată)
1: se iniţializează fluxul cu valoarea 0
2: while (∃ un drum de ameliorare d ı̂n GR ) do
3: măreşte fluxul f de-a lungul drumului de ameliorare d
4: end while
5: return |f |
Fig. 9.4: Reţeaua reziduală corespunzătoare reţelei de transport din figura 9.2
Fie d = (s, 2, 3, t) o cale de creştere (drum de ameriorare) ı̂n graful din figura 9.4. Capacitatea
reziduală a acestui drum este:
cR (d) = min{5, 4, 5} = 4
Vom creşte valoarea fluxul f pentru arcele din graful G ı̂ntâlnite pe parcursul drumului
de ameliorare d. Arcele ı̂ntâlnite ı̂n cadrul căii de creştere d se clasifică astfel:
Noul flux corespunzător grafului din figura 9.2 şi drumului de augmentare d = (s, 2, 3, t)
poate fi urmărit ı̂n figura 9.5.
Vom prezenta o variantă mai elaborată a algoritmului Ford-Fulkerson (a se vedea algo-
ritmul 70).
190
Fig. 9.5: Flux ı̂ntr-o reţea de transport după o creştere pe baza unui drum de ameliorare
Observaţia 9.18 Determinarea fluxului maxim ı̂ntr-o reţea de transport având capacităţile
arcelor numere raţionale, precum şi valorile fluxului tot numere raţionale, se reduce la de-
terminarea unui flux maxim având componentele fluxului cât şi capacităţile arcelor numere
naturale (această reducere este posibilă deoarece valorile raţionale pot fi amplificate cu o valoa-
re pozitivă, cel mai mic multiplu comun al numitorilor acestor numere).
Complexitatea algoritmului
În tabelul 9.1 [134] sunt prezentaţi principalii algoritmi pentru determinarea fluxului maxim
ı̂ntr-o reţea de transport, anul ı̂n care au apărut, precum şi complexitatea lor. Astfel aceşti
algoritmi se pot compara din punct de vedere al complexităţii teoretice şi se poate alege
varianta potrivită rezolvării unor probleme concrete.
191
Table 9.1: Principalii algoritmi pentru determinarea fluxului maxim ı̂ntr-o reţea de transport
192
Algoritm 71 Algoritm pentru determinarea unui drum de ameliorare
1: procedure FindAugmentingPath(s, t, Cost, Vizitat; Tata)
2: Q⇐s
3: vizitats ← 1
4: while (Q 6= ∅) do
5: Q⇒u
6: for (fiecare k ∈ lista de vecini a lui u) do
7: if ((vizitatk = 0) ∧ (costu,k > 0)) then
8: Q⇐k
9: vizitatk ← 1
10: tatak ← u
11: if (k = t) then
12: return
13: end if
14: end if
15: end for
16: end while
17: end procedure
193
Algoritm 73 Algoritmul Edmonds-Karp
Edmondskarp(n, s, t, C; F )
1: procedure
n - numărul de vârfuri ale grafului
s - nodul sursă
Input:
t - nodul destinaţie
C - matricea capacităţilor reţelei de transport
Output: F - matricea ce conţine fluxul pe fiecare din arcele grafului
2: for (fiecare (u, v) ∈ E) do
3: costu,v ← cu,v
4: end for
5: repeat
6: for (fiecare u ∈ V ) do
7: vizitatu ← 0, tatau ← −1
8: end for
9: call FindAugmentingPath(s, t, Cost, Vizitat; Tata)
10: capacity ← GetM axCapacity(s, t, Cost, T ata)
11: call UpdateResidualGraph(s, t, capacity, Tata; Cost)
12: until (capacity = 0)
13: for (fiecare (u, v) ∈ E) do
14: if (cu,v > 0) then
15: f (u, v) ← cu,v − costu,v
16: end if
17: end for
18: end procedure
/**
* Se citesc numarul de noduri, numarul de arce, nodul sursa, nodul destinatie
* precum si matricea cu capacitatile arcelor.
*/
194
Fig. 9.6: Algoritmul Edmonds-Karp pentru reţeaua de transport din figura 9.2
void readInput(int &n, int &m, int &s, int &t, Matrice& c) {
int i, u, v;
ifstream fin(INPUT_FILE);
195
fin.close();
}
/**
* Functie ce afiseaza valorile elementelor unei matrice patratice.
* @param n - numarul de linii/coloane
* @param cost - matricea ce contine capacitatile reziduale ale arcelor
* din graful rezidual
*/
void afisare(int n, Matrice& cost) {
int i, j;
/**
* Functie pentru determinarea unui drum de ameliorare.
* @param n - numarul de noduri al grafului
* @param s - nodul sursa
* @param t - nodul destinatie
* @param cost - matricea ce contine capacitatile reziduale ale arcelor
* @param vizitat - vector ce pastreaza starea (vizitat/nevizitat) nodurilor
* grafului
* @param tata - nodul anterior nodului curent pe drumul de la sursa catre nodul
* curent
*/
void FindAugmentingPath(int n, int s, int t, Matrice& cost,
BoolVector& vizitat, IntVector& tata) {
int u, k;
queue<int> Q;
vizitat[s] = true;
Q.push(s);
while (!Q.empty()) {
u = Q.front();
Q.pop();
for (k = 0; k < n; k++) {
if ((!vizitat[k]) && (cost[u][k] > 0)) {
vizitat[k] = true;
tata[k] = u;
Q.push(k);
if (k == t) {
return;
}
}
}
196
}
}
/**
* Functia determina cea mai mica valoare dintre doua valori intregi.
*/
int min(int a, int b) {
return (a < b) ? a : b;
}
/**
* Determina capacitatea reziduala a unui drum de ameliorare.
*/
int GetMaxCapacity(int s, int t, Matrice& cost, IntVector& tata) {
int u, prev;
int capacity = INFINIT;
u = t;
while (tata[u] != -1) {
prev = tata[u];
capacity = min(capacity, cost[prev][u]);
u = prev;
}
/**
* Actualizeaza fluxul in reteaua de transport si respectiv capacitatile reziduale
* ale arcelor din graful rezidual.
*/
void UpdateResidualGraph(int s, int t, int capacity,
IntVector& tata, Matrice& cost) {
int u, prev;
u = t;
while (tata[u] != -1) {
prev = tata[u];
cost[prev][u] = cost[prev][u] - capacity;
cost[u][prev] = cost[u][prev] + capacity;
u = prev;
}
}
/**
* Implementarea algoritmului Edmonds-Karp.
*/
void EdmondsKarp(int n, int s, int t, Matrice& c) {
197
Matrice cost;
Matrice f;
int i, j;
int capacity;
BoolVector vizitat;
IntVector tata;
do {
for (i = 0; i < n; i++) {
vizitat[i] = false;
tata[i] = -1;
}
afisare(n, f);
}
int main() {
Matrice c;
int n, m, s, t;
readInput(n, m, s, t, c);
EdmondsKarp(n, s, t, c);
return 0;
}
Datele de intrare se citesc dintr-un fişier, formatul acestuia fiind următorul:
198
nr_de_noduri nr_de_arce nodul_sursa nodul_destinatie
varf1 varf2 capacitate1
varf3 varf4 capacitate2
....
6 10 0 5
0 1 16
0 2 13
1 2 10
1 3 12
2 1 4
2 4 14
3 2 9
3 5 20
4 3 7
4 5 4
(numărul de noduri este 6, numărul de arce este 10, nodul sursă este 0 iar nodul destinaţie
este 5).
Definiţia 9.13 Se numeşte flux de saturare un flux f cu proprietatea că orice drum de
la s la t conţine un arc saturat.
Algoritmul lui Dinic ı̂şi propune să determine un flux de saturare f ′ , urmat de actualizarea
fluxului f (f = f + f ′ ) şi apoi algoritmul se reia.
199
S S
Atunci L(s) = ( Vi , Ei ) este un subgraf al grafului G. L(s) mai este denumit graf
stratificat sau reţea stratificată (eng. layered network ) unde fiecare nivel/strat/layer este
alcătuit din mulţimea nodurilor Vk . L(s) conţine numai arce ce fac legătura ı̂ntre un nod al
unui strat (u ∈ Vk ) şi un alt nod aflat pe stratul imediat următor (v ∈ Vk+1 ), arce de alte
tipuri ale grafului GR , cum ar fi arce de ı̂ntoarcere sau arce de traversare, identificate ı̂n urma
unei vizitări ı̂n lăţime, nefiind luate ı̂n considerare.
Construim L(s,b t), subgraful obţinut din L(s) prin restricţia acestuia doar la drumurile
de lungime minimă de la s la t. Din punct de vedere computaţional, L(s, b t) se poate obţine
astfel: mai ı̂ntâi se aplică algoritmul de vizitare ı̂n lăţime (BFS) pentru nodul de pornire s
având drept rezultat L(s) şi apoi se aplică din nou algoritmul de vizitare ı̂n lăţime (BFS)
pornind din nodul t, de data aceasta mergând pe arcele inverse din graful L(s).
Subrutina PathFinding determină un drum minim de la nodul sursă s la nodul destinaţie
b t). Pentru determinarea unui drum de ameliorare P se porneşte de la nodul s
t ı̂n graful L(s,
şi, folosind numai arborele de parcurgere (ı̂n lăţime) construit ı̂n urma aplicării algoritmului
de vizitare ı̂n lăţime, se ajunge, după un număr minim de paşi, la nodul t.
În procedura FlowChange se actualizează valoarea fluxului de-a lungul arcelor ce aparţin
drumului P . Se şterg din L b toate arcele de devin saturate. Notăm cu Sat mulţimea tuturor
arcelor ce devin saturate la pasul curent şi care sunt eliminate din L. b
Se iniţializează două mulţimi Qr şi Ql cu mulţimea Sat.
În procedura RightPass se ia fiecare element din Qr şi, dacă pentru e = (u, v) ∈ Qr nu
mai există alte arce care să-l aibă ca extremitate finală pe v (d− (v) = 0), atunci se şterge v
din L b şi se adaugă la Qr toate arcele e′ = (v, w) ce ı̂l au pe v drept extremitate iniţială.
Subrutina LeftPass presupune realizarea unor operaţii simetrice; pentru fiecare element
din Ql , e = (u, v) ∈ Ql , dacă nu există arce care să aibă nodul u drept extremitate iniţială
(d+ (u) = 0), atunci se şterge u din L b ı̂mpreună cu arcele ce ı̂l au pe u drept extremitate
finală, arce ce se adaugă la Ql .
200
9.6 Aplicaţii
Problema determinării fluxului maxim ı̂ntr-o reţea de transport are foarte multe aplicaţii
ı̂n domenii variate precum data mining, segmentarea de imagini, determinarea cuplajului
maxim ı̂ntr-un graf bipartit, selectarea proiectelor, programarea zborurilor, calcul distribuit,
identificarea ı̂ncercărilor de intruziune ı̂ntr-o reţea de calculatoare, fiabilitatea unei reţele.
Pentru o reţea de transport cu surse multiple {s1 , s2 , . . . , sn } şi destinaţii multiple {t1 , t2 ,
. . ., tm } se procedează astfel:
1. se adaugă o sursă s0 şi arce de la s0 către si , i = 1, n unde cs0 ,si = ∞ şi f (s0 , si ) = |f (si)|;
Se poate aplica apoi unul dintre algoritmii pentru determinarea fluxului maxim ı̂n reţeaua
de transport.
Fie o reţea de transport < G, c, s, t >. În această reţea, fiecărui arc i se alocă un cost
unitar, ϕ(u, v) ≥ 0, reprezentând costul plătit pentru fiecare unitate de flux ce trece prin
arcul (u, v). Problema determinării P
fluxului maxim de cost minim se referă la identificarea
unui flux maxim cu proprietatea că (u,v)∈G f (u, v) · ϕ(u, v) este minimă.
9.7 Exerciţii
1. Aplicându-se algoritmii Ford-Fulkerson, Edmonds-Karp şi Dinic să se determine fluxul
maxim ı̂n reţelele de transport din figura 9.7. Nodul 0 este nodul sursă iar nodul 5
respectiv nodul 6 este nodul destinaţie.
• Să se determine numărul maxim de drumuri disjuncte ı̂ntre u şi v, cât şi componenţa
acestora. Două drumuri sunt considerate disjuncte dacă nu au nici un arc ı̂n co-
mun.
4. Algorel are ı̂n pivniţă o reţea subterană prin care mişună şobolani. Reţeaua constă din
N adăposturi numerotate de la 1 la N şi M tunele ı̂ntre acestea. Adăposturile 1 şi N
comunică cu suprafaţa fiind singurele locuri din reţea cu această proprietate (nici un
alt adăpost sau tunel nu mai are ieşire la suprafaţă).
201
1 4 3
1 4 3 5 3
10 10
8 1
0 2 5 0 5
8 6 2
10 9 9
10
2 9 4 2 3 4
a) b)
1 9 4
1 1 4
10 10 1
15 15
4 3 4
2 6
0 5 2 8 5 10 5
0 2 2
3
9
4 6 15 1
15 10
3 5
3 6
3 30 4
c) d)
Algorel vrea să trimită un număr maxim de pisici ı̂n reţea. Pisicile intră prin adăpostul
1 şi ies prin adăpostul N. Nici o pisică nu poate rămâne ı̂n reţea. Pentru fiecare tunel
se ştie rezistenţa lui, adică numărul maxim de pisici ce pot trece prin el fără ca să se
dărâme. Dacă printr-un tunel cu rezistenţa nenulă a trecut o pisică, atunci rezistenţa
acestuia scade cu o unitate.
Algorel a observat existenţa unor anumite tunele cu proprietatea că, dacă rezistenţa lor
202
Table 9.2: Capacităţile mijloacelor de transport
B1 B2 B3 B4
A1 140 60 40 -
A2 100 80 20 -
A3 - 40 80 160
A4 - 40 80 160
creşte, ı̂n timp ce rezistenţa celorlalte tunele rămâne la fel, atunci va creşte şi numărul
maxim de pisici pe care le poate trimite prin reţea. El a denumit tunelele cu această
proprietate tunele critice.
Fiind dată reţeaua din pivniţa lui Algorel determinaţi tunelele critice.
Date de intrare
Pe prima linie a fişierului critice.in se găsesc numerele naturale N şi M (1 ≤ N ≤
1000, 1 ≤ M ≤ 10000) reprezentând numărul de adăposturi şi numărul de tunele din
reţeaua subterană. Urmează M linii conţinând trei numere naturale separate prin
spaţii, A B C, având următoarea semnificaţie: ı̂ntre adăposturile A şi B (A 6= B)
există un tunel cu rezistenţa C.
Date de ieşire
Prima linie a fişierului de ieşire critice.out conţine un număr natural K reprezentând
numărul de tunele critice din reţeaua lui Algorel. Următoarele K linii conţin câte un
număr natural reprezentând indicii muchiilor critice. Indicii vor fi ordonaţi crescător.
Muchiile sunt numerotate de la 1 la M după ordinea din fişierul de intrare.
Rezistenţele sunt numere naturale mai mici sau egale cu 10000. Între oricare două
noduri există cel mult un tunel. Orice tunel poate fi parcurs ı̂n ambele sensuri.
Exemplu
critice.in critice.out
5 6 2
2 1 2 1
2 3 3 4
3 5 4
1 4 7
4 3 2
4 5 6
(PreONI, 2005, Critice)
203
Datele de intrare se citesc din fişierul drumuri2.in: pe prima linie se află numerele
naturale n şi m, separate printr-un spaţiu (1 ≤ n ≤ 100, 1 ≤ m ≤ 5000). Pe fiecare
dintre următoarele m linii se găseşte câte o pereche de numere naturale i, j (1 ≤ i, j ≤ n)
separate printr-un spaţiu, cu semnificaţia că există arc de la vârful i la vârful j.
Rezultatul va fi afişat ı̂n fişierul de ieşire drumuri2.out. Acesta va conţine o singură
linie reprezentând numărul minim de drumuri cu care se poate acoperi graful din fişierul
de intrare.
Exemplu
drumuri2.in drumuri2.out
7 7 2
1 2
7 2
2 3
2 4
3 5
4 5
4 6
(Lot 2005, Drumuri)
6. După ce au ı̂mpărţit ţară ı̂n judeţe maimuţele au o altă problemă, trebuie să oprească
traficul de banane. Ţara maimuţelor are n oraşe numerotate de la 1 la n legate ı̂ntre ele
prin m şosele bidirecţionale. Între oricare două oraşe există maxim o şosea, dar există
drum - direct sau prin oraşe intermediare. Oraşele 1 şi n sunt capitale.
În ultima vreme ı̂ntre cele două capitale s-a intensificat traficul cu banane. Pentru a
combate traficul preşedintele are la dispoziţie G soldaţi pe care poate să-i aşeze oriunde
pe o şosea, oricât de aproape de un oraş, ı̂nsă nu ı̂n oras. În cazul unui atac asupra
uneia dintre capitale, toţi soldaţii trebuie să ajungă ı̂n acea capitală. Soldaţii se mişcă cu
aceeaşi viteză constantă. Timpul necesar unei astfel ce mobilizări este egal cu maximul
distanţelor de la soldaţi la una dintre capitale.
Să se realize o aplicaţie care să determine o aşezare a soldaţilor astfel ı̂ncât orice rută
de la o capitală la alta să treacă printr-o şosea cu cel puţin un soldat şi timpul de
mobilizare ı̂n cel mai rău caz să fie minim.
Date de intrare se citesc din fişierul trafic.in. Pe prima linie a acestuia sunt scrise
trei numere NMG separate prin câte un singur spaţiu (3 ≤ n ≤ 154, 3 ≤ m ≤ 5054,
3 ≤ G ≤ 4095). Pe următoarele m linii sunt scrise câte trei numere separate prin spaţii
abc cu semnificaţia ”există şosea bidirecţională de la oraşul a la oraşul b de lungime c”
(1 ≤ lungimea unei şosele ≤ 1023).
Date de ieşire se vor afişa ı̂n fişierul trafic.out. Prima linie a fişierului va conţine
timpul minim de mobilizare cu o zecimală exactă. Dacă nu există soluţie se va scrie
−1.
Exemplu
204
trafic.in trafic.out
6 6 2 2.5
1 2 1
2 3 2
3 6 1
1 4 1
4 5 3
5 6 1
(.campion 2005, Trafic)
205
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.
206
[16] J. Bentley, R. Sedgewick, Ternary Search Trees, Dr. Dobb’s Journal, 1998.
[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.
[25] B. Chazelle, A minimum spanning tree algorithm with inverse-Ackerman type complexity,
J. ACM, 47, pp. 1028-1047, 2000.
[26] D. Cherition, R. E. Tarjan, Finding minimum spanning trees, SIAM Journal on Com-
puting, vol. 5, pp. 724-741, 1976.
[27] J. Cheriyan, K. Mehlhorn, Algorithms for dense graphs and networks on the random
access computer, Algorithmica, vol. 15, pp. 521-549, 1996.
[28] 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.
[31] C. Croitoru, Tehnici de bază ı̂n optimizarea combinatorie, Editura Univ. Al. I. Cuza Iaşi,
Iaşi, 1992.
[32] G. B. Dantzig, Linear programming and extensions, University Press, Princeton, 1962.
[35] R. B. Dial, Algorithm 360: shortest-path forest with topological ordering [H], Communi-
cations of the ACM, vol. 12:11, pp. 632-633, 1969.
207
[36] Dicţionarul explicativ al limbii române, Academia Română, Institutul de Lingvistică
Iorgu Iordan, Editura Univers Enciclopedic, 1998.
[37] E. W. Dijkstra, A note on two problems in connections with graphs, Numerische Math-
ematik, 1, pp. 269-271, 1959.
[38] 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.
[39] 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.
[40] J. Edmonds, Paths, Trees and Flowers, Canadian J. Math, vol. 17, pp. 449-467, 1965.
[43] J. Farey, On a Curious Property of Vulgar Fractions, London, Edinburgh and Dublin
Phil. Mag. 47, 385, 1816.
[44] 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, Research Memo-
randum RM-1400, The RAND Corporation, Santa Monica, California, 1954 [published
in 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, Research Memorandum RM-1604,
The RAND Corporation, Santa Monica, California, 1955 [published in 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.
[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.
208
[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.
[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.
[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.
[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.
[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.
209
[71] I. N. Herstein, I. Kaplansky, Matters mathematical, 2nd Edition, Chelsea Publishing
Company, 1978.
[72] C. Hierholzer, C. Wiener, Ueber die Möglichkeit, einen Linienzug ohne Wiederholung
und ohne Unterbrechung zu umfahren, Mathematische Annalen, vol. 6(1), pp. 30–32,
1873.
5
[73] 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.
[74] J. E. Hopcroft, R. E. Tarjan, Algorithm 447: efficient algorithms for graph manipulation,
Communications of the ACM, vol. 16(6), pp. 372-378, 1973.
[78] 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.
[79] D. Jungnickel, Graphs, Networks and Algorithms, Algorithms and Computation in Math-
ematics, vol. 5, 3rd Edition, Springer, 2008.
[80] A. B. Kahn, Topological sorting of large networks, Communications of the ACM, vol.
5(11), pp. 558-562, 1962.
[82] 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.
[84] D. C. Kozen, The Design and Analysis of Algorithms. Texts and Monographs in Com-
puter Science, Springer, 1993.
[85] 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.
210
[89] D. E. Knuth, Arta programării calculatoarelor, vol. 3 Sortare şi Căutare, Teora, Bu-
cureşti, 2001.
[90] 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.
[92] H. W. Kuhn, The Hungarian Method for the assignment problem, Naval Research Lo-
gistics Quarterly, vol. 2, pp. 83-97, 1955.
[93] H. W. Kuhn, Variants of the Hungarian method for assignment problems, Naval Research
Logistics Quarterly, vol. 3, pp. 253-258, 1956.
[95] L. Livovschi, H. Georgescu, Analiza şi sinteza algoritmilor, Ed. Ştiinţifică şi Enciclope-
dică, Bucureşti, 1986.
[96] 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.
[97] K. Mehlhorn, Data Structures and Algorithms: Graph Algorithms and NP-Completeness,
Springer Verlag, 1984.
p
[98] 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.
[100] E. F. Moore, The shortest path through a maze, in Proceedings of International Sym-
posium on the Theory of Switching, Part II, pp. 285-292, 1959. (prezentat la simpozion
la Universitatea Harvard in aprilie 1957)
[101] R. Motwani, Average-case analysis of algorithms for matchings and related problems,
Journal of the ACM, vol. 41(6), pp. 1329-1356, 1994.
[102] 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.
[103] G. Nivasch, Cycle detection using a stack, Information Processing Letters, vol. 90(3),
pp. 135–140, 2004.
[105] I. Odăgescu, F. Furtună, Metode şi tehnici de programare, Computer Libris Agora,
Cluj–Napoca, 1998.
[106] 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 (ICALP’02), LNCS Vol. 2380, pp. 85-97, 2002.
211
[107] S. Pettie, V. Ramachandran, Computing shortest paths with comparisons and additions,
in Proceedings of the 13th Annual ACM-SIAM Symposium on Discrete Algorithms
(SODA’02), SIAM, pp. 267-276, 2002.
[109] R. C. Prim, Shortest connection networks and some generalizations, Bell System Tech-
nical Journal, 36, pp. 1389-1401, 1957.
[110] B. Schieber, U. Vishkin, On finding lowest common ancestors: Simplification and par-
allelization, SIAM J. Comput., vol. 17, pp. 1253-1262, 1988.
[111] L. Schmitz, An improved transitive closure algorithm, Computing, vol 30(4), pp. 359-
371, 1983.
[112] A. Schrijver, On the history of the transportation and maximum flow problems, Math-
ematical Programming, vol. 91, issue 3, pp. 437-445, 2002.
[114] R. Seidel, C. Aragon, Randomized Search Trees, Algorithmica, vol. 16, pp. 464-497,
1996.
[115] 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.
[116] Y. Shiloach, U. Vishkin, An O(n2 log n) parallel max-flow algorithm, Journal of Algo-
rithms, vol. 3(2), pp. 128-146, 1982.
[117] K. Simon, An improved algorithm for transitive closure on acyclic digraphs, Theoretical
Computer Science, vol 58(13), pp. 325–346, 1988.
[118] S. Skiena, The Algorithm Design Manual, 2nd Edition, Springer, 2008.
[119] D. D. Sleator, An O(EV log V ) algorithm for maximum network flow, Technical Report,
STAN-CS-80-831, 1980.
[120] D. D. Sleator, R. E. Tarjan, A data structure for dynamic trees, Journal of Computer
Sciences, vol. 26, pp. 362-391, 1983.
[121] J. Stasko, J. Vitter, Pairing heaps: Experiments and analysis, Communications of the
ACM, vol. 30(3), pp. 234-249, 1987.
[122] 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.
[123] R. E. Tarjan, Depth-first search and linear graph algorithms, SIAM Journal on Com-
puting, vol. 1(2), pp. 146-160, 1972.
[124] R. E. Tarjan, Edge-disjoint spanning trees and depth-first search, Algorithmica, vol.
6(2), pp. 171-185, 1976.
212
[125] R. E. Tarjan, Applications of path compression on balanced trees, Journal of the ACM,
vol. 26(4), pp. 690-715, 1979.
[126] R. E. Tarjan, A simple version of Karzanov’s blocking flow algorithm, Operation Re-
search Letters, vol. 2, pp. 265-268, 1984.
[128] I. Tomescu, Grafuri şi programare liniară, Ed. Didactică şi Pedagogică, Bucureşti, 1975.
[129] I. Tomescu, Probleme de combinatorică şi teoria grafurilor, Ed. Didactică şi Pedagogică,
Bucureşti, 1981.
[130] I. Tutunea, Algoritmi, logică şi programare, Reprografia Universităţii din Craiova, 1993.
[131] I. Tutunea, S. Pescăruş, Algoritmi şi programe Pascal. (Culegere de probleme), Repro-
grafia Universităţii din Craiova, 1994.
[133] J. Vuillemin, A unifying look at data structures, Communications of the ACM, vol.
23:4, pp. 229-239, 1980.
[134] G. A. Waissi, A new Karzanov-type O(n3 ) max-flow algorithm, Mathematical and Com-
puter Modelling, vol. 16(2), pp. 65-72, 1992.
[137] J. W. J. Williams, Algorithm 232 - Heapsort, Communications of the ACM, vol. 7(6),
pp. 347-348, 1964.
[138] 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.
[139] D. Zaharie, Introducere ı̂n proiectarea şi analiza algoritmilor, Eubeea, Timişoara, 2008.
213