Il concetto di grafo

Consideriamo il problema di rappresentare i collegamento tra gli utenti iscritti ad un social network. Ogni iscritto avrà come contatto diversi altri iscritti i quali, a loro volta, saranno collegati ad altri iscritti.

Un modo per rappresentare una rete sociale è quello di porre ogni persona come un nodo della rete e poi collegare tra di loro quei nodi che sono contatti diretti nel social network.

Nell’immagine a destra vediamo un esempio di rete sociale, in questo caso vi sono 6 iscritti (Alice, Bob, Carol, …) che sono tra loro collegati da segmenti. Questo potrebbe essere un modo per descrivere il concetto di contatto in un social network come Facebook nel quale il contatto è bidirezionale. Ad esempio Alice è un contatto di Bob, ma anche Bob è un contatto di Alice. Vedremo Altri tipo di social network prevedono contatti unidirezionali, ad esempio su Instagram, un utente Giulie più seguire un altro utente Henry senza che Henry segua Giulie.

Sempre guardando all’immagine a destra notiamo che:

  • gli iscritti non hanno tutti lo stesso numero di contatti, ad esempio Alice e Fred hanno quattro contatti, mentre Bob ed Eric solo due;
  • qualche iscritto, ad esempio Zoe potrebbe avere zero contatti;
  • non esiste una struttura sequenziale degli iscritti, ad esempio non ha senso dire che Alice viene prima di David e dopo Bob;
  • se immaginiamo di percorrere i collegamenti, possono esistere diverse “strade” per arrivare a destinazione, ad esempio per “raggiungere” David da Bob, posso “passare” da Alice o da Fred.
Esercizio

Pensa una situazione del mondo reale in cui la rappresentazione dei dati mediante strutture sequenziali (tipo array e liste) non è adatta. Fai, con un breve disegno simile a quello sopra, un esempio e spiega perché, secondo te, non è possibile la rappresentazione con una struttura a lista.

Definizione di grafo

Vediamo ora una definizione formale di grafo.

Definizione: Grafo

Un grafo è un insieme di nodi (nodes) collegati tra di loro mediante lati (edges) o archi (arcs), ogni lato o arco mette in collegamento due nodi diverso oppure un nodo con sé stesso, in questo secondo caso il collegamento viene anche detto self-loop (o semplicemente loop). Se il collegamento ha una direzione (c’è una partenza ed un arrivo), allora diciamo che il grafo è direzionato (directed), e chiamiamo i collegamenti archi altrimenti diciamo che il grafo è non direzionato (undirected) e chiamiamo i collegamenti lati

Attenzione

È importante non confondere il modo grafico di rappresentare un grafo mediante punti (cerchi) e segmenti con i concetti di nodi e archi. Ovviamente, ogni punto rappresenta un nodo ed ogni segmento un arco, ma sono possibile altre rappresentazioni. Una rappresentazione spesso usata in informatica è quella mediante matrici (che tratteremo sotto). In matematica, i grafi sono spesso rappresentati mediante insiemi, ad esempio, il grafo dell’immagine sopra sarà composta da:

  • l’insieme dei nodi: $$ \lbrace A, B, C, D, E, F, Z\rbrace $$
  • l’insieme dei lati: $$ \lbrace (A, B), (A, C), (A, D), (A,F), (B, F), (C,D), (C,E), (D,F), (F,E)\rbrace $$

Grado

Come visto nell’esempio sopra, ogni nodo può essere connesso a quanti altri nodi si vuole, anche a zero (Zoe, nell’esempio sopra, non ha lati incidenti). Inoltre, in un grafo orientato un nodo A può avere un arco uscente verso B, ma potrebbe non avere l’arco entrante in A e uscente da B. Vediamo nella seguente definizione come rendere precisi questi concetti.

Definizione: Grado

Per un grafo non orientato, il numero di lati incidenti ad un dato nodo A viene detto grado (degree) di A e si indica con \(\delta(A)\). Un self-loop in un dato nodo A va contato come due lati distinti.

Per un grafo orientato, il numeri di archi entranti ad un dato nodo A viene detto grado entrante e si indica con \(\delta^{+}(A)\), il numero di archi uscenti da un dato nodo A, viene detto grado uscente e si indica con \(\delta^{-}(A)\). Per i grafo orientato il grado (complessivo) di un dato nodo A è la differenza tra il grado entrante ed il grado uscente $$ \delta(A) = \delta^{+}(A) - \delta^{-}(B) $$ Un self-loop in un dato nodo A va considerato come un arco entrante ed un arco uscente.

Esempio: grafo orientato

In questo secondo esempio, vediamo i concetti esposti sopra applicati ad un grafo orientato (figura a destra).

Il grado entrante \(\delta^+\), uscente \(\delta^-\) e complessivo \(\delta\) di ogni singolo nodo del grafo è riportato nella seguente tabella

Nodo\(\delta^+\)\(\delta^-\)\( \delta \)
A211
B23-1
C211
D12-1
E211
In un grafo orientato i percorsi devono seguire le frecce affinché siano validi, nella figura sopra il cammino A, B, D è un cammino valida, ma non lo è il seguente cammino A, C, D non esiste infatti un arco tra A e C (ne esiste uno tra C ed A). Il seguente è anche un cammino valido A, B, B, A, in questo caso si è cammino una volta il loop (B,B), anche il seguente è perciò un cammino valido B, B, B, si noti però che questo non lo è A, A, A in quanto non c’è un loop (A,A).

Infine notiamo come un qualsiasi cammino che termini in E, ad esempio A, B, D, E, può unicamente rimanere in attraverso l’unico arco uscente da E che riporta d E stesso (tale arco è un loop).

Cammino e connettività

In un grafo è possibile “muoversi” da un nod ad un altro percorrendo i lati o gli archi. Ad esempio nel grafo sopra posso “spostarmi” da Alice ad Eric, ad esempio, attraverso Carol oppure attraverso Fred. È possibile raggiungere Eric da Alice attraverso Bob, Fred, David e Carol. Ognuno di questa sequenza di nodi che sono tra loro vicini, viene detta cammino, vediamo meglio la definizione di cammino in un grafo.

Definizione: Cammino

In un grafo un cammino (path) è una sequenza di nodi \(a, b, c, d, \ldots \) a patto che tra due nodi adiacenti nella lista \(x, y\) vi sia un lato nel caso di grafi non orientati o un arco uscente da \(x\) ed entrante in \(y\) nel caso di grafi orientati. Se il cammino inizia e conclude nello stesso nodo si dice circuito (cycle).

Esempio: grafo non orientato

Vediamo i concetti esposti sopra con un esempio su un grafo orientato in cui i nodi rappresentato stazioni ferroviarie e i lato rappresentano collegamenti ad alta velocità (vedi figura a destra).

Ricordiamo che il grado rappresenta il numero totale di lati incidenti, per cui il nodo TO ha grado 2, il nodo MI ha grado 4, VE grado 2 BO grado 4 e così via.

Ci sono diversi percorsi nel grafo a destra, ad esempio TO, MI, VE è un cammino che parte da TO ed arriva VE passando per MI. Un altro esempio è BO, FI, GE, MI, BO in questo caso l’inizio e la fine del cammino sono lo stesso nodo BO quindi diciamo che questo cammino è un circuito. Anche il seguente è un cammino VE, BO, MI, VE, BO, MI il quale percorre due volte lo stesso sotto cammino VE, BO, MI.

Grafi pesati

I grafi visti finora non differenziano tra i vari lati/archi nel senso che non hanno alcun valore associato ad essi. I grafi pesati, per contro, associano ad ogni lato/arco del grafo un peso (tipicamente un valore numerico).

La figura a destra mostra nuovamente l’esempio di grafo non orientato per le stazioni ferrovieri in cui sono stati inseriti dei pesi ad ogni lato. In questo esempio i pesi sono numerici e potrebbero rappresentare il tempo di percorrenza in minuti della tratta associata. Ad esempio, tra Milano e Bologna (lato tra MI e BO) il tempo di percorrenza, secondo questo grafo, è di 130 minuti, mentre tra Bologna e Firenze è di 45 minuti. Inoltre, essendo il grafo non orientato il peso di un lato vale in entrambe le direzioni, nell’esempio il tempo di percorrenza indicato vale qualsiasi sia il verso in cui si percorre il lato. Ad esempio anche il tempo tra Bologna e Milano sarà di 130 minuti, mentre sarà di 45 minuti quello tra Firenze e Bologna.

In caso di grafi orientati, tuttavia, sappiamo che un arco può essere percorso in una sola direzione (indicata dalla freccia nella rappresentazione grafica), perciò non possiamo dire che il peso di un arco valga indipendentemente dalla direzione. Infatti in un grafo orientato il peso deve essere dato a tutti gli archi, ad esempio l’arco da A a B avrà un peso e un peso, che potrebbe essere diverso, lo avrà anche l’arco da B ad A.

Rappresentazione di grafi

Quanto visto sopra è una rappresentazione matematica (utilizzando formule e simboli matematici) dei grafi, vediamo ora come traduciamo questo in un linguaggio di programmazione (Java negli esempi sotto).

Ci sono due modi principali per rappresentare grafi:

  • attraverso liste di adiacenza in cui dei nodi sono collegati tra di loro (simile a quanto abbiamo visto per alberi e liste);
  • attraverso una matrice che indica i collegamenti, ci sono due possibili tecniche di questo tipo chiamate matrice di adiacenza e matrice di incidenza.

Nelle prossime sezioni andremo a discutere queste due metodologie di rappresentazione di grafi.

Rappresentazione mediante liste di adiacenza

Per rappresentare i grafi mediante liste di adiacenza (adjacency list), si parte con il definire un nodo del grafo che contiene:

  • un valore (value), di tipo Object nel nostro esempio,
  • ed una lista di nodi adiacenti, un ArrayList nel nostro esempio.
public class GraphNode {
    public Object value;
    public ArrayList<GraphNode> neighbors;
}

Ogni arco tra nodi, ad esempio tra Alice e Bob, deve prevedere l’inserimento nella lista di un nodo di tutti i suoi vicini. Ad esempio il codice sotto inserisce come vicini del nodo eric i nodi carol e fred (nel codice si assume che le istanze siano già state create).

eric.neighbors.add(carol);
eric.neighbors.add(fred);

Nel caso di grafi non direzionati, se Carol è vicino di Eric allora anche Eric è vicino di Carol, per questo motivo il grafo sarà correttamente rappresentato se, oltre al codice sopra, viene eseguito anche il seguente

carol.neighbors.add(eric);
fred.neighbors.add(carol);
Importante

Nella creazione delle classi Graph e GraphNode, non si è tenuto conto del fatto che in un grafo non orientato ogni lato deve essere memorizzato due volte. Ad esempio un lato tra A e B sarà nell’ArrayList di A e di B. Questo crea un problema di ridondanza perché la stessa informazione è memorizzata in due posti diversi. Ogni volta che si aggiorna tale informazione (ad esempio si elimina il lato), l’aggiornamento deve essere riportato in tutti i punti in cui essa è presente (ad esempio sia A elimina B dai vicini, sia B elimina A dai vicini).

Osserva

La rappresentazione dei grafi mediante lista di adiacenza non è adatta a rappresentare grafi pesati. Per convincersi basta porsi la seguente domanda: dove inseriremmo l’informazione del peso di un lato (ad esempio tra A e B)?

Dopo averci pensato un po’, ci si rende conto che ogni possibile scelta presenta dei problemi in termini di complicazione del codice. Il problema risiede nel fatto che la lista di adiacenza, non comprende il concetto di lato/arco, bensì usa il concetto di nodo vicino per indicare la presenza di un lato/arco. Per questo motivo il peso, che è associato ad un arco, non ha un posto “naturale” dove possa essere memorizzato.

Rappresentazione mediante matrici

Una matrice è una tabella di numeri, in una matrice abbiamo righe e colonne che indichiamo con interi 0,1,2,... (come gli indici di un array bidimensionale in Java). Per indicare una cella di una matrice domanda, quindi, dire quale indice di riga i e quale indice di colonna j. Consideriamo la seguente matrice (in matematica le matrici si includono tra quadre o tra tonde, qui scegliamo di usare le quadre). $$ \mathbf{M} = \begin{bmatrix} 1 & -2 & 1 & 0 & 3\\ 0 & 0 & 1 & 0 & 1\\ 1 & 1 & 1 & 1 & 1\\ \end{bmatrix} $$

La matrice indicata con \(\mathbf{M}\):

  • ha 3 righe di indice 0,1,2,
  • ha 5 colonne di indice 0,1,2,3,4,
  • si dice una matrice \(3 \times 5 \) (si legge tre per cinque),
  • non è una matrice \(5 \times 3\),
  • ha un totale di \(15\)(\(=3\times 5\)) elementi,
  • ha il valore -2 nella cella di indici \(i=0, j=1\),
  • ha il valore 1 nella cella di indici \( i=1, j=2 \)
  • ha l’ultima riga tutta di valori uguali (1 in questo caso).

Matrice di adiacenza

Dato un grafo con \( n\ \) nodi, la sua matrice di adiacenza è una matrice con \(n\) righe e \(n\) colonne. Ognuno degli \(n\) nodi è associato ad un indice 0,1,...,n-1 (attenzione al meno 1!), in questo modo ogni riga è associata ad un nodo ed ogni colonna è associata ad un nodo. Quindi possiamo indicare con un numero ogni nodo del grafo.

Esempio

Consideriamo l’esempio del grafo sociale all’inizio della pagina, possiamo immaginare di ordinare i nodi in ordine alfabetico e associarli ai numeri interi:

Alice -> 0
Bob   -> 1
Carol -> 2
David -> 3
Eric  -> 4
Fred  -> 5
Zoe   -> 6

Come ci aspettavamo, gli indici vanno da 0 a 6 essendoci \(7\) nodi nel grafo.

Attenzione

A differenza delle tabelle (ad esempio in un foglio di calcolo), le matrici non possono avere celle vuote.

Passiamo ora a definire formalmente il contenuto di una matrice di adiacenza.

Definizione: Matrice di adiacenza

Dato un grafo con \(n\) nodi, la sua matrice di adiacenza (adjacency matrix) è una matrice \(n \times n\) (con \(n\) righe e \(n\) colonne) i cui valori delle celle dipendono dalla presenza o meno di un lato/arco. La cella \((i,j)\) della matrice ha:

  • il valore 1 se esiste un arco o un lato dal nodo \(i\) al nodo \(j\),
  • il valore 0 se non esiste un arco o un lato lato dal nodo \(i\) al nodo \(j\),
  • il valore 2 se \(i=j\) e esiste il self-loop in \(i\) (o in \(j\) visto che sono uguali).

Esempio: Matrice di adiacenza per un grafo non orientato

Consideriamo il grafo sociale (Alice, Bob, …) con i nodi associati agli indici come nell’esempio sopra. La matrice di adiacenza del grafo sarà $$ \begin{bmatrix} 0 & 1 & 1 & 1 & 0 & 1 & 0 \\ 1 & 0 & 0 & 0 & 0 & 1 & 0 \\ 1 & 0 & 0 & 1 & 1 & 0 & 0 \\ 1 & 0 & 1 & 0 & 0 & 1 & 0 \\ 0 & 0 & 1 & 0 & 0 & 1 & 0 \\ 1 & 1 & 0 & 1 & 1 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ \end{bmatrix} $$

Esempio: Matrice di adiacenza per un grafo orientato

Consideriamo il grafo orientato dell’esempio sopra con i nodi in ordine alfabetico (A -> 0, B -> 1, …, E -> 4), la matrice di adiacenza del grafo è la seguente $$ \begin{bmatrix} 0 & 1 & 0 & 0 & 0 \\ 1 & 1 & 1 & 1 & 0 \\ 1 & 0 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 1 \\ 0 & 0 & 0 & 0 & 1 \\ \end{bmatrix} $$

Matrice di incidenza

Nella matrice di incidenza anziché memorizzare nodi sia sulle righe che sulle colonne, si memorizzano sulle righe i nodi e sulle colonne gli archi. I valori presenti nella matrice dipendono dal fatto che il grafo sia orientato o non orientato:

  • nei grafi non orientati ogni colonna ha esattamente due valori ad 1, precisamente nelle righe dei nodi collegati da tale arco, tutti gli altri valori sono 0;
  • nei grafi non orientati ogni colonna ha esattamente un valore ad 1 ed un volare -1, il valore 1 nella riga associata al nodo in cui l’arco entra, il valore -1 nella riga associata al nodo da cui l’arco esce, tutti gli altri valori sono 0.
Riprendendo a destra il nostro esempio di grafo orientato e supponendo che nelle righe ci siano i nodi nell’ordine alfabetico A, B, … $$ \begin{bmatrix} 1 & -1 & 0 & 0 & -1 & 0 & 0 & 0 & 0\\ -1 & 1 & 1 & 1 & 0 & 0 & 0 & 0 & 1/-1\\ 0 & 0 & -1 & 0 & 1 & -1 & 0 & 0 & 0\\ 0 & 0 & 0 & -1 & 0 & 1 & 1 & 0 & 0\\ 0 & 0 & 0 & 0 & 0 & 0 & -1 & 1/-1 & 0\\ \end{bmatrix} $$ Si nota subito un problema dovuto al fatto che i self-loop dovrebbero avere un valore che indica la loro esistenza, il valore 0 è già utilizzato per indicare i nodi non connessi dall’arco. Fortunatamente si può usare un qualsiasi valore diverso da 0 (anche 1 o -1), infatti le colonne associate a self-loop avranno un unico valore diverso da 0 in corrispondenza della riga associata al nodo stesso (nodo che è unico in quanto un loop collega un nodo a se stesso)
Osserva

La rappresentazione mediante matrice di incidenza non viene usata di frequente, quando si vuole rappresentare un grafo utilizzando matrici si preferisci utilizzare la matrice di adiacenza discussa sopra.

Esercizio

Un altro motivo per preferire la matrice di adiacenza alla matrice di incidenza è che la seconda, in certi casi, occupa più spazio in memoria. Lo spazio occupato in memoria è il numero di celle quindi il numero di righe per il numero di colonne $$ \textrm{Spazio in memoria} = \textrm{Numero righe} \times \textrm{Numero colonne} $$

Per vedere che la differenza tra le due matrici scriverle nei casi di un grafo non orientato e completo (cioè contenente tutti i possibili lati) fatto di \(n\) nodi per vari valori \(n=3,4,5,6\).

  • Quante righe e quante colonne hanno le due matrici nei vari casi?
  • Perché la matrice di incidenza, per i valori alti di \(n\) molte più colonne?

  • Michele Schimd © 2024
  • Ultimo aggiornamento: 17/02/2024
  • Materiale di studio e di esercizio per gli alunni dello Zuccante.

Creative Commons License