Grafi in C
I grafi sono una struttura dati fondamentale utilizzata per rappresentare relazioni complesse tra oggetti. Un grafo è composto da nodi (o vertici) e collegamenti (o archi) tra di essi. I grafi sono ampiamente utilizzati in molti campi, come il networking, l’intelligenza artificiale, la teoria dei giochi, e molto altro. In questa guida, esploreremo come implementare grafi in C, come attraversarli e le loro applicazioni più comuni.
Cos’è un Grafo?
Un grafo è una struttura composta da un insieme di nodi collegati tra loro da archi. I grafi possono essere:
- Orientati: Gli archi hanno una direzione specifica.
- Non Orientati: Gli archi non hanno direzione e rappresentano una relazione bidirezionale.
- Ponderati: Gli archi hanno un peso associato, rappresentando una “costo” o “distanza” tra i nodi.
- Non Ponderati: Gli archi non hanno peso.
Tipi di Rappresentazione dei Grafi
Ci sono due modi comuni per rappresentare un grafo in C:
- Matrice di Adiacenza: Una matrice in cui l’elemento
G[i][j]
è 1 se c’è un arco tra il nodoi
e il nodoj
, altrimenti è 0. - Lista di Adiacenza: Un array di liste, in cui ogni elemento dell’array è una lista dei nodi adiacenti a un nodo specifico.
Implementazione di un Grafo usando la Matrice di Adiacenza
Dichiarazione della Matrice di Adiacenza
#include <stdio.h>
#include <stdlib.h>
#define N 5 // Numero di nodi
void init_grafo(int grafo[N][N]) {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
grafo[i][j] = 0; // Inizializza la matrice con 0
}
}
}
Aggiunta di un Arco
void aggiungi_arco(int grafo[N][N], int u, int v) {
grafo[u][v] = 1; // Aggiunge un arco da u a v
grafo[v][u] = 1; // Per un grafo non orientato, aggiungi anche l'arco inverso
}
Stampa del Grafo
void stampa_grafo(int grafo[N][N]) {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
printf("%d ", grafo[i][j]);
}
printf("\n");
}
}
Esempio Completo
int main() {
int grafo[N][N];
init_grafo(grafo);
aggiungi_arco(grafo, 0, 1);
aggiungi_arco(grafo, 0, 4);
aggiungi_arco(grafo, 1, 2);
aggiungi_arco(grafo, 1, 3);
aggiungi_arco(grafo, 1, 4);
aggiungi_arco(grafo, 2, 3);
aggiungi_arco(grafo, 3, 4);
printf("Matrice di Adiacenza:\n");
stampa_grafo(grafo);
return 0;
}
Uscita:
Matrice di Adiacenza:
0 1 0 0 1
1 0 1 1 1
0 1 0 1 0
0 1 1 0 1
1 1 0 1 0
Implementazione di un Grafo usando la Lista di Adiacenza
La lista di adiacenza è più efficiente in termini di spazio, specialmente per grafi sparsi (grafi con pochi archi rispetto al numero di nodi).
Definizione della Struttura del Nodo
struct Nodo {
int vertice;
struct Nodo* prossimo;
};
Definizione della Lista di Adiacenza
struct ListaAdiacenza {
struct Nodo* testa;
};
Definizione del Grafo
struct Grafo {
int numero_nodi;
struct ListaAdiacenza* array;
};
Creazione di un Grafo
struct Grafo* crea_grafo(int numero_nodi) {
struct Grafo* grafo = (struct Grafo*)malloc(sizeof(struct Grafo));
grafo->numero_nodi = numero_nodi;
grafo->array = (struct ListaAdiacenza*)malloc(numero_nodi * sizeof(struct ListaAdiacenza));
for (int i = 0; i < numero_nodi; i++)
grafo->array[i].testa = NULL;
return grafo;
}
Aggiunta di un Arco alla Lista di Adiacenza
void aggiungi_arco(struct Grafo* grafo, int src, int dest) {
struct Nodo* nuovo_nodo = (struct Nodo*)malloc(sizeof(struct Nodo));
nuovo_nodo->vertice = dest;
nuovo_nodo->prossimo = grafo->array[src].testa;
grafo->array[src].testa = nuovo_nodo;
// Poiché il grafo è non orientato, aggiungiamo anche l'arco inverso
nuovo_nodo = (struct Nodo*)malloc(sizeof(struct Nodo));
nuovo_nodo->vertice = src;
nuovo_nodo->prossimo = grafo->array[dest].testa;
grafo->array[dest].testa = nuovo_nodo;
}
Stampa del Grafo
void stampa_grafo(struct Grafo* grafo) {
for (int v = 0; v < grafo->numero_nodi; ++v) {
struct Nodo* nodo = grafo->array[v].testa;
printf("\n Lista di adiacenza per il nodo %d\n head ", v);
while (nodo) {
printf("-> %d", nodo->vertice);
nodo = nodo->prossimo;
}
printf("\n");
}
}
Esempio Completo
int main() {
int numero_nodi = 5;
struct Grafo* grafo = crea_grafo(numero_nodi);
aggiungi_arco(grafo, 0, 1);
aggiungi_arco(grafo, 0, 4);
aggiungi_arco(grafo, 1, 2);
aggiungi_arco(grafo, 1, 3);
aggiungi_arco(grafo, 1, 4);
aggiungi_arco(grafo, 2, 3);
aggiungi_arco(grafo, 3, 4);
stampa_grafo(grafo);
return 0;
}
Uscita:
Lista di adiacenza per il nodo 0
head -> 4-> 1
Lista di adiacenza per il nodo 1
head -> 4-> 3-> 2-> 0
Lista di adiacenza per il nodo 2
head -> 3-> 1
Lista di adiacenza per il nodo 3
head -> 4-> 2-> 1
Lista di adiacenza per il nodo 4
head -> 3-> 1-> 0
Traversamento di un Grafo
Due algoritmi comuni per attraversare un grafo sono:
1. Depth-First Search (DFS)
Il DFS esplora il piĂą possibile lungo ogni ramo prima di tornare indietro.
void DFS_util(int v, int visitato[], struct Grafo* grafo) {
visitato[v] = 1;
printf("%d ", v);
struct Nodo* nodo = grafo->array[v].testa;
while (nodo) {
int adj = nodo->vertice;
if (!visitato[adj])
DFS_util(adj, visitato, grafo);
nodo = nodo->prossimo;
}
}
void DFS(struct Grafo* grafo, int v) {
int* visitato = (int*)malloc(grafo->numero_nodi * sizeof(int));
for (int i = 0; i < grafo->numero_nodi; i++)
visitato[i] = 0;
DFS_util(v, visitato, grafo);
}
2. Breadth-First Search (BFS)
Il BFS esplora tutti i vicini di un nodo prima di passare ai nodi di livello successivo.
void BFS(struct Grafo* grafo, int start) {
int* visitato = (int*)malloc(grafo->numero_nodi * sizeof(int));
for (int i =
0; i < grafo->numero_nodi; i++)
visitato[i] = 0;
struct Queue* queue = crea_queue(grafo->numero_nodi);
visitato[start] = 1;
enqueue(queue, start);
while (!is_empty(queue)) {
int v = dequeue(queue);
printf("%d ", v);
struct Nodo* nodo = grafo->array[v].testa;
while (nodo) {
int adj = nodo->vertice;
if (!visitato[adj]) {
visitato[adj] = 1;
enqueue(queue, adj);
}
nodo = nodo->prossimo;
}
}
}
Applicazioni Comuni dei Grafi
I grafi sono utilizzati in numerose applicazioni pratiche:
- Reti di Comunicazione: Per modellare le connessioni tra dispositivi.
- Social Networks: Per rappresentare le connessioni tra persone.
- Intelligenza Artificiale: Per rappresentare spazi di ricerca o reti neurali.
- Percorsi Minimi: Algoritmi come Dijkstra sono usati per trovare il percorso piĂą breve in un grafo ponderato.
Conclusioni
I grafi sono una struttura dati essenziale per rappresentare relazioni complesse e risolvere problemi in cui gli oggetti sono interconnessi. Comprendere come implementare, attraversare e manipolare grafi in C è fondamentale per affrontare una vasta gamma di problemi in informatica e ingegneria. Con un’implementazione efficiente, i grafi possono offrire soluzioni eleganti e potenti per problemi complessi.