Noduri, muchii, arce ... (5) Introducere în teoria GRAFURILORÎn cadrul acestui episod ne propunem să discutăm câteva aspecte legate de existența ciclurilor într-un graf: verificarea ciclicității grafurilor, determinarea ciclurilor elementare, găsirea muchiilor și a punctelor critice într-un graf, testarea biconexității și găsirea componentelor biconexe.
Cicluri Determinarea existenței ciclurilor într-un graf, împreună cu toate celelalte probleme enunțate în prezentarea acestui articol, sunt aplicații ale parcurgerii DF a grafurilor. Presupunem în cele ce urmează că grafurile pe care vom exemplifica algoritmii prezentați în acest articol sunt conexe. În cazul grafurilor neconexe, se vor aplica acești algoritmi pentru fiecare componentă conexă. Problema determinării existenței ciclurilor într-un graf conex este o problemă deosebit de simplă. În primul rând, să ne reamintim că un arbore parțial al unui graf este conex și fără cicluri. De asemenea, acesta conține n-1 muchii. Am arătat într-un articol anterior că un arbore parțial este maximal în raport cu aciclicitatea, adăugarea oricărei muchii în plus determinând apariția ciclurilor. Prin urmare, pentru a determina existența ciclurilor într-un graf neorientat conex, trebuie doar să testăm dacă graful are n-1 muchii. Metoda care este însă cea mai folosită pentru rezolvarea acestei probleme este, așa cum am precizat, parcurgerea DF. Pentru aceasta, vom prezenta conceptul de "etajare a grafului pe niveluri". Să considerăm graful din figura 1. Etajarea acestuia pe niveluri este reprezentată în figura 2.
Împărțirea grafurilor pe niveluri se face printr-o parcurgere DF. Nivelul unui nod reprezintă adâncimea la care a fost vizitat nodul respectiv în parcurgerea DF. De exemplu, nodurile 7 și 9 se află pe nivelul 6, nodul 11 pe nivelul 8, iar nodul 1 pe nivelul 1. Atașarea nivelelor corespunzătoare nodurilor grafului, se face ușor printr-o parcurgere DF:
Parcurge_DF(nod k, level L) Vizitat(k) Ź Adevărat Nivel(k) Ź L i Ź primul_vecin(k) cât timp nu am parcurs toată lista vecinilor lui k dacă nu Vizitat(i) atunci Parcurge_DF(i, L+1) i Ź următorul_vecin(k)
În ceea ce privește muchiile, etajarea pe niveluri a grafului le împarte în două categorii: muchii de avansare și muchii de întoarcere. Muchiile de avansare sunt muchiile care leagă un nod de pe un nivel inferior cu unul de pe un nivel imediat superior, deci muchiile care realizează avansarea de la un nivel la altul al grafului. În exemplul considerat acestea sunt (1, 2), (2, 3), (3, 4), (3, 5), (4, 6), (5, 12), (6, 7), (6, 9), (7, 8), (9, 10), (10, 11). Referitor la muchiile de întoarcere acestea sunt muchiile care unesc un nod aflat pe un nivel superior cu unul aflat pe un nivel inferior, diferența absolută dintre nivelurile acestor noduri fiind cel puțin 2. Pentru graful din figură, muchiile de întoarcere sunt: (4, 1), (8, 6) și (11, 9). Bineînțeles, muchiile care nu sunt de avansare sunt muchii de întoarcere și invers. De asemenea, trebuie reținut faptul că, privite ca muchii de avansare și de întoarcere, muchiile grafului sunt orientate, deci sunt considerate arce. Revenind la problema determinării existenței ciclurilor într-un graf, remarcăm faptul că muchiile de avansare determină un arbore parțial al grafului respectiv. Prin urmare, existența ciclurilor într-un graf neorientat devine echivalentă cu existența a cel puțin unei muchii de întoarcere în acel graf. Acest test se face ușor, complexitatea algoritmului fiind O(n+m):
Există_Ciclu(nod k) Vizitat(k) Ź Adevărat i Ź primul_vecin(k) cât timp nu am parcurs toată lista vecinilor lui k // considerăm toți vecinii lui k mai // puțin nodul din care a fost apelat dacă nu Vizitat(i) atunci Există_Ciclu(i) altfel scrie "Am găsit ciclu !" i Ź următorul_vecin(k)
De multe ori suntem interesați să găsim numărul maxim de cicluri disjuncte între ele, adică numărul maxim de cicluri cu proprietatea că oricare dintre ele conține cel puțin o muchie care nu apare în nici un alt ciclu, muchie pe care o vom numi muchie proprie a ciclului respectiv. De exemplu, pentru graful din figura 3, două astfel de partiționări în cicluri sunt reprezentate în figurile 4 și respectiv 5:
Mulțimea de cicluri care îndeplinesc această proprietate se numește mulțimea ciclurilor elementare. Remarcăm că într-un graf conex neorientat, numărul de cicluri care îndeplinesc această proprietate poate fi cel mult m-(n-1), deoarece un arbore parțial (cu n-1 muchii) este graful parțial cel mai mic (din punctul de vedere al numărului de muchii) care nu conține cicluri. În cele ce urmează vom arăta constructiv că pentru orice graf neorientat conex, putem construi m-n+1 cicluri. Pentru aceasta vom considera un arbore parțial oarecare al grafului dat. Pentru simplitate, vom considera chiar arborele parțial obținut în urma parcurgerii DF. Se observă ușor că prin introducerea fiecăreia dintre cele m-n+1 muchii care nu aparțin arborelui, se formează un ciclu. Să presupunem că avem de introdus muchia (i, j). Fie k părintele lui i și j, adică nodul cel mai apropiat de i și j. Atunci în mod sigur, vom avea un drum unic de la i la k și un drum unic de la j la k, drumuri care nu au în comun decât nodul k. Atunci, adăugând muchia (i, j) se va forma un ciclu care va conține muchia proprie (i, j). Pentru un graf neorientat conex, avem deci un număr de m-n+1 cicluri elementare. Lăsăm pe seama cititorului sarcina determinării numărului de cicluri elementare pentru un graf oarecare (nu neapărat conex) cât și implementarea algoritmului pentru aflarea acestor cicluri.
Muchii și puncte critice În cele ce urmează vom introduce noțiunea de muchie critică și de punct critic.
Definiție Într-un graf neorientat conex, o muchie se numește muchie critică dacă și numai dacă prin eliminarea ei graful își pierde conexitatea. Definiția se poate extinde ușor și la nivel de componente conexe. De exemplu, în figura 1, muchiile critice sunt (3, 5), (5, 12), (4, 6) și (6, 9). Cea mai simplă metodă de determinare a muchiilor critice constă în a elimina pe rând fiecare muchie a grafului și de a verifica dacă graful astfel obținut mai este conex. Dacă graful astfel obținut nu mai este conex, atunci, conform definiției, muchia este critică, în caz contrar ea este o muchie oarecare a grafului. Complexitatea acestui algoritm este O(m2), deoarece determinarea conexității unui graf se face așa cum am arătat într-un articol anterior în O(n+m):
boolean Critice(muchie (x,y)) elimină muchia (x,y) dacă Conex(G) // funcție prezentată în cadrul unui // episod anterior atunci Critice Ź Fals altfel Critice Ź Adevărat
Pentru a prezenta algoritmul optim de găsire a muchiilor critice într-un graf, algoritm de complexitate O(n+m), observăm mai întâi că muchiile critice sunt muchiile care nu aparțin nici unui ciclu. Pentru a le determina, vom folosi etajarea grafului pe niveluri. În primul rând remarcăm că o muchie de întoarcere nu poate fi critică, deoarece o astfel de muchie închide întotdeauna un ciclu. Rămâne de verificat care dintre muchiile de avansare (care sunt în număr de n-1) fac parte dintr-un ciclu. Să considerăm că dorim să verificăm muchia de avansare de la i la j. Afirmăm că, dacă din nodul j putem să ajungem pe un nivel mai mic sau egal cu nivelul lui i (avansând în sensul muchiilor de la parcurgerea DF), atunci muchia nu este critică, în caz contrar ea este critică. Demonstrația acestei afirmații este ușor de intuit: în momentul în care ajungem pe un nivel mai mic sau egal cu nivelul lui i, închidem un ciclu, deci muchia nu este critică. În caz contrar, nu există nici un ciclu care să conțină muchia (i, j), deci aceasta este critică. În exemplul de față, muchiile (4, 6), (6, 9), (3, 5) și (5, 12) sunt critice, celelalte fiind muchii normale. De exemplu, muchia (2, 3) nu este critică deoarece din 3 se poate ajunge pe drumul 3-4-1 în nodul 1, care se află pe nivelul 1, nivel mai mic decât nivelul 2 pe care se află nodul 2. Vă prezentăm în continuare procedura de determinare a muchiilor critice în complexitatea O(n+m) (parcurgere DF modificată), procedură care implementează algoritmul descris mai sus:
Muchii_Critice(nod k, level L, val_min) // procedura recursivă apelată prin // Muchii_Critice(1,1,aux) dacă Vizitat(k) // am ajuns într-un nod vizitat atunci val_min Ź Nivel(k) * ieșire forțată din procedura recursivă Vizitat(k) Ź Adevărat; Nivel(k) Ź L; val_min Ź L; // inițializăm cu k valoarea minimă a // nivelului pe care poate ajunge nodul k i Ź primul_vecin(k) // considerăm toți vecinii lui k // mai puțin nodul din care a fost apelat cât timp nu am parcurs toata lista vecinilor lui k Muchii_Critice(i,L+1,aux) dacă aux<val_min atunci val_min Ź aux dacă aux=L+1 atunci scrie "Muchia (k,i) este critică" i Ź următorul_vecin(k)
Definiție Într-un graf neorientat conex, un nod se numește punct critic dacă și numai dacă prin eliminarea lui și a muchiilor adiacente acestuia, graful își pierde conexitatea. Ca și în cazul muchiilor critice, definiția poate fi extinsă și la nivel de componente conexe. Algoritmii descriși în cazul aflării muchiilor critice pot fi folosiți cu mici modificări și pentru a afla punctele critice ale unui graf. Primul algoritm presupune eliminarea pe rând a fiecărui nod împreună cu muchiile adiacente lui, urmată de verificarea conexității grafului astfel obținut. Algoritmul are complexitatea O(m2). Cel de-al doilea algoritm, bazat pe etajarea grafului pe niveluri, este și el asemănător celui prezentat mai sus, cu specificarea faptului că, pentru ca un nod k să nu fie punct critic, trebuie ca pentru fiecare muchie de avansare (k, j), nodul j să poată ajunge pe un nivel mai mic decât nivelul nodului k. De remarcat faptul că nodul din care se începe parcurgerea (în general nodul 1) face excepție de la această regulă, deoarece nivelul său nu va putea fi depășit niciodată, neexistând noduri pe niveluri mai mici decât 1. De aceea nodul 1 va fi considerat punct critic dacă și numai dacă are mai mulți fii. Numai în acest caz, nodul 1 va fi critic, el deconectând subarborii respectivi. Implementarea acestor algoritmi îi lăsăm pe seama cititorului.
Biconexitate Definiție Prin graf biconex înțelegem un graf conex cu proprietatea că eliminând oricare nod al acestuia, graful rămâne conex. Ca și în cazul conexității și tare conexității, definim componentele biconexe ca mulțimi maximale de noduri ale unui graf care respectă proprietatea de mai sus. În cele ce urmează, vom prezenta un algoritm eficient de determinare a componentelor biconexe ale unui graf. De exemplu, pentru graful din figura 6, componentele biconexe sunt cele reprezentate în figura 7:
Remarcăm că împărțirea în componente biconexe nu introduce ca și până acum (conexitate, tare conexitate) o partiție a nodurilor grafului, ci o partiție a muchiilor acestuia.
De asemenea, se remarcă ușor că o componentă biconexă este o componentă care nu conține puncte critice. Astfel, pentru a determina componentele biconexe ale unui graf, vom adapta algoritmul de aflare a punctelor critice prezentat mai sus, reținând o stivă în care vom memora toate muchiile de avansare și de întoarcere parcurse până la un moment dat. Atunci când găsim un nod critic, se va forma o nouă componentă biconexă, pe care o vom determina extrăgând din stivă muchiile corespunzătoare. Complexitatea algoritmului este deci O(n+m), iar implementarea acestuia o lăsăm pe seama cititorului, fiind foarte asemănătoare rutinei de determinare a muchiilor critice, a cărei descriere în pseudocod a fost prezentată în acest episod.
Cristian Cadar este student în anul II la Universitatea Politehnică din București. Poate fi contactat la adresa ccadar@pcnet.pcnet.ro. Cristian Cadar [cuprins] |