Noduri, muchii, arce... Introducere în teoria GRAFURILORCristian Cadar O teorie pe care dacă o știi, ți se pare foarte frumoasă, rezolvarea de probleme, folosind modele din teoria grafurilor ți se pare ușoară. În caz contrar, poate fi un coșmar. Și cum noi vrem să "sărim" în ajutorul vostru...
Noțiuni introductiveDefiniție Se numește graf neorientat o pereche ordonată G=(X,U), unde X este o mulțime finită și nevidă de elemente numite noduri sau vârfuri, iar U este o mulțime de perechi de elemente ale lui X, numite muchii. În cazul în care perechile din U sunt ordonate, acestea se numesc arce și graful se va numi orientat. Vom nota cu card X=n și card U=m. De asemenea, nodurile grafului vor fi numerotate cu numere consecutive de la 1 la n .
În figurile 1 și 2 avem reprezentarea grafică a unui graf neorientat și respectiv a unuia orientat. Această reprezentare grafică asociază fiecărui nod al grafului un cerc în care este precizat numărul vârfului, iar fiecărei muchii o linie dreaptă sau curbă care unește vârfurile corespunzătoare. În cazul grafurilor orientate, arcele vor fi reprezentate ca și muchiile, doar că în plus vor avea asociate săgeți care vor indica sensul lor. Grafurile au numeroase aplicații practice, ele fiind folosite pentru a reprezenta relațiile dintre diferite tipuri de elemente. Un exemplu clasic de folosire a grafurilor este cel al memorării orașelor dintr-o anumită regiune și a drumurilor care le leagă. Bineînțeles, orașele vor reprezenta vârfurile grafului, iar drumurile dintre ele vor reprezenta muchiile sau arcele grafului (în funcție de restricțiile de sens unic existente).
Definiție Un graf parțial al grafului G=(X,U) este un graf G'=(X,U') astfel încât U'ÍU. Deci, un graf parțial al unui anumit graf se obține prin eliminarea a zero sau mai multe muchii ale grafului. În figura 3 este reprezentat un graf parțial al grafului din figura 1.
Definiție Un subgraf al grafului G=(X,U) este un graf H=(Y,V) astfel încât YÍX și V conține toate muchiile din U care unesc vârfuri din Y. În figura 4 este reprezentat un subgraf al grafului din figura 2.
În cele ce urmează vom determina numărul grafurilor neorientate cu n vârfuri. Numărul maxim de muchii pe care îl poate avea un graf neorientat este de . Pentru a forma toate grafurile neorientate vom considera pe rând toate submulțimile de muchii din mulțimea totală de muchii. Numărul de astfel de submulțimi este egal cu:
care este și numărul de grafuri neorientate cu n vârfuri. În mod similar se determină și numărul grafurilor orientate care este 2n(n-1). Folosind același raționament, rezultă că numărul grafurilor parțiale ale unui graf este 2m, (m reprezintă numărul muchiilor) iar numărul subgrafurilor unui graf este 2n.
Definiție Într-un graf neorientat, numărul muchiilor incidente cu un anumit vârf v reprezintă gradul vârfului respectiv și se notează cu d(v). Într-un graf orientat numărul muchiilor care "ies" dintr-un vârf v reprezintă gradul exterior al vârfului respectiv și se notează cu d_ext(v), iar numărul muchiilor care "intră" într-un vârf v reprezintă gradul interior al vârfului respectiv și se notează cu d_int(v) . Un vârf cu gradul 0 se numește vârf izolat, iar un vârf cu gradul 1 se numește vârf terminal. Avem următoarele relații importante între gradele vârfurilor unui graf (x1, x2, ..., xn sunt vârfurile grafului): ;
deoarece fiecare muchie contribuie cu câte o unitate la gradele celor două vârfuri pe care le unește; 3) Numărul gradelor impare dintr-un graf este par - rezultă imediat din (2) 4) Un graf cu nł2 vârfuri conține cel puțin două vârfuri cu același grad - rezultă din (2) și din principiul lui Dirichlet.
Problemă Una dintre problemele care pot interveni constă în determinarea echivalenței a două grafuri: dându-se două grafuri (orientate sau neorientate) să se determine dacă există o numerotare a vârfurilor primului graf astfel încât să se obțină cel de-al doilea graf. În figura 5 sunt reprezentate două grafuri echivalente.
Fig. 5 Grafuri echivalente
Rezolvare Rezolvarea acestei probleme este exponențială, se obține însă un timp foarte bun în felul următor: se determină șirul gradelor pentru cele două grafuri și se ordonează crescător aceste șiruri. Dacă după ordonare cele două șiruri nu sunt identice, atunci problema nu admite soluție. În caz contrar, se încearcă "potrivirea" celor două șiruri, prin interschimbarea succesivă a vârfurilor având același grad. Bineînțeles, cu cât numărul vârfurilor cu același grad este mai mic, cu atât algoritmul va fi mai rapid.
Definiție Un șir d=(d1, d2, ..., dn) se numește șir grafic dacă există un graf care să aibă ca grade numerele din acest șir.
Problemă Dându-se un șir a=(a1, a2, ..., an), să se determine dacă acesta este un șir grafic.
Rezolvare Mai întâi ordonăm descrescător șirul dat. Obținem șirul d: d1łd2ł...łdn. Condițiile necesare și suficiente pentru ca șirul d să fie grafic sunt: 1) d1 Ł n-1, dnł0 2) (d2-1, d3-1, ..., dd1+1-1, dd1+2, ..., dn) să fie șir grafic. Cea de-a doua condiție ne dă o metodă de construcție a grafului. Presupunem că (d2-1, d3-1, ..., dd1+1 -1, dd1+2, ..., dn) este un șir grafic corespunzător unui graf cu vârfurile x1, x2, ..., xn. Atunci prin adăugarea vârfului x1 și a muchiilor (x1, x2), (x1, x3), ..., (x1, xn) obținem un graf cu șirul gradelor (d1, d2, ..., dn). După fiecare eliminare a nodului cu grad maxim din graf, secvența obținută conform celei de-a doua condiții trebuie ordonată, deci complexitatea acestui algoritm va fi n·O(sortare) = O(n2·log n).
Problemă Aceeași problemă pentru un graf orientat: se dau două șiruri d_int=(d_int1, d_int2, ..., d_intn) și d_ext= (d_ext1, d_ext2, ..., d_extn). Să se determine dacă există un graf orientat cu șirul gradelor interioare d_int și cu șirul gradelor exterioare d_ext.
Rezolvare O primă condiție necesară este ca
Dacă această condiție este îndeplinită, problema se rezolvă folosind un algoritm de calcul al cuplajului maxim într-un graf1. În scopul rezolvării problemei formăm rețeaua de transport din figura 6, în care nodurile grafului sunt dublate. În prima serie de noduri intră de la sursă (nodul s) arce având costul egal cu cel al gradului interior corespunzător nodului respectiv, iar din cea de-a doua serie de noduri ies spre destinație (nodul d) arce având costul egal cu cel al gradului exterior corespunzător vârfului respectiv. De la fiecare nod din prima serie de noduri ducem arce de cost 1 către toate nodurile din seria a doua de noduri. Acest fapt are semnificația că într-un nod v pot intra maxim d_int(v) arce, pot ieși maxim d_ext(v) arce, iar între oricare două noduri poate exista cel mult un arc. Având această rețea construită, aplicăm un algoritm de cuplaj maxim. Dacă cuplajul maxim are valoarea
atunci problema are soluție, în caz contrar nu are.
Fig. 6 Continuăm prezentarea mai multor clase speciale de grafuri:
Definiție Se numește graf complet cu n vârfuri un graf cu proprietatea că oricare două vârfuri sunt adiacente. Un graf complet cu n vârfuri se notează cu Kn și se referă în general la grafurile neorientate. În ceea ce privește grafurile orientate, un graf orientat este complet dacă între oricare două noduri x, y există fie un arc de la x la y, fie un arc de la y la x, fie ambele arce, deci pentru fiecare dintre cele perechi de noduri ale unui graf orientat cu n noduri, există trei posibilități ca aceste noduri să fie adiacente. Deci, există grafuri complete orientate cu n vârfuri.
Definiție Se numește graf turneu un graf orientat pentru care între oricare două vârfuri există un singur arc (într-un sens sau altul). Folosind același raționament ca și în cazul grafurilor complete, există grafuri turneu cu n noduri. Grafurile turneu au următoarele proprietăți, care sunt foarte ușor de dedus:
Definiție Un graf G=(X,U) se numește bipartit dacă mulțimea vârfurilor X poate fi partiționată în două mulțimi Y și Z (YÈZ=X, YÇZ=Æ) și orice muchie uÎU are o extremitate în Y și una în Z (figura 7). Un graf bipartit este complet dacă pentru orice vârf y din Y și orice vârf z din Z, există muchia (x, z). Dacă card Y=p și card Z=q, atunci graful bipartit complet se notează cu Kp,q. Evident, numărul de muchii ale grafului bipartit Kp,q este pŚq.
Problemă Dându-se un graf să se stabilească dacă este bipartit.
Rezolvare O primă metodă, care presupune memorarea grafului, constă în parcurgerea grafului și atașarea alternantă a etichetelor 1 și 2 nodurilor parcurse (cu semnificația că nodul respectiv face parte din mulțimea Y respectiv mulțimea Z). Dacă se ajunge într-un nod vizitat cu o etichetă diferită de cea deja asociată, graful respectiv nu poate fi bipartit. Dacă nu se ajunge niciodată într-o astfel de situație, atunci graful este bipartit, construirea lui făcându-se pe baza etichetelor asociate. O altă variantă, care nu necesită memorarea grafului - fiind astfel aplicabilă pentru grafuri de dimensiuni foarte mari - folosește modul de memorare al mulțimilor disjuncte, în care reuniunea a două mulțimi necesită timp constant, iar apartenența unui element la o mulțime necesită timp logaritmic. Folosind o astfel de structură, în momentul în care citim o muchie (x, y), găsim tatăl comun al celor două extremități. Dacă există două noduri pentru care lungimea drumului dintre ele (aflat ca suma lungimilor drumurilor de la tatăl comun la cele două vârfuri) este pară, atunci graful nu poate fi bipartit. În caz contrar el este bipartit, cele două mulțimi fiind cele rezultate în finalul algoritmului.
Definiție Un graf pentru care între oricare două perechi de vârfuri pot exista oricât de multe muchii (sau arce) se numește multigraf. Memorarea grafurilor Matricea de adiacență Cea mai cunoscută metodă de memorare a grafurilor neorientate este matricea de adiacență, definită în felul următor:
1, dacă există muchie (arc) de la vârful i la vârful j 0, în caz contrar Bineînțeles, în cazul grafurilor neorientate, această matrice este simetrică, folosindu-se efectiv numai jumătate din spațiul matricei. De aceea, în unele probleme este eficient ca cealaltă jumătate a matricei să fie folosită pentru reținerea altor informații. Matricea de adiacență este folosită în general pentru grafuri cu un număr mic de noduri, deoarece dimensiunea ei este limitată de dimensiunea stivei. Deoarece stiva are aproximativ 65000 de bytes, vom putea aloca spațiu pentru o valoare aproximativă de cel mult 250 de noduri. Totuși, putem depăși această valoare în cazul în care alocăm matricea în heap, putând fi alocate în acest fel un număr de 800 de noduri. Mai mult chiar, lucrând pe biți, se poate ajunge la aproximativ 2200 de vârfuri.
Listele de muchii Această metodă de memorare presupune reținerea unei matrice cu 2 linii și m coloane, pe fiecare coloană fiind reținute două extremități ale unei muchii. Listele de muchii sunt folosite în cazul algoritmilor care prelucrează secvențial muchiile, cum ar fi de exemplu algoritmul Kruskal de aflare a arborelui parțial de cost minim. De asemenea, se recomandă folosirea acestei structuri în cazul grafurilor rare (cu număr mic de muchii). Principala problemă a acestei structuri este faptul că găsirea unei muchii se face în complexitatea O(m). Pentru a remedia acest fapt, se sortează muchiile în funcție de prima extremitate și în caz de egalitate, în funcție de cea de a doua, căutarea va deveni logaritmică (se folosește bineînțeles căutarea binară).
Listele de vecini Listele de vecini sunt cele mai recomandate în tratarea algoritmilor din teoria grafurilor din două motive principale: 1) spațiul folosit este gestionat dinamic, putându-se memora astfel grafuri de dimensiuni mari; 2) complexitatea optimă pentru majoritatea algoritmilor fundamentali din teoria grafurilor (parcurgeri, conexitate, muchii și puncte critice, algoritmi de drum minim etc.) este obținută numai folosind liste de vecini. Există două modalități de implementare a listelor de vecini. Prima dintre ele folosește o matrice T cu 2 linii și 2m coloane și un vector C cu n elemente care reține pentru fiecare nod indicele coloanei din T pe care este memorat primul element al listei nodului respectiv (sau 0 dacă vârful respectiv nu are vecini). Apoi, pentru o anumită coloană i din T, T(i,1) reține un element din lista curentă, iar T(i,2) reține coloana pe care se găsește următorul element din lista respectivă sau 0 dacă lista s-a terminat. Cea de-a doua implementare folosește pentru fiecare nod o listă simplu înlănțuită memorată în heap. De reținut că pentru fiecare listă este suficient să păstrăm o singură santinelă (cea de început a listei), introducerea vârfurilor făcându-se mereu la începutul listei (deoarece ordinea vârfurilor în listă nu contează). În cazul în care numărul muchiilor este cunoscut, se recomandă primul mod de implementare. În cazul în care numărul muchiilor nu este cunoscut inițial, se recomandă cea de-a doua implementare (prima dintre ele fiind de multe ori în acest caz de neutilizat).
Parcurgerea grafurilorParcurgerea unui graf presupune examinarea în vederea prelucrării tuturor vârfurilor acelui graf într-o anumită ordine, ordine care să permită prelucrarea optimă a informațiilor atașate grafului.
Parcurgerea DF (parcurgerea în adâncime) Parcurgerea DF (Depth First) presupune ca dintr-un anumit nod v, parcurgerea să fie continuată cu primul vecin al nodului v nevizitat încă. Cel mai cunoscut mod de implementare a parcurgerii DF se realizează cu ajutorul unei funcții recursive, dar există și cazuri în care este recomandată o implementare nerecursivă. De reținut că în cazul în care memorarea grafului se realizează prin intermediul matricei de adiacență, complexitatea parcurgerii este O(n2), în cazul în care sunt folosite listele de muchii complexitatea este O(m+n·log m), în timp ce dacă sunt folosite listele de vecini, complexitatea va fi doar O(m+n).
Parcurge_DF(nod k) Vizitat(k) Ź adevărat i Ź primul_vecin(k) cât timp nu am parcurs toată lista vecinilor lui k dacă nu Vizitat(i) atunci Parcurge_DF(i) i Ź următorul_vecin(k)
Parcurgerea BF (parcurgerea în lățime) Parcurgerea BF (Breath First) presupune faptul că după vizitarea unui anumit nod v, sunt parcurși toți vecinii nevizitați ai acestuia, apoi toți vecinii nevizitați ai acestora din urmă, până la vizitarea tuturor nodurilor grafului. Implementarea acestei metode se face folosind o coadă. Aceasta este inițializată cu un nod oarecare al grafului. La fiecare pas, se vizitează nodul aflat în vârful cozii și se adaugă în coadă toți vecinii nevizitați ai nodului respectiv. Ca și în cazul parcurgerii DF, complexitatea optimă pentru această parcurgere, O(m+n), se obține numai atunci când graful este memorat cu ajutorul listelor de vecini.
Parcurgere_BF(nod start) Coada Ź start Vizitat(start) Ź adevărat cât timp coada nu este vidă ic Ź nodul de la începutul cozii adaugă toți vecinii nevizitați ai lui ic în coadă Vizitat(i) Ź adevărat, unde i este un vecin nevizitat al nodului ic
Parcurgerea BF vizitează vârfurile crescător în funcție de distanța de la nodul din care se pornește parcurgerea (nodul start). Astfel, dacă într-un graf cu costuri egale pe toate muchiile se dorește aflarea distanței minime dintre două vârfuri, se poate folosi parcurgerea BF. De asemenea, pentru un astfel de graf, se poate obține printr-o parcurgere BF un arbore pentru care suma costurilor de la vârfurile sale la nodul de start este minimă.
Cristian Cadar este student în anul II la Universitatea Politehnică București. Poate fi contactat prin e-mail la adresa ccadar@pcnet.pcnet.ro [cuprins] |