Docsity
Docsity

Prepara i tuoi esami
Prepara i tuoi esami

Studia grazie alle numerose risorse presenti su Docsity


Ottieni i punti per scaricare
Ottieni i punti per scaricare

Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium


Guide e consigli
Guide e consigli


Grafi - Appunti completi con implementazioni Python | Algoritmi e Strutture Dati, Appunti di Algoritmi E Strutture Di Dati

Partendo da dispense scarne e poco chiare, ho riscritto ogni argomento da zero cercando di spiegare non solo il "come" ma anche il "perché" dietro ogni algoritmo. Contenuto: grafi orientati e non orientati, rappresentazione con matrice e lista di adiacenza con confronto delle complessità, visita BFS e DFS (iterativa e ricorsiva), chiusura transitiva di Warshall, rilevamento dei cicli con DFS+CPath, componenti fortemente connesse con algoritmo di Kosaraju, grafi pesati con entrambe le rappresentazioni. Per ogni algoritmo trovi: l'intuizione dietro il funzionamento, un'animazione step-by-step, l'implementazione Python commentata e l'analisi della complessità.

Tipologia: Appunti

2025/2026

In vendita dal 30/05/2026

tarfm
tarfm 🇮🇹

4 documenti

1 / 20

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
Grafi
Algoritmi e Strutture Dati
Appunti rielaborati
0
1
2
3
4
5
6
7
Grafi orientati e non orientati Visita BFS e DFS Chiusura transitiva
Rilevamento cicli Componenti fortemente connesse Grafi pesati
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14

Anteprima parziale del testo

Scarica Grafi - Appunti completi con implementazioni Python | Algoritmi e Strutture Dati e più Appunti in PDF di Algoritmi E Strutture Di Dati solo su Docsity!

Grafi

Algoritmi e Strutture Dati

Appunti rielaborati

0

1

2

3

4

5

6

7

Grafi orientati e non orientati • Visita BFS e DFS • Chiusura transitiva

Rilevamento cicli • Componenti fortemente connesse • Grafi pesati

Indice

  • 1 Introduzione ai Grafi
    • 1.1 Definizione formale
    • 1.2 Grafi orientati e non orientati
    • 1.3 Terminologia di base
    • 1.4 Altre proprietà
  • 2 Rappresentazione in memoria
    • 2.1 Matrice di adiacenza
    • 2.2 Lista di adiacenza
    • 2.3 Confronto tra le due implementazioni
  • 3 Visita di un Grafo
    • 3.1 Visita in ampiezza (BFS – Breadth-First Search)
    • 3.2 Visita in profondità (DFS – Depth-First Search)
  • 4 Chiusura Transitiva
  • 5 Rilevamento dei Cicli
  • 6 Componenti Fortemente Connesse
    • 6.1 Algoritmo di Kosaraju
  • 7 Grafi Pesati
    • 7.1 Matrice di adiacenza pesata
    • 7.2 Lista di adiacenza pesata
  • 8 Riepilogo e Linee Guida
  • Gli adiacenti (o vicini ) di u sono tutti i nodi v tali che esiste un arco da u a v.

Definizione: Percorso, cammino, ciclo

  • Un percorso di lunghezza n è una sequenza di vertici v 0 , v 1 ,... , vn collegati da archi (vertici non necessariamente distinti).
  • Un cammino è un percorso senza archi ripetuti.
  • Un circuito è un cammino chiuso (v 0 = vn) senza archi ripetuti.
  • Un ciclo è un cammino chiuso (v 0 = vn) senza nodi ripetuti (eccetto il primo e l’ultimo).

Definizione: Connessione e componenti connesse

  • Due nodi u e v si dicono connessi se esiste un cammino tra loro in entrambe le direzioni.
  • Una componente connessa è un sottografo massimale in cui tutti i nodi sono connessi tra loro.
  • Un grafo è detto connesso se ha una sola componente connessa.
  • Un nodo isolato è un nodo con grado 0.
  • Un ponte è un arco la cui rimozione disconnette il grafo.
  • Uno snodo è un nodo la cui rimozione disconnette il grafo.

1.4 Altre proprietà

Definizione: Grafo completo e densità

  • Un grafo è completo se ogni coppia di vertici è collegata da un arco.
  • La densità di un grafo è la massima cardinalità di un suo sottografo com- pleto.
  • Un grafo è sparso se |E| ≪ |V |^2 ; è denso se |E| si avvicina a |V |^2.
  • Un arco (v, v) si chiama cappio ( self-loop ). Un grafo non orientato senza cappi è detto grafo semplice.

Nota

In questa trattazione useremo nodi numerati 0 , 1 ,... , n− 1 , convenzione tipica delle implementazioni Python, dove i nodi coincidono con gli indici di un array.

2 Rappresentazione in memoria

Dato un grafo G = (V, E) con n = |V | nodi e m = |E| archi, esistono due strategie principali: la matrice di adiacenza e la lista di adiacenza. La scelta dipende dalle operazioni più frequenti e dalla densità del grafo.

2.1 Matrice di adiacenza

Si usa una matrice n × n dove la cella (i, j) vale 1 se esiste un arco dal nodo i al nodo j, e 0 altrimenti.

A[i][j] =

1 se esiste l’arco (i → j) 0 altrimenti

Esempio: il grafo con archi 0 → 1 , 0 → 2 , 1 → 3 :

(^0 )

2 3

0 1 2 3

0 0 1 1 0 1 0 0 0 1 2 0 0 0 0 3 0 0 0 0

Matrice di adiacenza

Implementazione Python

GraphM.py — Grafo con matrice di adiacenza

1 class Graph: 2 def init( self , n): 3 """Crea un grafo orientato con n nodi (matrice n x n).""" 4 self .matrix = [[0 for _ in range (n)] for _ in range (n)] 5 6 def size( self ): 7 """Numero di nodi.""" 8 return len ( self .matrix) 9 10 def nodes( self ): 11 """Lista dei nodi: [0, 1, ..., n-1].""" 12 return list ( range ( len ( self .matrix))) 13 14 def isEdge( self , i, j): # O(1) 15 """Controlla se esiste un arco da i verso j.""" 16 return self .matrix[i][j] == 1 17 18 def insertEdge( self , i, j): # O(1) 19 """Inserisce un arco orientato da i verso j.""" 20 n = len ( self .matrix) 21 if 0 <= i < n and 0 <= j < n: 22 self .matrix[i][j] = 1 23 24 def deleteEdge( self , i, j): # O(1) 25 """Rimuove l’arco da i verso j.""" 26 n = len ( self .matrix)

5 6 def size( self ): 7 return len ( self .adj) 8 9 def nodes( self ): 10 return list ( range ( len ( self .adj))) 11 12 def isEdge( self , i, j): # O(d) 13 """Costo: O(out-degree(i)), scorre la lista di i.""" 14 return j in self .adj[i] 15 16 def insertEdge( self , x, y): # O(d) 17 """Inserisce arco x->y (evita duplicati).""" 18 if 0 <= x < len ( self .adj) and 0 <= y < len ( self .adj): 19 if y not in self .adj[x]: 20 self .adj[x].append(y) 21 22 def deleteEdge( self , i, j): # O(d) 23 """Rimuove l’arco i->j se esiste.""" 24 if 0 <= i < len ( self .adj): 25 if j in self .adj[i]: 26 self .adj[i].remove(j) 27 28 def neighbors( self , i): # O(1) 29 """Lista gia’ pronta -- costo costante.""" 30 return list ( self .adj[i]) 31 32 def outDegree( self , i): # O(1) 33 """Lunghezza della lista: costo costante.""" 34 return len ( self .adj[i]) 35 36 def inDegree( self , j): # O(n+m) 37 """Scorre tutte le liste di adiacenza.""" 38 count = 0 39 for neighbors_list in self .adj: 40 if j in neighbors_list: 41 count += 1 42 return count 43 44 def copy( self ): 45 new_g = Graph( len ( self .adj)) 46 new_g.adj = [row[:] for row in self .adj] 47 return new_g 48 49 def display( self ): 50 print (f"Liste ({len(self.adj)} nodi):") 51 for i, neighbors in enumerate ( self .adj): 52 print (f" Nodo {i}: {neighbors}")

Nota

La complessità di neighbors si riflette direttamente su BFS e DFS: con la lista costa Θ(out-degree), con la matrice costa Θ(n).

2.3 Confronto tra le due implementazioni

Operazione Matrice (GraphM) Lista (GraphL)

Spazio occupato Θ(n^2 ) Θ(n + m) Inizializzazione Θ(n^2 ) Θ(n) Inserimento arco Θ(1) Θ(out-degree) Cancellazione arco Θ(1) O(out-degree) Verifica arco (i, j) Θ(1) O(out-degree) Trovare i vicini Θ(n) Θ(out-degree) Grado di uscita Θ(n) Θ(1) Grado di entrata Θ(n) Θ(n + m) Copia del grafo Θ(n^2 ) Θ(n + m)

Quando usare quale?

  • Usa la matrice quando il grafo è denso (m ≈ n^2 ) o quando hai bisogno di verificare archi in O(1) molto frequentemente.
  • Usa la lista quando il grafo è sparso (m ≪ n^2 ), come nella stragrande maggioranza dei problemi reali. Gli algoritmi di visita BFS e DFS sono molto più efficienti con la lista.

3 Visita di un Grafo

Visitare un grafo significa esplorare sistematicamente tutti i nodi raggiungibili a partire da un nodo sorgente s. Esistono due strategie principali: la visita in ampiezza (BFS) e la visita in profondità (DFS).

3.1 Visita in ampiezza (BFS – Breadth-First Search)

La BFS esplora il grafo “a livelli”: prima tutti i nodi a distanza 1 dalla sorgente, poi quelli a distanza 2, e così via. La struttura dati chiave è una coda ( FIFO ).

Algoritmo BFS

Input: Grafo G, nodo sorgente s Output: Nodi raggiungibili da s in ordine di visita

4 - Queue: coda FIFO dei nodi da visitare. 5 - isVisited: array booleano per controllo O(1). 6 Restituisce i nodi nell’ordine di visita. 7 """ 8 Queue = [s] 9 Visited = [] 10 isVisited = [ False ] * G.size() 11 12 while Queue: 13 next_node = Queue.pop(0) # estrae dalla testa (FIFO) 14 if not isVisited[next_node]: 15 Visited.append(next_node) 16 isVisited[next_node] = True 17 for x in G.neighbors(next_node): 18 if not isVisited[x]: 19 Queue.append(x) 20 21 return Visited

Complessità

Con la lista di adiacenza, ogni arco viene esaminato al più una volta. PoichéP n− 1 i=0 out-degree(i) =^ m, la complessità è^ Θ(n^ +^ m). Con la matrice,^ neighbors costa Θ(n) e la BFS diventa Θ(n^2 ).

3.2 Visita in profondità (DFS – Depth-First Search)

La DFS esplora il grafo “in profondità”: segue un cammino fino in fondo prima di tornare indietro. La struttura dati chiave è una pila ( LIFO ).

Animazione step-by-step — DFS su 0 → 1 , 0 → 2 , 1 → 3

Passo 0 — Inizializzazione

0

1 3

2

Pila (cima → fondo): 0

Passo 1 — Pop 0, push 2 poi 1 (1 in cima)

0

1 3

2

Pila : 1 2

Passo 2 — Pop 1, push 3

0

1 3

2

Pila : 3 2

Passo 3 — Pop 3, pop 2 (nessun vicino)

0

1 3

2

Pila: vuota Ordine: 0 → 1 → 3 → 2

Versione iterativa (con pila esplicita)

DFS iterativa

1 def depthVisit(G, s): 2 """ 3 Visita in profondita’ del grafo G da s. 4 Identica alla BFS ma usa pop() anziche’ pop(0): estrae dall’ultimo. 5 """ 6 Stack = [s] 7 Visited = [] 8 isVisited = [ False ] * G.size() 9 10 while Stack: 11 next_node = Stack.pop() # estrae dalla cima (LIFO) 12 if not isVisited[next_node]: 13 Visited.append(next_node) 14 isVisited[next_node] = True 15 for x in G.neighbors(next_node): 16 if not isVisited[x]: 17 Stack.append(x) 18 19 return Visited

Versione ricorsiva

DFS ricorsiva

1 def depthVisitRec(G, s): 2 """Wrapper: inizializza e avvia la ricorsione.""" 3 Visited = [s] 4 _dfs_helper(G, s, Visited) 5 return Visited 6 7 def _dfs_helper(G, x, Visited): 8 """ 9 Funzione ricorsiva ausiliaria. 10 Per ogni vicino y di x non ancora visitato: lo aggiunge e ricorre. 11 """ 12 for y in G.neighbors(x): 13 if y not in Visited:

18 return C

Complessità

I tre cicli annidati danno Θ(n^3 ). L’algoritmo è indipendente dalla struttura (matrice o lista), ma è più naturale con la matrice per l’accesso O(1) a isEdge.

5 Rilevamento dei Cicli

Un grafo orientato è detto aciclico (DAG – Directed Acyclic Graph ) se non contiene cicli. Rilevare cicli è fondamentale per molte applicazioni (es. dipendenze circolari in un progetto).

Idea dell’algoritmo

Si usa una DFS con due array booleani:

  • Visited[x]: vero se x è stato visitato in qualsiasi momento.
  • CPath[x]: vero se x è nel cammino corrente (dalla radice della DFS fino al nodo attuale).

Se durante la DFS si raggiunge un nodo y con CPath[y] = True, abbiamo trovato un ciclo. Quando si torna indietro da x, si imposta CPath[x] = False.

0 1 2

3

ciclo!

Il grafo ha un ciclo : 0 → 1 → 2 → 0

- Perché CPath e non basta Visited?

Visited diventa True anche dopo che siamo tornati indietro da un ramo. Nel grafo 0 → 1 ← 2 , quando esploriamo il nodo 2 troviamo 1 già in Visited, ma non c’è alcun ciclo: siamo arrivati a 1 da due percorsi indipendenti. CPath risolve questo: indica se y fa parte del percorso che stiamo attualmente seguendo , non solo se è già stato visitato in passato. Un ciclo esiste solo se torniamo su un nodo ancora aperto nella pila di chiamate corrente.

Rilevamento cicli

1 def cyclicGraph(G): 2 """True se G contiene almeno un ciclo."""

3 Visited = [ False ] * G.size() 4 CPath = [ False ] * G.size() 5 6 for x in G.nodes(): 7 if not Visited[x]: 8 if cyclicPath(G, x, Visited, CPath): 9 return True 10 return False 11 12 def cyclicPath(G, x, Visited, CPath): 13 """DFS ricorsiva: True se viene rilevato un ciclo.""" 14 Visited[x] = True 15 CPath[x] = True 16 17 for y in G.neighbors(x): 18 if CPath[y]: 19 return True # ciclo trovato! 20 if not Visited[y]: 21 if cyclicPath(G, y, Visited, CPath): 22 return True 23 24 CPath[x] = False # x non e’ piu’ nel cammino corrente 25 return False

6 Componenti Fortemente Connesse

Definizione: Componente Fortemente Connessa (SCC)

Dato G = (V, E) orientato, un insieme S ⊆ V è una SCC massimale se:

  1. Per ogni coppia u, v ∈ S, esiste un cammino da u a v e un cammino da v a u.
  2. S è massimale: non esiste S′^ ⊃ S che soddisfi la condizione.

Ogni grafo orientato si decompone in SCC, che formano un DAG (il grafo delle compo- nenti ).

SCC 1 SCC 2

SCC 3

0 1

2

3

4

5

6

6.1 Algoritmo di Kosaraju

19 # --- FASE 1: DFS su G ------------------------------------------- 20 for n in G.nodes(): 21 if n not in visited: 22 dfs(n, [], G) 23 24 # --- Costruzione di G^T ----------------------------------------- 25 Grev = type (G)(G.size()) 26 for i in G.nodes(): 27 for j in G.neighbors(i): 28 Grev.insertEdge(j, i) # inverte ogni arco i->j in j->i 29 30 # --- FASE 2: DFS su G^T estraendo dallo stack ------------------- 31 visited = [] 32 while stack: 33 node = stack.pop() 34 if node not in visited: 35 comp = [] 36 dfs(node, comp, Grev) 37 sccs.append(comp) 38 39 return sccs

Complessità

  • Temporale: O(V + E) – due DFS, ciascuna O(V + E), più la costruzione di GT^.
  • Spaziale: O(V + E) – per GT^ , lo stack, le liste di visita.

Esempio di esecuzione

Grafo: 0 → 1 , 0 → 2 , 1 → 3 , 3 → 0 , 2 → 4 , 4 → 2.

SCC 1 = { 0 , 1 , 3 }

SCC 2 = { 2 , 4 }

0 1 3

2 4

  • Fase 1 (DFS su G da 0): visita 0 → 1 → 3 → 0 (già vis.) → poi 2 → 4. Stack finale: [3, 1 , 4 , 2 , 0].
  • Fase 2 (DFS su GT^ ): - Estrae 0: DFS su GT^ ⇒ { 0 , 1 , 3 } – SCC 1. - Estrae 2: DFS su GT^ ⇒ { 2 , 4 } – SCC 2.

7 Grafi Pesati

Fino ad ora gli archi avevano solo due stati: esiste o non esiste. In molte applicazioni reali ogni arco ha un peso (o costo ): distanza tra città, costo di una connessione, latenza di rete, ecc.

Definizione: Grafo pesato

Un grafo pesato è un grafo G = (V, E, w) dove ogni arco (u, v) ∈ E ha associato un peso w(u, v) ∈ R.

7.1 Matrice di adiacenza pesata

Si usa math.inf per indicare l’assenza di un arco (anziché 0 , che potrebbe essere un peso valido).

A[i][j] =

w(i, j) se esiste l’arco (i → j) con peso w(i, j) +∞ altrimenti

GraphMW.py — Matrice di adiacenza pesata

1 import math 2 3 class Graph: 4 def init( self , n): 5 """Usa math.inf per distinguere ’nessun arco’ da ’arco con peso 0’.""" 6 self .matrix = [[math.inf] * n for _ in range (n)] 7 8 def size( self ): 9 return len ( self .matrix) 10 11 def nodes( self ): 12 return list ( range ( len ( self .matrix))) 13 14 def isEdge( self , i, j): 15 return self .matrix[i][j] != math.inf 16 17 def getWeight( self , i, j): 18 return self .matrix[i][j] 19 20 def insertEdge( self , i, j, w=1): 21 """Inserisce arco da i a j con peso w (default 1).""" 22 n = len ( self .matrix) 23 if 0 <= i < n and 0 <= j < n: 24 self .matrix[i][j] = w 25 26 def deleteEdge( self , i, j):

15 return any (node == j for node, weight in self .adj[i]) 16 17 def getWeight( self , i, j): 18 for node, weight in self .adj[i]: 19 if node == j: 20 return weight 21 return math.inf 22 23 def insertEdge( self , x, y, w=1): 24 """Inserisce arco x->y con peso w; aggiorna se esiste gia’.""" 25 if 0 <= x < len ( self .adj) and 0 <= y < len ( self .adj): 26 for idx, (node, weight) in enumerate ( self .adj[x]): 27 if node == y: 28 self .adj[x][idx] = (y, w) 29 return 30 self .adj[x].append((y, w)) 31 32 def deleteEdge( self , i, j): 33 if 0 <= i < len ( self .adj): 34 for idx, (node, weight) in enumerate ( self .adj[i]): 35 if node == j: 36 self .adj[i].pop(idx) 37 break 38 39 def neighbors( self , i): 40 return [node for node, weight in self .adj[i]] 41 42 def outDegree( self , i): 43 return len ( self .adj[i]) 44 45 def outStrength( self , i): 46 return sum (weight for node, weight in self .adj[i]) 47 48 def inDegree( self , j): 49 return sum (1 for neighbors_list in self .adj 50 for node, weight in neighbors_list if node == j) 51 52 def inStrength( self , j): 53 return sum (weight for neighbors_list in self .adj 54 for node, weight in neighbors_list if node == j) 55 56 def copy( self ): 57 new_g = Graph( len ( self .adj)) 58 new_g.adj = [ list (row) for row in self .adj] 59 return new_g

Nota

outStrength e inStrength (somma dei pesi) sono usati nell’analisi di reti di trasporto (capacità, distanza) e nelle reti neurali (peso delle connessioni).

Confronto tra le implementazioni pesate

Operazione Matrice pesata Lista pesata

Verifica arco (i, j) O(1) O(out-degree)

Peso arco (i, j) O(1) O(out-degree)

Inserimento/aggiornamento O(1) O(out-degree)

Cancellazione arco O(1) O(out-degree)

Vicini di i O(n) O(out-degree)

Spazio Θ(n^2 ) Θ(n + m)

8 Riepilogo e Linee Guida

Quale struttura scegliere?

  • Grafo sparso + BFS/DFSLista di adiacenza (GraphL / GraphLW).
  • Grafo denso + verifica frequente archiMatrice di adiacenza (GraphM / GraphMW).
  • Raggiungibilità tra tutte le coppieChiusura Transitiva — O(n^3 ).
  • Rilevare cicliDFS con CPath — O(n + m).
  • Gruppi mutualmente raggiungibiliKosaraju — O(n + m).

Riepilogo complessità algoritmi principali

Algoritmo Tempo Spazio

BFS O(n + m) O(n) DFS O(n + m) O(n) Chiusura transitiva Θ(n^3 ) O(n^2 ) Rilevamento cicli O(n + m) O(n) Kosaraju (SCC) O(n + m) O(n + m) Nota: n = |V |, m = |E|. I costi si riferiscono all’uso con liste di adiacenza.