Invatare Atomica

Parcurgere in latime (BFS)

Progres lectie:
0%
🎯

Obiectivul lectiei

Vei intelege algoritmul BFS (Breadth-First Search), vei sti sa-l implementezi pas cu pas folosind o coada si vei folosi BFS pentru a gasi drumul minim intr-un graf neponderat.

Dupa aceasta lectie vei putea:

  • Sa explici principiul parcurgerii in latime si diferenta fata de DFS
  • Sa implementezi BFS in Python folosind collections.deque
  • Sa calculezi distantele minime (in numar de muchii) de la un nod sursa la toate celelalte
  • Sa reconstruiesti drumul minim intre doua noduri folosind vectorul de parinti
  • Sa analizezi complexitatea timp si spatiu O(V + E) a algoritmului BFS
  • Sa implementezi BFS in C++ cu queue<int> si liste de adiacenta EXCLUSIV INTENSIV

Incearca singur!

Provocare:

Imagineaza-ti un grup de prieteni conectati intr-o retea. Plecand de la tine, vrei sa afli cate "cercuri de prietenie" te despart de fiecare persoana. Ce strategie ai folosi: ai explora toti prietenii directi intai, sau ai merge adanc pe o singura ramura?

💡 Ai nevoie de un indiciu?

Strategia corecta este: exploreaza TOI prietenii directi (nivel 1), apoi toti prietenii prietenilor (nivel 2) etc. Aceasta este exact ideea BFS: nivel cu nivel, nu pe o singura ramura adanca.

Instrumentul care asigura aceasta ordine este coada (queue): primul intrat, primul iesit.

1

1. Ideea din spatele BFS

BFS (Breadth-First Search) parcurge graful nivel cu nivel: mai intai toate nodurile la distanta 1 de sursa, apoi la distanta 2, si asa mai departe. Instrumentul care impune aceasta ordine este coada (queue) — First In, First Out.
Analogie: valuri concentrice pe apa

Arunci o piatra intr-un lac. Valurile se extind uniform in toate directiile simultan. BFS functioneaza la fel: pleaca din nodul sursa si "valurile" sale ating toate nodurile de la distanta 1, apoi 2 etc. Niciun val nu sare peste un nivel.

Comparatie BFS vs DFS

BFS — coada → nivel cu nivel → gaseste drumul MINIM (ca numar de muchii) in grafuri neponderate.

DFS — stiva/recursivitate → adanc pe o ramura intai → nu garanteaza drum minim, dar mai simplu de implementat recursiv.

Proprietate cheie: BFS garanteaza ca primul drum gasit spre orice nod este cel mai scurt (ca numar de muchii). Aceasta proprietate nu este valabila pentru DFS.
2

2. Algoritmul BFS — pasii si urmarirea

// BFS pornind din nodul sursa s
INITIALIZEAZA coada cu s
MARCHEAZA s ca vizitat
CAT TIMP coada nu e goala:
    nod ← SCOATE primul element din coada
    PENTRU FIECARE vecin al lui nod:
        DACA vecin NU e vizitat:
            MARCHEAZA vecin ca vizitat
            ADAUGA vecin in coada
Urmarire pas cu pas pe graful exemplu

Graful are 6 noduri cu muchiile: 1-2, 1-3, 2-4, 2-5, 3-6. Pornind din nodul 1:

Initial: coada=[1], vizitat=[1]
Pas 1: extras=1, adaugati=[2, 3], coada=[2, 3], vizitat=[1, 2, 3]
Pas 2: extras=2, adaugati=[4, 5], coada=[3, 4, 5], vizitat=[1, 2, 3, 4, 5]
Pas 3: extras=3, adaugati=[6],    coada=[4, 5, 6], vizitat=[1, 2, 3, 4, 5, 6]
Pas 4: extras=4, adaugati=[],     coada=[5, 6],    vizitat=[1, 2, 3, 4, 5, 6]
Pas 5: extras=5, adaugati=[],     coada=[6],       vizitat=[1, 2, 3, 4, 5, 6]
Pas 6: extras=6, adaugati=[],     coada=[],        vizitat=[1, 2, 3, 4, 5, 6]

Ordinea de vizitare: 1, 2, 3, 4, 5, 6 — exact nivel cu nivel.

Nivelurile (distantele) din sursa 1
Nivel 0: 1
Nivel 1: 2, 3
Nivel 2: 4, 5, 6
3

3. Implementare BFS in Python

Graful este reprezentat ca dictionar de liste de adiacenta. Coada este un deque din modulul collections.

# BFS simplu - ordine de vizitare
from collections import deque

def bfs(graf, start):
    vizitat = set()
    coada = deque([start])
    vizitat.add(start)
    ordine = []
    while coada:
        nod = coada.popleft()
        ordine.append(nod)
        for vecin in sorted(graf[nod]):
            if vecin not in vizitat:
                vizitat.add(vecin)
                coada.append(vecin)
    return ordine

# Graful din exemplu (noduri 1-6)
G = {
    1: [2, 3],
    2: [1, 4, 5],
    3: [1, 6],
    4: [2],
    5: [2],
    6: [3]
}

rezultat = bfs(G, 1)
print("Ordinea BFS pornind din nodul 1:", rezultat)
Output real (rulat cu python):
Ordinea BFS pornind din nodul 1: [1, 2, 3, 4, 5, 6]
De retinut

Marcam nodul vizitat cand il adaugam in coada (nu cand il scoatem). Daca am marca la scoatere, acelasi nod ar putea fi adaugat de mai multe ori de catre vecini diferiti, ducand la parcurgere gresita sau bucla infinita in grafuri cu cicluri.

4

4. Distante minime si reconstructia drumului

Intr-un graf neponderat (fiecare muchie are costul 1), BFS calculeaza distanta minima (numarul minim de muchii) de la sursa la orice alt nod. Inlocuim setul de vizitati cu un dictionar de distante: dist[v] != -1 inseamna vizitat.
# BFS - distante minime de la sursa
from collections import deque

def bfs_distante(graf, start):
    dist = {start: 0}
    coada = deque([start])
    while coada:
        nod = coada.popleft()
        for vecin in sorted(graf[nod]):
            if vecin not in dist:
                dist[vecin] = dist[nod] + 1
                coada.append(vecin)
    return dist

G = {
    1: [2, 3],
    2: [1, 4, 5],
    3: [1, 6],
    4: [2],
    5: [2],
    6: [3]
}
distante = bfs_distante(G, 1)
for nod in sorted(distante):
    print(f"dist[{nod}] = {distante[nod]}")
Output real (rulat cu python):
dist[1] = 0
dist[2] = 1
dist[3] = 1
dist[4] = 2
dist[5] = 2
dist[6] = 2

Adaugam vectorul de parinti pentru a reconstitui drumul efectiv:

# BFS - drum minim cu reconstructie
from collections import deque

def drum_minim(graf, start, stop):
    dist = {start: 0}
    parinte = {start: None}
    coada = deque([start])
    while coada:
        nod = coada.popleft()
        if nod == stop:
            break
        for vecin in sorted(graf[nod]):
            if vecin not in dist:
                dist[vecin] = dist[nod] + 1
                parinte[vecin] = nod
                coada.append(vecin)
    # Reconstruire drum: mers inapoi prin parinti
    drum = []
    crt = stop
    while crt is not None:
        drum.append(crt)
        crt = parinte[crt]
    drum.reverse()
    return drum, dist.get(stop, -1)

G = {
    1: [2, 3],
    2: [1, 4, 5],
    3: [1, 6],
    4: [2],
    5: [2],
    6: [3]
}
drum, lungime = drum_minim(G, 1, 4)
print("Drum minim de la 1 la 4:", " -> ".join(map(str, drum)))
print("Lungime drum:", lungime)
Output real (rulat cu python):
Drum minim de la 1 la 4: 1 -> 2 -> 4
Lungime drum: 2
5

5. Complexitate, reprezentari si proprietati

Complexitate timp: O(V + E)
• Fiecare nod este introdus si scos din coada exact o singura data → V operatii.
• Fiecare muchie este examinata de exacta doua ori (o data din fiecare capat) → 2E operatii.
• Total: O(V + E).

Complexitate spatiu: O(V)
Coada contine cel mult V noduri simultan. Vectorii de distante si parinti au V intrari.
Lista de adiacenta vs. matrice de adiacenta

Lista de adiacenta — spatiu O(V + E), traversare vecini O(grad(nod)). Recomandata pentru grafuri rare (putine muchii).

Matrice de adiacenta — spatiu O(V²), verificare muchie O(1), traversare vecini O(V). Practica doar la V mic sau grafuri dense.

In subiecte BAC cls. XI, grafurile au V ≤ 10..20 → ambele sunt acceptabile, dar lista de adiacenta este standard la olimpiada.

Proprietati esentiale BFS (de memorat pentru BAC):
  • Gaseste drumul minim ca numar de muchii in grafuri neponderate.
  • Intr-un graf ponderat (muchii cu costuri diferite), BFS NU garanteaza drum minim — se foloseste Dijkstra.
  • BFS poate determina daca un graf este conex: daca dintr-un nod oarecare BFS viziteaza toate V nodurile, graful este conex.
  • BFS poate determina numarul de componente conexe: se repeta BFS din fiecare nod nevizitat.
6

6. BFS in C++ EXCLUSIV INTENSIV

⚡ Sectiune exclusiv pentru profilul intensiv informatica

In C++ folosim #include <queue> pentru coada STL si vector<vector<int>> pentru lista de adiacenta. Conventie olimpiada: indexare de la 1, distanta initiala -1 inseamna nevizitat.

Exemplu 1: Calcul distante minime

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

int main() {
    int n = 6;
    vector<vector<int>> adj(n + 1);
    adj[1] = {2, 3}; adj[2] = {1, 4, 5};
    adj[3] = {1, 6}; adj[4] = {2};
    adj[5] = {2}; adj[6] = {3};

    vector<int> dist(n + 1, -1);
    queue<int> q;
    dist[1] = 0;
    q.push(1);

    while (!q.empty()) {
        int nod = q.front(); q.pop();
        for (int vecin : adj[nod]) {
            if (dist[vecin] == -1) {
                dist[vecin] = dist[nod] + 1;
                q.push(vecin);
            }
        }
    }

    for (int i = 1; i <= n; i++)
        cout << "dist[" << i << "] = " << dist[i] << "\n";
    return 0;
}
Output real (compilat g++ -std=c++17, apoi rulat):
dist[1] = 0
dist[2] = 1
dist[3] = 1
dist[4] = 2
dist[5] = 2
dist[6] = 2

Exemplu 2: Reconstructie drum minim

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

int main() {
    int n = 6;
    vector<vector<int>> adj(n + 1);
    adj[1] = {2, 3}; adj[2] = {1, 4, 5};
    adj[3] = {1, 6}; adj[4] = {2};
    adj[5] = {2}; adj[6] = {3};

    vector<int> dist(n + 1, -1);
    vector<int> parinte(n + 1, -1);
    queue<int> q;
    dist[1] = 0;
    q.push(1);

    while (!q.empty()) {
        int nod = q.front(); q.pop();
        for (int vecin : adj[nod]) {
            if (dist[vecin] == -1) {
                dist[vecin] = dist[nod] + 1;
                parinte[vecin] = nod;
                q.push(vecin);
            }
        }
    }

    int dest = 4;
    vector<int> drum;
    for (int crt = dest; crt != -1; crt = parinte[crt])
        drum.push_back(crt);
    reverse(drum.begin(), drum.end());

    cout << "Drum minim de la 1 la 4: ";
    for (int i = 0; i < (int)drum.size(); i++) {
        if (i) cout << " -> ";
        cout << drum[i];
    }
    cout << "\nLungime drum: " << dist[dest] << "\n";
    return 0;
}
Output real (compilat g++ -std=c++17, apoi rulat):
Drum minim de la 1 la 4: 1 -> 2 -> 4
Lungime drum: 2

Observatie: dist[v] == -1 joaca rolul de "nevizitat". Este idiomul standard C++ pentru BFS la olimpiada si BAC.

Exercitii practice

Exercitiul 1 (Nivel minim) — Urmarire manuala BFS

Considera graful neorientat cu 5 noduri si muchiile: 1-2, 1-3, 2-4, 3-4, 4-5. Pornind BFS din nodul 1, scrie in ordine:

  • a) Ordinea in care nodurile sunt vizitate.
  • b) Distanta minima de la nodul 1 la nodul 5.

Indiciu: urmareste coada pas cu pas ca in atomul 2. Raspuns asteptat: ordinea 1, 2, 3, 4, 5; distanta 3.

Vezi rezolvarea

Lista de adiacenta (graf neorientat): 1:[2,3], 2:[1,4], 3:[1,4], 4:[2,3,5], 5:[4].

  1. Coada=[1], vizitat={1}
  2. Extrag 1, vecini 2 si 3 nevizitati, deci ii adaug. Coada=[2,3], vizitat={1,2,3}
  3. Extrag 2, vecinul 4 nevizitat, deci il adaug. Coada=[3,4], vizitat={1,2,3,4}
  4. Extrag 3, vecinul 4 e deja vizitat, nimic nou. Coada=[4]
  5. Extrag 4, vecinul 5 nevizitat, deci il adaug. Coada=[5], vizitat={1,2,3,4,5}
  6. Extrag 5, fara vecini noi. Coada goala, BFS s-a terminat.

a) Ordinea de vizitare: 1, 2, 3, 4, 5.

b) Distante (dist[nod] este dist[parinte] plus 1): dist[1]=0, dist[2]=dist[3]=1, dist[4]=2, dist[5]=3. Distanta minima de la nodul 1 la nodul 5 este 3 (drum 1-2-4-5, are 3 muchii).

Exercitiul 2 (Nivel standard) — Numararea componentelor conexe

Implementeaza in Python functia numar_componente(n, muchii) care primeste numarul de noduri si o lista de perechi (muchii) si returneaza numarul componentelor conexe ale grafului. Foloseste BFS repetat: porneste BFS din fiecare nod nevizitat si incrementeaza contorul.

Exemplu: numar_componente(5, [(1,2),(2,3),(4,5)]) returneaza 2 (componentele {1,2,3} si {4,5}).

Vezi rezolvarea

Solutie completa, BFS repetat din fiecare nod nevizitat (ca la atomul 2 din lectie):

  1. from collections import deque
  2. def numar_componente(n, muchii):
  3. adj = {i: [] for i in range(1, n+1)}
  4. for u, v in muchii: adj[u].append(v); adj[v].append(u)
  5. vizitat, contor = set(), 0
  6. for nod in range(1, n+1):
  7. if nod in vizitat: continue
  8. contor += 1; coada = deque([nod]); vizitat.add(nod)
  9. while coada:
  10. crt = coada.popleft()
  11. for v in adj[crt]:
  12. if v not in vizitat: vizitat.add(v); coada.append(v)
  13. return contor

Verificare: pentru numar_componente(5, [(1,2),(2,3),(4,5)]), BFS din 1 acopera {1,2,3}, apoi BFS din 4 acopera {4,5}, deci functia returneaza 2, exact ca in enunt.

Exercitiul 3 (Nivel performanta) — Labirint 2D EXCLUSIV INTENSIV

Un labirint este reprezentat ca o matrice n x m de caractere: '.' = liber, '#' = zid. Gaseste numarul minim de pasi pentru a ajunge de la celula start la celula finish (miscare in 4 directii: sus, jos, stanga, dreapta).

Modeleaza fiecare celula ca un nod si aplica BFS. Starea BFS este o pereche (linie, coloana). Distanta initiala este 0 pentru celula start.

Implementeaza in C++ sau Python. Testeaza cu labirintul: [".##", "..#", "#.#", "..."], start=(0,0), finish=(3,2). Raspuns asteptat: 5 pasi. (Drum: (0,0)→(1,0)→(1,1)→(2,1)→(3,1)→(3,2))

Vezi rezolvarea

Nivel performanta: nu primesti cod complet, ci schema de rezolvare. Modeleaza fiecare celula (linie, coloana) ca nod al unui graf implicit; doua celule libere aflate la distanta 1 (sus/jos/stanga/dreapta) sunt legate printr-o muchie.

  1. Matrice vizitat[n][m] (sau set de perechi (linie,coloana)), initial toate False.
  2. BFS clasic ca la atomii 2-4 din lectie, dar starea din coada e perechea (linie, coloana), nu un singur numar.
  3. La extragere, incearca cele 4 directii; verifica intai limitele matricei (linie intre 0 si n-1, coloana intre 0 si m-1), apoi ca celula e libera ('.') si nevizitata.
  4. Marcheaza vizitat=True in momentul ADAUGARII in coada, nu la extragere (regula de la atomul 3).
  5. dist[start]=0; cand extragi celula finish din coada, dist[finish] e raspunsul.

Capcane: uitarea verificarii de limite (index in afara matricei), confuzia (linie,coloana) cu (x,y), marcarea vizitat prea tarziu (acelasi nod intra de mai multe ori in coada).

Criterii de evaluare: complexitate O(n*m), drum reconstituit corect cu vector de parinti, codul ruleaza pe testul dat si da 5.

Ce ai invatat astazi

  • BFS exploreaza graful nivel cu nivel, folosind o coada (FIFO)
  • Nodul se marcheaza vizitat cand este adaugat in coada (nu la scoatere)
  • BFS garanteaza drumul minim ca numar de muchii in grafuri neponderate
  • Vectorul de parinti permite reconstructia drumului efectiv, parcurgand inapoi pana la sursa
  • Complexitate: O(V + E) timp, O(V) spatiu
  • In Python: collections.deque cu popleft() O(1); in C++: queue<int> cu front() si pop()
  • BFS NU gaseste drum minim in grafuri ponderate (costuri diferite) — acolo se foloseste Dijkstra

Vrei mai mult?

Provocare: Ruleaza bfs_distante din Atomul 4 pe graful din lectie, dar de data asta pornind din nodul 6 in loc de 1. Scrie toate distantele obtinute. Verifica: distanta gasita de la 6 la 1 este egala cu cea gasita anterior de la 1 la 6? Acum imagineaza ca muchiile ar deveni arce orientate (ca in graful GO din lectia trecuta) — ar mai fi garantata aceasta simetrie?

De ce? BFS gaseste mereu primul drum ca fiind cel mai scurt intr-un graf neponderat. Dar daca ai rula acelasi algoritm pe un graf PONDERAT, cu muchii de costuri diferite, rezultatul ar putea fi gresit. Care este ipoteza ascunsa, nescrisa nicaieri in cod, pe care se bazeaza corectitudinea BFS?

Dincolo de lectie: Ideea de nivel-cu-nivel din BFS sta la baza calculului gradelor de separare in retelele sociale (cati prieteni intermediari intre doua persoane) si a protocoalelor de retea care numara numarul de hop-uri intre routere.

Urmatoarea lectie

Continua cu Lectia 4: Conexitate — componente conexe, verificare conexitate si algoritm de numarat componente.

Continua →