Introducere în teoria grafurilor
În cele ce urmează vom prezenta o structură de date cu foarte multe aplicații atât în algoritmică, cât și în viața de zi cu zi, acestea fiind grafurile. Problema aflării existenței unor conexiuni sau aflării distanței minime între două noduri reprezintă un punct de plecare pentru majoritatea algoritmilor pe grafuri, teoria folosită în algoritmică fiind una vastă și plină de abordări ce se dovedesc a fi esențiale în foarte multe situații, atât competiționale, cât și în aplicații practice.
Noțiuni introductive
Terminologie
Un graf este o structură care corespunde unui grup de obiecte, în care unele perechi de obiecte sunt într-un anumit sens „legate” reciproc. Obiectele corespund unor abstracții matematice numite într-un graf noduri/vârfuri (numite și puncte) și fiecare legătură dintre perechile de obiecte asociate se numește muchie (numită și arc sau linie, prin care este și reprezentată).
O definiție mai riguroasă ce se va dovedi utilă este prezentată aici:
Noțiunea de graf
Un graf este o structură matematică compusă din două mulțimi:
-
(mulțimea vârfurilor sau nodurilor), care reprezintă obiectele;
-
(mulțimea muchiilor sau arcelor), care reprezintă legăturile între perechi de vârfuri.
Fiecare element este numit vârf (sau nod), iar fiecare element este numit muchie (sau arc). În mod obișnuit, grafurile sunt reprezentate grafic printr-un set de puncte (corespunzătoare vârfurilor) conectate prin linii sau curbe (corespunzătoare muchiilor).
Voi continua prin a defini termeni ce se dovedesc a fi esențiali pentru înțelegerea grafurilor.
Graf orientat și neorientat
Un graf neorientat este un graf în care perechile de vârfuri sunt neordonate. Aceasta înseamnă că, dacă este o muchie de la la , atunci și .
Graf orientat
Prin comparație, un graf orientat este un graf în care perechile de vârfuri sunt ordonate. Aceasta înseamnă că, dacă , atunci .
Noduri adiacente
Două noduri și sunt adiacente în graful dacă există o muchie . Adică, există o legătură directă între și .
Formal:
Incidență
Folosim noțiunea de incidență pentru a descrie relația dintre noduri și muchii. O muchie este incidentă cu nodurile și .
Gradul unui nod
Definim gradul unui nod dintr-un graf ca fiind numărul de muchii incidente cu .
Într-un graf neorientat, gradul nodului , notat , este numărul de muchii care au ca una dintre extremități.
Într-un graf orientat, se pot defini două tipuri de grad:
-
Gradul intern (numărul de muchii care intră în nodul ), notat :
-
Gradul extern (numărul de muchii care ies din nodul ), notat :
Observație
Într-un graf neorientat :
Explicația este dată de faptul că pentru fiecare muchie adăugată, gradul a două noduri crește cu 1.
Lanț
Numim lanț o secvență de noduri cu proprietatea că oricare ar fi . Un lanț este -elementar* dacă oricare ar fi . Un lanț este simplu dacă oricare ar fi .
Altfel spus, un lanț elementar este un lanț cu nodurile distincte, iar un lanț simplu este un lanț cu muchii distincte.
Ciclu
O secvență de muchii formează un ciclu dacă pentru orice și . Un ciclu este simplu dacă pentru orice .
Altfel spus, un ciclu reprezintă o secvență de muchii ce nu se repetă, pleacă de la un nod și parcurgând în ordine acele muchii, se ajunge tot la nodul . Un ciclu simplu este un ciclu în care nu se repetă noduri.
Lungimea unui lanț
Lungimea unui lanț este (numărul de muchii). Uneori, aceasta se definește ca fiind numărul de noduri, așadar lungimea acestui lanț este .
Graf parțial și subgraf
Definim graf parțial al unui graf dat ca fiind ceea ce rămâne din graful dat păstrând toate nodurile și eliminând eventual unele muchii, fără a adăuga muchii noi.
Formal spus, un graf parțial a grafului este un graf unde .
Subgraf
Definim subgraf al unui graf dat ca fiind ceea ce rămâne din graful dat eliminând unele noduri și doar muchiile incidente lor, deci nu și alte muchii și fără să adăugăm alte muchii.
Formal spus, un subgraf al unui graf este un graf unde și .
Observație
Numărul de subgrafuri ale unui graf este , iar numărul de grafuri parțiale este , unde este numărul de noduri, iar este numărul de muchii al grafului.
Câteva tipuri speciale de grafuri
Se remarcă faptul că în funcție de tipul grafului, mai putem defini următoarele tipuri de grafuri, care se vor folosi în diferite aplicații. De notat ca pentru unele din aceste tipuri, vom avea probleme unde vom explica în detaliu noțiunile și aplicațiile unde folosim aceste concepte.
Graf complet $$K_n$$
Definim un graf complet cu ca fiind un graf unde . Altfel spus, fiecare nod este conectat cu toate celelalte noduri.
Numărul de muchii ale unui graf complet este .
Graf bipartit
Definim un graf bipartit ca fiind un graf care poate fi împărțit în două submulțimi cu , astfel încât, dacă , atunci acesta se poate conecta doar cu și viceversa.
Observație
Are loc următoarea relație pentru un graf bipartit :
Observație
Un graf este bipartit dacă și numai dacă acesta nu conține un ciclu de lungime impară.
Graf planar
Definim un graf planar ca fiind un graf care are proprietatea că poate fi reprezentat grafic fără ca două muchii să se intersecteze.
Graf regulat
Un graf regulat este un graf în care . Adică, fiecare nod din graf are același număr de muchii incidente.
Un graf regulat cu nodurile de gradul se numește graf -regulat.
Observație
Condiția necesară și suficientă pentru ca un graf -regulat de ordin să existe este ca și că este par.
Numărul de muchii este maxim într-un graf complet , acesta fiind cu fiecare nod de gradul . Așadar, , sau este minim pentru un anume. De asemenea, după relația de mai sus, avem muchii, deci trebuie să fie par.
Lucrul cu grafuri. Moduri de reprezentare în memorie
Un concept foarte important în teoria grafurilor reprezintă modul în care parcurgem aceste structuri de date și cum putem verifica proprietățile de care avem nevoie, de la o problemă la alta.
Să considerăm graful neorientat din figura următoare:
Acest graf are 13 noduri și 12 muchii, acestea fiind , , , , , , , , , , , .
Pentru a reprezenta un graf în memorie, există trei moduri principale de a o face, cu distincția că în practică se va folosi doar reprezentarea prin liste de vecini.
Matrice de adiacență
Definim matricea de adiacență a unui graf ca fiind o matrice binară pentru care dacă și numai dacă avem muchie de la nodul la nodul și în caz contrar.
Definiție
Pentru un graf neorientat, matricea este mereu simetrică, adică .
Pentru graful nostru de mai sus, aceasta este matricea de adiacență la care ajungem.
Listă de vecini
Definim o listă de vecini ca fiind o listă (de regulă, alocată dinamic) pe care o folosim pentru a ține în memorie pentru fiecare nod doar nodurile adiacente cu acesta, această metodă fiind cea mai eficientă din punct de vedere practic pentru a parcurge grafurile.
Exemplu
Pentru graful de mai sus, aceasta este lista de vecini:
| Nod | Vecini |
|---|---|
| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | |
| 8 | |
| 9 | |
| 10 | |
| 11 | |
| 12 | |
| 13 |
Definiție
Definim o listă de muchii ca fiind o listă pe care o folosim pentru a ține toate muchiile în memorie. Deși nu este o variantă prea practică de a efectua parcurgerile, această metodă poate fi utilă pentru anumiți algoritmi ce se bazează în principal pe prelucrarea muchiilor, un astfel de exemplu fiind arborele parțial de cost minim.
Exemplu
În cazul nostru, lista de muchii este: , , , , , , , , , , , .
Conexitate. Parcurgerea DFS
Problema aflării conexității unui graf este una din problemele fundamentale ale teoriei grafurilor, fiind adesea folosită drept un exemplu esențial în explicarea și înțelegerea grafurilor.
Graf conex
Un graf conex este un graf neorientat în care există o cale între oricare două noduri. Cu alte cuvinte, oricare două noduri din graf sunt conectate direct sau indirect printr-o serie de muchii.
Componenta conexă
O componentă conexă a unui graf este un subgraf conex maximal, adică un subgraf în care oricare două noduri sunt conectate printr-o serie de muchii, iar acest subgraf nu poate fi extins adăugând vreun alt nod sau vreo altă muchie fără a pierde proprietatea de conexitate.
Pentru a rezolva problema aflării conexității unui graf, va trebui să parcurgem graful folosind unul din algoritmii consacrați pentru această problemă. În cazul de față, vom continua prin a explica parcurgerea în adâncime a grafului (DFS sau depth-first search), una din parcurgerile optime pentru această problemă.
Parcurgerea în adâncime (DFS)
Parcurgerea în adâncime (DFS) este un algoritm de explorare a grafului care începe de la un nod ales și vizitează cât mai mult posibil din vecinii acestuia înainte de a se întoarce înapoi. Aceasta se realizează printr-o strategie de backtracking recursivă sau prin utilizarea unei stive (stack).
DFS începe de la un nod și vizitează toate nodurile pe care le poate atinge în adâncime înainte de a reveni la nodurile anterioare și verifică dacă un nod a fost deja vizitat pentru a evita bucle infinite.
Observație
Complexitatea parcurgerii în adâncime (DFS) este , unde reprezintă numărul de noduri sau vârfuri și reprezintă numărul de muchii.
Observație
În probleme se notează convențional cu de la noduri, respectiv cu de la muchii.
Observație
Se remarcă faptul că un nod va fi vizitat la un moment dat doar o singură dată, deci dacă avem muchiile , și , iar DFS-ul pleacă din 1, 2 va fi accesat din 1, iar 3 va fi accesat din 2.
Observație
Se poate remarca faptul că ordinea în care vizităm nodurile în graf depinde de ordinea în care sunt adăugate muchiile în graf, acest lucru înseamnă că nu putem folosi DFS pentru anumite probleme, de exemplu cele la care trebuie aflată distanța minimă în graf.
Ca o recapitulare (sau de fapt comparație) între BFS și DFS, să comparăm fiecare abordare împreună cu o funcție:
=== "DFS"
--8 < --"usor/graphs/dfsbfs.cpp:dfs"=== "BFS"
--8 < --"usor/graphs/dfsbfs.cpp:bfs"Problema connected components
Se dă un graf neorientat cu noduri și muchii. Să se afle câte componente conexe are graful dat.
Pentru a afla numărul de componente conexe ale unui graf, putem folosi parcurgerea DFS pentru a afla toate nodurile din care apelăm DFS din funcția main, acesta fiind și răspunsul la problema noastră.
--8 < --"usor/graphs/connectedcomponents.cpp"Drumuri minime. Parcurgerea BFS
Dacă în cazul parcurgerii DFS putem să o aplicăm fără mari probleme pentru o varietate destul de largă de probleme cu grafuri, totuși nu este suficientă pentru problemele ce țin de distanțe. Un exemplu fundamental este acela al aflării drumului minim între două sau mai multe noduri într-un graf dat.
Drum minim
Un drum minim este lungimea minimă a unui lanț care leagă două noduri din graf.
Motivul pentru care nu putem afla drumul minim între două noduri folosind DFS este acela că ordinea în care nodurile sunt parcurse în DFS depinde de ordinea în care sunt date muchiile de la intrare, parcurgerea recursivă făcând aflarea distanțelor minime imposibilă. Astfel, vom introduce un alt mod de a parcurge graful nostru.
Parcurgerea în lățime (BFS)
Parcurgerea în lățime** (BFS, engl. breadth-first search) a unui graf ca fiind o parcurgere iterativă ce pleacă de la unul sau mai multe noduri, iar la fiecare pas, dacă ne aflăm la un nod , vom vizita vecinii nevizitați ai nodului , adăugându-i într-o coadă, nodurile fiind parcurse în ordinea în care au fost adăugate în coadă.
Observație
Complexitatea parcurgerii în lățime (BFS) este , unde reprezintă numărul de noduri sau vârfuri și reprezintă numărul de muchii.
Observație
Se poate remarca faptul că ordinea în care vizităm nodurile în graf va fi aceeași cu ordinea crescătoare a distanței minime față de nodul sau nodurile inițiale, datorită faptului că ele vor fi inserate în coadă în ordinea în care acestea au fost adăugate.
Problema Simple Shortest Path
Se dă un graf neorientat cu noduri și muchii, precum și un nod . Să se afle lungimea drumului minim dintre și fiecare nod din graf, inclusiv .
Pentru a rezolva această problemă, vom pleca cu un BFS din nodul și vom afla pe parcurs, distanțele minime față de toate celelalte noduri.
--8 < --"usor/graphs/simpleshortestpath.cpp"Problema grarb
Se dă un graf neorientat cu noduri numerotate de la 1 la și muchii. Determinați numărul minim de muchii care trebuie eliminate și numărul minim de muchii care trebuie adăugate în graful astfel încât acesta sa devina arbore.
Această problemă se împarte în două subprobleme relativ ușor de identificat - aflarea componentelor conexe ale grafului (dacă avem componente conexe, va fi nevoie de muchii pentru a transforma graful într-unul conex), precum și aflarea numărului de muchii care trebuie scoase pentru a transforma graful în arbore (la final, trebuie să ne rămână muchii). Astfel, vom avea nevoie de muchii noi și va trebui să scoatem = muchii pentru a avea un arbore.
--8 < --"usor/graphs/grarb.cpp"Problema graf
Se dă un graf neorientat conex cu noduri și două noduri și , să se afle nodurile ce aparțin tuturor lanțurilor optime între și .
Pentru a rezolva această problemă, va trebui mai întâi să aflăm folosind o parcurgere de tip BFS distanțele minime de la și spre toate celelalte noduri. Apoi, pentru fiecare distanță de la 0 la , vrem să aflăm câte noduri se află pe unul din drumurile optime de la la la o distanță față de . În cele din urmă, vrem să afișăm nodurile situate la distanțele care apar o singură dată în mulțimea nodurilor ce fac parte din cel puțin un drum optim de la la .
Codul sursă se poate viziona mai jos.
--8 < --"usor/graphs/graf.cpp"Detectarea unui ciclu simplu - Round Trip
În această problemă, trebuie să găsim un ciclu simplu într-un graf neorientat. Există foarte mulți algoritmi care rezolvă această problemă corect, dar un algoritm care este foarte simplu și ușor de înțeles constă în următorii pași:
- Pentru fiecare componentă conexă, vom rula un DFS din unul din noduri și vom afla un arbore DFS (pentru fiecare nod din componentă, vom ține drept nodul de unde am ajuns să vizităm nodul , într-un mod similar cu modul în care ținem părinții unui nod în arbore).
- Dacă la un pas dăm de un nod care a fost deja vizitat și nu este părintele nodului curent, înseamnă că avem un ciclu între cele două noduri în cauză.
- Pentru cele două noduri găsite, vom folosi vectorul prv creat anterior pentru a merge înapoi prin nodurile găsite, iar acest drum va fi ciclul pe care îl vom obține.
Mai jos găsiți implementarea C++ a soluției descrise mai sus.
--8 < --"usor/graphs/roundtrip.cpp"Probleme suplimentare
- Kilonova unire
- GasesteCiclu pbinfo
- CSES Message Route
- CSES Building Teams
- CSES Round Trip
- USACO Bronze Milk Factory
- USACO Bronze The Great Revegetation
- USACO Silver Closing the Farm
- USACO Silver Moocast
- infoarena multiplu
- ONI 2004 base3
- Probleme cu grafuri de pe kilonova
- Probleme cu grafuri de pe codeforces, ordonate după dificultate
Lectură suplimentară
- Grafuri - noțiuni teoretice de bază
- Articol introductiv de pe USACO Guide
- Articol despre parcurgeri de pe USACO Guide
- Find a cycle in O(m) - cp-algorithms