Responsabili:
În urma parcurgerii acestui laborator, studentul va fi capabil să:
Un graf este o pereche de mulţimi G = (V, E)
. Mulțimea V
conține nodurile grafului (vertices), iar mulțimea E
conține muchiile sale (edges), fiecare muchie stabilind o relație de vecinătate între două noduri. Mulţimea E
este inclusă în mulţimea VxV
.
Dacă pentru orice element al mulţimii E
, e = (u, v)
, elementul e' = (v, u)
aparţine de asemenea mulţimii E
, atunci spunem că graful este neorientat. În caz contrar, graful este orientat. În cazul grafului orientat, muchiile se mai numesc şi arce.
În funcţie de problemă şi de tipul grafurilor, avem 2 reprezentări: liste de adiacenţă sau matrice de adiacenţă.
Reprezentarea prin liste de adiacenţă constă într-un tablou Adj
cu |V|
liste, una pentru fiecare vârf din V
. Pentru fiecare u
din V
, lista de adiacenţă Adj[u]
conţine referinţe către toate vârfurile v
pentru care există muchia (u, v)
în E
. Cu alte cuvinte, Adj[u]
este formată din totalitatea vârfurilor adiacente lui u
în G
.
Această reprezentare este preferată pentru grafurile rare ( |E|
este mult mai mic decât |V|x|V|
).
Pentru graful de mai sus, lista de adiacenţă este următoarea:
Reprezentarea prin matrice de adiacenţă a unui graf constă într-o matrice A[i][j]
de dimensiune |V|x|V|
astfel încât:
A[i][j] = 1
, dacă muchia (i,j)
aparţine lui E
A[i][j] = 0
, în caz contrar.
Această reprezentare este preferată pentru grafurile dense ( |E|
este aproximativ egal cu |V|x|V|
).
Pentru graful de mai sus, matricea de adiacenţă este următoarea:
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
0 | 0 | 1 | 1 | 0 | 0 |
1 | 1 | 0 | 1 | 1 | 1 |
2 | 1 | 1 | 0 | 1 | 0 |
3 | 0 | 1 | 1 | 1 | 0 |
4 | 0 | 1 | 0 | 1 | 0 |
Parcurgerea în lățime (Breadth-first Search - BFS) presupune vizitarea nodurilor în următoarea ordine:
Caracteristica esențială a acestui tip de parcurgere este, deci, că se preferă explorarea în lățime, a nodurilor de pe același nivel (aceeași depărtare față de sursă) în detrimentul celei în adâncime, a nodurilor de pe nivelul următor.
d[u]
se reține distanța până la nodul sursă (poate fi util în unele probleme) p[u]
.
Pentru implementarea BFS se utilizează o coadă (Q
) în care inițial se află doar nodul sursă. Se vizitează pe rând vecinii acestui nod şi se pun și ei în coada. În momentul în care nu mai există vecini nevizitați, nodul sursă este scos din coadă.
Arborele obținut în urma execuției este următorul:
// inițializări pentru fiecare nod u din V { culoare[u] = alb d[u] = infinit p[u] = null } culoare[sursa] = gri d[sursa] = 0 enqueue(Q,sursa) // punem nodul sursă în coada Q // algoritmul propriu-zis cât timp coada Q nu este vidă { v = dequeue(Q) // extragem nodul v din coadă pentru fiecare u dintre vecinii lui v dacă culoare[u] == alb { culoare[u] = gri p[u] = v d[u] = d[v] + 1 enqueue(Q,u) // adăugăm nodul u în coadă } culoare[v] = negru // am terminat de explorat toți vecinii lui v }
Dacă graful are mai multe componente conexe, algoritmul, în forma dată, va parcurge doar componenta din care face parte nodul sursă. Pe grafuri cu mai multe componente conexe se va aplica în continuare algoritmul pentru fiecare nod rămas nevizitat și astfel se vor obține mai mulți arbori, câte unul pentru fiecare componentă.
O(|E|+|V|)
- unde |E|
este numărul de muchii, iar |V|
este numărul de noduri.
Parcurgerea în adâncime (Depth-First Search - DFS) presupune explorarea nodurilor în următoarea ordine:
V1
)V1
(îl vom numi V2
)V2
Vn
, continuăm cu următorul vecin nevizitat al nodului anterior, Vn-1
Așadar, spre deosebire de BFS, acest tip de parcurgere pune prioritate pe explorarea în adâncime (la distanțe tot mai mari față de nodul sursă), în detrimentul celei în lățime (pe același nivel).
tDesc[u]
- momentul descoperirii nodului (și a schimbării culorii din alb în gri)tFin[u]
- momentul în care procesarea nodului s-a încheiat (și culoarea acestuia s-a schimbat din gri în negru) p[u]
.Un exemplu de aplicare al DFS este următorul:
Nodul de pornire este I, iar pentru simplificare vecinii sunt aleși în ordine alfabetică. În stânga nodului este notat tDesc
, iar în dreapta tFin
. Dacă se afișează nodurile, în urma parcurgerii se obține următorul output: I, E, B, A, C, D, G, F, H
Arborele obținut în urma parcurgerii este următorul:
// inițializări pentru fiecare nod u din V { culoare[u] = alb p[u] = NULL tDesc[u] = 0 tFin[u] = 0 } contor_timp = 0 // funcţie de vizitare a nodului vizitare(nod) { contor_timp = contor_timp + 1 tDesc[nod] = contor_timp culoare[nod] = gri printeaza nod; } // algoritmul propriu-zis DFS(nod) { stiva s; viziteaza nod; s.introdu(nod); cât timp stiva s nu este goală { nodTop = nodul din vârful stivei vecin = află primul vecin nevizitat al lui nodTop. dacă vecin există { p[v] = nodTop viziteaza v; s.introdu(v); } altfel { contor_timp = contor_timp + 1 tFin[nodTop] = contor_timp culoare[nodTop] = negru s.scoate(nodTop); } } }
La fel ca în cazul BFS, complexitatea este O(|E|+|V|)
- unde |E|
este numărul de muchii, iar |V|
este numărul de noduri.
Dacă toate muchiile au același cost, putem afla distanța minimă între două noduri A
și B
efectuând o parcurgere BFS
din nodul A
și oprindu-ne atunci când nodul B
a fost descoperit. Reamintindu-ne că nivelul unui nod este analog distanței, în muchii, față de sursă, și că BFS
descoperă un nod de pe nivelul N
numai după ce toate nodurile de pe nivele inferioare au fost descoperite, este ușor de văzut că nivelul nodului B
în parcurgere corespunde distanței minime între A
și B
.
Pentru a reține distanța și drumul exact de la A
la B
, se vor reține pentru fiecare nod d[x]
(distanța de la sursă
la x
) și p[x]
(părintele lui x
în drumul de la sursă spre x
). În momentul descoperirii unui nod y
al cărui părinte este x
, se vor face următoarele atribuiri:
d[y] = d[x] + 1 p[y] = x
sursa având d[A] = 0
și p[A] = NULL
.
Observații:
Se dă un graf orientat aciclic. Orientarea muchiilor corespunde unei relații de ordine de la nodul sursă către cel destinație. O sortare topologică a unui astfel de graf este o ordonare liniară a vârfurilor sale astfel încât, dacă (u,v)
este una dintre muchiile grafului, u
trebuie să apară înaintea lui v
în înșiruire. Dacă graful ar fi ciclic, nu ar putea exista o astfel de înșiruire (nu se poate stabili o ordine între nodurile care alcătuiesc un ciclu).
Sortarea topologică poate fi văzută și ca plasarea nodurilor de-a lungul unei linii orizontale astfel încât toate muchiile să fie direcționate de la stânga la dreapta (să nu existe nici o muchie înapoi, spre părinte).
Un exemplu: Profesorul Bumstead își sortează topologic hainele înainte de a se îmbrăca.
(u, v
) înseamna că obiectul de îmbrăcăminte u
trebuie îmbrăcat înaintea obiectului de îmbrăcaminte v
. Timpii de descoperire (tDesc)
și de finalizare (tFin)
obținuți în urma parcurgerii DFS sunt notați lângă noduri.tFin
. Observați că toate muchiile sunt orientate de la stânga la dreapta. Acum profesorul Bumstead se poate îmbrăca liniștit. Așa cum se observă din poză, sortarea topologică constă în sortarea nodurilor descrescător după timpii de finalizare. Demonstrația acestei afirmații se face simplu, arătând că nodul care se termină mai târziu trebuie să fie efectuat înaintea celorlalte noduri finalizate.
Grafurile sunt utile pentru a modela diverse probleme şi se regăsesc implementaţi în multiple aplicaţii practice:
Porniți exercițiile de la scheletul de cod oferit.
1.[7p] Se citesc din fişierul graf.in
M muchii ale unui graf neorientat cu N vârfuri. Folosind scheletul de cod dat implementaţi următoarele cerinţe:
N
și de muchii M
ale grafuluiM
linii: perechi de noduri (u,v) pentru care există muchii în graf[]
) - întoarce elementul de pe poziția istd::sort(v.begin(),v.end());
for (unsigned int i = 0; i < v.size(); ++i) { std::cout << v[i] << std::endl; }
for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it) { std::cout << *it << std::endl; }
std::ostream_iterator<int> out_it(std::cout, std::endl); std::copy(v.begin(), v.end(), out_it);
Această secțiune nu este punctată și încearcă să vă facă o oarecare idee a tipurilor de întrebări pe care le puteți întâlni la un job interview (internship, part-time, full-time, etc.) din materia prezentată în cadrul laboratorului.
Cum multe din companiile mari folosesc date stocate sub formă de grafuri (Facebook Open Graph, Google Social Graph şi Page Rank, etc.) la angajare vor dori să vadă ca ştiţi grafuri:
Puteţi căuta mai multe întrebări pe http://www.careercup.com/ şi pe http://www.glassdoor.com/
[1] - BFS
[2] - Distanţa minimă
[3] - DFS
[4] - Sortare topologică
[5] - Dijkstra
[6] - Bellman-Ford
[7] - Floyd-Warshall
[8] - A*