Table of Contents

Laborator 11 - Grafuri - Advanced

Responsabili

Obiective

În urma parcurgerii acestui laborator, studentul va fi capabil să:

Importanţă

Grafurile sunt utile pentru a modela diverse probleme şi se regăsesc implementaţi în multiple aplicaţii practice:

Aplicaţii parcurgeri

Componente conexe

Se numește componentă conexă a unui graf neorientat G = (V, E) un subgraf G1 = (V1, E1) în care pentru orice pereche de noduri (A, B) din V1 există un lanț de la A la B și implicit de la B la A.

Observație: Nu există un alt subgraf al lui G, G2 = (V2, E2), care să îndeplinească această condiție și care să îl conțină pe G1. În acest caz, G2 ar fi componenta conexă, iar G1 nu.

Algoritm

Complexitate

Complexitate Reprezentare prin liste de adiacență Reprezentare prin matrice de adiacență
Timp O(|V| + |E|) O(|V|²)
Spațiu O(|V| + |E|) O(|V|²)

Pseudocod

// Inițializări
pentru fiecare nod u din V
{
    stare[u] = nevizitat
}
componente_conexe = 0

// Funcție de vizitare a nodului
vizitare(nod)
{
    stare[nod] = vizitat
    printeaza nod
}

// Parcurgerea în adâncime
DFS(nod)
{
    stiva s

    vizitare(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ă
        {
            vizitare(vecin)
            s.introdu(vecin)
        }
        altfel
        {
            s.scoate(nodTop)
        }
    }
}

// Parcurgerea nodurilor din V
pentru fiecare nod u din V
{
    dacă stare[u] == nevizitat
    {
        componente_conexe = componente_conexe + 1
        DFS(u)
    }
}

Exemplu

Graful din imagine are 4 componente conexe.

Aflarea distanței minime între două 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. Deoarece BFS descoperă nodurile în ordinea crescătoare a distanței față de sursă, 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 păstrează pentru fiecare nod:

În momentul descoperirii unui nod y al cărui părinte este x, se fac atribuirile:

d[y] = d[x] + 1
p[y] = x

Sursa având d[A] = 0 și p[A] = NULL.

  • Dacă parcurgerea BFS se încheie fără ca nodul B să fi fost descoperit, nu există drum între A și B, deci distanța este infinită.
  • Algoritmul funcționează corect numai pentru grafuri cu cost uniform (toate muchiile au același cost). Pentru grafuri cu muchii de costuri diferite sunt necesari algoritmi mai avansați: Dijkstra, Bellman-Ford sau Floyd-Warshall.

Complexitate

Complexitate Reprezentare prin liste de adiacență Reprezentare prin matrice de adiacență
Timp O(|V| + |E|) O(|V|²)
Spațiu O(|V| + |E|) O(|V|²)

Pseudocod

// Inițializări
pentru fiecare nod u din V
{
    stare[u] = nevizitat
    d[u] = infinit
    p[u] = null
}

// Distanța între sursă și destinație
distanta(sursa, destinatie)
{
    stare[sursa] = vizitat
    d[sursa] = 0
    enqueue(Q, sursa)               // Punem nodul sursă în coada Q

    // BFS
    cât timp coada Q nu este vidă
    {
        v = dequeue(Q)              // Extragem nodul v din coadă
        pentru fiecare u dintre vecinii lui v
            dacă stare[u] == nevizitat
            {
                stare[u] = vizitat
                p[u] = v
                d[u] = d[v] + 1
                enqueue(Q, u)       // Adăugăm nodul u în coadă
            }
    }
    return d[destinatie]            // Dacă este infinit, nu există drum
}

Sortarea topologică

Se dă un graf orientat aciclic (DAG - Directed Acyclic Graph). 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 apare înaintea lui v în înșiruire.

Dacă graful este ciclic, sortarea topologică nu este posibilă, deoarece nu se poate stabili o ordine între nodurile care alcătuiesc un ciclu.

Sortarea topologică poate fi vizualizată ca plasarea nodurilor de-a lungul unei linii orizontale astfel încât toate muchiile să fie orientate de la stânga la dreapta, fără nicio muchie îndreptată înapoi spre un părinte.

Algoritm

Sortarea topologică se realizează printr-o parcurgere DFS, în care se rețin pentru fiecare nod:

La final, nodurile sunt sortate descrescător după tFin. Nodul care se finalizează cel mai târziu trebuie să apară primul în sortare, deoarece nu depinde de niciun alt nod nedescoperit încă.

Complexitate

Complexitate Reprezentare prin liste de adiacență Reprezentare prin matrice de adiacență
Timp O(|V| + |E|) O(|V|²)
Spațiu O(|V| + |E|) O(|V|²)

Pseudocod

// Inițializări
pentru fiecare nod u din V
{
    stare[u] = nevizitat
    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
    stare[nod] = vizitat
    printeaza nod
}

// Parcurgere în adâncime (recursiv)
DFS(nod)
{
    vizitare(nod)
    pentru fiecare vecin al lui nod
    {
        dacă stare[vecin] == nevizitat
        {
            p[vecin] = nod
            DFS(vecin)
        }
    }
    contor_timp = contor_timp + 1
    tFin[nod] = contor_timp
}

// Parcurgere noduri și calculare tDesc și tFin pentru fiecare nod
pentru fiecare nod u din V
{
    dacă stare[u] == nevizitat
    {
        DFS(u)
    }
}

// Sortare topologică
sortează nodurile din V descrescător în funcție de tFin[nod]

Exemplu

Profesorul Bumstead își sortează topologic hainele înainte de a se îmbrăca:

topologie.jpg

Sortarea topologică constă în sortarea nodurilor descrescător după timpii de finalizare. Nodul care se finalizează cel mai târziu nu depinde de niciun alt nod rămas, deci trebuie plasat primul în ordine topologică.

Graf bipartit

Se numește graf bipartit un graf G = (V, E) în care mulțimea nodurilor poate fi împărțită în două mulțimi disjuncte A și B astfel încât V = A ∪ B și E ⊆ A × B.

Altfel spus, nodurile grafului se pot împărți în 2 mulțimi astfel încât nu există muchii între noduri din aceeași mulțime.

Observație: Un graf nu este bipartit dacă conține un ciclu de lungime impară.

Algoritm

Complexitate

Complexitate Reprezentare prin liste de adiacență Reprezentare prin matrice de adiacență
Timp O(|V| + |E|) O(|V|²)
Spațiu O(|V|) O(|V|)

Pseudocod

cât timp încă sunt noduri nevizitate
{
    n = primul nod nevizitat

    nivel[n] = par
    enqueue(Q, n)        // Punem nodul sursă în coada Q

    // BFS
    cât timp coada Q nu este vidă
    {
        v = dequeue(Q)   // Extragem nodul v din coadă
        pentru fiecare u dintre vecinii lui v
        {
            dacă nivel[u] nedefinit
            {
                nivel[u] = (nivel[v] == par) ? impar : par
                enqueue(Q, u)    // Adăugăm nodul u în coadă
            }
            altfel dacă nivel[u] == nivel[v]
            {
                // Două noduri adiacente au același nivel
                // Graful nu este bipartit
                return false
            }
        }
    }
}
// Parcurgerea BFS s-a finalizat fără noduri adiacente pe același nivel
// Graful este bipartit
return true

Exemplu

Ciclu hamiltonian

Un lanț hamiltonian într-un graf orientat sau neorientat G = (V, E) este o cale ce trece prin fiecare nod din V o singură dată. Dacă nodul de început și cel de sfârșit coincid, lanțul formează un ciclu hamiltonian.

Un graf ce conține un ciclu hamiltonian se numește graf hamiltonian.

Problema găsirii unui ciclu hamiltonian este NP-completă, ceea ce înseamnă că nu se cunoaște niciun algoritm eficient (polinomial) pentru cazul general.

Algoritm

În cadrul acestui laborator, vom folosi metoda backtracking pentru găsirea unui ciclu hamiltonian. Se menține o listă în care sunt adăugate nodurile parcurse:

Complexitate

Complexitate Valoare
Timp O(|V|!) în cazul cel mai defavorabil
Spațiu O(|V|) pentru stiva de recursivitate și lanț

Pseudocod

// Inițializări
numar_noduri = număr de noduri din V

// Verifică dacă un nod este nou în lanț
nouInLant(nod, lant)
{
    return !lant.contine(nod)
}

// Construiește lanțul hamiltonian
construireLant(lant, lungime_lant)
{
    dacă lungime_lant == numar_noduri
    {
        inceput = lant[0]
        sfarsit = ultimul element din lant
        // Verifică dacă există muchie de la sfarsit spre inceput
        dacă muchie(sfarsit, inceput)
        {
            afiseaza ciclul
            return true
        }
    }
    altfel
    {
        pentru orice nod u din V
        {
            sfarsit = ultimul element din lant
            // Verifică dacă există muchie de la sfarsit spre u
            dacă muchie(sfarsit, u) si nouInLant(u, lant)
            {
                addLast(lant, u)       // Adaugă u la lanț

                construireLant(lant, lungime_lant + 1)
                // Pentru afișarea unui singur ciclu hamiltonian,
                // linia anterioară se înlocuiește cu:
                // dacă construireLant(lant, lungime_lant + 1) == true
                //     return true

                removeLast(lant, u)    // Backtrack
            }
        }
    }
    return false
}

// Apelează construirea ciclurilor hamiltoniene
cicluriHamiltoniene()
{
    // Din moment ce formează un ciclu, lanțul poate începe cu orice nod
    sursa = alegem un nod aleator din V
    addLast(lant, sursa)
    construireLant(lant, 1)
}

Exemplu

Exerciții

Trebuie să vă creați cont de Devmind, dacă nu v-ați creat deja, pe care îl veți folosi la SD pe toată durata semestrului.

1) [3.5p] Rezolvați problema Connected Components. 2) [3.5p] Rezolvați problema Minimum Path. 3) [3p] Rezolvați problema Check Bipartite.

Interviu

Această secțiune nu este punctată și încearcă să vă ofere o idee despre tipurile de întrebări pe care le puteți întâlni la un job interview din materia prezentată în cadrul laboratorului.

Probleme recomandate

Sortare topologică:

Componente conexe și grafuri bipartite:

Drumuri minime:

Probleme avansate:

Bibliografie