www.agora.ro

Noduri, muchii, arce ... (2)

Introducere în teoria GRAFURILOR

Cristian Cadar

Unul dintre cele mai interesante domenii ale teoriei grafurilor este determinarea de drumuri minime în grafuri. În cadrul acestui episod vă vom prezenta cătiva algoritmi folositi pentru a determina astfel de drumuri.

Algoritmii de drum minim se împart în patru categorii:

  • sursă unică - destinație unică (pentru determinarea drumului minim de la un nod numit sursă la un nod numit destinație );
  • sursă unică - destinații multiple (pentru determinarea drumurilor minime de la un nod numit sursă la toate celelalte noduri ale grafului);
  • surse multiple - destinație unică (pentru determinarea drumurilor minime de la toate nodurile grafului la un nod numit destinație);
  • surse multiple - destinații multiple (pentru determinarea drumurilor minime între oricare două noduri ale grafului).

Trebuie menționat faptul că algoritmii de tip sursă unică - destinație unică nu au o complexitate mai bună decât cei de tip sursă unică - destinații multiple. În ceea ce privește algoritmii de tip surse multiple - destinație unică, ei sunt echivalenți cu cei de tip sursă unică - destinații multiple, inversând inițial sensul arcelor în cazul grafurilor orientate. Algoritmii de tip surse multiple - destinații multiple pot fi implementați folosind algoritmii de tipul sursă unică - destinații multiple pentru fiecare nod al grafului dat. To-tuși, pentru această problemă există și abordări mai eficiente. Rezumând cele spuse mai sus, rezultă că în esență există două tipuri de algoritmi de drum minim: sursă unică - destinații multiple și surse multiple - destinații multiple.

O altă clasificare a algoritmilor de drum minim este în funcție de costul muchiilor. Vom avea astfel patru situații:

  • graful este neponderat, deci costul pe muchii este unitar;
  • graful este ponderat cu toate costurile pozitive;
  • graful este ponderat cu costurile pe muchii pozitive și negative, însă fără cicluri negative;
  • graful este ponderat cu costurile pe muchii pozitive și negative și conține cicluri negative.

În ceea ce privește prima situație (a unui graf neponderat) lungimea drumului minim dintre două noduri este egală cu numărul minim de muchii dintre cele două noduri. Pentru a afla lungimea drumului minim dintre două noduri în această situație, vom aplica algoritmul BF prezentat în articolul anterior, în care vom memora pentru fiecare nod timpul la care a fost vizitat (timpul la care a fost vizitat nodul de start este considerat 0) și predecesorul (tatăl) său din parcurgere.

Parcurgere_BF(nod start)

Coada Ź start

Vizitat(start) Ź True

Timp(start) Ź 0

Tata(start) Ź 0

cât timp coada nu este vidă

ic Ź nodul de la inceputul cozii

pentru fiecare vecin v nevizitat al lui ic

Vizitat(v) Ź True

Timp(v) Ź Timp(ic)+1

Tata(v) Ź ic

Adaugă v în coadă

Avansează în coadă

În finalul algoritmului, vom avea în vectorul Timp lungimile drumurilor minime de la nodul start la toate celelalte noduri ale grafului.

Reamintim că acest algoritm are complexitatea O(n+m). Pentru a reconstitui drumul minim de la nodul start la un anumit nod k, se folosește bineînțeles vectorul taților:

Afișare_Drum(nod k)

dacă Tata(k) č 0

atunci Afișare_Drum(Tata(k))

scrie k

De remarcat faptul că fiecare drum minim între două puncte ale unui graf conține în interiorul său alte drumuri minime. Mai precis, dacă i1, i2, ..., in este un drum minim și ik un nod aparținând acestui drum, atunci i1, i2, ..., ik și ik, ik+1, ..., in sunt drumuri minime. Demonstrația acestui fapt este evidentă și stă la baza majorității algoritmilor de drum minim existenți.

În ceea ce privește grafurile care conțin cicluri de cost negativ, cum este cazul grafului din figura 1, drumul minim dintre oricare două noduri este -„. Acest lucru este evident, deoarece oricare drum dintre două noduri poate fi micșorat prin parcurgerea repetată a ciclului negativ.

Cel mai cunoscut, dar și unul dintre cei mai eficienți algoritmi de tip sursă unică - destinații multiple pentru grafuri cu muchii de cost pozitiv este algoritmul Dijkstra. În cele ce urmează, vom prezenta și vom demonstra algoritmul Dijkstra.

Acest algoritm află toate drumurile minime de la un nod s (sursă) la toate celelalte noduri ale grafului în ordine crescătoare a lungimii acestor drumuri. De exemplu, pentru graful din figura 2, în care sursa este considerată nodul 1, drumurile sunt generate în ordinea:

În cazul în care două drumuri au același cost, algoritmul Dijkstra le poate genera în orice ordine, în funcție de modul în care au fost memorate muchiile grafului.

Algoritmul lui Dijkstra folosește o mulțime S care reține nodurile pentru care (la un anumit moment) s-au generat drumurile minime de la sursă la nodurile respective. Mulțimea S este inițializată cu nodul sursă. Apoi, aceasta este completată rând pe rând cu toate celelalte noduri ale grafului, în ordine crescătoare față de lungimea drumului minim de la s (nodul sursă) la acele noduri. Pentru graful din figura 2, ordinea în care sunt introduse nodurile în S este: 1, 4, 3, 2, 6, 7, 5.

De asemenea, algoritmul folosește un tablou unidimensional DIST, DIST(i) reținând lungimea drumului minim (distanța) de la s la i. La începutul algoritmului, vectorul DIST este inițializat cu lungimile muchiilor de la s la nodurile respective. În cazul în care nu există muchie între nodul s și un nod k, DIST(k) va fi inițializat cu „. DIST(s) va fi inițializat cu valoarea 0.

Algoritmul Dijkstra are n-1 pași. La fiecare pas, este introdus în S un nod care bineînțeles nu aparține mulțimii și pentru care distanța de la s la acesta este minimă (deci acel kÏS pentru care DIST(k) este minim).

Având aceste structuri, putem face următoarele observații care prezintă și demonstrează modul în care acționează algoritmul Dijkstra.

1. În momentul în care se generează un nou drum minim, atunci acel drum începe din s, se termină într-un nod v care nu aparține lui S și conține numai noduri ce aparțin lui S. Vom demonstra în cele ce urmează că toate nodurile de pe drumul minim (s, ..., v) aparțin lui S. Pentru aceasta, vom presupune prin absurd că există un nod w care aparține acestui drum și care nu aparține lui S. Atunci drumul (s, ..., w, ..., v) poate fi împărțit în două drumuri minime (s, ..., w) și (w, ..., v). Dar lungimea drumului (s, ..., w) este mai mică decât cea a drumului (s, ..., w, ..., v). Rezultă contradicție cu faptul că nodurile sunt introduse în S în ordine crescătoare a distanței față de s. Deci, în DIST(k) vom reține la un moment dat lungimea drumului minim de la s la k, trecând doar prin noduri din S.

2. La un anumit moment, după generarea unui drum minim pentru un nod v, acesta este introdus în mulțimea S, deci lungimea unui drum minim de la nodul s la un nod w care nu aparține lui S și cu toate celelalte noduri intermediare în S se poate modifica (mai precis se poate micșora). Dacă acest drum se schimbă, atunci înseamnă că există un drum mai scurt de la s la nodul respectiv, care trece prin ultimul nod introdus în S, nodul v. Atunci, așa cum am demonstrat anterior, toate nodurile intermediare care aparțin drumului (s, ..., v, ..., w) trebuie să aparțină lui S. Drumul (s, ..., v, ..., w) poate fi descompus (așa cum am arătat anterior) în două drumuri minime (s, ..., v) și (v, ..., w). Bineînțeles, drumul (v, ..., w) poate fi ales chiar muchia (v, w), iar lungimea drumului (s, ..., v) va fi reținută în DIST(v). Este corect să alegem muchia (v, w) deoarece pe parcursul algoritmului se va încerca optimizarea lungimii drumului minim de la s la w prin toate nodurile v din S.

Pe lângă demonstrarea corectitudinii algoritmului, aceste observații oferă indicațiile necesare implementării algoritmului Dijkstra:

Dijkstra(graf G,nod s)

Inițializare S,DIST

pentru pas=1,n-1

selectează nodul vÏS pentru care

DIST(v) este minim

S=S+{v}

pentru wÏS și există muchia (v,w)

dacă DIST(w)>DIST(v)+cost(v,w)

atunci DIST(w)=DIST(v)+cost(v,w)

Se observă că modul în care este implementată mulțimea S este decisiv pentru o bună complexitate a algoritmului Dijkstra. Dacă mulțimea S este un vector cu elemente de tip logic, atunci complexitatea algoritmului este O(n2+m) = O(n2), găsirea minimului într-o astfel de structură fiind O(n) (linia (1)), iar introducerea unui element (linia (2)) și stabilirea apartenenței (linia (3)) O(1). Pentru a reduce complexitatea algoritmului, vom reține pe lângă vectorul logic S, mulțimea V/S sub forma unui heap. Se știe că structura de heap permite:

  1. - găsirea minimului în O(1);
  2. - introducerea unui element în O(log n);
  3. - ștergerea unui element în O(log n);
  4. - căutarea unui element în O(n).

Folosind doar această structură, evaluarea liniei (1) se va face în O(n)3, a liniei (2) în O(n log n), a liniei (3) în O(n-m), iar a liniei (4) în O(m log n). Folosind însă și vectorul logic S, vom reuși să reducem linia (3) la complexitatea O(m), deci complexitatea totală a algoritmului Dijkstra devine O(m log n). Menționăm că folosirea heap-urilor de tip Fibonacci reduc complexitatea la O(n log n+m), complexitate optimă pentru acest algoritm.

Dacă graful conține muchii de cost negativ, algoritmul Dijkstra nu mai funcționează corect, deoarece nu putem găsi - ca și până acum - nodurile cele mai apropiate de sursă, în ordine crescătoare a distanței față de aceasta. Să presupunem prin reducere la absurd că putem determina și în acest caz mulțimea S folosită de algoritmul Dijkstra. Fie v ultimul nod introdus în mulțimea S, deci un nod pentru care cunoaștem drumul minim de la s la el, drum ce conține numai noduri din S. Fie w un nod care nu aparține lui S și (v, w) o muchie de cost negativ foarte mic. Atunci drumul minim de la s la v va fi compus din drumul minim de la s la w urmat de muchia (w,v). Obținem astfel contradicție cu faptul că vârfurile de pe drumul minim de la s la v aparțin lui S, deci nu putem reține - ca în cazul algoritmului Dijkstra - această mulțime S.

Această problemă este rezolvată de algoritmul Bellman-Ford, care în plus determină și existența ciclurilor de cost negativ care pot fi atinse pornind din nodul sursă. Ca și în cazul algoritmului Dijkstra, vom folosi vectorul DIST care reține distanța minimă găsită la un moment dat de la s la celelalte noduri. De asemenea, algoritmul folosește o coadă Q, care este inițializată cu nodul s. Apoi, la fiecare pas, algoritmul scoate un nod v din coadă și găsește toate nodurile w a căror distanță de la sursă la acestea poate fi optimizată prin folosirea nodului v. Dacă nodul w nu se află deja în coadă, el este adăugat acesteia. Acești pași se repetă până când coada devine vidă. În continuare este prezentat algoritmul Bellman-Ford în pseudocod.

Bellman-Ford(graf G, nod s)

Inițializare Q,DIST

cât timp Q nu este vidă

scoate v din coadă

pentru fiecare muchie (v,w)

dacă DIST(w)>DIST(v)+cost(v,w)

atunci DIST(w)=DIST(v)+cost(v,w)

dacă wÏQ atunci inserează w în Q

Observăm că fiecare nod poate fi scos din coadă de cel mult n ori, deci complexitatea algoritmului este O(n-m). Așa cum este prezentat algoritmul, el va rula la infinit în cazul ciclurilor de cost negativ. Această problemă se rezolvă foarte ușor, deoarece în momentul în care un nod a fost scos pentru a n+1 oară din coadă, atunci în mod sigur există un ciclu negativ în graful dat.

O situație particulară deosebit de importantă a algorit-mului Dijkstra este cazul grafurilor orientate aciclice, având costuri pozitive. În acest caz, complexitatea algoritmului Dijkstra va fi redusă la O(n+m). Pentru aceasta, nodurile vor fi parcurse în ordine topologică. Astfel linia (1) va fi eliminată, ordinea de parcurgere a nodurilor fiind cunoscută dinainte. Corectitudinea acestei strategii este evidentă, deoarece din s nu vom putea ajunge la un nod i înainte de a ajunge la un nod k, dacă nodul i se află după nodul k în sortarea topologică (deci există un drum de la nodul k la nodul i, dar nu și invers).

În cele ce urmează, ne vom ocupa de algoritmi de tipul surse multiple - destinații multiple. O primă posibilitate de aflare a drumurilor minime între oricare două perechi de vârfuri ale unui graf este aplicarea de n ori a algoritmului Dijkstra, complexitatea astfel obținută fiind O(n3), pentru cazul în care mulțimea S este implementată sub formă de vector de tip logic. În cele ce urmează, vom prezenta însă și un alt algoritm care realizează acest lucru, și care, deși are aceeași complexitate, este mult mai simplu de implementat. Acest algoritm este algoritmul lui Floyd și se bazează pe principiul programării dinamice. Algoritmul folosește o matrice tridimensională D, D(i, j, k) reținând lungimea drumului minim de la i la j, folosindu-se în componența drumurilor doar primele k noduri. Prin definiție, D(0, i, j) = A(i, j), unde

Bineînțeles, lungimile drumurilor minime între două vârfuri se vor afla în matricea bidimensională D(n).

Se poate determina ușor relația de recurență pentru a completa matricea D:

D(k, i, j) = min( D(k-1, i, j), D(k-1, i, k) + D(k-1, k, j))

Se observă că pentru a completa matricea D(k) avem nevoie doar de matricea D(k-1), deci în total vom avea nevoie doar de două matrice bidimensionale. Mai mult chiar, observând că D(k, i, k) = D(k-1, i, k) și că D(k, k, j) = = D(k-1, k, j), va rezulta că nu vom avea nevoie decât de o singură matrice. Egalitățile enunțate mai sus sunt evident corecte, deoarece - cu excepția cazului în care există un ciclu de cost negativ - nu putem optimiza un drum care se termină și care începe cu k folosind tot nodul k.

Rezumând cele spuse mai sus, rezultă algoritmul foarte concis de mai jos:

Floyd(graf G)

D Ź A

pentru k=1,n

pentru i=1,n

pentru j=1,n

dacă D(i,j)>D(i,k)+D(k,j)

atunci D(i,j)=D(i,k)+D(k,j)

Menționăm că algoritmul lui Floyd funcționează corect pentru grafuri cu costuri negative pe muchii.

Având la bază acest algoritm este ușor să realizăm un algoritm care transformă matricea de adiacență în matricea drumurilor (matricea drumurilor este o matrice binară pentru care elementul (i, j) este 1 doar dacă există drum între vârfurile i și j). Acest algoritm se numește algoritmul lui Roy-Warshall.

Roy-Warshall(graf G)

D Ź A // A este matricea de adiacență

pentru k=1,n

pentru i=1,n

pentru j=1,n

dacă D(i,k)=1 și D(k,j)=1

atunci D(i,j)=1

În algoritmii prezentați în acest articol nu am precizat cum se face reconstituirea drumurilor minime. Am omis intenționat acest lucru, deoarece este foarte simplu de realizat, prin folosirea unui vector de predecesori, așa cum am arătat în cazul algoritmului BF de generare a drumurilor minime într-un graf neponderat.

Cristian Cadar este student în anul II la Univeritatea Politehnică București. Poate fi contactat prin e-mail la adresa ccadar@pcnet.pcnet.ro

[cuprins]