Parcurgerea în adâncime reprezintă explorarea “naturală” a unui graf neorientat. Este foarte asemănătoare cu modul în care un turist vizitează un oraș în care sunt obiective turistice (vârfurile grafului) și căi de acces între obiective (muchiile). Vizitarea orașului va avea loc din aproape în aproape: se pleacă de la un obiectiv de pornire, se continuă cu un obiectiv învecinat cu acesta, apoi unul învecinat cu al doilea, etc.
Parcurgerea în adâncime se face astfel:
- Se începe cu un vârf inițial
x
, care este în acest moment vârf curent. - Vârful
x
se vizitează. Se determină primul său vecin nevizitaty
al luix
, care devine vârf curent. - Apoi se vizitează primul vecin nevizitat al lui
y
, şi aşa mai departe, mergând în adâncime, până când ajungem la un vârf care nu mai are vecini nevizitați. Când ajungem într-un astfel de vârf, ne întoarcem la “părintele” acestuia – vârful din care am ajuns în acesta. - Dacă acest vârf mai are vecini nevizitați, alegem următorul vecin nevizitat al său și continuam parcurgerea în același mod.
- Dacă nici acest vârf nu mai are vecini nevizitați, revenim în vârful său părinte și continuăm în același mod, până când toate vârfurile accesibile din vârful de start sunt vizitate.
Observație
Dacă graful nu este conex, nu se vor vizita toate vârfurile.
Exemplu
Parcurgerea din nodul 5
: 5 2 1 4 6 3 7 8 9
Alte exemple de parcurgere pe acest graf:
- Parcurgerea din nodul
1
:1 2 3 5 4 6 7 8 9
- Parcurgerea din nodul
2
:2 1 4 5 7 8 9 6 3
- Parcurgerea din nodul
9
:9 7 5 2 1 4 6 3 8
Animație configurabilă pentru parcurgerea DFS
Pentru implementarea algoritmului se foloseşte un vector caracteristic pentru memorarea faptului că un anume vârf a fost sau nu vizitat, la un anumit moment al parcurgerii:
v[i] = 0
, vârfuli
nu a fost (încă) vizitatv[i] = 1
, vârfuli
a fost vizitat
Pentru a determina ordinea în care se parcurg nodurile care pot fi vizitate, se folosește o stivă:
- se analizează mereu nodurile adiacente cu nodul din vârful stivei
- dacă pentru nodul din vârful stivei găsim un vecin nevizitat, adăugăm nodul vecin pe stivă
- dacă pentru nodul din vârful stivei nu mai găsim niciun vecin nevizitat, îl eliminăm de pe stivă
Pentru implementare se poate folosi ca stivă memoria STACK, prin intermediul recursivității.
Implementare C++
Presupunem că graful are n
noduri și este prezentat prin matricea de adiacență a
. Starea unui vârf (vizitat sau nu) este memorată în vectorul caracteristic v
. Toate aceste variabile sunt globale.
void dfs(int k) { v[k]=1; //vizitam varful curent x for(int i=1;i<=n;i++) // determinam vecinii nevizitati ai lui x if(a[k][i]==1 && v[i]==0) { dfs(i); // continuam parcurgerea cu vecinul curent i } }
Alte informații determinate prin parcurgerea în adâncime
Arborele de parcurgere
În urma parcurgerii în adâncime, muchiile folosite pentru parcurgere formează un arbore. Acest arbore este graf parțial al grafului dat (dacă graful este conex), și se numește arbore parțial de parcurgere. Arborii de parcurgere ai unui graf sunt diferiți, în funcție de vârful de start.
Exemplu:
Arborele de parcurgere pentru vârful de start 5 |
Arborele de parcurgere pentru vârful de start 9 |
Acest arbore poate fi privit ca arbore cu rădăcină, rădăcina fiind vârful de start. Pentru graful de mai sus, parcurgerea în adâncime pornind din vârful 5
produce următorul arbore de parcurgere:
Acest arbore poate fi stocat în memorie prin intermediul unui vector de tați:
t[k] = 0
, vârfulk
este rădăcina arboreluit[k] = x
, vârfulx
este tatăl vârfuluik
Pentru arborele de mai sus vectorul de tați este:
k |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
t[k] |
2 |
5 |
2 |
1 |
0 |
4 |
5 |
7 |
8 |
Următoarea secvență C++ realizează parcurgerea în adâncime cu determinarea vectorului de tați. Acesta este declarat global, t[]
:
void dfs(int k, int tata) { v[k]=1; //vizitam varful curent x t[k] = tata; // completăm vectorul de tați for(int i=1;i<=n;i++) // determinam vecinii nevizitati ai lui x if(a[k][i]==1 && v[i]==0) { dfs(i , k); // continuam parcurgerea cu vecinul curent i; acesta il va avea ca tată pe k } }
Clasificarea muchiilor
Muchiile grafului se pot clasifica în urma parcurgerii în adâncime în:
- muchii de arbore: muchiile care sunt folosite la parcurgere și fac parte din arborele de parcurgere;
- muchii de întoarcere (muchii înapoi): toate celelalte – o muchie de întoarcere unește un vârf
x
cu un strămoșy
al lui în arbore, diferit det[x]
– nodulx
este valoarea din vârful stivei iary
este o valoare din stivă – diferită de tatăl luix
.
În figura de mai jos este redesenat graful din exemplu. Muchiile de arbore sunt desenate cu linii continue, iar cele de întoarcere cu linii punctate.
Muchiile de întoarcere vor închide cicluri în graf.
Timpii de descoperire și finalizare ai vârfurilor
În timpul parcurgerii în adâncime a unui graf se pot atașa fiecărui vârf două marcaje de timp:
d[x]
– momentul când vârfulx
este descoperit și adăugat pe stivăf[x]
– momentul când se termină de vizitat vecinii luix
(si toți descendenții lor), iar vârfulx
se elimină de pe stivă
Aceste momente de timp vor fi numere naturale între 1
și 2*n
, unde n
este numărul de vârfuri din graf.
Exemplu:
Redesenăm graful de mai sus formă de arbore, precizând și cele două marcaje de timp (timpul de descoperire este scris cu verde, cel de finalizare cu protocaliu):
k |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
d[k] |
3 |
2 |
9 |
4 |
1 |
5 |
12 |
13 |
14 |
f[k] |
8 |
11 |
10 |
7 |
18 |
6 |
12 |
13 |
14 |
Observații:
- pentru orice vârf
x
, are loc relațiad[x] < f[x]
; - pentru oricare două vârfuri
x
șiy
, are loc exact una dintre următoarele condiții:- intervalele
[d[x],f[x]]
și[d[y],f[y]]
sunt total disjuncte; - intervalul
[d[x],f[x]]
este conținut în întregime în intervalul[d[y],f[y]]
, iarx
este descendent al luiy
în arborele de parcurgere; - intervalul
[d[y],f[y]]
este conținut în întregime în intervalul[d[x],f[x]]
, iary
este descendent al luix
în arborele de parcurgere;
- intervalele