Capitolul Cuvinte-cheie Graf, digraf, nod, arc, muchie, Parcurgeri în adâncime, în lățime, sortare topologică IC.07. Aspecte generale IC.07.. Definții Definiție: [L0] Un graf este o pereche G = ( V, E), unde V este o mulțime de vârfuri, iar E este o mulțime de perechi neordonate de vârfuri { u, v} denumite muchii. Dacă e = { u, v} este o muchie din graf, atunci: [L0] - u și v se numesc extremitățile muchiei e. - e este incidentă în u și v. - vârfurile u și v sunt adiacente. Dacă un graf G conține muchii de forma { u, u}, atunci o astfel de muchie se numește buclă, iar graful G poartă numele de graf general sau pseudograf. Definiție: [C0] Un digraf este o pereche D = ( V, A), unde V este o mulțime de vârfuri, iar A este o mulțime de perechi ordonate de vârfuri ( u, v) denumite arce. Altfel spus, un arc este o muchie pentru care se stabilește un sens. Dacă a = ( u, v) este un arc într-un digraf, atunci: [C0] - u este extremitatea inițială (sau sursa) și v este extremitatea finală (sau destinația) a arcului a. - u se numește predecesor imediat al lui v, iar v este succesor imediat al lui u. - a este incident spre interior cu v și incident spre exterior cu u (sau, altfel spus, arcul a pleacă din u și sosește în v). Definiție: [C0] Dat fiind un digraf D = ( V, A), atunci înlocuind fiecare arc a = ( u, v) cu mulțimea { u, v}, se obține un graf G (D) numit graful suport al digrafului D. Reformulând definiția anterioară, se poate spune că graful G suport al unui digraf D se obține prin eliminarea sensului (sau a orientării) arcelor. 0 G = ( V, E), unde: V = {0,,,3} E ={{0,},{,},{,3},{,3}} 3 Figura : Exemplu de graf --
0 D = ( V, A), unde: V = {0,,,3} A = {(0,),(,),(,3),(3,)} 3 Figura : Exemplu de digraf Considerând exemplele din Figurile și, se observă că graful G prezentat în Figura este graful suport pentru digraful D prezentat în Figura. Definiție: [L0] Se numește mers de la u la v în graful G o secvență: u v { v, v }, v,...,{ v, v }, v unde v i = 0, 0 n n n = v, 0 i n sunt vârfuri în G, iar { vi, vi}, i n sunt muchii în G. Definiții: [L0] Un mers pentru care toate vârfurile sunt distincte se numește parcurs. Un mers pentru care toate muchiile sunt distincte se numește drum. Un drum pentru care extremitățile coincid se numește circuit. Definiții: Fie un graf G = ( V, E). Gradul unui vârf u din graful G reprezintă numărul total de muchii incidente cu vârful u. Fie un digraf D = ( V, A). Pentru orice vârf u se definesc: - gradul interior al vârfului numărul total de arce incidente spre interior cu u (sau, reformulând, numărul total de arce care intră în vârful u); - gradul exterior al vârfului numărul total de arce incidente spre exterior cu u (sau, altfel spus, numărul total de arce care pleacă din vârful u). Observație: În continuare, vârfurile unui graf/digraf vor fi numite noduri. IC.07.. Tipuri de grafuri Un graf G/digraf D este complet dacă fiecare nod este conectat cu toate celelalte noduri din graf/digraf (altfel spus, toate nodurile sunt adiacente). Un graf G = ( V, E) este bipartit dacă: V = V V, a. i. V V =φ și { i, j} E, atunci i V, j V sau j V, i V. [C0] Similar, un digraf D = ( V, A) este bipartit dacă: V = V V, a. i. V V =φ și ( i, j) A, atunci i V, j V sau j V, i V. [C0] Un graf G = ( V, E) se numește conex dacă u, v V, există drum de la u la v. Un digraf D = ( V, A) se numește conex dacă graful său suport este conex. Un digraf D = ( V, A) se numește tare conex dacă u, v V, atunci există un drum de la u la v și un drum de la v la, --
u. Un digraf D = ( V, A) se numește unilateral conex dacă u, v V, atunci există un drum de la u la v sau un drum de la v la u [C0]. IC.07..3 Operații pe grafuri [L0] Nume operație Descriere GrafVid Intare: nimic Ieșire: un graf G Rol: o astfel de metodă poate fi utilizată pentru instanțierea unui obiect de tip graf EsteGrafVid Intrare: un graf G = ( V, E) Ieșire: valoare de tip boolean - true dacă graful este vid (nu conține noduri); - false în caz contrar. InsereazăNod Intrare: un graf G = ( V, E), cu V = { 0,,..., n } Ieșire: graful G la care se adaugă vârful izolat n. InsereazăMuchie Intrare: un graf G = ( V, E) și două noduri diferite i, j V Ieșire: graful G la care se adaugă muchia { i, j} ȘtergeVârf Intrare: un graf G = ( V, E) și un nod k V Ieșire: graful G din care au fost eliminate toate muchiile incidente în k, iar nodurile i > k au fost redenumite i. ȘtergeMuchie Intrare: un graf G = ( V, E) și două noduri diferite i, j V Ieșire: graful G din care a fost eliminată muchia { i, j}. ListăAdiacență Intrare: Intrare: un graf G = ( V, E) și un nod k V Ieșire: mulțimea nodurilor adiacente cu nodul k. ListăVârfuriAccesibile Intrare: Intrare: un graf G = ( V, E) și un nod k V Ieșire: mulțimea nodurilor pentru care există un drum de la nodul k. Observație: Operațiile definite anterior se pot aplica și peste digrafuri. IC.07. Modalități reprezentare a grafurilor Structurile de tip graf/digraf sunt în mod uzual reprezentate fie prin matrici de adiacență, fie prin liste de adiacență. IC.07.. Reprezentarea grafurilor prin matrici de adiacență O matrice de adiacență reprezintă un tablou bidimensional. În cadrul acestui tablou, locația [i,j] indică prin valoarea stocată dacă există o muchie (sau un arc în cazul digrafurilor) între nodul i și nodul j (sau, pentru digrafuri, un arc de la nodul i la nodul j). În general, pentru a marca faptul că există o muchie/un arc între nodurile i și j se stochează în matrice în locația corespunzătoare valorea, în caz contrar memorându-se valoarea 0. Considerând n numărul total de noduri din graf/digraf, o matrice de adiacență are dimensiune n n (sau, altfel spus, necesarul de memorie destinat stocării unui graf/digraf este de clasă O ( n )). Pentru exemplele prezentate în Figurile și matricile de adiacență corespunzătoare sunt prezentate în tabelul următor: -3-
Graf Figura 0 3 0 0 0 0 0 0 0 3 0 0 Digraf Figura 0 3 0 0 0 0 0 0 0 0 0 0 3 0 0 0 Un prim lucru ce se poate remarca este faptul că matricea de adiacență prin care se descrie un graf este o matrice simetrică față de diagonala principală. Un graf/digraf pentru care, din punct de vedere logic, este importantă doar existența muchiilor/arcelor mai poartă numele graf/digraf neponderat. Există situații în care pentru grafuri/digrafuri este importantă atât existența muchiei (sau a arcului), cât și un cost asociat. În aceste cazuri grafurile/digrafurile se numesc grafuri/digrafuri ponderate. Acestea din urmă sunt reprezentate prin intermediul unor matrici de adiacență ponderate. Un exemplu este prezentat în figura de mai jos: 0 0 5 5 4 3 Matricea de adiacență 0 3 0 0 0 0 0 5 0 0 4 3 0 5 4 0 4 3 Matricea de adiacență 0 3 0 0 0 0 0 0 0 5 0 0 0 3 0 0 4 0 a) Graf ponderat b) Digraf pronderat Figura 3: Exemplu de graf/digraf ponderat IC.07.. Reprezentarea grafurilor prin liste de adiacență Reprezentarea grafurilor (sau a digrafurilor) prin liste de adiacență implica stocarea unui vector de n pointeri (unde n este numărul de noduri din graf/digraf) către structuri de tipul liste liniare simplu înlănțuite. Un element în cadrul unei liste liniare va stoca nodul adiacent cu nodul curent și informația de înlănțuire. Declararea unui astfel de element se realizează (limbajul C++): struct Element { int vecin; Element* leg; }; Declararea unui graf/digraf utilizând liste de adiacență (limbajul C++): Declarare statică (n numărul de noduri) Element graf[n]; Declarare dinamică (n numărul de noduri) Element *graf; graf = new Element[n]; -4-
Declarațiile de mai sus pot fi, la rândul lor, încapsulate în cadrul unei structuri astfel încât informațiile ce descriu un graf/digraf să fie stocate în interiorul aceleiași entități: Structură statică pentru reprezentrea grafurilor struct Graf { int nrnoduri; Element liste[dim_max]; };... Graf g; Structură dinamică pentru reprezentrea grafurilor struct Graf { int nrnoduri; Element * liste; };... Graf g; Observații:. În cadrul declarațiilor de mai sus, constanta DIM_MAX reprezintă numărul maxim de noduri pe care le poate avea un graf descris de structura respectivă (constantă definită de utilizator).. Pentru grafurile neorientate, o muchie între nodurile i și j va fi memorată de două ori: o dată în lista de adiacență a nodului i și, a doua oară, în lista de adiacență a nodului j. Necesarul de memorie pentru stocarea unui graf/digraf prin intermediul listelor de adiacență este de ordinul O ( n + E ) (n numărul de noduri din graf/digraf, E - numărul de muchii din graf/numărul de arce din digraf). Schematic, reprezentarea prin intermediul listelor de adiacență a grafurilot din Figurile și sunt prezentate în figura următoare: g 0 3 0 3 Figura 4: Reprezentarea grafului expus în Figura d 0 3 Figura 5: Reprezentarea digrafului expus în Figura Observație: Deși este o tehnică mai puțin utilizată, un graf/digraf ponderat poate fi reprezentat și prin intermediul listelor de adiacență ponderate. Pentru a putea realiza acest lucru, structura Element declarată anterior se modifică după cum urmează: struct Element { int vecin, pondere; Element* leg; }; 3 3-5-
IC.07.3 Modalități de parcurgere a grafurilor: în adâncime, în lățime În cazul cel mai general, problema parcugerii unui graf înseamnă vizitarea, după un anumit set de constrângeri, a uneia dintre cele două mulțimi caracteristice grafului/digrafului. De cele mai multe ori, muțimea vizată în explorare este mulțimea nodurilor. În acest caz, problema explorării sistematice a unui graf/digraf se poate defini astfel [C0]: Dat fiind un vârf i într-un graf/digraf, să se genereze lista vârfurilor accesibile din i. Pseudocodul general al algoritmului (Tarjan, 7) [C0] Notații: S mulțimea nodurilor accesibile din i la un moment dat:. explorare(d,i). S <- {i}; 3. marcheaza toate arcele ca fiind neutilizate; 4. while(exista arce neutilizate care pleca din vfufuri din s) do 5. alege j din S si un arc neutilizat (j,k) din A; 6. marcheaza (j,k) ca fiind utilizat; 7. adauga k la S; 8. end-while 9. end O variantă mai completă a pseudocodului anterior este următoarea: Pseudocod general al algoritmului de explorare variantă completă: Notații: S mulțimea nodurilor accesibile din i la un moment dat; Sprim mulțimea nodurilor care au încă arce neutilizate.. explorare(d,i). S <- {i}; Sprim <- {i}; 3. while (Sprim contine noduri) do 4. alege j din Sprim; 5. if(nu mai există muchii neutilizate incidente din j)then 6. elimina j din Sprim; 7. else 8. fie (j,k) urmatorul arc neutilizat incident din j; 9. if (k nu este in S) then 0. proceseaza (k);. S <- S U {k};. Sprim <- Sprim U {k}; 3. end-if 4. end-if 5. end-while 6. end În funcție de restricțiile ce se impun asupra mulțimii Sprim (și, respectiv, în funcție de modul de implementare practic) se pot obține două tipuri de explorări ale grafurilor/digrafurilor [C0]: - dacă Sprim este o stivă, atunci procedura explorare realizează explorarea în adâncime a grafului/digrafului țintă; - dacă Sprim este o coadă, atunci procedura explorare realizează explorarea în lățime a grafului/digrafului țintă. Conceptual, cele două tipuri de explorări pot fi formulate astfel: -6-
Explorarea în adâncime Pentru fiecare nod nou accesibil, se aplică rând pe rând o procedură de vizitare pentru fiecare nod vecin neprocesat încă (sintagma rând pe rând trebuie interpretată în acest caz în sensul de se aplică în iterații diferite ). Explorarea în lățime Pentru fiecare nod nou accesibil, se aplică simultan o procedură de vizitare pentru toate nodurile vecine neprocesate încă (cuvântul cheie simultan se interpretează în acest caz în sensul de se aplică în cadrul aceleiași iterații). IC.07.3. Parcurgerea grafurilor în adâncime Pseudocod algoritm iterativ [C0]: Notații: - S vector de n locații prin intermediul căruia se marchează dacă nodul de index i a fost vizitat sau nu; vectorul se inițializează cu valori de 0 (în sensul că nodurile nu au fost procesate/vizitate); - st o structură de tip stivă; - Graf G o structură de tip graf după modelul menționat anterior; - i (parametru de intrare) indexul nodului de start pentru parcurgere.. DFS_iter(G, i). for (j=0,...,n-) do 3. p[j] = G.liste[j]; 4. end-for 5. proceseaza (i); //se proceseaza nodul de start 6. S[i] = ; //se marcheaza nodul de start ca fiind vizitat 7. initstack (st); 8. push (st, i); 9. while (!isempty (st)) do 0. j = top (st);. if (p[j] == NULL) then //nu mai exista vecini ai nodului. //j care nu au fost vizitati 3. pop (st); 4. else 5. k = p[j]->vecin; 6. p[j] = p[j]->leg; 7. if (S[k] == 0) then //nodul k nevizitat 8. proceseaza (k); 9. S[k] = ; 0. push (st, k);. end-if. end-if 3. end-while 4. end Structura de date caracteristică acestui tip de explorare este o stivă. În acest caz, pseudocodul prezentat mai sus poate fi implementat într-o manieră recursivă. -7-
Pseudocod algoritm recursiv [C0]. DFS_rec(G, i). proceseaza (i); //se proceseaza nodul de start 3. S[i] = ; //se marcheaza nodul de start ca fiind vizitat 4. p = G.liste[i]; 5. while (p!= NULL) do 6. if (S[p->vecin] == 0) then 7. DFS_rec (G, p->vecin); 8. end-if 9. p = p->leg; 0. end-while. end IC.07.3. Parcurgerea grafurilor în lățime Pentru acest caz, după cum a fost menționat și în paragrafele anterioare, structura caracteristică de explorare este o structură de tip coadă. Acest lucru implică un algoritm iterativ. Pseudocodul algoritmului [C0]: Notații: - S (similar parcugerii în adâncime) vector de n locații prin intermediul căruia se marchează dacă nodul de index i a fost vizitat sau nu; vectorul se inițializează cu valori de 0 (în sensul că nodurile nu au fost procesate/vizitate); - q o structură de tip coadă; - Graf G o structură de tip graf după modelul menționat anterior; - i (parametru de intrare) indexul nodului de start pentru parcurgere.. BFS (G, i). proceseaza (i); 3. S[i] = ; 4. put (q, i); 5. while (!isempty (q)) do 6. j = get (q); 7. p = G.liste[j]; 8. while (p!= NULL) do 9. k = p->vecin; 0. if (S[k] == 0) then. proceseaza (k);. S[k] = ; 3. put (q, k); 4. end-if 5. p = p->leg; 6. end-while 7. end-while 8. end Aplicații ale parcurgerii în lățime sortarea topologică Sortarea topologică a unui digraf aciclic constă în determinarea unei liste liniare a nodurilor din digraf pe baza unui criteriu numit precedență. Astfel, oricare ar fi două noduri u și v într-un digraf, dacă există arc de la u la v, atunci nodul u apare înaintea nodului v în lista liniară sortată [L0][C0]. Altfel spus, pentru un digraf aciclic D = (V, A), u, v V,u < v (u, v) A. Conform acestei relații de ordine, primele valori din lista sortată -8-
vor fi alese dintre nodurile fără predecesori (se spune despre un nod că nu are predecesori dacă acel nod nu are arce incidente în el). Aceste noduri se mai numesc surse [L0][C0]. Principiul algoritmlui este următorul [L0][C0]:. se inițializează o coadă de explorare cu cu nodurile sursă;. se extrage un nod u din coada de explorare și se adaugă în lista parțial ordonată; 3. se elimină din reprezentarea lui digrafului nodul u și toate arcele incidente din u (arcele care pleacă din nodul u); 4. dacă în timpul eliminărilor de la pasul 3. se identifică noduri v care rămân fără predecesori, atunci aceste noduri vor fi introduse în coada de explorare; 5. se repetă pașii. 4. până coada de explorare devine vidă. Notă: Stuctura de date prin intermediul căreia vom stoca digraful pentru sortare topologică va fi: struct Graf { int nrnoduri; Element * liste; int * np; }; În cadrul acestei structuri membrul np nou adăugat este un pointer ce va indica un vector de nrnoduri locații de tip int. Pentru orice nod i {0,..., nrnoduri } din digraf, D.np[i] va stoca numărul de predecesori ai nodului i. Pseudocodul algoritmului de sortare topologică [L0][C0]: Notații: - q o structură de tip coadă; - l o listă liniară simplu înlănțuită pentru care metoda insertend realizează inserarea la sfârșit de listă; - Graf D (parametrul de intrare) o structură de tip graf după modelul de mai sus; - Element *p un pointer către un element de tip listă liniară simplu înlănțuită.. sortaretopologicabfs (D). init (q); 3. for (u=0; u<d.nrnoduri; u++) do 4. if (D.np[i] == 0) then 5. put (q, u); 6. end-if 7. end-for 8. for (k=0; k<d.nrnoduri; k++) do 9. if (isempty (q)) then 0. return ( digraful contine cicluri );. end-if. u = get (q); 3. insertend (l, u); 4. p = D.liste[u]; 5. while (p!= NULL) do 6. D.np[p->vecin] = D.np[p->vecin] ; 7. if (D.np[p->vecin] == 0) then 8. put (q, p->vecin]); 9. end-if 0. p = p->leg;. end-while. end-for 3. end -9-
Considerăm digraful din figura următoare: Figura 6: Digraf exemplu pentru sortare topologică Numărul de predecesori ai fiecărui nod în parte este: - nodurile 0, 6 și 7 nu au arce incidente în nod, ceea ce implică 0 predecesori; - nodurile, 3 și 5 au fiecare câte un arc incident în nod, deci fiecare nod are câte predecesor; - nodurile 4 și au fiecare câte două arce incidente în nod, ceea ce implică un număr de predecesori pe nod. După etapa de inițializare (etapa în cadrul căreia se determină numărul de predecesori pentru fiecare nod în parte), urmează etapa de inițializare a cozii de explorare. Conform observațiilor anterioare, în această coadă vor fi introduse nodurile 0, 6, 7. Analizând Figura 6, se pot face următoarele observații: - oricare ar fi un drum care conține nodurile 0, 6 sau 7, acest drum începe din aceste noduri; - nu există nici un drum care să includă simultan cele 3 noduri amintite la punctul anterior, ceea ce implică faptul că între aceste noduri nu se poate stabili o relație de ordine (altfel spus, pentru aceste 3 noduri nu există un criteriu de precedență). Presupunem acum că ordinea în care se inserează cele 3 noduri în coada de explorare este: 0, 6, 7 (ordinea crescătoare a valorilor din noduri). Conform algoritmului prezentat (linia ), nodul 0 va fi primul extras din coada de explorare pentru a fi inserat apoi în lista liniară simplu înlănțuită ce va reține în final ordinea topologică a nodurilor. Pentru acest nod, vor fi apoi procesați simultan (în cadrul aceleiași iterații liniile de cod 5 -> ) toți vecinii direcți (practic, datorită acestui mod de procesare se spune ca algoritmul de sortare topologică este o aplicație directă a algoritmului de parcurgere BFS). Procesarea constă în decrementarea numărului de predecesori pentru vecinii nodului curent și includerea în coada de procesare a acelor vecini care rămân fără predecesori. Bucla principală a algoritmului se repetă apoi în aceeași manieră până când sunt atinse toate nodurile din graf. Să considerăm acum digraful din Figura 7. Figura 7: Contra-exemplu pentru algoritmul de sortare topologică -0-
Analizând digraful prezentat în Figura 7, se pot obține următoarele drumuri: 0 0 0 Considerând criteriul de precendeță amintit anterior (nodul i este mai mic decât nodul j dacă, oricare ar fi un drum care trece simultan prin cele două noduri, i apare înaintea lui j, obținem: 0 << < 0 < < < 0 Relațiile de mai sus reprezintă, în mod evident, o absurditate. În concluzie, algoritmul de sortare topologică nu poate fi aplicat pe digrafuri ce conțin cicluri. Aplicații. Evaluați complexitatea timp pentru algoritmii prezentați în cadrul acestui capitol. Se consideră pentru analiză cazurile în care grafurile/digrafurile sunt reprezentate prin liste de adiacență. Cum se modifică aceste clase de complexitate în momentul în care grafurile/digrafurile sunt reprezentate prin matrici de adiacență?. Implementați în C/C++ algoritmii propuși în cadrul acestui sub-capitol. Contorizați pentru fiecare rulare numărul de operații efectuate. Corelați această informație cu analiza realizată în cadrul primei probleme. 3. Propuneți un algoritm bazat pe explorarea DFS pentru problema sortării topologice. Bibliografie [L0] Lucanu D., Craus M., Proiectarea Algoritmilor, Cap. : Tipuri de date de nivel înalt, Editura Polirom, ISBN 978-973-46-40-9, 008 [C0] Craus M., Proiectarea Algoritmilor Note de curs, Universitatea Tehnică Gheorghe Asachi din Iași, Facultatea de Automatică și Calculatoare --