1. Graf conex si componenta conexa — definitii
O componenta conexa a unui graf este un subgraf conex maximal: un set de noduri intre care exista lanturi, si care nu poate fi extins cu niciun alt nod din graf pastrand aceasta proprietate.
Graf CONEX (1 componenta): Graf NECONEX (3 componente):
1 --- 2 --- 4 1 --- 2 --- 3
| |
3 --- 5 --- 6 4 --- 5 6 --- 7
Poti ajunge din 1 in 6: Nu poti ajunge din 1 in 4,
1-2-5-6 (lant de lungime 3) nici din 5 in 7.
Componentele grafului neconex:
C1 = {1, 2, 3}
C2 = {4, 5}
C3 = {6, 7}
- Componentele conexe partitioneaza multimea nodurilor: fiecare nod apartine exact unei componente.
- Un graf cu n noduri si 0 muchii are n componente conexe (fiecare nod izolat este propria sa componenta).
- Un graf conex are exact 1 componenta conexa.
- Adaugarea unei muchii intre doua componente diferite reduce numarul de componente cu 1.
2. Verificarea conexitatii cu un singur BFS
INITIALIZEAZA vizitat[] = False pentru toti
RULEAZA BFS pornind din nodul 1
DACA numarul nodurilor vizitate == n:
RETURNEAZA "conex"
ALTFEL:
RETURNEAZA "neconex"
Implementare Python cu BFS:
# Verifica conexitatea cu BFS from collections import deque def este_conex(n, muchii): adj = {i: [] for i in range(1, n + 1)} for u, v in muchii: adj[u].append(v) adj[v].append(u) vizitat = set() coada = deque([1]) vizitat.add(1) while coada: nod = coada.popleft() for vecin in adj[nod]: if vecin not in vizitat: vizitat.add(vecin) coada.append(vecin) return len(vizitat) == n # Graf conex: 5 noduri, lant 1-2-3-4-5 print(este_conex(5, [(1,2),(2,3),(3,4),(4,5)])) # Graf neconex: 5 noduri, 2 componente print(este_conex(5, [(1,2),(2,3),(4,5)]))
True False
BFS viziteaza fiecare nod exact o data (V operatii) si examineaza fiecare muchie exact de doua ori (2E operatii). Total: O(V + E). Nu putem face mai bine: trebuie sa examinam fiecare muchie cel putin o data, deci O(V + E) este optim pentru verificarea conexitatii.
3. Numarat componente conexe — DFS recursiv
INITIALIZEAZA vizitat[] = False; nr_componente = 0
PENTRU FIECARE nod din 1 la n:
DACA nod NU este vizitat:
nr_componente ← nr_componente + 1
RULEAZA DFS(nod) // marcheaza toata componenta
RETURNEAZA nr_componente
Implementare Python cu DFS recursiv:
# DFS recursiv - marcheaza toti vecinii accesibili din 'nod' def dfs(adj, nod, vizitat): vizitat[nod] = True for vecin in adj[nod]: if not vizitat[vecin]: dfs(adj, vecin, vizitat) def numar_componente_dfs(n, muchii): adj = {i: [] for i in range(1, n + 1)} for u, v in muchii: adj[u].append(v) adj[v].append(u) vizitat = {i: False for i in range(1, n + 1)} nr = 0 for nod in range(1, n + 1): if not vizitat[nod]: nr += 1 dfs(adj, nod, vizitat) return nr # Exemplu: 7 noduri, 3 componente ({1,2,3}, {4,5}, {6,7}) n = 7 muchii = [(1,2), (2,3), (4,5), (6,7)] print("Graf:", n, "noduri, muchii =", muchii) print("Numar componente conexe:", numar_componente_dfs(n, muchii))
Graf: 7 noduri, muchii = [(1, 2), (2, 3), (4, 5), (6, 7)] Numar componente conexe: 3
nod=1: nevizitat -> nr=1, DFS(1) marcheaza {1, 2, 3}
nod=2: deja vizitat -> sare
nod=3: deja vizitat -> sare
nod=4: nevizitat -> nr=2, DFS(4) marcheaza {4, 5}
nod=5: deja vizitat -> sare
nod=6: nevizitat -> nr=3, DFS(6) marcheaza {6, 7}
nod=7: deja vizitat -> sare
Rezultat final: nr = 3
4. Numarat componente cu BFS — varianta recomandata
# BFS - numara componentele conexe from collections import deque def numar_componente(n, muchii): adj = {i: [] for i in range(1, n + 1)} for u, v in muchii: adj[u].append(v) adj[v].append(u) vizitat = {i: False for i in range(1, n + 1)} nr_componente = 0 for nod in range(1, n + 1): if not vizitat[nod]: nr_componente += 1 # BFS din nodul curent - marcheaza toata componenta coada = deque([nod]) vizitat[nod] = True while coada: crt = coada.popleft() for vecin in adj[crt]: if not vizitat[vecin]: vizitat[vecin] = True coada.append(vecin) return nr_componente # Exemplul din lectie: 8 noduri, 3 componente n = 8 muchii = [(1,2), (1,3), (2,4), (5,6), (7,8)] print(f"Graf cu {n} noduri si muchiile: {muchii}") print(f"Numar componente conexe: {numar_componente(n, muchii)}")
Graf cu 8 noduri si muchiile: [(1, 2), (1, 3), (2, 4), (5, 6), (7, 8)] Numar componente conexe: 3
1 --- 2 --- 4 5 --- 6 7 --- 8
|
3
Componenta 1: {1, 2, 3, 4}
Componenta 2: {5, 6}
Componenta 3: {7, 8}
Numar total componente: 3
5. Analiza complexitatii O(V + E) — de ce este optima
Bucla exterioara itereaza prin toate V nodurile. Dar nu fiecare iteratie declanseaza o parcurgere costisitoare — in total, pe intreaga executie a algoritmului:
- Fiecare nod este adaugat in coada exact o data (la prima intalnire, cand este marcat vizitat).
- Fiecare muchie (u, v) este examinata exact de doua ori: o data din u, o data din v.
- Suma tuturor parcurgerilor BFS/DFS = V adaugari + 2E examinari = O(V + E).
Reprezentare | Spatiu | Parcurgere BFS/DFS | Recomandata cand -------------------+-----------+---------------------+-------------------- Lista de adiacenta | O(V + E) | O(V + E) | Grafuri rare (E << V^2) Matrice adiacenta | O(V^2) | O(V^2) | V mic, E mare (dens) La olimpiada/BAC: lista de adiacenta este MEREU preferata.
Graf conex cu n noduri: - Are cel putin n-1 muchii (exact n-1 muchii => arbore) - BFS/DFS dintr-un nod viziteaza toate n nodurile Graf neconex cu n noduri si k componente: - BFS/DFS dintr-un nod viziteaza doar nodurile componentei sale Numar componente = numarul de apeluri BFS/DFS din bucla exterioara
6. Componente conexe in C++ EXCLUSIV INTENSIV
In C++ folosim vector<vector<int>> pentru lista de adiacenta si queue<int> pentru BFS. Conventie olimpiada: indexare noduri de la 1, vector<bool> vizitat(n+1, false) pentru marcare.
#include <iostream> #include <vector> #include <queue> using namespace std; // BFS dintr-un nod sursa - marcheaza toata componenta void bfs(vector<vector<int>>& adj, vector<bool>& vizitat, int sursa) { queue<int> q; q.push(sursa); vizitat[sursa] = true; while (!q.empty()) { int crt = q.front(); q.pop(); for (int vecin : adj[crt]) { if (!vizitat[vecin]) { vizitat[vecin] = true; q.push(vecin); } } } } int main() { int n = 8; vector<vector<int>> adj(n + 1); // Muchiile grafului (neorientat: adaugam in ambele directii) auto addEdge = [&](int u, int v) { adj[u].push_back(v); adj[v].push_back(u); }; addEdge(1,2); addEdge(1,3); addEdge(2,4); addEdge(5,6); addEdge(7,8); vector<bool> vizitat(n + 1, false); int nr_componente = 0; for (int nod = 1; nod <= n; nod++) { if (!vizitat[nod]) { nr_componente++; bfs(adj, vizitat, nod); } } cout << "Graf cu " << n << " noduri si muchiile: {1-2, 1-3, 2-4, 5-6, 7-8}" << "\n"; cout << "Numar componente conexe: " << nr_componente << "\n"; cout << "Este conex: " << (nr_componente == 1 ? "da" : "nu") << "\n"; return 0; }
Graf cu 8 noduri si muchiile: {1-2, 1-3, 2-4, 5-6, 7-8}
Numar componente conexe: 3
Este conex: nu
Python C++ (intensiv)
--------------------------------+----------------------------------
deque([nod]) queue<int> q; q.push(nod);
coada.popleft() q.front(); q.pop();
vizitat = {i: False ...} vector<bool> vizitat(n+1, false);
adj = {i: [] for ...} vector<vector<int>> adj(n+1);
adj[u].append(v) adj[u].push_back(v);
Logica algoritmului este identica in ambele limbaje — diferenta este sintaxa si tipizarea explicita a lui C++.