De ce contează?
Imaginează-ți un arhipelag: insule de oameni legate între ele prin poduri. De pe o insulă poți ajunge pe vecină dacă există un lanț de poduri între voi, dar n-ai cum să treci pe o insulă ruptă complet de restul. Întrebarea „câte grupuri de oameni se pot vizita reciproc?” este exact întrebarea „câte componente conexe are graful?”.
Intuiția
Un graf neorientat se sparge natural în insule: grupuri de noduri în care din oricare ajungi la oricare altul mergând pe muchii. O astfel de insulă maximală se numește componentă conexă. Dacă întreg graful e o singură insulă, spunem că e conex. A „înțelege conexitatea” înseamnă să vezi graful nu ca o grămadă de noduri, ci ca aceste insule separate — și să le poți număra.
Ideea centrală: o parcurgere (DFS sau BFS) pornită din orice nod al unei insule vizitează exact acea insulă — nici mai mult (nu poate sări peste apă, nu există muchii spre alte insule), nici mai puțin (există drum către fiecare nod al ei). Deci „o pornire de parcurgere” = „o componentă”.
Vezi cum funcționează
Pornește animația și urmărește cum o singură parcurgere (DFS) intră într-un nod și „colorează” toată insula din care face parte. Când nu mai are unde să meargă, căutăm următorul nod necolorat și pornim o insulă nouă.
Folosește ← și → ca să pășești nod cu nod prin parcurgere, sau Redă pentru animație. Observă: fiecare schimbare de culoare = o nouă componentă, adică o nouă pornire de DFS dintr-un nod care era încă necolorat.
Cum afli dacă două noduri sunt în aceeași insulă? Prima idee: pentru fiecare
pereche (u, v) pornești un BFS din u și verifici dacă ajungi la v. Repeți
asta pentru toate perechile ca să grupezi nodurile.
Sunt aproximativ n^2 perechi, iar fiecare BFS costă O(n+m). Total
O(n*(n+m)) — pe un graf cu n = 100000 noduri devine miliarde de operații și
ia TLE. Mai rău: recalculezi de la zero aceeași insulă iar și iar.
Nu trebuie să verifici perechi. Un singur DFS (sau BFS) pornit dintr-un nod vizitează toată componenta lui — toate nodurile la care există drum — și le marchează ca vizitate. Insula întreagă cade dintr-o singură parcurgere.
Parcurgi nodurile 1..n. Când dai de un nod încă nevizitat, pornești un DFS de
acolo (care îi colorează toată insula) și incrementezi un contor. Numărul de
componente = de câte ori a trebuit să pornești o nouă parcurgere. Graful e conex
exact când acest număr e 1. Cost total: O(n+m).
Algoritmul pas cu pas
Fie un graf cu 6 noduri și muchiile 1-2, 2-3, 4-5, 5-6. Se văd două insule:
{1,2,3} și {4,5,6}. Ținem un vector comp[] care zice, pentru fiecare nod, în
ce componentă a căzut (0 = încă nevizitat). La început totul e 0:
| comp | 0 | 0 | 0 | 0 | 0 | 0 |
| nod | 1 | 2 | 3 | 4 | 5 | 6 |
Parcurgem nodurile de la 1. Nodul 1 are comp = 0, deci e nevizitat: pornim
prima parcurgere și îi dăm eticheta 1. DFS pleacă din 1, ajunge la 2
(muchia 1-2), apoi la 3 (muchia 2-3) — ordinea de vizitare este
1 → 2 → 3 și toată insula {1,2,3} primește eticheta 1:
| comp | 1 | 1 | 1 | 0 | 0 | 0 |
| nod | 1 | 2 | 3 | 4 | 5 | 6 |
start-DFS-1 |
Continuăm. Nodurile 2 și 3 au deja comp != 0, le sărim. Următorul nevizitat
e 4 (comp = 0): pornim a doua parcurgere, etichetă 2. DFS vizitează
4 → 5 → 6 — insula {4,5,6} primește eticheta 2:
| comp | 1 | 1 | 1 | 2 | 2 | 2 |
| nod | 1 | 2 | 3 | 4 | 5 | 6 |
start-DFS-2 |
Nodurile 5 și 6 sunt deja vizitate, le sărim. Am terminat: am pornit DFS-ul de
2 ori, deci graful are 2 componente conexe. Cum 2 != 1, graful nu e
conex.
Contorul nu crește la fiecare nod, ci doar când dai de un nod încă necolorat —
adică o insulă pe care n-ai mai văzut-o. De aceea comp[] (sau un simplu viz[]
boolean) este esențial: el te oprește să renumeri o insulă deja explorată.
Implementare C++
#include <iostream>
#include <vector>
using namespace std;
int n, m;
vector<int> adj[100005]; // liste de adiacenta
int comp[100005]; // comp[u] = componenta nodului u (0 = nevizitat)
// DFS care eticheteaza toata componenta nodului u cu c
void dfs(int u, int c) {
comp[u] = c;
for (int vec : adj[u]) {
if (comp[vec] == 0) {
dfs(vec, c);
}
}
}
int main() {
// exemplu: 6 noduri, muchiile 1-2, 2-3, 4-5, 5-6
n = 6;
m = 4;
int E[4][2] = {{1, 2}, {2, 3}, {4, 5}, {5, 6}};
for (int i = 0; i < m; i++) {
int a = E[i][0], b = E[i][1];
adj[a].push_back(b); // graf neorientat: ambele sensuri
adj[b].push_back(a);
}
int componente = 0;
for (int u = 1; u <= n; u++) {
if (comp[u] == 0) { // insula noua, inca neexplorata
componente++; // o noua pornire = o noua componenta
dfs(u, componente); // eticheteaza toata insula lui u
}
}
cout << "Componente conexe: " << componente << "\n"; // 2
if (componente == 1) {
cout << "Graful este conex\n";
} else {
cout << "Graful NU este conex\n"; // se afiseaza asta
}
return 0;
}Pe exemplul nostru: dfs(1, 1) etichetează {1,2,3} cu 1, apoi dfs(4, 2)
etichetează {4,5,6} cu 2 — exact vectorul comp = [1,1,1,2,2,2] din trace-ul
de mai sus. Contorul ajunge la 2, deci graful nu e conex. Bonus: comp[]
răspunde instant la întrebarea „sunt u și v în aceeași insulă?” —
comp[u] == comp[v].
Complexitate
| Caz | Timp | Spațiu |
|---|---|---|
| Orice graf | O(n+m) | O(n) |
Fiecare nod e vizitat o singură dată (comp[u] != 0 îl blochează la a doua
întâlnire), iar fiecare muchie e parcursă de două ori în total (o dată din fiecare
capăt) — de aici O(n+m), indiferent câte porniri de DFS faci: parcurgerile nu se
suprapun, ci își împart graful. Spațiul O(n) vine din vectorul comp[] și din
stiva recursiei.
Trei capcane clasice:
-
Pornești DFS din toate nodurile fără să verifici marcajul. Atunci numeri fiecare nod ca o componentă nouă și obții ceva apropiat de
n, nu numărul real de insule. Pornirea unei componente noi se face DOAR pentru noduri cucomp[u] == 0. -
Resetezi marcajul de vizitat între porniri. Tentant („încep o parcurgere nouă, curăț vectorul”), dar fatal: fără memoria insulelor deja explorate, fiecare pornire re-descoperă noduri vechi și numărătoarea explodează — plus că pierzi garanția
O(n+m). Vectorulcomp[]se inițializează o singură dată. -
Stack overflow la DFS recursiv. Pe un graf-lanț cu
n = 100000noduri, recursia atinge adâncimea100000și stiva crapă (runtime error, nu TLE). Soluții: DFS iterativ custack<int>sau, mai simplu, BFS cuqueue<int>— numărătoarea componentelor funcționează identic cu orice parcurgere.