Scop:Parcurgerea in latime se foloseste:
- pentru Inteligenta Artificiala (metoda neinformata de cautare... mai multe la cursul de IA);
- intr-o multime de aplicatii din teoria grafurilor (in special legate de drum minim de la un nod dat la un alt nod, componente conexe, grafuri bipartite);
- in jocuri pe calculator, pentru gasirea drumurilor, in special in jocuri Real Time Strategy (un mic exemplu in aplicatie);
Parcurgerea in adancime se foloseste:
- pentru Inteligenta Artificiala (metoda neinformata de cautare... mai multe la cursul de IA), insa combinarea sa cu euristici poate fi mai fructuoasa decat la parcurgerea in latime;
- sortarea topologica si alte aplicatii cu grafuri legate de componente conexe si tare conexe.
1. Parcurgerea in latime (Breadth First Search, BFS):1.1. TeoriaParcurgerea in latime foloseste o coada (Q) intr-un mod asemanator celui folosit de algoritmul AC-3. Initial in aceasta coada se gaseste doar nodul sursa. Vizitand pe rand vecinii sai ii adaugam si pe ei in coada. La sfarsit, dupa ce nu mai sunt vecini ai sursei nevizitati, scoatem nodul sursa din coada.
Pentru fiecare din nodurile prezente in coada, aplicam procedura de mai sus. Atentie insa: vizitand vecinii unui nod trebuie sa verificam ca acestia nu au mai fost deja vizitati (adica sunt inca albi).
. . .
In afara de coada q mai sunt necesare sau utile cateva structuri de date.
a) Putem diferentia intre nodurile nevizitate, cele asezate in coada si cele complet prelucrate printr-un vector denumit culoare:
c[nod] = alb, daca nodul nu a fost vizitat
gri, daca nodul a fost vizitat si asezat in q
negru, daca nodul a fost prelucrat si scos din q
b) Un vector d (distanta) care sa retina distanta (in numar de muchii) fata de sursa se poate dovedi util in unele probleme.
c) Vectorul de parinti p este necesar pentru a reface drumul de la sursa la un alt nod dupa parcurgere.
1.2. AlgoritmulAlgoritmul conform specificatiilor de mai sus:
// =============== initializari ===================
pentru fiecare nod u
{
c[u] = alb;
d[u] = infinit;
p[u] = null;
}
c[sursa] = gri;
d[s] = 0;
q = {s};
// ================ algoritm ===================
cattimp (q nu este vida)
{
v = extrage nod din coada q;
pentru fiecare u dintre vecinii lui v
if (c[u] == alb)
{
c[u] = gri;
p[u] = v;
d[u] = d[v]+1;
insereaza in coada q (u);
}
c[v] = negru;
}
Obs: Pentru ca muchiile care unesc noduri deja gri nu sunt practic folosite, se intuieste asezarea muchiilor folosite intr-un graf conex si fara cicluri (adica un arbore):
Obs: Daca graful are mai multe componente conexe, algoritmul, in forma data aici, va parcurge doar componenta din care face parte nodul sursa. Pe grafuri alcatuite din mai multe componente conexe se vor obtine astfel mai multi arbori, cate unul pentru fiecare componenta.
2. Parcurgerea in adancime (Depth First Search, DFS):2.1. Teoria:
DFS poate fi imaginata mai usor sub forma recursiva. Alegem un nod sursa. Dupa ce il marcam ca vizitat, consideram pe rand fiecare dintre vecinii sai ca fiind sursa. Muchiile catre noduri deja vizitate (inclusiv nodul original) practic nu vor conta (desi trebuiesc testate). In desenele de mai jos incerc sa simulez perspectiva nodului din pasul curent (observati cum acesta nu are acces la nodurile incetosate din cauza vecinilor deja vizitati):
Semnificatia culorilor ramane aceeasi ca la BFS:
- alb daca nodul nu a fost vizitat;
- gri daca nodul a fost vizitat dar nu i-am parcurs toti vecinii;
- negru daca am parcurs toti vecinii nodului.
Vom mai introduce doua variabile care se vor dovedi utile in unele aplicatii:
- tDesc (timpul descoperirii): pasul la care se marcheaza nodul cu gri;
- tFin (timpul final): pasul la care se marcheaza nodul cu negru.
2.2. Algoritmul:
Punand cap la cap informatiile de pana acum obtinem urmatorul algoritm:
// ================== initializari ====================
pentru fiecare nod u al grafului
{
culoare[u] = alb;
p[u] = NULL;
tDesc = 0;
tFin = 0;
}
timp = 0;
// === pentru a ne asigura ca se parcurg toate componentele conexe =
pentru fiecare nod al grafului
daca (culoare[nod] == alb)
PARCURGE(u);
// ==================== va parcurge o componenta conexa =======
PARCURGE(u)
{
culoare[u] = gri;
tDesc[u] = ++ timp;
pentru fiecare v vecin al lui u
{
daca (culoare[v] == alb)
{
p[v] = u;
PARCURGE[v];
}
}
culoare[u] = negru;
tFin[u] = ++timp;
}
Obs: Algoritmul este facut pentru a acoperi toate nodurile, chiar daca graful are mai multe componente conexe. Metoda PARCURGE corespunde acoperirii unei componente conexe.
Obs: Ganditi-va la asemanarile cu BKT. Daca am avea un arbore in care fiecare nod ar reprezenta asocierea unei variabile cu o valoare... si de la fiecare nod s-ar porni catre toate atribuirile posibile ale urmatoarei variabile... parca seamana, nu? Mai mult decat atat, si la DFS, ca si la BKT, ne putem intreba in ce ordine sa luam vecinii ca sa ajungem mai repede intr-un nod anume...
Obs.: Si la DFS, ca si la BFS, in urma parcurgerii se obtin arbori. Acestia se numesc arbori de adancime. Spre exemplu:
Evident, pe acest arbore s-ar putea adauga si muchiile pe care nu le-am folosit in parcurgere, dar l-ar face sa nu mai fie arbore. Totusi, in cazul grafurilor orientate, si aceste muchii au o semnificatie speciala.
Am transformat graful din exemplul precedent intr-unul orientat si i-am mai adaugat cateva noduri. Si de data asta i-am figurat toate muchiile. Acestea vor fi:
- muchii inainte: muchia (1,4);
- muchii inapoi: muchia (3,1);
- muchii de traversare: muchia (9,1).
2.3. Sortarea topologica:Se da un graf orientat. Orientarea muchiilor corespunde unei relatii de ordine de la nodul sursa catre cel destinatie. Ni se cere sa asezam nodurile in asa fel incat pentru orice muchie (u,v), u sa apara inaintea lui v.
Sortarea topologica are sens numai daca este aplicata asupra unui graf orientat si aciclic. De ce? Daca nu ar fi graf orientat, nu am avea despre ce ordine sa discutam. Iar un ciclu ar fi indeajuns pentru a nu putea stabili o ordine intre nodurile care il alcatuiesc.
Sa consideram urmatorul exemplu:
Ei bine, de aici si pana la a obtine o succesiune de noduri ordonate nu este decat un singur pas: avem grija sa asezam nodul radacina al fiecarui subarbore intr-o stiva, iar pentru afisare scoatem pe rand cate un nod din stiva tiparind odata cu el si nodurile din arborele sau in ordinea obtinuta.
Obs: Pentru un graf dat pot exista mai multe insiruiri de noduri care sa respecte cerinta.
Convingeti-va ca solutia obtinuta este intr-adevar corecta si ca toate solutiile care se pot obtine prin aceasta metoda sunt si ele corecte.
Obs: Nu este singurul algoritm pentru sortare topologica.
Optional: Un alt algoritm pentru sortarea topologica:
Algoritmul urmator se aplica recursiv. La fiecare pas se elimina un nod in care nu intra nici o muchie, nod pe care il furnizam catre output. Prin eliminare se intelege implicit ca se elimina si muchiile adiacente nodului respectiv. Din moment ce nu erau muchii care sa intre in el, asta inseamna ca se elimina muchiile care pornesc din el.
Daca la un moment dat mai avem noduri si nu mai putem face eliminare, inseamna ca exista un ciclu in graf si nu exista sortare topologica pentru el.
3. Rezumat:Parcurgerea in latime (BFS) foloseste o coada pentru a putea vizita toti vecinii unui nod dat inainte de a-l marca drept prelucrat. Se poate folosi inclusiv pentru determinarea drumurilor de lungime minima si returneaza cate un arbore pentru fiecare componenta conexa.
Are complexitatea O(E+V).
Parcurgerea in adancime (DFS) se poate implementa intuitiv in forma recursiva, iar principiul sau de functionare sta la baza BKT. DFS se foloseste in sortarea topologica si furnizeaza unul sau mai multi arbori de adancime. De o utilitate interesanta sunt timpii de descoperire si de finalizare a prelucrarii unui nod in cadrul operatiei de parcurgere.
Are complexitatea Θ(E+V).
Complexitatea spatiala este mai buna decat la BFS.