












Studia grazie alle numerose risorse presenti su Docsity
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
Prepara i tuoi esami
Studia grazie alle numerose risorse presenti su Docsity
Prepara i tuoi esami con i documenti condivisi da studenti come te su Docsity
Trova i documenti specifici per gli esami della tua università
Preparati con lezioni e prove svolte basate sui programmi universitari!
Rispondi a reali domande d’esame e scopri la tua preparazione
Riassumi i tuoi documenti, fagli domande, convertili in quiz e mappe concettuali
Studia con prove svolte, tesine e consigli utili
Togliti ogni dubbio leggendo le risposte alle domande fatte da altri studenti come te
Esplora i documenti più scaricati per gli argomenti di studio più popolari
Ottieni i punti per scaricare
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
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
1 / 20
Questa pagina non è visibile nell’anteprima
Non perderti parti importanti!













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
Definizione: Percorso, cammino, ciclo
Definizione: Connessione e componenti connesse
Definizione: Grafo completo e densità
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.
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).
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?
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).
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 ).
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).
Si usa una DFS con due array booleani:
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:
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
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à
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
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.
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).
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
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.