Teoria Grafurilor: Grafuri Neorientate – Metode de Reprezentare
1. Obiectivele lecției:
- Să înțeleagă metodele comune de reprezentare a grafurilor neorientate.
- Să compare avantajele și dezavantajele fiecărei metode.
- Să implementeze fiecare metodă în C++ pentru grafuri neorientate.
2. Ce este un graf neorientat?
Un graf neorientat este definit prin:
- Noduri (vârfuri): Setul VVV de entități distincte.
- Muchii: Setul EEE de perechi neordonate {u,v}\{u, v\}{u,v}, unde uuu și vvv sunt noduri.
3. Metode de reprezentare a grafurilor
1. Matricea de adiacență
- Definiție:
Matricea de adiacență este o matrice AAA de dimensiune ∣V∣×∣V∣|V| \times |V|∣V∣×∣V∣, unde:- A[i][j]=1A[i][j] = 1A[i][j]=1, dacă există o muchie între nodurile iii și jjj.
- A[i][j]=0A[i][j] = 0A[i][j]=0, altfel.
- Caracteristici:
- Matricea este simetrică pentru grafuri neorientate (A[i][j]=A[j][i]A[i][j] = A[j][i]A[i][j]=A[j][i]).
- Diagonala principală este zero în cazul în care nu există bucle (A[i][i]=0A[i][i] = 0A[i][i]=0).
- Avantaje:
- Reprezentare simplă și acces rapid (O(1)O(1)O(1)) pentru verificarea existenței unei muchii.
- Dezavantaje:
- Consumă O(∣V∣2)O(|V|^2)O(∣V∣2) memorie, ineficient pentru grafuri sparse.
- Exemplu: Graful:
1–2
| |
3–4
Matricea de adiacență:
0 1 1 1
1 0 0 1
1 0 0 1
1 1 1 0
- Implementare în C++:
#include <iostream>
using namespace std;
void afisareMatrice(int n, int matrice[100][100]) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
cout << matrice[i][j] << ” „;
}
cout << endl;
}
}
int main() {
int n = 4;
int matrice[100][100] = {0};
// Adăugare muchii
matrice[0][1] = matrice[1][0] = 1; // Muchie 1-2
matrice[0][2] = matrice[2][0] = 1; // Muchie 1-3
matrice[0][3] = matrice[3][0] = 1; // Muchie 1-4
matrice[1][3] = matrice[3][1] = 1; // Muchie 2-4
matrice[2][3] = matrice[3][2] = 1; // Muchie 3-4
// Afișare matrice de adiacență
afisareMatrice(n, matrice);
return 0;
}
2. Lista de adiacență
- Definiție:
Fiecare nod este asociat cu o listă care conține toate nodurile adiacente acestuia. - Caracteristici:
- Este eficientă pentru grafuri sparse.
- Necesită spațiu O(∣V∣+∣E∣)O(|V| + |E|)O(∣V∣+∣E∣).
- Avantaje:
- Consum de memorie redus pentru grafuri sparse.
- Traversarea vecinilor unui nod este rapidă.
- Dezavantaje:
- Verificarea existenței unei muchii necesită timp O(d(u))O(d(u))O(d(u)), unde d(u)d(u)d(u) este gradul nodului.
- Exemplu: Graful:
1–2
| |
3–4
Lista de adiacență:
1: 2, 3, 4
2: 1, 4
3: 1, 4
4: 1, 2, 3
- Implementare în C++:
#include <iostream>
#include <vector>
using namespace std;
void afisareListaAdiacenta(int n, vector<int> lista[]) {
for (int i = 0; i < n; i++) {
cout << i + 1 << „: „;
for (int vecin : lista[i]) {
cout << vecin + 1 << ” „;
}
cout << endl;
}
}
int main() {
int n = 4;
vector<int> lista[100];
// Adăugare muchii
lista[0].push_back(1); lista[1].push_back(0); // Muchie 1-2
lista[0].push_back(2); lista[2].push_back(0); // Muchie 1-3
lista[0].push_back(3); lista[3].push_back(0); // Muchie 1-4
lista[1].push_back(3); lista[3].push_back(1); // Muchie 2-4
lista[2].push_back(3); lista[3].push_back(2); // Muchie 3-4
// Afișare lista de adiacență
afisareListaAdiacenta(n, lista);
return 0;
}
3. Lista muchiilor
- Definiție:
Fiecare muchie este reprezentată ca o pereche (u,v)(u, v)(u,v), unde uuu și vvv sunt nodurile conectate. - Caracteristici:
- Simplu de implementat.
- Necesar pentru algoritmi care operează direct pe muchii.
- Avantaje:
- Eficientă pentru grafuri dense.
- Utilă pentru algoritmi care necesită sortarea muchiilor (ex.: Kruskal).
- Dezavantaje:
- Traversarea vecinilor unui nod necesită timp O(∣E∣)O(|E|)O(∣E∣).
- Exemplu: Graful:
1–2
| |
3–4
Lista muchiilor:
(1, 2), (1, 3), (1, 4), (2, 4), (3, 4)
- Implementare în C++:
#include <iostream>
#include <vector>
using namespace std;
void afisareListaMuchii(vector<pair<int, int>>& muchii) {
for (auto muchie : muchii) {
cout << „(” << muchie.first + 1 << „, ” << muchie.second + 1 << „) „;
}
cout << endl;
}
int main() {
vector<pair<int, int>> muchii;
// Adăugare muchii
muchii.push_back({0, 1}); // Muchie 1-2
muchii.push_back({0, 2}); // Muchie 1-3
muchii.push_back({0, 3}); // Muchie 1-4
muchii.push_back({1, 3}); // Muchie 2-4
muchii.push_back({2, 3}); // Muchie 3-4
// Afișare lista de muchii
afisareListaMuchii(muchii);
return 0;
}
4. Compararea metodelor de reprezentare
Metodă | Spațiu | Verificare muchie | Traversare vecini | Aplicații |
Matrice de adiacență | ( O( | V | ^2) ) | O(1)O(1)O(1) |
Lista de adiacență | ( O( | V | + | E |
Lista muchiilor | ( O( | E | ) ) | ( O( |
5. Activități practice pentru elevi
- Implementați un program care convertește o matrice de adiacență într-o listă de adiacență.
- Scrieți o funcție care determină dacă un graf reprezentat printr-o listă de adiacență este complet.
- Implementați funcții de traversare (DFS, BFS) pentru grafuri reprezentate prin fiecare metodă.
6. Concluzie
- Reprezentarea unui graf depinde de tipul de aplicație și de caracteristicile grafului (dense/sparse).
- Matricea de adiacență este ideală pentru grafuri dense, în timp ce lista de adiacență este mai eficientă pentru grafuri sparse.
- Alegerea metodei potrivite optimizează atât consumul de memorie, cât și timpul de execuție al algoritmilor.
Teoria Grafurilor: Grafuri Neorientate – Parcurgerea Grafurilor
1. Obiectivele lecției:
- Să înțeleagă conceptele de parcurgere a grafurilor.
- Să învețe și să implementeze cele două metode principale de parcurgere:
- Parcurgerea în adâncime (Depth-First Search – DFS).
- Parcurgerea în lățime (Breadth-First Search – BFS).
- Să identifice aplicații practice ale parcurgerii grafurilor.
2. Ce este parcurgerea unui graf?
Parcurgerea unui graf reprezintă vizitarea tuturor nodurilor, urmând un anumit algoritm.
Aceasta poate fi utilizată pentru:
- Explorarea conexiunilor între noduri.
- Determinarea componentelor conexe.
- Găsirea unui drum între două noduri.
3. Metode principale de parcurgere
1. Parcurgerea în adâncime (DFS)
- Definiție:
DFS explorează un graf mergând cât mai profund posibil pe fiecare ramură, înainte de a reveni și a explora alte ramuri. - Pași generali:
- Se vizitează nodul curent și se marchează ca vizitat.
- Se recursă pe toți vecinii nodului curent care nu au fost încă vizitați.
- Reprezentare grafică:
Exemplu:
1–2
| |
4–3
DFS(1): 1 → 2 → 3 → 4
- Implementare în C++ (cu lista de adiacență):
#include <iostream>
#include <vector>
using namespace std;
void dfs(int nod, vector<int> lista[], vector<bool>& vizitat) {
cout << nod + 1 << ” „;
vizitat[nod] = true;
for (int vecin : lista[nod]) {
if (!vizitat[vecin]) {
dfs(vecin, lista, vizitat);
}
}
}
int main() {
int n = 4; // Numărul de noduri
vector<int> lista[4]; // Lista de adiacență
// Adăugare muchii
lista[0].push_back(1); lista[1].push_back(0); // Muchie 1-2
lista[0].push_back(3); lista[3].push_back(0); // Muchie 1-4
lista[1].push_back(2); lista[2].push_back(1); // Muchie 2-3
lista[3].push_back(2); lista[2].push_back(3); // Muchie 4-3
vector<bool> vizitat(n, false); // Inițial, niciun nod nu este vizitat
cout << „DFS începe de la nodul 1: „;
dfs(0, lista, vizitat);
cout << endl;
return 0;
}
2. Parcurgerea în lățime (BFS)
- Definiție:
BFS explorează un graf nivel cu nivel, pornind de la nodul inițial, și vizitează toți vecinii unui nod înainte de a trece la nivelul următor. - Pași generali:
- Se adaugă nodul inițial într-o coadă și se marchează ca vizitat.
- Se scoate nodul din coadă, se vizitează și se adaugă vecinii săi nevizitați în coadă.
- Reprezentare grafică:
Exemplu:
1–2
| |
4–3
BFS(1): 1 → 2 → 4 → 3
- Implementare în C++ (cu lista de adiacență):
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
void bfs(int start, vector<int> lista[], vector<bool>& vizitat) {
queue<int> coada;
coada.push(start);
vizitat[start] = true;
while (!coada.empty()) {
int nod = coada.front();
coada.pop();
cout << nod + 1 << ” „;
for (int vecin : lista[nod]) {
if (!vizitat[vecin]) {
coada.push(vecin);
vizitat[vecin] = true;
}
}
}
}
int main() {
int n = 4; // Numărul de noduri
vector<int> lista[4]; // Lista de adiacență
// Adăugare muchii
lista[0].push_back(1); lista[1].push_back(0); // Muchie 1-2
lista[0].push_back(3); lista[3].push_back(0); // Muchie 1-4
lista[1].push_back(2); lista[2].push_back(1); // Muchie 2-3
lista[3].push_back(2); lista[2].push_back(3); // Muchie 4-3
vector<bool> vizitat(n, false); // Inițial, niciun nod nu este vizitat
cout << „BFS începe de la nodul 1: „;
bfs(0, lista, vizitat);
cout << endl;
return 0;
}
4. Compararea DFS și BFS
Caracteristică | DFS | BFS |
Metodă | Explorează cât mai profund. | Explorează nivel cu nivel. |
Structură utilizată | Stivă (implicit recursivitate). | Coadă. |
Consum de memorie | Depinde de adâncimea arborelui. | Depinde de lățimea arborelui. |
Aplicații | Detectare cicluri, parcurgerea labirintului. | Găsirea celui mai scurt drum într-un graf neponderat. |
5. Aplicații practice ale parcurgerii grafurilor
- DFS:
- Detectarea componentelor conexe ale unui graf.
- Detectarea ciclurilor într-un graf.
- Parcurgerea labirinturilor.
- BFS:
- Găsirea celui mai scurt drum într-un graf neponderat.
- Găsirea tuturor nodurilor accesibile dintr-un nod inițial.
6. Activități practice pentru elevi
- Implementați un program care determină numărul de componente conexe ale unui graf folosind DFS.
- Scrieți un program care determină dacă un graf este bipartit folosind BFS.
- Extindeți implementările pentru a parcurge grafuri reprezentate prin matrice de adiacență.
7. Concluzie
- Parcurgerea grafurilor este o tehnică fundamentală în teoria grafurilor și are multiple aplicații practice.
- DFS este util pentru explorarea completă a unui graf, în timp ce BFS este ideal pentru determinarea drumurilor minime.
- Alegerea metodei potrivite depinde de problema pe care o rezolvăm.
Teoria Grafurilor: Grafuri Neorientate – Noțiuni Introdutive și Parcurgerea în Adâncime (DFS)
1. Noțiuni introductive
Ce este un graf?
Un graf este o structură matematică care constă din:
- Noduri (sau vârfuri): Entitățile individuale din graf (VVV).
- Muchii: Legături între perechi de noduri (EEE).
Grafuri neorientate
Un graf neorientat este un graf în care muchiile nu au direcție, adică {u,v}={v,u}\{u, v\} = \{v, u\}{u,v}={v,u}.
Elemente fundamentale:
- Noduri: Mulțimea VVV de noduri.
- Muchii: Mulțimea EEE de perechi neordonate de noduri.
- Gradul unui nod: Numărul de muchii care sunt incidente la un nod.
Reprezentarea grafurilor
- Matrice de adiacență: Matrice binară A[i][j]A[i][j]A[i][j] care indică dacă există o muchie între nodurile iii și jjj.
- Lista de adiacență: Fiecare nod are o listă de noduri adiacente.
- Lista muchiilor: Fiecare muchie este reprezentată ca o pereche {u,v}\{u, v\}{u,v}.
2. Parcurgerea în adâncime (DFS)
Definiție:
DFS (Depth-First Search) explorează un graf mergând cât mai profund posibil pe fiecare ramură, înainte de a reveni și a explora alte ramuri.
Caracteristici:
- Utilizează o stivă pentru gestionarea nodurilor (recursiv sau explicit).
- Este utilă pentru:
- Detectarea ciclurilor.
- Determinarea componentelor conexe.
- Generarea arborilor de parcurgere.
Algoritm general pentru DFS:
- Pornim de la un nod inițial și îl marcăm ca vizitat.
- Explorăm recursiv toți vecinii nevizitați ai nodului curent.
- Repetăm procesul până când toate nodurile accesibile au fost vizitate.
Exemplu grafic:
Graful:
1–2
| |
4–3
- Lista de adiacență:
1: 2, 4
2: 1, 3
3: 2, 4
4: 1, 3
- DFS(1): 1 → 2 → 3 → 4
3. Implementare DFS în C++
Cu lista de adiacență:
#include <iostream>
#include <vector>
using namespace std;
// Funcția DFS
void dfs(int nod, vector<int> lista[], vector<bool>& vizitat) {
cout << nod + 1 << ” „; // Afișăm nodul curent
vizitat[nod] = true; // Marcăm nodul ca vizitat
// Vizităm vecinii nodului curent
for (int vecin : lista[nod]) {
if (!vizitat[vecin]) {
dfs(vecin, lista, vizitat);
}
}
}
int main() {
int n = 4; // Numărul de noduri
vector<int> lista[4]; // Lista de adiacență
// Adăugare muchii
lista[0].push_back(1); lista[1].push_back(0); // Muchie 1-2
lista[0].push_back(3); lista[3].push_back(0); // Muchie 1-4
lista[1].push_back(2); lista[2].push_back(1); // Muchie 2-3
lista[3].push_back(2); lista[2].push_back(3); // Muchie 4-3
vector<bool> vizitat(n, false); // Inițial, niciun nod nu este vizitat
cout << „DFS începe de la nodul 1: „;
dfs(0, lista, vizitat);
cout << endl;
return 0;
}
Cu matrice de adiacență:
#include <iostream>
using namespace std;
void dfs(int nod, int n, int matrice[100][100], bool vizitat[]) {
cout << nod + 1 << ” „; // Afișăm nodul curent
vizitat[nod] = true; // Marcăm nodul ca vizitat
for (int i = 0; i < n; i++) {
if (matrice[nod][i] == 1 && !vizitat[i]) { // Dacă există muchie și nodul nu a fost vizitat
dfs(i, n, matrice, vizitat);
}
}
}
int main() {
int n = 4; // Numărul de noduri
int matrice[100][100] = {0}; // Matricea de adiacență
// Adăugare muchii
matrice[0][1] = matrice[1][0] = 1; // Muchie 1-2
matrice[0][3] = matrice[3][0] = 1; // Muchie 1-4
matrice[1][2] = matrice[2][1] = 1; // Muchie 2-3
matrice[3][2] = matrice[2][3] = 1; // Muchie 4-3
bool vizitat[100] = {false}; // Inițial, niciun nod nu este vizitat
cout << „DFS începe de la nodul 1: „;
dfs(0, n, matrice, vizitat);
cout << endl;
return 0;
}
4. Aplicații practice ale DFS
- Detectarea componentelor conexe:
DFS poate determina numărul de subgrafuri conexe dintr-un graf. - Detectarea ciclurilor:
DFS poate verifica dacă există cicluri într-un graf. - Colorarea grafurilor:
DFS poate fi utilizat pentru colorarea nodurilor unui graf bipartit. - Generarea unui arbore de acoperire:
DFS poate construi un arbore care conectează toate nodurile unui graf.
5. Activități practice pentru elevi
- Scrieți o funcție care determină numărul de componente conexe ale unui graf folosind DFS.
- Implementați o funcție care verifică dacă un graf conține un ciclu.
- Extindeți implementarea DFS pentru a ține evidența ordinii de intrare și ieșire a nodurilor.
6. Avantaje și dezavantaje ale DFS
Avantaje | Dezavantaje |
Eficient pentru explorarea completă | Poate consuma memorie mare pentru grafuri mari (stivă recursivă). |
Ușor de implementat | Nu garantează cel mai scurt drum între noduri. |
Util pentru grafuri dense |
7. Concluzie
- Parcurgerea în adâncime (DFS) este o metodă fundamentală pentru explorarea grafului.
- Este utilă pentru detectarea conexiunilor, ciclurilor și generarea arborilor de parcurgere.
- Implementarea DFS în C++ este ușor de realizat și oferă o bază solidă pentru algoritmi mai avansați.