1. Ce este un graf? Definitie formala
- V — multimea varfurilor (nodurilor), V ≠ ∅
- E — multimea muchiilor (legaturilor intre noduri); E ⊆ V × V
Scriem |V| = n (numarul de noduri) si |E| = m (numarul de muchii).
Reteaua de drumuri intre orase: orasele = noduri, drumurile = muchii.
Reteaua de calculatoare: calculatoarele = noduri, cablurile de retea = muchii.
Orarul unui profesor: materiile = noduri, conflictele de timp = muchii.
In Python putem reprezenta un graf simplu cu doua seturi:
# Graf neorientat: G = (V, E) V = {1, 2, 3, 4, 5} E = {(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)} print("Noduri (V):", sorted(V)) print("Muchii (E):", sorted(E)) print("Numar noduri |V| =", len(V)) print("Numar muchii |E| =", len(E))
Noduri (V): [1, 2, 3, 4, 5] Muchii (E): [(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)] Numar noduri |V| = 5 Numar muchii |E| = 6
2. Gradul unui nod si Lema Mainilor
Lema Mainilor (Handshaking Lemma):
∑ d(v) = 2 · |E|
Consecinta imediata: Numarul de noduri cu grad impar este intotdeauna par (nu poate exista un numar impar de noduri cu grad impar).
# Calculul gradului fiecarui nod intr-un graf neorientat muchii = [(1, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)] noduri = [1, 2, 3, 4, 5] grad = {} for nod in noduri: grad[nod] = 0 for (u, v) in muchii: grad[u] += 1 grad[v] += 1 print("Gradul fiecarui nod:") for nod in noduri: print(f" grad({nod}) = {grad[nod]}") suma_grade = sum(grad.values()) print(f"\nSuma gradelor = {suma_grade}") print(f"2 * |E| = {2 * len(muchii)}") print("Lema mainilor: suma grade = 2 * numar muchii ->", suma_grade == 2 * len(muchii))
Gradul fiecarui nod: grad(1) = 2 grad(2) = 3 grad(3) = 3 grad(4) = 2 grad(5) = 2 Suma gradelor = 12 2 * |E| = 12 Lema mainilor: suma grade = 2 * numar muchii -> True
Nodurile 2 si 3 au grad 3 (gradul maxim in acest graf). Nodurile 1, 4, 5 au grad 2. Un nod cu grad 0 se numeste nod izolat. Complexitate calcul grade: O(|V| + |E|).
3. Graf orientat vs. Graf neorientat
Graf orientat (digraf): Arcul (u, v) ≠ (v, u): legatura unidirectionala, de la u la v.
In graful orientat distingem:
- grad interior d−(v) = numarul arcelor care intra in v
- grad exterior d+(v) = numarul arcelor care ies din v
# Graf orientat (digraf): arcele au directie arce = [(1, 2), (1, 3), (3, 2), (2, 4), (4, 3), (3, 5)] noduri = [1, 2, 3, 4, 5] grad_in = {nod: 0 for nod in noduri} grad_out = {nod: 0 for nod in noduri} for (u, v) in arce: grad_out[u] += 1 grad_in[v] += 1 print("Gradul nodurilor in graful orientat:") print(f"{'Nod':>4} | {'grad_out (exterior)':>20} | {'grad_in (interior)':>18}") print("-" * 50) for nod in noduri: print(f"{nod:>4} | {grad_out[nod]:>20} | {grad_in[nod]:>18}")
Gradul nodurilor in graful orientat: Nod | grad_out (exterior) | grad_in (interior) -------------------------------------------------- 1 | 2 | 0 2 | 1 | 2 3 | 2 | 2 4 | 1 | 1 5 | 0 | 1
Nodul 1 are grad interior 0 (nicio sageata intra in el — este sursa). Nodul 5 are grad exterior 0 (nicio sageata iese din el — este destinatie/absorbtie).
4. Tipuri de grafuri: complet, conex, bipartit
Graf conex: Intre orice doua noduri u si v exista cel putin un lant (secventa de noduri si muchii). Daca nu este conex, graful are mai multe componente conexe.
Graf bipartit: Nodurile se pot imparti in doua multimi disjuncte A si B astfel incat orice muchie are un capat in A si celalalt in B. Detectie: 2-colorare prin BFS (un graf este bipartit daca si numai daca nu are cicluri de lungime impara).
# Verificare graf complet si bipartit def este_complet(noduri, muchii): n = len(noduri) muchii_max = n * (n - 1) // 2 return len(muchii) == muchii_max def este_bipartit_simplu(noduri, muchii): culoare = {} adj = {nod: [] for nod in noduri} for (u, v) in muchii: adj[u].append(v) adj[v].append(u) for start in noduri: if start not in culoare: coada = [start] culoare[start] = 0 while coada: nod = coada.pop(0) for vecin in adj[nod]: if vecin not in culoare: culoare[vecin] = 1 - culoare[nod] coada.append(vecin) elif culoare[vecin] == culoare[nod]: return False return True # Graf K4 (complet cu 4 noduri) V4 = [1, 2, 3, 4] E4 = [(1,2),(1,3),(1,4),(2,3),(2,4),(3,4)] print("Graf K4 (complet cu 4 noduri):") print(f" Noduri: {V4}, Muchii: {len(E4)}") print(f" Este complet? {este_complet(V4, E4)}") print(f" Este bipartit? {este_bipartit_simplu(V4, E4)}") print() # Graf bipartit: A={1,2}, B={3,4} V_bip = [1, 2, 3, 4] E_bip = [(1,3),(1,4),(2,3),(2,4)] print("Graf bipartit: A={1,2}, B={3,4}") print(f" Muchii: {E_bip}") print(f" Este complet? {este_complet(V_bip, E_bip)}") print(f" Este bipartit? {este_bipartit_simplu(V_bip, E_bip)}")
Graf K4 (complet cu 4 noduri):
Noduri: [1, 2, 3, 4], Muchii: 6
Este complet? True
Este bipartit? False
Graf bipartit: A={1,2}, B={3,4}
Muchii: [(1, 3), (1, 4), (2, 3), (2, 4)]
Este complet? False
Este bipartit? True
5. Conexitate, lant, ciclu — verificare DFS
Drum (in graf orientat): Secventa in care arcele respecta directia (de la vi la vi+1).
Ciclu: Lant cu v0 = vk (incepe si termina in acelasi nod). Lungime minima ciclu simplu = 3 (triunghi).
Graf aciclic: Nu contine niciun ciclu. Un graf neorientat conex si aciclic se numeste arbore (studiat in modulul urmator).
# Verificare conexitate cu DFS iterativ def este_conex(noduri, muchii): adj = {nod: [] for nod in noduri} for (u, v) in muchii: adj[u].append(v) adj[v].append(u) if not noduri: return True vizitat = set() stiva = [noduri[0]] vizitat.add(noduri[0]) while stiva: nod = stiva.pop() for vecin in adj[nod]: if vecin not in vizitat: vizitat.add(vecin) stiva.append(vecin) return len(vizitat) == len(noduri) # Graf conex V1 = [1, 2, 3, 4, 5] E1 = [(1,2),(2,3),(3,4),(4,5),(5,1),(2,4)] print("Graf conex:") print(f" Noduri: {V1}") print(f" Muchii: {E1}") print(f" Este conex? {este_conex(V1, E1)}") print() # Graf neconex (nodul 4 este izolat) V2 = [1, 2, 3, 4] E2 = [(1,2),(2,3)] print("Graf neconex (nodul 4 izolat):") print(f" Noduri: {V2}") print(f" Muchii: {E2}") print(f" Este conex? {este_conex(V2, E2)}")
Graf conex: Noduri: [1, 2, 3, 4, 5] Muchii: [(1, 2), (2, 3), (3, 4), (4, 5), (5, 1), (2, 4)] Este conex? True Graf neconex (nodul 4 izolat): Noduri: [1, 2, 3, 4] Muchii: [(1, 2), (2, 3)] Este conex? False
Verificarea conexitatii prin DFS/BFS ruleaza in O(|V| + |E|) — liniar in numarul de noduri plus numarul de muchii.
6. Grafuri in C++ EXCLUSIV INTENSIV
La profilul intensiv implementam grafuri in C++ folosind vector<pair<int,int>> pentru lista de muchii si vector<int> pentru grade. Folosim structured bindings C++17 (auto& [u, v]) pentru iterare eleganta — evitam .first si .second.
#include <iostream> #include <vector> using namespace std; int main() { // Graf neorientat: 5 noduri, lista de muchii int n = 5; vector<pair<int,int>> muchii = { {1,2},{1,3},{2,3},{2,4},{3,5},{4,5} }; // Gradul fiecarui nod (indexat de la 1) vector<int> grad(n + 1, 0); for (auto& [u, v] : muchii) { grad[u]++; grad[v]++; } cout << "Graf neorientat cu " << n << " noduri si " << muchii.size() << " muchii" << endl; cout << "Gradele nodurilor:" << endl; for (int i = 1; i <= n; i++) { cout << " grad(" << i << ") = " << grad[i] << endl; } int suma = 0; for (int i = 1; i <= n; i++) suma += grad[i]; cout << "Suma grade = " << suma << " (= 2 * " << muchii.size() << " = " << 2 * (int)muchii.size() << ")" << endl; return 0; }
Graf neorientat cu 5 noduri si 6 muchii Gradele nodurilor: grad(1) = 2 grad(2) = 3 grad(3) = 3 grad(4) = 2 grad(5) = 2 Suma grade = 12 (= 2 * 6 = 12)
Aceleasi rezultate ca in Python — acelasi algoritm O(|V| + |E|), doar sintaxa difera. In lectia urmatoare vom trece la reprezentari mai eficiente: matrice de adiacenta si liste de adiacenta.