This is an old revision of the document!


Laborator 07: Parcurgerea Grafurilor. Sortare Topologica

Obiective laborator

  • Intelegerea conceptului de graf si a modurilor de parcurgere aferente
  • Utilitatea si aplicabilitatea sortarii topologice

Importanţă – aplicaţii practice

Grafurile sunt utile pentru a modela diverse probleme si se regasesc implementati in multiple aplicatii practice:

  • Retele de calculatoare (ex: stabilirea unei topologii fara bucle)
  • Pagini Web (ex: Google PageRank [1])
  • Retele sociale (ex: calcul centralitate [2])
  • Harti cu drumuri (ex: drum minim)
  • Modelare grafica (ex: prefuse [3], graph-cut [4] )

Descrierea problemei și a rezolvărilor

Graful poate fi modelat drept o pereche de multimi G = (V, E). Multimea V contine nodurile grafului (vertices), iar multimea E contine muchiile (edges), fiecare muchie stabilind o relatie de vecinatate intre doua noduri. O mare varietate de probleme se modeleaza folosind grafuri, iar rezolvarea acestora presupune explorarea spatiului. O parcurgere isi propune sa ia in discutie fiecare nod al grafului, exact o singura data, pornind de la un nod ales, numit in continuare nod sursa.

Reprezentarea in memorie a grafurilor se face, de obicei, cu liste de adiacenta sau cu matrice de adiacenta. Se pot folosi insa si alte structuri de date, de exemplu un map de perechi < <sursa,destinatie>,cost> .

Pe parcursul rularii algoritmilor de parcurgere, un nod poate avea 3 culori:

  • Alb = nedescoperit
  • Gri = a fost descoperit si este in curs de prcesare
  • Negru = a fost procesat

Se poate face o analogie cu o pata neagra care se extinde pe un spatiu alb. Nodurile gri se afla pe frontiera petei negre. Algoritmii de parcurgere pot fi caracterizati prin completitudine si optimalitate. Un algoritm de explorare complet va descoperi intotdeauna o solutie, daca problema accepta solutie. Un algoritm de explorare optimal va descoperi solutia optima a problemei din perspectiva numarului de pasi care trebuie efectuati.

Parcurgerea in lățime - BFS

Parcurgerea in latime (Breadth-first Search - BFS) este un algoritm de cautare in graf, in care, atunci cand se ajunge intr-un nod oarecare v, nevizitat, se viziteaza toate nodurile nevizitate adiacente lui v, apoi toate varfurile nevizitate adiacente varfurilor adiacente lui v, etc. Atentie! BFS depinde de nodul de start. Plecand dintr-un nod se va parcurge doar componenta conexa din care acesta face parte. Pentru grafuri cu mai multe componente conexe se vor obtine mai multi arbori de acoperire.

In urma aplicarii algoritmului BFS asupra fiecarei componente conexe a grafului, se obtine un arbore de acoperire (prin eliminarea muchiilor pe care nu le folosim la parcurgere). Pentru a putea reconstitui acest arbore, se pastreaza pentru fiecare nod dat identitatea parintelui sau. In cazul in care nu exista o functie de cost asociata muchiilor, BFS va determina si drumurile minime de la radacina la oricare nod.

Pentru implementarea BFS se foloseste o coada. In momentul adaugarii in coada, un nod trebuie colorat gri (a fost descoperit si urmeaza sa fie prelucrat).

Algoritmul de explorare BFS este complet si optimal.

Algoritm:

BFS(s, G) {
    foreach (u ∈ V) {
        p(u) = null; // initializari
        dist(s,u) = inf;
        c(u) = alb; 
    }
    dist(s) = 0; // distanta pana la sursa este 0
    c(s) = gri; //incepem prelucrarea nodului, deci culoarea devine gri
    Q = (); //se foloseste o coada cu nodurile de prelucrat
    Q = Q + s; // adaugam sursa in coada
    while (!empty(Q)) { // cat timp mai am noduri de prelucrat
        u = top(Q); // se determina nodul din varful cozii
        foreach v ∈ succs(u) { // pentru toti vecinii
            if (c(v) = alb) {// nodul nu a fost gasit, nu e in coada
                // actualizam structura date 
                dist(v) = dist(u) + 1;
                p(v) = u;
                c(v) = gri;
                Q = Q + v;
            } // close if
        } // close foreach
        c(u) = negru; //am terminat de prelucrat nodul curent
        Q = Q - u; //nodul este eliminat din coada
    } //close while
}

Complexitate:

  • cu lista: O(|E|+|V|)
  • cu matrice: O(|V|^2)

Parcurgerea in adancime – DFS

Parcurgerea in adancime (Depth-First Search - DFS) porneste de la un nod dat (nod de start), care este marcat ca fiind in curs de procesare. Se alege primul vecin nevizitat al acestui nod, se marcheaza si acesta ca fiind in curs de procesare, apoi si pentru acest vecin se cauta primul vecin nevizitat, si asa mai departe. In momentul in care nodul curent nu mai are vecini nevizitati, se marcheaza ca fiind deja procesat si se revine la nodul anterior. Pentru acest nod se cauta primul vecin nevizitat. Algoritmul se repeta pana cand toate nodurile grafului au fost procesate.

In urma aplicarii algoritmului DFS asupra fiecarei componente conexe a grafului, se obtine pentru fiecare dintre acestea cate un arbore de acoperire (prin eliminarea muchiilor pe care nu le folosim la parcurgere). Pentru a putea reconstitui acest arbore, pastram pentru fiecare nod dat identitatea parintelui sau.

Pentru fiecare nod se vor retine:

  • timpul descoperirii
  • timpul finalizarii
  • parintele
  • culoarea

Algoritmul de explorare DFS nu este nici complet (in cazul unei cautari pe un subarbore infinit), nici optimal (nu gaseste nodul cu adancimea minima).

Spre deosebire de BFS, pentru implementarea DFS se foloseste o stiva (abordare LIFO in loc de FIFO). Desi se poate face aceasta inlocuire in algoritmul de mai sus, de cele mai multe ori este mai intuitiva folosirea recusivitatii.

Algoritm:

DFS(G) {
    V = noduri(G)
    foreach (u ∈ V) { 
        // initializare structura date
        c(u) = alb;
        p(u)=null;      
    }
    timp = 0; // retine distanta de la radacina pana la nodul curent
    foreach (u ∈ V)
        if (c(u) = alb) explorare(u); // explorez nodul
}
 
explorare(u) {
    d(u) = timp++; // timpul de descoperire al nodului u
    c(u) = gri; // nod in curs de explorare
    foreach (v ∈ succes(u)) // incerc sa prelucrez vecinii
        if (c(v) = alb) { // daca nu au fost prelucrati deja
            p(v) = u;
            explorare(v);
        }
    c(u) = negru; // am terminat de prelucrat nodul curent
    f(u) = timp++; // timpul de finalizare al nodului u
}

Complexitate:

  • cu lista: O(|E|+|V|)
  • cu matrice: O(|V|^2)

Sortarea Topologica

Dandu-se un graf orientat aciclic, sortarea topologica realizeaza o aranjare liniara a nodurilor in functie de muchiile dintre ele. Orientarea muchiilor corespunde unei relatii de ordine de la nodul sursa catre cel destinatie. Astfel, daca (u,v) este una dintre muchiile grafului, u trebuie sa apara inaintea lui v in insiruire. Daca graful ar fi ciclic, nu ar putea exista o astfel de insiruire (nu se poate stabili o ordine intre nodurile care alcatuiesc un ciclu).

Sortarea topologica poate fi vazuta si ca plasarea nodurilor de-a lungul unei linii orizontale astfel incat toate muchiile sa fie directionate de la stanga la dreapta.

Exemplu:

Profesorul Bumstead isi sorteaza topologic hainele inainte de a se imbraca.

(a) Fiecare muchie (u,v) inseamna ca obiectul de imbracaminte u trebuie imbracat inaintea obiectului de imbracaminte v. Timpii de descoperire d(u) si de finalizare f(u) obtinuti in urma parcurgerii DFS sunt notati langa noduri.

(b) Acelasi graf, sortat topologic. Nodurile lui sunt aranjate de la stanga la dreapta in ordinea descrescatoare a timpului de finalizare f(u). Observati ca toate muchiile sunt orientate de la stanga la dreapta. Acum profesorul Bumstead se poate imbraca linistit.

Algoritm:

Sunt doi algoritmi cunoscuti pentru sortarea topologina.

Algoritmul bazat pe DFS:

  • parcurgere DFS pentru determinarea timpilor
  • sortare descrescatoare in functie de timpul de finalizare

Pentru a evita sortarea nodurilor in functie de timpul de finalizare, se poate folosi o stiva ce retine aceste noduri in ordinea terminarii parcurgerii.

Un alt algoritm este cel descris de Kahn:

TopSort(G) {
    V = noduri(G)
    L = vida;// lista care va contine elementele sortate
    // initializare S cu nodurile care nu au in-muchii
    foreach (u ∈ V) {
        if (u nu are in-muchii)
	    S = S + u;
    }
    while (!empty(S)) { // cat timp mai am noduri de prelucrat
        u = random(S); // se scoate un nod din multimea S
        L = L + u;  // adaug U la lista finala
        foreach v ∈ succs(u) { // pentru toti vecinii
	    sterge u-v; // sterge muchia u-v
            if (v nu are in-muchii)
	        S = S + v;  // adauga v la multimea S
        } // close foreach
    } //close while
    if (G are muchii)
        print(eroare);  // graf ciclic
    else
        print(L); // ordinea topologica
}

Complexitate optima: O(|E|+|V|)

Concluzii si observatii

Grafurile sunt foarte importante pentru reprezentarea si rezolvarea unei multitudini de probleme. Cele mai uzuale moduri de reprezentare a unui graf sunt:

  • liste de adiacenta
  • matrice de adiacenta

Cele doua moduri uzuale de parcurgere neinformata a unui graf sunt:

  • BFS – parcurgere in latime
  • DFS – parcurgere in adancime

Sortarea topologica este o modalitate de aranjare a nodurilor in functie de muchiile dintre ele. In functie de nodul de start al DFS, se pot obtine sortari diferite, pastrand insa proprietatile generale ale sortarii topologice.

Exercitii

In acest laborator vom folosi scheletul de laborator din arhiva skel-lab07.zip.

Pentru tot restrul semestrului, vom face conventia ca noduril sunt indexate de la 1 (1, 2, 3, …, n).

BFS

Se da un graf neorientat cu n noduri si m muchii. Se mai da un nod special source, pe care il vom numi sursa.

Se cere sa se gaseasca numarul minim de muchii ce trebuie parcurse de la source la toate celelalte noduri.

Restrictii si precizari:

  • $ n, m <= 10^5 $

Rezultatul se va returna sub forma unui vector d cu n elemente.

Conventie:

  • d[node] = numarul minim de muchii ce trebuie parcurse de la source la nodul node
  • d[source] = 0
  • d[node] = -1, daca nu se poate ajunge de la source la node

Exemplu 1

Exemplu 1

$n = 5$ $m = 4$ $source = 3$

$muchii: { (1,2); (1,3); (2,3); (2,4);} $

Raspuns:

node12345
d1102-1

Explicatie: Graful dat este cel din figura urmatoare.

  • d[3] = 0 pentru ca 1 este sursa
  • d[1] = d[2] = 1 pentru ca exista muchie directa de la 2 la fiecare nod
  • d[4] = 2 pentru ca trebuie sa parcurgem 2 muchii ($3-2-4$)
  • d[5] = -1 pentru ca nu se poate ajunge de la 3 la 5

Exemplu 2

Exemplu 2

$n = 7$ $m = 7$ $source = 1$

$muchii: { (1,2); (1,4); (2,3); (4,5); (5,6); (3,7); (7,6) } $

Raspuns:

node1234567
d0121233

Explicatie: Graful dat este cel din figura urmatoare.

  • d[1] = 0 pentru ca 1 este sursa
  • d[2] = d[4] = 1 pentru ca exista muchie directa de la 2 la fiecare nod
  • d[3] = d[5] = 2 pentru ca trebuie sa parcurgem 2 muchii ($1-2-3$, $1-4-5$)
  • d[6] = d[7] = 3 pentru ca trebuie sa parcurgem 3 muchii ($1-2-3-7$ sau $1-4-5-6$)

Exemplu 3

Exemplu 3

$n = 7$ $m = 8$ $source = 1$

$muchii: { (1,2); (1,4); (2,3); (4,5); (5,6); (3,7); (7,6); (1, 6) } $

Raspuns:

node1234567
d0121212

Explicatie: Graful dat este cel din figura urmatoare.

  • d[1] = 0 pentru ca 1 este sursa
  • d[2] = d[4] = d[6] = 1 pentru ca exista muchie directa de la 2 la fiecare nod
  • d[3] = d[5] = d[7] = 2 pentru ca trebuie sa parcurgem 2 muchii ($1-2-3$, $1-4-5$, $1-6-7$)

TOPOLOGICAL SORT

Referinte

pa/laboratoare/laborator-07.1523483832.txt.gz · Last modified: 2018/04/12 00:57 by darius.neatu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0