Invatare Atomica

Parcurgere in adancime (DFS)

Progres lectie:
0%
🎯

Obiectivul lectiei

Vei intelege cum functioneaza algoritmul de parcurgere in adancime (DFS), il vei implementa iterativ si recursiv, si vei aplica DFS pentru a detecta cicluri si a numara componentele conexe ale unui graf.

Dupa aceasta lectie vei putea:

  • Sa descrii principiul DFS: mergem cat mai adanc, marcam nodurile vizitate
  • Sa implementezi DFS iterativ folosind o stiva (Python)
  • Sa implementezi DFS recursiv in Python si C++ (la intensiv)
  • Sa analizezi complexitatea temporala si spatiala a DFS: O(V + E)
  • Sa aplici DFS pentru detectia ciclurilor si numararea componentelor conexe
  • Sa rezolvi probleme tip Bacalaureat cu parcurgeri in adancime

Incearca singur!

Provocare initiala:

Considera graful cu nodurile 0, 1, 2, 3 si muchiile: 0-1, 0-2, 1-3. Daca incepem din nodul 0 si mergem intotdeauna la primul vecin (cel mai mic) nevizitat, in ce ordine vizitam nodurile?

💡 Ai nevoie de un indiciu?

Din 0, mergem la 1 (primul vecin). Din 1, mergem la 3. Din 3 nu mai avem vecini nevizitati, revenim la 1, revenim la 0, mergem la 2.

Ordinea: 0 → 1 → 3 → 2.

1

1. Ce este DFS si ideea de baza

DFS (Depth-First Search) = parcurgere in adancime: din nodul curent, mergem cat mai departe pe un drum pana nu mai putem continua, apoi revenim si exploram alta ramura.
Analogie: explorarea unui labirint pe hartie

Tii o evidenta a nodurilor vizitate cu un creion. Alegi un drum si mergi pe el cat mai departe, marcand fiecare nod. Cand ajungi la un nod fara iesiri noi, te intorci si iei urmatorul drum neexplorat.

Idei cheie:
  • Fiecare nod este vizitat exact o data (marcam cu vizitat[])
  • Implementare iterativa: folosim o stiva (LIFO)
  • Implementare recursiva: stiva de apeluri a programului joaca rolul stivei
  • Complexitate: O(V + E) unde V = nr. noduri, E = nr. muchii
Graful folosit in toata lectia (6 noduri, 6 muchii)
# Noduri: 0, 1, 2, 3, 4, 5
# Muchii: 0-1, 0-2, 1-3, 1-4, 2-5, 4-5
#     0
#    / \
#   1   2
#  / \   \
# 3   4---5
2

2. DFS iterativ in Python (cu stiva)

# DFS iterativ - Python
graf = {
    0: [1, 2],
    1: [0, 3, 4],
    2: [0, 5],
    3: [1],
    4: [1, 5],
    5: [2, 4]
}

def dfs_iterativ(graf, start):
    vizitat = set()
    stiva = [start]
    ordine = []
    while stiva:
        nod = stiva.pop()  # scoatem din varful stivei
        if nod not in vizitat:
            vizitat.add(nod)
            ordine.append(nod)
            # punem vecinii in ordine INVERSA
            # ca sa ii procesam in ordine crescatoare
            for vecin in sorted(graf[nod], reverse=True):
                if vecin not in vizitat:
                    stiva.append(vecin)
    return ordine

rezultat = dfs_iterativ(graf, 0)
print("Ordine DFS (iterativ, start=0):", rezultat)
Output real (rulat cu python):
Ordine DFS (iterativ, start=0): [0, 1, 3, 4, 5, 2]
Urmarire pas cu pas (primii 4 pasi)
Stiva initiala: [0]
Pas 1: scoatem 0, vizitat={0}, punem [2,1] -> stiva=[2,1]
Pas 2: scoatem 1, vizitat={0,1}, punem [4,3] -> stiva=[2,4,3]
Pas 3: scoatem 3, vizitat={0,1,3}, niciun vecin nou -> stiva=[2,4]
Pas 4: scoatem 4, vizitat={0,1,3,4}, punem [5] -> stiva=[2,5]
...
3

3. DFS recursiv in Python

Varianta recursiva este mai eleganta. Stiva este gestionata implicit de stiva de apeluri a programului: fiecare apel dfs(vecin) creeaza un nou cadru pe stiva sistemului.
# DFS recursiv - Python
graf = {
    0: [1, 2],
    1: [0, 3, 4],
    2: [0, 5],
    3: [1],
    4: [1, 5],
    5: [2, 4]
}

def dfs_recursiv(graf, nod, vizitat=None, ordine=None):
    if vizitat is None:
        vizitat = set()
        ordine = []
    vizitat.add(nod)
    ordine.append(nod)
    for vecin in sorted(graf[nod]):
        if vecin not in vizitat:
            dfs_recursiv(graf, vecin, vizitat, ordine)
    return ordine

print("Ordine DFS (recursiv, start=0):",
      dfs_recursiv(graf, 0))
Output real (rulat cu python):
Ordine DFS (recursiv, start=0): [0, 1, 3, 4, 5, 2]
Arborele de apeluri recursive
dfs(0)
  dfs(1)     <- primul vecin al lui 0
    dfs(3)   <- primul vecin al lui 1
    <return> <- 3 fara vecini nevizitati
    dfs(4)   <- al doilea vecin al lui 1
      dfs(5) <- vecinul nevizitat al lui 4
        dfs(2) <- via 5
        <return>
      <return>
    <return>
  <return>
<return>
4

4. DFS recursiv in C++ EXCLUSIV INTENSIV

⚡ Sectiune doar pentru intensiv informatica

La profilul intensiv implementam DFS si in C++. Diferente fata de Python: lista de adiacenta se declara ca vector<int> adj[NMAX], vectorul viz[] e de obicei global, tipurile se declara explicit, fiecare instructiune se termina cu ;.

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int n = 6;
vector<int> adj[6];
bool viz[6];

void dfs(int nod) {
    viz[nod] = true;
    cout << nod << " ";
    for (int vecin : adj[nod])
        if (!viz[vecin]) dfs(vecin);
}

int main() {
    adj[0] = {1, 2};
    adj[1] = {0, 3, 4};
    adj[2] = {0, 5};
    adj[3] = {1};
    adj[4] = {1, 5};
    adj[5] = {2, 4};
    for (int i = 0; i < n; i++)
        sort(adj[i].begin(), adj[i].end());
    cout << "DFS recursiv (start=0): ";
    dfs(0);
    cout << endl;
    return 0;
}
Output real (compilat g++ -std=c++17, apoi rulat):
DFS recursiv (start=0): 0 1 3 4 5 2 

Acelasi algoritm, acelasi rezultat. In C++ vectorul viz[] e un array global simplu, iar dfs() apeleaza recursiv pentru fiecare vecin nevizitat.

5

5. Aplicatie: Detectia ciclurilor cu DFS

In timpul DFS, daca gasim un vecin deja vizitat care nu este parintele nodului curent, am gasit un ciclu.
# Detectie ciclu cu DFS - Python
def are_ciclu(graf, n):
    vizitat = [False] * n

    def dfs(nod, parinte):
        vizitat[nod] = True
        for vecin in graf[nod]:
            if not vizitat[vecin]:
                if dfs(vecin, nod):
                    return True
            elif vecin != parinte:  # vizitat si nu parinte = ciclu!
                return True
        return False

    for i in range(n):
        if not vizitat[i]:
            if dfs(i, -1):
                return True
    return False

g1 = {0:[1], 1:[0,2], 2:[1,3], 3:[2]}  # lant, fara ciclu
g2 = {0:[1,2], 1:[0,2], 2:[1,0]}  # triunghi, cu ciclu
print("Graf 0-1-2-3 are ciclu?", are_ciclu(g1, 4))
print("Graf 0-1-2-0 are ciclu?", are_ciclu(g2, 3))
Output real (rulat cu python):
Graf 0-1-2-3 are ciclu? False
Graf 0-1-2-0 are ciclu? True
De ce excludem parintele?

In graful neorientat, muchia 0-1 exista si ca 1-0. Cand suntem in nodul 1 (venind din 0), nodul 0 este deja vizitat dar este parintele nostru, deci nu este ciclu real. Ciclul apare doar daca gasim alt nod vizitat, diferit de parinte.

6

6. Aplicatie: Numara componentele conexe

Un singur apel DFS pornit dintr-un nod viziteaza toata componenta conexa care contine acel nod. Dupa DFS, nodurile nevizitate apartin altor componente.
# Componente conexe cu DFS - Python
def componente_conexe(graf, n):
    vizitat = [False] * n
    componente = []

    def dfs(nod, comp):
        vizitat[nod] = True
        comp.append(nod)
        for vecin in sorted(graf.get(nod, [])):
            if not vizitat[vecin]:
                dfs(vecin, comp)

    for i in range(n):
        if not vizitat[i]:  # nod nevizitat = noua componenta
            comp = []
            dfs(i, comp)
            componente.append(comp)
    return componente

# Graf cu 2 componente: {0,1,2} si {3,4}
graf = {0:[1,2], 1:[0,2], 2:[0,1],
        3:[4], 4:[3]}
print("Componente conexe:",
      componente_conexe(graf, 5))
Output real (rulat cu python):
Componente conexe: [[0, 1, 2], [3, 4]]
Complexitate DFS - rezumat

Timp: O(V + E) - fiecare nod e vizitat o data (V), fiecare muchie e examinata de doua ori la grafuri neorientate.

Spatiu: O(V) - pentru vectorul vizitat + stiva de apeluri recursive (profunda maxim V in cazul unui graf-lant).

Exercitii practice

Exercitiul 1 (Nivel minim) - Urmarire DFS

Consideram graful cu 4 noduri (0, 1, 2, 3) si muchiile: 0-1, 0-3, 1-2, 2-3. Scrie ordinea de vizitare DFS pornind din nodul 0, cu vecinii procesati in ordine crescatoare. Urmareste stiva pas cu pas.

Exercitiul 2 (Nivel standard) - Detectie ciclu manual

Fie graful cu 5 noduri si muchiile: 0-1, 1-2, 2-3, 3-4, 4-1. Aplicand algoritmul din Atomul 5, determina daca graful are ciclu. Indica exact care pereche (nod curent, vecin vizitat diferit de parinte) declanseaza detectia.

Exercitiul 3 (Nivel performanta) INTENSIV - Implementare C++ componente conexe

Implementeaza in C++ o functie int nr_componente(vector<vector<int>>& adj, int n) care returneaza numarul de componente conexe. Testeaza pe graful din Atomul 6 (5 noduri, 2 componente). Complexitate ceruta: O(V + E).

Ce ai invatat astazi

  • DFS = parcurgere in adancime: mergem cat mai departe, marcam cu vizitat[]
  • DFS iterativ foloseste o stiva (stack); DFS recursiv foloseste stiva de apeluri
  • Complexitate: O(V + E) timp, O(V) spatiu
  • Detectia ciclurilor: vecin vizitat diferit de parinte = ciclu
  • Componente conexe: fiecare DFS independent acopera exact o componenta

Urmatoarea lectie

Continua cu Parcurgere BFS pentru a invata cum gasim distantele minime intr-un graf folosind o coada.

Continua →